module Neo4j
module Wrapper
module Rule
# 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, :triggers => :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 ?
# Yes, since operations are performed in an transaction. However you may get a deadlock exception:
# @see http://docs.neo4j.org/chunked/stable/transactions-deadlocks.html Transaction Deadlock
#
module ClassMethods
# 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::NodeMixin
# property :age
# rule :all
# rule :young { self[:age] < 10 }
# rule(:old, :functions => [Sum.new[:age]) { age > 20 }
# 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
# p1.sum(old, :age) # the some of the old people's age
#
def rule(rule_name, props = {}, &block)
singleton = class << self;
self;
end
# define class methods
singleton.send(:define_method, rule_name) do
rule_node = Rule.rule_node_for(self)
rule_node.traversal(rule_name)
end unless respond_to?(rule_name)
# define instance methods
self.send(:define_method, "#{rule_name}?") do
instance_eval &block
end
rule = Rule.add(self, rule_name, props, &block)
rule.functions && rule.functions.each do |func|
singleton.send(:define_method, func.class.function_name) do |r_name, *args|
rule_node = Rule.rule_node_for(self)
function_id = args.empty? ? "_classname" : args[0]
function = rule_node.find_function(r_name, func.class.function_name, function_id)
function.value(rule_node.rule_node, r_name)
end unless respond_to?(func.class.function_name)
end
end
def ref_node_for_class
Neo4j.ref_node #The reference node for a type falls back to the threadlocal ref node by default.
end
# Assigns the reference node for a class via a supplied block.
# Example of usage:
# class Person
# include Neo4j::NodeMixin
# ref_node { Neo4j.default_ref_node }
# end
#
def ref_node(&block)
singleton = class << self;
self;
end
singleton.send(:define_method, :ref_node_for_class) { block.call }
end
def inherit_rules_from(clazz)
Rule.inherit(clazz, self)
end
# This is typically used for RSpecs to clean up rule nodes created by the #rule method.
# It also remove all the rule class methods.
def delete_rules
singelton = class << self;
self;
end
rule_node = Rule.rule_node_for(self)
rule_node.rule_names.each { |rule_name| singelton.send(:remove_method, rule_name) }
rule_node.rules.clear
end
# Force to trigger the rules.
# You don't normally need that since it will be done automatically.
# This can be useful if you need to trigger rules on already existing nodes in the database.
# Can also be called from an migration.
#
def trigger_rules(node, *changes)
Rule.trigger_rules(node, *changes)
end
# Returns a proc that will call add method on the given function
# Can be used in migration to trigger rules on already existing nodes.
# Parameter function_id is default to '_classname' which means that
# the function 'reacts' on changes on the property '_classname'.
# That property changes only when a node is created or deleted.
# Function using function_id '_classname' is typically used for counting number of nodes of a class.
def add_function_for(rule_name, function_name_or_class, function_id = '_classname')
function_for(:add, rule_name, function_name_or_class, function_id)
end
# See #add_function_for
# Calls the delete method on the function.
def delete_function_for(rule_name, function_name_or_class, function_id = '_classname')
function_for(:delete, rule_name, function_name_or_class, function_id)
end
# Returns a proc that calls the given method on the given function.
def function_for(method, rule_name, function_name_or_class, function_id = '_classname')
function_name = function_name_or_class.is_a?(Symbol) ? function_name_or_class : function_name_or_class.function_name
rule_node = Rule.rule_node_for(self)
rule = rule_node.find_rule(rule_name)
rule_node_raw = rule_node.rule_node
function = rule_node.find_function(rule_name, function_name, function_id)
lambda do |node|
new_value = node[function_id]
function.send(method, rule.rule_name, rule_node_raw, new_value)
end
end
end
end
end
end