class Object
# The hidden singleton lurks behind everyone
def metaclass; class << self; self; end; end
def meta_eval &blk; metaclass.instance_eval &blk; end
# Adds methods to a metaclass
def meta_def name, &blk
meta_eval { define_method name, &blk }
end
# Defines an instance method within a class
def class_def name, &blk
class_eval { define_method name, &blk }
end
end
module ModelFormatter # :nodoc:
DEFAULT_FORMAT_PREFIX = 'formatted_'
def self.append_features(base) # :nodoc:
super
base.extend(ClassMethods)
end
def self.init_options(defaults, model, attr) # :nodoc:
options = defaults.dup
options[:attr] = attr
options[:prefix] ||= DEFAULT_FORMAT_PREFIX
options[:formatted_attr] ||= "#{options[:prefix]}#{attr}"
# If :as is set, then it must be either a formatter Class, formatter Object, Symbol, or String
options[:formatter] = formatter_for(options[:as], options[:options]) unless options[:as].nil?
# Define the formatter from a :block if :block is defined
options[:formatter] = define_formatter(attr, &options[:block]) unless options[:block].nil?
# Define :formatter from a block based on :from and :to if they're both set
if !options[:from].nil? and !options[:to].nil?
options[:formatter] = Module.new
options[:formatter].class.send :define_method, :from, options[:from]
options[:formatter].class.send :define_method, :to, options[:to]
end
# If :as is still not defined raise an error
raise 'No formatting options have been defined.' if options[:formatter].nil?
options
end
# Define a formatter like the actual physical classes
# this could easily be done with text and an eval...
# but this should be faster
def self.define_formatter(attr, &formatter) # :nodoc:
# The convention is to name these custom formatters
# differently than the other formatting classes
class_name = "CustomFormat#{attr.to_s.camelize}"
# Create a class in the same module as the others
clazz = Class.new(Formatters::Format)
silence_warnings do
Formatters.const_set class_name, clazz
end
# Define the class body
clazz.class_eval &formatter
return clazz.new
end
# Return the formatter for a class, formatter object, symbol, or
# string defining the name of a formatter class. If it's a symbol, check
# the Formatters module for the class that matches the camelized name
# of the symbol with 'Format' prepended. Options hash is passed to the
# instantiation of the formatter object.
def self.formatter_for(type_name, options = {}) # :nodoc:
# If the type_name is an instance of a formatter, just return with it
return type_name if type_name.is_a? Formatters::Format
# If the type_name is a class just assign it to the formatter_class for instantiation later
formatter_class = type_name if type_name.is_a? Class
# Format a symbol or string into a formatter_class
if type_name.is_a? Symbol or type_name.is_a? String
type_name = type_name.to_s.camelize
# Construct the class name from the type_name
formatter_name = "Format#{type_name}"
formatter_class = nil
begin
formatter_class = Formatters.const_get(formatter_name)
rescue NameError => ne
# Ignore this, caught below
end
end
# Make sure the name of this is found in the Formatters module and that
# it's the correct superclass
return formatter_class.new(options) unless formatter_class.nil? or
formatter_class.superclass != Formatters::Format
raise Formatters::FormatNotFoundException.new("Cannot find formatter 'Formatters::#{formatter_name}'")
end
require File.dirname(__FILE__) + '/formatters.rb'
# == Usage
# class Widget < ActiveRecord::Base
# # Set an integer field as a symbol
# format_column :some_integer, :as => :integer
#
# # Specify the type as a class
# format_column :sales_tax, :as => Formatters::FormatCurrency
# format_column :sales_tax, :as => Formatters::FormatCurrency.new(:precision => 4)
#
# # Change the prefix of the generated methods and specify type as a symbol
# format_column :sales_tax, :prefix => 'fmt_', :as => :currency, :options => {:precision => 4}
#
# # Use specific procedures to convert the data +from+ and +to+ the target
# format_column :area, :from => Proc.new {|value, options| number_with_delimiter sprintf('%2d', value)},
# :to => Proc.new {|str, options| str.gsub(/,/, '')}
#
# # Use a block to define the formatter methods
# format_column :sales_tax do
# def from(value, options = {})
# number_to_currency value
# end
# def to(str, options = {})
# str.gsub(/[\$,]/, '')
# end
# end
#
# ...
# end
module ClassMethods
# You can override the default options with +model_formatter+'s +options+ parameter
DEFAULT_OPTIONS = {
:prefix => nil,
:formatted_attr => nil,
:as => nil,
:from => nil,
:to => nil,
:options => {}
}.freeze
# After calling "format_column :sales_tax" as in the example above, a number of instance methods
# will automatically be generated, all prefixed by "formatted_" unless :prefix or :formatter_attr
# have been set:
#
# * Widget.sales_tax_formatter(value): Format the sales tax and return the formatted version
# * Widget.sales_tax_unformatter(str): "Un"format sales tax and return the unformatted version
# * Widget#formatted_sales_tax=(value): This will set the sales_tax of the widget using the value stripped of its formatting.
# * Widget#formatted_sales_tax: This will return the sales_tax of the widget using the formatter specified in the options to +format+.
# * Widget#sales_tax_formatting_options: Access the options this formatter was created with. Useful for declaring field length and later referencing it in display helpers.
#
# === Options:
# * :formatted_attr - The actual name used for the formatted attribute. By default, the formatted attribute
# name is composed of the :prefix, followed by the name of the attribute.
# * :prefix - Change the prefix prepended to the formatted columns. By default, formatted columns
# are prepended with "formatted_".
# * :as - Format the column as the specified format. This can be provided in either a String,
# a Symbol, or as a Class reference to the formatter. The class should subclass Formatters::Format and define from(value, options = {}) and to(str, options = {}).
# * :from - Data coming from the attribute retriever method is sent to this +Proc+, then returned as a
# manipulated attribute.
# * :to - Data being sent to the attribute setter method is sent to this +Proc+ first to be manipulated,
# and the returned attribute is then sent to the attribute setter.
# * :options - Passed to the formatter blocks, instantiating formatter classes and/or methods as additional formatting options.
def format_column(attr, options={}, &fmt_block)
options[:block] = fmt_block if block_given?
options = DEFAULT_OPTIONS.merge(options) if options
# Create the actual options
my_options = ModelFormatter::init_options(options,
ActiveSupport::Inflector.underscore(self.name).to_s,
attr.to_s)
# Create the class methods
attr_fmt_options_accessor = "#{my_options[:formatted_attr]}_formatting_options".to_sym
attr_formatter_method = "#{my_options[:formatted_attr]}_formatter".to_sym
attr_unformatter_method = "#{my_options[:formatted_attr]}_unformatter".to_sym
metaclass.class_eval do
# Create an options accessor
define_method attr_fmt_options_accessor do
my_options
end
# Define a formatter accessor
define_method attr_formatter_method do |value|
return value if value.nil?
from_method = my_options[:formatter].method(:from)
from_method.call(value, my_options[:options])
end
# Define an unformatter accessor
define_method attr_unformatter_method do |str|
to_method = my_options[:formatter].method(:to)
to_method.call(str, my_options[:options])
end
end
# Define the instance method formatter for attr
define_method my_options[:formatted_attr] do |*params|
value = self.send(attr, *params)
self.class.send attr_formatter_method, value
end
# Define the instance method unformatter for attr
define_method my_options[:formatted_attr] + '=' do |str|
value = self.class.send(attr_unformatter_method, str)
self.send(attr.to_s + '=', value)
end
end
def is_formatted?(attr)
!public_methods.reject {|method| method !~ /#{attr.to_s}_formatting_options$/}.empty?
end
end
end
ActiveRecord::Base.send(:include, ModelFormatter)