lib/glue/property.rb in glue-0.23.0 vs lib/glue/property.rb in glue-0.24.0

- old
+ new

@@ -1,457 +1,250 @@ -require 'mega/synchash' -require 'mega/syncarray' +require 'mega/annotation' +require 'mega/inheritor' +require 'mega/ohash' -require 'nano/time/change' +require 'glue/on_included' +require 'og/entity' +require 'glue/validation' +require 'og/relation/all' +require 'glue/aspects' -require 'glue/attribute' -require 'glue/flexob' - -module Glue - -# Ruby attributes are typeless and generally this is good. -# Some times we need extra metadata though, for example in -# relational mapping, or web form population. -# -# Only Fixnums, Strings, Floats, Times, Booleans are -# converted. -# -# The default = methods do not force the types. A special -# __force_set method should be used instead. +# A convienience structure that holds property +# metadata. #-- -# TODO: -# Perhaps a sync is needed in evals (!!!!) +# TODO: reimplement type checking. #++ class Property - - # If set to true, perform type checking on property set. - # Useful when debugging. - - cattr_accessor :type_checking, false - - # The symbol of the property. + attr_accessor :hash - attr_accessor :symbol + def initialize(hash) + @hash = hash + @hash = @hash.to_h unless @hash.is_a?(Hash) + end - # The class of the property. + def [](key) + @hash[key] + end + alias_method :method_missing, :[] - attr_accessor :klass - - # Additional metadata (like sql declaration, sql index, etc) - # Here is a list of predefined metadata: - # - # [+:reader+] - # create reader? - # - # [+:writer+] - # create writer? - # - # [+:sql_index+] - # create an sql index for the column this poperty maps to? - # - # You can use this mechanism to add your own, custom, - # metadata. - - attr_accessor :metadata - - # Support legacy code, will be deprecated. - - alias_method :meta, :metadata - alias_method :meta=, :metadata= - - def initialize(symbol, klass, metadata = {}) - @symbol, @klass = symbol, klass - @metadata = metadata + def []=(key, val) + @hash[key] = val end - def ==(other) - return @symbol == other.symbol + def <=>(other) + @hash[:symbol] <=> other.hash[:symbol] end def to_s - return @symbol.to_s - end + @hash[:symbol].to_s + end end -# A collection of Property related utility methods. +# Property related utils and helpers. -module PropertyUtils - - # Add accessors to the properties to the given target - # (Module or Class). For simplicity also create the - # meta accessors. +class Property + class << self + + # Populate an object from a hash of values. + # This is a truly dangerous method. # - # [+target+] - # The target class or module - #-- - # gmosx: Perhaps we 'll optimize this in the future. - #++ + # === 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) - def self.enchant(target, force = false) - unless target.instance_variables.include?('@__props') - # FIXME: should be thread safe here! - target.instance_variable_set('@__meta', Flexob.new) - target.instance_variable_set('@__props', SyncArray.new) - - # gmosx: Ruby surprises and amazes me! We are in the Metaclass - # when defining methods and attributes so @__props is really - # a class scoped variable that unlike @@__props is not shared - # through the hierarchy. + for prop in obj.class.properties.values + unless options[:all] + next if :oid == prop.symbol or prop.editor == :none # TODO check for real key. + end + prop_name = prop.symbol.to_s - target.module_eval %{ - def self.__props - @__props - end - - def self.properties - @__props - end - - def self.__props=(props) - @__props = props - end + # See if there is an incoming request param for this prop. + + if values.has_key?(prop_name) - def self.__meta - @__meta - end - - def self.__meta=(meta) - @__meta = meta - end + # if incoming file then read contents into a string. - def self.metadata - @__meta - end - } - - if target.is_a?(Class) - - # Add some extra code to append features to - # subclasses. - - target.module_eval %{ - def self.inherited(child) - Glue::PropertyUtils.enchant(child) - Glue::PropertyUtils.copy_props(self, child) - # gmosx: We have to define @@__props first to avoid - # reusing the hash from the module. super must stay - # at the end. - super - end - } - + prop_value = values[prop_name] + prop_value.read if prop_value.respond_to?(:read) + 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? + + # Pass to property force. + + obj.send("__force_#{prop_name}", prop_value) + elsif options[:force_boolean] and (prop.klass == TrueClass or prop.klass == FalseClass) + # Set a boolean property to false if it is not in the request. + # Only enabled if force_boolean == true. + + obj.send("__force_#{prop_name}", 0) else - - # Add some extra code for modules to append - # their features to classes that include it. + # check if there is a hashed version of the property (in the form values[symbol.part]) - target.module_eval %{ - def self.append_features(base) - # gmosx: We have to define @@__props first to avoid - # reusing the hash from the module. super must stay - # at the end. - Glue::PropertyUtils.copy_features(self, base) - super + property_parts = Hash.new + values.each do |k,v| + if k =~ /^#{prop_name}\./ + part_key = k.sub("#{prop_name}.", "") + property_parts[part_key.to_sym] = values[k].to_s end - } - + end + + # pass the hashed version to __force_hash_ for processing. + + obj.send("__force_hash_#{prop_name}", property_parts) unless property_parts.empty? end end - end - # Copy properties from src (Module or Class) to dest. - - def self.copy_props(src, dest) - src.__props.each do |p| - add_prop(dest, p) - end - - # copy the metadata. - src.__meta.each do |k, val| - if val.is_a?(TrueClass) - dest.__meta[k] = val - else - dest.__meta[k] = val.dup - end - # val.each { |v| dest.meta(k, v) } if val - end - end - - # Add the property to the target (Class or Module) - - def self.add_prop(target, prop) - if idx = target.__props.index(prop) - # override in case of duplicates. Keep the order of the props. - target.__props[idx] = prop - else - target.__props << prop - end - - # Store the property in the :props_and_relations - # metadata array. - - target.meta :props_and_relations, prop - - # Precompile the property read/write methods - - s, klass = prop.symbol, prop.klass - - if prop.meta[:reader] - target.module_eval %{ - def #{s} - return @#{s} + if options[:assign_relations] + for rel in obj.class.relations + unless options[:all] + next if rel.options[:editor] == :none end - } - end - - # gmosx: __force_xxx reuses xxx= to allow for easier - # overrides. - - if prop.meta[:writer] - code = %{ - #{prop_setter(prop)} - def __force_#{s}(val) - self.#{s}=(} + case klass.name - when Fixnum.name - "val.to_i()" - when String.name - "val.to_s()" - when Float.name - "val.to_f()" - when Time.name - "Time.parse(val.to_s())" - when TrueClass.name, FalseClass.name - "val.to_i() > 0" - else - "val" - end + %{) - end + rel_name = rel.name.to_s - def __force_hash_#{s}(hash) - } + # Renew the relations from values + # TODO, farms: custom callbacks for processing relations? - case klass.name - when Time.name - code << %{ - ihash = {} - hash = hash.map { |k, v| ihash[k.intern] = v.to_i } - @#{s} = @#{s}.change(ihash) - } - end + if rel.kind_of? Og::RefersTo + if foreign_oid = values[rel_name] + foreign_oid = nil if foreign_oid == 'nil' or foreign_oid == 'none' + end + obj.send("__force_#{rel.foreign_key}", foreign_oid) + elsif rel.kind_of? Og::JoinsMany or rel.kind_of? Og::HasMany + # Empty current relationships. - code << %{ - end - } - - target.module_eval(code) + collection = obj.send(rel_name) + collection.remove_all + + # Set new. + + if values.has_key?(rel_name) + values[rel_name].each do |v| + child = rel.target_class[v.to_s.to_i] + collection << child + end + end + end + end end - end - # Generates the property setter code. Can be overriden - # to support extra functionality (example: markup) + return obj + end - def self.prop_setter(prop) - s = prop.symbol - + def eval_helpers(writer, m, sym, klass) + return unless writer + code = %{ - def #{s}=(val) + def __force_#{sym}(val) + self.#{sym}=(} + case klass.name + when 'Fixnum': 'val.nil? ? nil : val.to_i' + when 'String': 'val.to_s' + when 'Float': 'val.nil? ? nil : val.to_f' + when 'Time': 'Time.parse(val.to_s)' + when 'TrueClass', 'FalseClass': '(val == "on" or val == "true") ? true : (val.to_i > 0)' + when 'Og::Blob': 'val' + else + 'val' + end + %{) + end + + def __force_hash_#{sym}(hash) } - - if Glue::Property.type_checking - code << %{ - unless #{prop.klass} == val.class - raise "Invalid type, expected '#{prop.klass}', is '\#\{val.class\}'." - end - } + + case klass.name + when Time.name + code << %{ + self.#{sym} = Time.local(hash[:year],hash[:month],hash[:day],hash[:hour],hash[:min]) + } end code << %{ - @#{s} = val - end + end } - return code - end + m.module_eval(code) + end - # Get the property metadata for the given symbol. - - def self.get_prop(klass, sym) - return klass.__props.find { |p| p.symbol == sym } end - - # Include meta-language mixins - - def self.include_meta_mixins(target) - target.module_eval %{ include Glue::Validation } if defined?(Glue::Validation) - # gmosx: TODO, make Og::MetaLanguage equivalent to Validation. - # target.module_eval %{ extend Og::MetaLanguage } if defined?(Og::MetaLanguage) - target.module_eval %{ include Glue::Aspects } if defined?(Glue::Aspects) - target.send(:include, Og::EntityMixin) if defined?(Og::EntityMixin) - end - - def self.copy_features(this, other) - Glue::PropertyUtils.enchant(other) - Glue::PropertyUtils.copy_props(this, other) - Glue::PropertyUtils.include_meta_mixins(other) - end - - # Resolves the parameters passed to the propxxx macros - # to generate the meta, klass and symbols variables. This - # way the common functionality is factored out. - # - # [+params+] - # The params to resolve. - # [+one_symbol+] - # If true, only resolves one symbol (used in prop). - - def self.resolve_prop_params(*params) - meta = {} - klass = Object - symbols = [] - - for param in params.flatten - if param.is_a?(Class) - klass = param - elsif param.is_a?(Symbol) - symbols << param - elsif param.is_a?(TrueClass) or param.is_a?(TrueClass) - writer = param - elsif param.is_a?(Hash) - # the meta hash. - meta.update(param) { |k, a, b| [a,b].join(' ') } - else - raise 'Error when defining property!' - end - end - - raise 'No symbols provided!' if symbols.empty? - - return meta, klass, symbols - 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, {}, :merge) unless @properties - # Define a property (== typed attribute) - # This works like Ruby's standard attr method, ie creates - # only one property. - # - # Use the prop_reader, prop_writer, prop_accessor methods - # for multiple properties. - # - # === Examples - # - # prop String, :name, :sql => "char(32), :sql_index => "name(32)" - # --> creates only writer. - # prop Fixnum, :oid, writer = true, :sql => "integer PRIMARY KEY" - # --> creates reader and writer. - - def prop(*params) - meta, klass, symbols = Glue::PropertyUtils.resolve_prop_params(params) - symbol = symbols.first - - Glue::PropertyUtils.enchant(self) + args = args.flatten + harg = {} + while args.last.is_a?(Hash) + harg.update(args.pop) + end - property = Glue::Property.new(symbol, klass, meta) - - reader = meta[:reader] || true - writer = writer || meta[:writer] || false - - meta[:reader] = true if meta[:reader].nil? - if defined?(writer) - meta[:writer] = writer - else - meta[:writer] = true if meta[:writer].nil? - end + harg[:klass] = args.pop if args.last.is_a?(Class) + harg[:klass] ||= String + + raise if args.empty? and harg.empty? - Glue::PropertyUtils.add_prop(self, property) - - # gmosx: should be placed AFTER enchant! + 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 + + Module.__add_prop_hook__(self) + end + } - Glue::PropertyUtils.include_meta_mixins(self) + module_eval(code) end + alias :property :prop_accessor + + # NITRO specific!! leave blank in nano/mega. + # TODO: factor in eval_helpers into the hook! - # Helper method. Accepts a collection of symbols and generates - # properties. Only generates reader. - # - # Example: - # prop_reader String, :name, :title, :body, :sql => "char(32)" - - def prop_reader(*params) - meta, klass, symbols = Glue::PropertyUtils.resolve_prop_params(params) - - meta[:reader] = true - meta[:writer] = false - - for symbol in symbols - prop(klass, symbol, meta) - end + 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, Glue::Aspects) unless m.ancestors.include?(Glue::Aspects) end - - # Helper method. Accepts a collection of symbols and generates - # properties. Only generates writer. - # - # Example: - # prop_writer String, :name, :title, :body, :sql => "char(32)" - def prop_writer(*params) - meta, klass, symbols = Glue::PropertyUtils.resolve_prop_params(params) - - meta[:reader] = false - meta[:writer] = true - - for symbol in symbols - prop(klass, symbol, meta) - end - end - - # Helper method. Accepts a collection of symbols and generates - # properties. Generates reader and writer. - # - # Example: - # prop_accessor String, :name, :title, :body, :sql => "char(32)" - - def prop_accessor(*params) - meta, klass, symbols = Glue::PropertyUtils.resolve_prop_params(params) - - meta[:reader] = true - meta[:writer] = true - - for symbol in symbols - prop(klass, symbol, meta) - end - end - alias_method :property, :prop_accessor - - # Attach metadata. - # Guard against duplicates, no need to keep order. - # This method uses closures :) - #-- - # gmosx: crappy implementation, recode. - #++ - - def meta(key, *val) - Glue::PropertyUtils.enchant(self) - - if val.empty? - self.module_eval %{ - @__meta[key] = true - } - else - val = val.first if val.size == 1 - - self.module_eval %{ - @__meta[key] ||= [] - @__meta[key].delete_if { |v| val == v } - @__meta[key] << val - } - end - end - end # * George Moschovitis <gm@navel.gr> +# * Tom Sawyer <transfire@gmail.com> +# * Chris Farmiloe <chris.farmiloe@farmiloe.com>