require 'glue/attribute' require 'glue/flexob' require 'glue/array' require 'glue/hash' 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. #-- # TODO: # Perhaps a sync is needed in evals (!!!!) #++ 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 :symbol # The class of the property. 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 :meta def initialize(symbol, klass, meta = {}) @symbol, @klass = symbol, klass @meta = meta end def ==(other) return @symbol == other.symbol end def to_s return @symbol.to_s end end # A collection of Property related utility methods. module PropertyUtils # Add accessors to the properties to the given target # (Module or Class). For simplicity also create the # meta accessors. # # [+target+] # The target class or module #-- # gmosx: Perhaps we 'll optimize this in the future. #++ 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', SafeArray.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. target.module_eval %{ def self.__props @__props end def self.properties @__props end def self.__props=(props) @__props = props end def self.__meta @__meta end def self.__meta=(meta) @__meta = meta end 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 } else # Add some extra code for modules to append # their features to classes that include it. 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 end } 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} end } end # gmosx: __force_xxx reuses xxx= to allow for easier # overrides. if prop.meta[:writer] target.module_eval %{ #{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 } end end # Generates the property setter code. Can be overriden # to support extra functionality (example: markup) def self.prop_setter(prop) s = prop.symbol code = %{ def #{s}=(val) } if Glue::Property.type_checking code << %{ unless #{prop.klass} == val.class raise "Invalid type, expected '#{prop.klass}', is '\#\{val.class\}'." end } end code << %{ @#{s} = val end } return 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 class Module # 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) 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 Glue::PropertyUtils.add_prop(self, property) # gmosx: should be placed AFTER enchant! Glue::PropertyUtils.include_meta_mixins(self) end # 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 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