lib/riveter/attributes.rb in riveter-0.3.0 vs lib/riveter/attributes.rb in riveter-0.6.0

- old
+ new

@@ -24,13 +24,17 @@ end end module ClassMethods def attr_string(name, options={}, &block) + options = { + :validate => true + }.merge(options) + converter = block_given? ? block : Converters.converter_for(:string) - attr_reader_with_converter name, converter + attr_reader_with_converter name, converter, options attr_writer name add_attr(name, :string, converter, options) end @@ -63,12 +67,19 @@ [:min, :max].each do |limit| limit_value = options.delete(limit) define_method :"#{name}_#{limit}" do - limit_value.respond_to?(:call) ? limit_value.call : limit_value + instance_variable_get("@#{name}_#{limit}") || + (limit_value.respond_to?(:call) ? instance_exec(&limit_value) : limit_value) end + + # can manually assign the min/max + define_method :"#{name}_#{limit}=" do |value| + instance_variable_set("@#{name}_#{limit}", value) + end + end converter = block_given? ? block : Converters.converter_for(:date) # return from and to as range @@ -103,11 +114,11 @@ end # break down into parts [:from, :to].each do |part| - attr_reader_with_converter :"#{name}_#{part}", converter + attr_reader_with_converter :"#{name}_#{part}", converter, options define_method :"#{name}_#{part}?" do send(:"#{name}_#{part}").present? end @@ -125,65 +136,41 @@ date_from = send(:"#{name}_from") date_to = send(:"#{name}_to") date_from && date_to end - # return from and to as range in UTC - define_method :"#{name}_utc" do - date_from = send(:"#{name}_from") - date_to = send(:"#{name}_to") - if date_from && date_to - if date_from == date_to - date = date_from.to_utc_date - DateTime.new(date.year, date.month, date.day, 0, 0, 0)..DateTime.new(date.year, date.month, date.day, 23, 59, 59) - else - date_from.to_utc_date..date_to.to_utc_date - end - else - nil - end - end - end def attr_boolean(name, options={}, &block) options = { :validate => true }.merge(options) converter = block_given? ? block : Converters.converter_for(:boolean) - attr_reader_with_converter name, converter + attr_reader_with_converter name, converter, options alias_method "#{name}?", name attr_writer name add_attr(name, :boolean, converter, options) end + # NB: enum must respond to values def attr_enum(name, enum, options={}, &block) + raise ArgumentError, 'enum' unless enum && enum.respond_to?(:values) + options = { :enum => enum, :validate => true }.merge(options) required = options[:required] == true converter = block_given? ? block : Converters.converter_for(:enum, options) - attr_reader_with_converter name, converter + attr_reader_with_converter name, converter, options - # helpers - # TODO: would be nicer to emulate an association - - define_singleton_method "#{name}_enum" do - enum - end - - define_singleton_method name.to_s.pluralize do - enum.collection - end - validates name, :allow_blank => !required, :allow_nil => !required, :inclusion => { :in => enum.values } if options[:validate] @@ -201,11 +188,11 @@ converter = block_given? ? block : Converters.converter_for(data_type) define_method name do array = instance_variable_get("@#{name}") || [] - array.map {|v| converter.call(v) } + array.map {|v| converter.call(v, options) } end attr_writer name add_attr(name, :array, converter, options) @@ -220,77 +207,58 @@ converter = block_given? ? block : Converters.converter_for(data_type) define_method name do hash = instance_variable_get("@#{name}") || {} - hash.merge(hash) {|k, v| converter.call(v) } + hash.merge(hash) {|k, v| converter.call(v, options) } end attr_writer name add_attr(name, :hash, converter, options) end - ## - # FIXME: this doesn't work as expected - # - def attr_model(name, model_or_scope, options={}, &block) + def attr_object(name, options={}, &block) options = { - :model => model_or_scope, - :validate => true, - :find_by => :id + :validate => true }.merge(options) - required = options[:required] == true - converter = if block_given? - block - elsif model_or_scope.respond_to?(:find_by) - Converters.converter_for(:model, options) - else - Converters.converter_for(:object, options) - end + converter = block_given? ? block : Converters.converter_for(:object, options) - # helpers - define_singleton_method "#{name}_model" do - model_or_scope - end + attr_reader_with_converter name, converter, options - attr_reader_with_converter name, converter - - # only add validation of the model instance if supported - if model_or_scope.instance_methods.include?(:valid?) && options[:validate] - validate :"validate_#{name}" - - # need a "custom" associated validation since - # we don't reference active record... - define_method :"validate_#{name}" do - instance = self.send(name) - return unless required && instance.present? - self.errors.add(name, :invalid) unless instance.valid? - end - end - attr_writer name - add_attr(name, :model, converter, options) + add_attr(name, :object, converter, options) end - def attr_object(name, options={}, &block) - converter = block_given? ? block : Converters.converter_for(:object, options) + def attr_class(name, classes, options={}, &block) + options = { + :validate => true, + :classes => classes + }.merge(options) - attr_reader_with_converter name, converter + required = options[:required] == true + converter = block_given? ? block : Converters.converter_for(:class, options) + attr_reader_with_converter name, converter, options + + validates name, + :allow_blank => !required, + :allow_nil => !required, + :inclusion => { :in => classes } if options[:validate] + attr_writer name - add_attr(name, :object, converter, options) + add_attr(name, :class, converter, options) end private - def attr_reader_with_converter(name, converter) + def attr_reader_with_converter(name, converter, options={}) define_method name do - converter.call instance_variable_get("@#{name}") + converter.call(instance_variable_get("@#{name}"), options) end end def attr_numeric(type, name, options={}, &block) options = { @@ -298,11 +266,11 @@ }.merge(options) required = options[:required] == true converter = block_given? ? block : Converters.converter_for(type) - attr_reader_with_converter name, converter + attr_reader_with_converter name, converter, options validates name, :allow_blank => !required, :allow_nil => !required, :numericality => { :only => type } if options[:validate] @@ -318,11 +286,11 @@ }.merge(options) required = options[:required] == true converter = block_given? ? block : Converters.converter_for(type) - attr_reader_with_converter name, converter + attr_reader_with_converter name, converter, options validates name, :allow_blank => !required, :allow_nil => !required, :timeliness => { :type => type } if options[:validate] @@ -331,39 +299,41 @@ add_attr(name, type, converter, options) end def add_attr(name, type, converter=nil, options={}) - self._attributes[name] = attribute_info = AttributeInfo.new(name, type, converter, options) + self._attributes[name] = attribute_info = AttributeInfo.new(self, name, type, converter, options) validates name, :presence => true if attribute_info.required? attribute_info end end - class AttributeInfo < Struct.new(:name, :type, :converter, :options) + class AttributeInfo < Struct.new(:target, :name, :type, :converter, :options) def required? @required ||= (options[:required] == true) end def default @default ||= options[:default] - @default.respond_to?(:call) ? @default.call : @default end def default? !self.default.nil? end + + def evaluate_default(scope) + default? && default.respond_to?(:call) ? scope.instance_exec(&default) : default + end end attr_reader :options def initialize(params=nil, options={}) # assign default values self.class._attributes.each do |name, attribute_info| next unless attribute_info.default? - value = attribute_info.default - send("#{name}=", value.respond_to?(:call) ? value.call : value) + send("#{name}=", attribute_info.evaluate_default(self)) end @options = options.freeze # filter and clean params before applying @@ -459,38 +429,38 @@ # supply a built-in converter for the given type def self.converter_for(data_type, options={}) case data_type when :string - lambda {|v| v.to_s } + lambda {|v, opt| v.to_s } when :boolean - lambda {|v| v.to_b } + lambda {|v, opt| v.to_b } when :integer - lambda {|v| Integer(v) rescue v } + lambda {|v, opt| Integer(v) rescue v } when :decimal, :float - lambda {|v| Float(v) rescue v } + lambda {|v, opt| Float(v) rescue v } when :date - lambda {|v| Date.parse(v) rescue v } + lambda {|v, opt| Date.parse(v) rescue v } when :time - lambda {|v| Time.parse(v) rescue v } + lambda {|v, opt| Time.parse(v) rescue v } when :enum - lambda {|enum, v| + lambda {|enum, v, opt| enum.values.include?(v) ? v : enum.value_for(v) }.curry[options[:enum]] - when :model - lambda {|model, attrib, v| - v.is_a?(model) ? v : model.find_by(attrib => v) - }.curry[options[:model], options[:find_by]] + when :class + lambda {|models, v, opt| + models[v] || v + }.curry[options[:classes].inject({}) {|list, klass| list[klass.name] = klass; list }] else # object etc... - lambda {|v| v } + lambda {|v, opt| v } end end end