require 'nano/kernel/constant' require 'nano/string/capitalized' require 'mega/orm_support' require 'mega/inheritor' require 'mega/annotation' module Og # A relation between Entities. #-- # Relations are resolved in multiple passes. First # the relations are logged in :relations... #++ class Relation # The parameters of this relation. attr_accessor :options # A generalized initialize method for all relations. # Contains common setup code. def initialize(args, options = {}) @options = options @options.update(args.pop) if args.last.is_a?(Hash) target_name = if collection :target_plural_name else :target_singular_name end # Check that all needed options are provided. if args.empty? or (not (args.last.is_a?(Class) or args.last.is_a?(Symbol))) raise 'Class of target not defined' end # Try to set the target class. Checks for class and # class symbol. if args.last.to_s.capitalized? @options[:target_class] = args.pop end # Try to set the target name. if args.last.is_a? Symbol @options[target_name] = args.pop end # Inflect target_class if not provided. @options[:target_class] ||= @options[target_name].to_s.singular.camelize.intern # FIXME: this is a hack! # setup() rescue nil end # Get an option. def [](key) @options[key] end # Set an option. def []=(key, val) @options[key] = val end # Is this a polymorphic marker? def polymorphic_marker? target_class == Object end # Is this a polymorphic relation ? def polymorphic? target_class.ann.self[:polymorphic] end # Resolve a polymorphic target class. # Overrided in subclasses. def resolve_polymorphic end # This method is implemented in subclasses. def enchant end def to_s @options[:target_name] end # Access the hash values as methods. def method_missing(sym, *args) return @options[sym] end end # A collection of helper methods and resolvers # for relations. class Relation class << self # To avoid forward declarations, references to undefined # (at the time of the creation of the relation) classes are # stored as symbols. These symbols are resolved by this # method. #-- # FIXME: do something more elegant here. #++ def symbol_to_class(sym, owner_class) c = owner_class.name.dup c = "::" + c unless c =~ /::/ c.gsub!(/::.*$/, '::') c << sym.to_s begin return constant(c) rescue unless c == sym c = sym retry end end end alias_method :resolve_symbol, :symbol_to_class def resolve_targets(klass) for r in klass.relations if r.target_class.is_a? Symbol klass = symbol_to_class(r.target_class, r.owner_class) r.options[:target_class] = klass end end end # If the target class is just an Object mark this class # (self) as a polymorphic parent class. This class acts # as template to generate customized versions of this class. # # For example: # # class Comment # belongs_to :parent, Object # <= polymorphic # ... # end def resolve_polymorphic_markers(klass) for r in klass.relations if r.polymorphic_marker? r.owner_class.ann :self, :polymorphic => r.owner_class end end end # Resolve polymorphic relations. # If the target class is polymorphic, create a specialized # version of that class (the target) enclosed in the # owner namespace. # # For example: # # class Article # has_many Comment # ... # end def resolve_polymorphic_relations(klass) for r in klass.relations if r.polymorphic? target_dm = r.target_class.to_s.demodulize r.owner_class.module_eval %{ class #{r.owner_class}::#{target_dm} < #{r.target_class} end } # Replace the target class. r[:target_class] = eval("#{r.owner_class}::#{target_dm}") end r.resolve_polymorphic end end # Resolve the names of the relations. def resolve_names(klass) for r in klass.relations target_name = if r.collection :target_plural_name else :target_singular_name end # Inflect the relation name. unless r[target_name] r[target_name] = if r.collection r.target_class.to_s.demodulize.underscore.downcase.plural.intern else r.target_class.to_s.demodulize.underscore.downcase.intern end end r[:name] = r[target_name] end end # General resolve method. def resolve(klass, action = :resolve_polymorphic) for r in klass.relations r.send(action) end end # Perform relation enchanting on this class. def enchant(klass) # update inherited relations. for r in klass.relations r[:owner_class] = klass end # enchant. for r in klass.relations # p "=== #{klass} : #{r.class} : #{r.name}" r.enchant() unless r.polymorphic_marker? end end end end # Relations domain specific language (DSL). This # language defines macros that are used to define Entity # relations. Additional macros allow for relation # inspection. module RelationDSL inheritor(:relations, [], :+) #unless @relations class_inherit do # === Examples # # belongs_to :article # inflects Article # belongs_to Article # inflects :article # belongs_to :article, Article # belongs_to :article, Article, :view => 'lala' def belongs_to(*args) require 'og/relation/belongs_to' relations! << Og::BelongsTo.new(args, :owner_class => self) end # === Examples # # refers_to :topic # inflects Topic # refers_to Topic # inflects :topic def refers_to(*args) require 'og/relation/refers_to' relations! << Og::RefersTo.new(args, :owner_class => self) end # === Examples # # has_one User def has_one(*args) require 'og/relation/has_one' relations! << Og::HasOne.new(args, :owner_class => self) end # === Examples # # has_many Comment # has_many :comments, Comment def has_many(*args) require 'og/relation/has_many' relations! << Og::HasMany.new(args, :owner_class => self, :collection => true) end # .. def joins_many(*args) require 'og/relation/joins_many' relations! << Og::JoinsMany.new(args, :owner_class => self, :collection => true) end # .. def many_to_many(*args) require 'og/relation/many_to_many' relations! << Og::ManyToMany.new(args, :owner_class => self, :collection => true) end def inspect_relation(name) relations.find { |r| r[:name] == name } end alias_method :relation, :inspect_relation end end end # * George Moschovitis