module Neo4j::Mapping module ClassMethods # Holds all defined rules and trigger them when an event is received. # # See Rule # class Rules class << self def add(clazz, field, props, &block) clazz = clazz.to_s @rules ||= {} # was there no ruls for this class AND is neo4j running ? if !@rules.include?(clazz) && Neo4j.running? # maybe Neo4j was started first and the rules was added later. Create rule nodes now create_rule_node_for(clazz) end @rules[clazz] ||= {} filter = block.nil? ? Proc.new { |*| true } : block @rules[clazz][field] = filter @triggers ||= {} @triggers[clazz] ||= {} trigger = props[:trigger].nil? ? [] : props[:trigger] @triggers[clazz][field] = trigger.respond_to?(:each) ? trigger : [trigger] end def inherit(parent_class, subclass) # copy all the rules @rules[parent_class.to_s].each_pair do |field, filter| subclass.rule field, &filter end if @rules[parent_class.to_s] end def trigger_other_rules(node) clazz = node[:_classname] @rules[clazz].keys.each do |field| rel_types = @triggers[clazz][field] rel_types.each do |rel_type| node.incoming(rel_type).each { |n| n.trigger_rules } end end end def fields_for(clazz) clazz = clazz.to_s return [] if @rules.nil? || @rules[clazz].nil? @rules[clazz].keys end def delete(clazz) clazz = clazz.to_s # delete the rule node if found if Neo4j.ref_node.rel?(clazz) Neo4j.ref_node.outgoing(clazz).each { |n| n.del } end @rules.delete(clazz) if @rules end def on_neo4j_started(*) @rules.each_key { |clazz| create_rule_node_for(clazz) } if @rules end def create_rule_node_for(clazz) if !Neo4j.ref_node.rel?(clazz) Neo4j::Transaction.run do node = Neo4j::Node.new Neo4j.ref_node.outgoing(clazz) << node node end end end def trigger?(node) @rules && node.property?(:_classname) && @rules.include?(node[:_classname]) end def rule_for(clazz) if Neo4j.ref_node.rel?(clazz) Neo4j.ref_node._rel(:outgoing, clazz)._end_node else # this should be called if the rule node gets deleted create_rule_node_for(clazz) end end def on_relationship_created(rel, *) trigger_start_node = trigger?(rel._start_node) trigger_end_node = trigger?(rel._end_node) # end or start node must be triggered by this event return unless trigger_start_node || trigger_end_node on_property_changed(trigger_start_node ? rel._start_node : rel._end_node) end def on_property_changed(node, *) trigger_rules(node) if trigger?(node) end def trigger_rules(node) trigger_rules_for_class(node, node[:_classname]) trigger_other_rules(node) end def trigger_rules_for_class(node, clazz) return if @rules[clazz].nil? agg_node = rule_for(clazz) @rules[clazz].each_pair do |field, rule| if run_rule(rule, node) # is this node already included ? unless connected?(field, agg_node, node) agg_node.outgoing(field) << node end else # remove old ? break_connection(field, agg_node, node) end end # recursively add relationships for all the parent classes with rules that also pass for this node if clazz = eval("#{clazz}.superclass") trigger_rules_for_class(node, clazz.to_s) end end # work out if two nodes are connected by a particular relationship # uses the end_node to start with because it's more likely to have less relationships to go through # (just the number of superclasses it has really) def connected?(relationship, start_node, end_node) end_node.incoming(relationship).each do |n| return true if n == start_node end false end # sever a direct one-to-one relationship if it exists def break_connection(relationship, start_node, end_node) end_node.rels(relationship).incoming.each do |r| return r.del if r.start_node == start_node end end def run_rule(rule, node) if rule.arity != 1 node.wrapper.instance_eval(&rule) else rule.call(node) end end end end # Allows you to group nodes by providing a rule. # # === Example, finding all nodes of a certain class # Just add a rule without a code block, then all nodes of that class will be grouped under the given key (all # for the example below). # # class Person # include Neo4j::NodeMixin # rule :all # end # # Then you can get all the nodes of type Person (and siblings) by # Person.all.each {|x| ...} # # === Example, finding all nodes with a given condition on a property # # class Person # include Neo4j::NodeMixin # property :age # rule(:old) { age > 10 } # end # # Now we can find all nodes with a property age above 10. # # === Chain Rules # # class NewsStory # include Neo4j::NodeMixin # has_n :readers # rule(:featured) { |node| node[:featured] == true } # rule(:young_readers) { !readers.find{|user| !user.young?}} # end # # You can combine two rules. Let say you want to find all stories which are featured and has young readers: # NewsStory.featured.young_readers.each {...} # # === Trigger Other Rules # You can let one rule trigger another rule. # Let say you have readers of some magazine and want to know if the magazine has old or young readers. # So when a reader change from young to old you want to trigger all the magazine that he reads (a but stupid example) # # Example # class Reader # include Neo4j::NodeMixin # property :age # rule(:young, :trigger => :readers) { age < 15 } # end # # class NewsStory # include Neo4j::NodeMixin # has_n :readers # rule(:young_readers) { !readers.find{|user| !user.young?}} # end # # === Performance Considerations # If you have many rules and many updates this can be a bit slow. # In order to speed it up somewhat you can use the raw java node object instead by providing an argument in your block. # # Example: # # class Person # include Neo4j::NodeMixin # property :age # rule(:old) {|node| node[:age] > 10 } # end # # === Thread Safe ? # Not sure... # module Rule # Creates an rule node attached to the Neo4j.ref_node # Can be used to rule all instances of a specific Ruby class. # # Example of usage: # class Person # include Neo4j # rule :all # rule :young { self[:age] < 10 } # end # # p1 = Person.new :age => 5 # p2 = Person.new :age => 7 # p3 = Person.new :age => 12 # Neo4j::Transaction.finish # Person.all # => [p1,p2,p3] # Person.young # => [p1,p2] # p1.young? # => true # def rule(name, props = {}, &block) singelton = class << self; self; end # define class methods singelton.send(:define_method, name) do agg_node = Rules.rule_for(self) raise "no rule node for #{name} on #{self}" if agg_node.nil? traversal = agg_node.outgoing(name) # TODO possible to cache this object Rules.fields_for(self).each do |filter_name| traversal.filter_method(filter_name) do |path| path.end_node.rel?(filter_name, :incoming) end end traversal end unless respond_to?(name) # define instance methods self.send(:define_method, "#{name}?") do instance_eval &block end Rules.add(self, name, props, &block) end def inherit_rules_from(clazz) Rules.inherit(clazz, self) end # This is typically used for RSpecs to clean up rule nodes created by the #rule method. # It also remove the given class method. def delete_rules singelton = class << self; self; end Rules.fields_for(self).each do |name| singelton.send(:remove_method, name) end Rules.delete(self) end # Force to trigger the rules. # You don't normally need that since it will be done automatically. def trigger_rules(node) Rules.trigger_rules(node) end end Neo4j.unstarted_db.event_handler.add(Rules) unless Neo4j.read_only? end end