require 'facet/annotation' require 'facet/inheritor' require 'facet/dictionary' require 'facets/core/module/on_included' require 'facets/more/aspects' require 'glue/validation' require 'og/entity' require 'og/relation/all' # A convienience structure that holds property # metadata. A property is a special type of class annotation. # Typically used in Og managed classes (ie Entities), but it # is *not* Og specific. #-- # TODO: reimplement type checking. # TODO: try to clean this up. #++ class Property # The hash used to store the property options. attr_accessor :hash def initialize(hash) @hash = hash @hash = @hash.to_h unless @hash.is_a?(Hash) end def [](key) @hash[key] end alias_method :method_missing, :[] def []=(key, val) @hash[key] = val end def <=>(other) @hash[:symbol] <=> other.hash[:symbol] end def to_s @hash[:symbol].to_s end alias_method :name, :to_s # :section: Property related utils and helpers. class << self # Populate an object from a hash of values. # This is a truly dangerous method. # # === Options: # * name # * force_boolean def populate_object(obj, values, options = {}) # If a class is passed create an instance. obj = obj.new if obj.is_a?(Class) for prop in obj.class.properties.values unless options[:all] next if prop.symbol == obj.class.primary_key.symbol or prop.control == :none or prop.disable_control end prop_name = prop.symbol.to_s # See if there is an incoming request param for this prop. if values.has_key?(prop_name) prop_value = values[prop_name] # to_s must be called on the prop_value incase the # request is IOString. prop_value = prop_value.to_s unless prop_value.is_a?(Hash) or prop_value.is_a?(Array) # If property is a Blob dont overwrite current # property's data if "". break if prop.klass == Og::Blob and prop_value.empty? # If custom preprocessing is active then try and # preprocess. prop_value = preprocess_value(obj, prop, prop_value) if options[:preprocess] # assign using __force_ methods. obj.send("__force_#{prop_name}", prop_value) # Set a boolean property to false if it is not in the request. # requires force_boolean == true. elsif options[:force_boolean] and (prop.klass == TrueClass or prop.klass == FalseClass) obj.send("__force_#{prop_name}", 0) end end if options[:assign_relations] for rel in obj.class.relations unless options[:all] next if rel.options[:control] == :none or rel.options[:disable_control] end rel_name = rel.name.to_s # Renew the relations from values if rel.kind_of?(Og::RefersTo) if foreign_oid = values[rel_name] foreign_oid = foreign_oid.to_s unless foreign_oid.is_a?(Hash) or foreign_oid.is_a?(Array) foreign_oid = nil if foreign_oid == 'nil' or foreign_oid == 'none' # if custom preprocessing is active then try and preprocess foreign_oid = preprocess_value(obj, rel, foreign_oid) if options[:preprocess] end obj.send("__force_#{rel.foreign_key}", foreign_oid) elsif rel.kind_of?(Og::JoinsMany) || rel.kind_of?(Og::HasMany) collection = obj.send(rel_name) collection.remove_all if values.has_key?(rel_name) primary_keys = values[rel_name] # if custom preprocessing is active then try and preprocess primary_keys = preprocess_value(obj, rel, primary_keys) if options[:preprocess] primary_keys.each do |v| next if v.to_s == "nil" or v.to_s == "none" collection << rel.target_class[v.to_s.to_i] end end end end end #-- # gmosx, FIXME: this is a hack, will be replaced with proper # code soon. #++ for callback in obj.class.assign_callbacks callback.call(obj, values, options) end if obj.class.respond_to?(:assign_callbacks) return obj end # This method will attempt to process [value] through the # any on_populate preprocessors available. # # first looks if a method was specified on the property # # ie. property :this, String, :on_populate => :change_value # ==> would mean #change_value(val) would get called # TODO: set :on_populate to false to exclude it from any preprocessing # # next an #on_populate method will be looked for on any # associated Nitro::Control (if controls is running) # # otherwise [value] is left untouched def preprocess_value(obj, prop_or_rel, value) if prop_or_rel.on_populate && obj.respond_to?(prop_or_rel.on_populate.to_sym) return obj.send(prop_or_rel.on_populate.to_sym, value) elsif Object.const_defined? :Nitro if control = Nitro::Form::Control.fetch(obj, prop_or_rel, :default => nil) and control.respond_to?(:on_populate) return control.on_populate(value) end else return value end end def eval_helpers(writer, m, sym, klass) return unless writer code = %{ def __force_#{sym}(val) if respond_to?(:force_#{sym}) self.#{sym} = force_#{sym}(val) else self.#{sym}=(} << case klass.name when Fixnum.name: 'val.to_s.empty? ? nil : val.to_i' when String.name: 'val.to_s' when Float.name: 'val.to_f' when Time.name: 'val.is_a?(Hash) ? Time.local(val["year"],val["month"],val["day"],val["hour"],val["min"]) : Time.parse(val.to_s)' when Date.name: 'val.is_a?(Hash) ? Time.local(val["year"],val["month"],val["day"]).to_date : Time.parse(val.to_s).to_date' when TrueClass.name, FalseClass.name: 'val == "on" or val == "true" ? true: val.to_i > 0' else 'val' end + %{) end end } m.module_eval(code) end end end #-- # Extend the default Module. # # The properties hash, keeps the property metadata as a hash to avoid # the null issue. #++ class Module [ nil, :_reader, :_writer, :_accessor].each do |m| writer = (m != :reader) code = %{ def prop#{m} (*args) inheritor(:properties, Dictionary.new, :merge) unless @properties args = args.flatten harg = {} while args.last.is_a?(Hash) harg.update(args.pop) end harg[:klass] = args.pop if args.last.is_a?(Class) klass = harg[:klass] ||= String raise if args.empty? and harg.empty? # If the class defines the include_as_property callback # call to modify the base with custom code. skip = false if klass.respond_to? :included_as_property skip = klass.included_as_property(self, args, harg) end unless skip if !args.empty? undef_keys = args.select{ |a| !method_defined?(a) } unless undef_keys.empty? attr#{m} *undef_keys end args.each { |a| a = a.to_sym an = define_annotation(a, harg) ah = an.to_h.merge(:symbol => a) # gmosx: allow for duplicate declarations. properties![a] = Property.new(ah) Property.eval_helpers(#{writer}, self, a, harg[:klass]) } else undef_keys = harg.keys.select{ |a| !method_defined?(a) } attribute#{m} *undef_keys harg.each { |a,h| a = a.to_sym an = define_annotation(a, h) ah = an.to_h.merge(:symbol => a) # gmosx: allow for property redefinitions. properties![a] = Property.new(ah) Property.eval_helpers(#{writer}, self, a, harg[:klass]) } end end Module.__add_prop_hook__(self) end } module_eval(code) end alias :property :prop_accessor # NITRO specific!! leave blank in facets. # TODO: factor in eval_helpers into the hook! def self.__add_prop_hook__(m) m.send(:include, Og::EntityMixin) unless m.ancestors.include?(Og::EntityMixin) m.send(:include, Glue::Validation) unless m.ancestors.include?(Glue::Validation) m.send(:include, ::Aspects) unless m.ancestors.include?(::Aspects) end end # * George Moschovitis # * Tom Sawyer # * Chris Farmiloe # * Bryan Soto