Skip to content

Commit

Permalink
Cypher Traversal and Rules neo4jrb/activegraph#181
Browse files Browse the repository at this point in the history
Support for things like getting the rooms for a dungeon where there are dangerous monsters using rules:
@dungeon.monsters.dangerous { |m| rooms = m.incoming(Room.monsters); rooms}
  • Loading branch information
andreasronge committed Apr 17, 2012
1 parent 458d73a commit 662e0ad
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 25 deletions.
15 changes: 11 additions & 4 deletions lib/neo4j-wrapper/has_n/class_methods.rb
Expand Up @@ -53,14 +53,21 @@ def inherited(klass)
# has_one(:folder).from(FolderNode, :files)
# end
#
# @example Using Cypher
# # from FolderNode example above
# folder.files.query{ cypher query DSL, see neo4j-core}
# folder.files{ } # same as above
# folder.files.query(:name => 'file.txt') # a cypher query with WHERE and statements
# folder.files(:name => 'file.txt') # same as above
# folder.files.query.to_s # the cypher query explained as a String
#
# @return [Neo4j::Wrapper::HasN::DeclRel] a DSL object where the has_n relationship can be futher specified
# @return [Neo4j::Wrapper::HasN::DeclRel] a DSL object where the has_n relationship can be further specified
def has_n(rel_type)
clazz = self
module_eval(%Q{
def #{rel_type}
def #{rel_type}(cypher_hash_query = nil, &cypher_block)
dsl = _decl_rels_for('#{rel_type}'.to_sym)
Neo4j::Wrapper::HasN::Nodes.new(self, dsl)
Neo4j::Wrapper::HasN::Nodes.new(self, dsl, cypher_hash_query, &cypher_block)
end}, __FILE__, __LINE__)


Expand Down Expand Up @@ -107,7 +114,7 @@ def has_one(rel_type)
end}, __FILE__, __LINE__)

module_eval(%Q{def #{rel_type}
dsl = _decl_rels_for(:#{rel_type})
dsl = _decl_rels_for('#{rel_type}'.to_sym)
dsl.single_node(self)
end}, __FILE__, __LINE__)

Expand Down
33 changes: 31 additions & 2 deletions lib/neo4j-wrapper/has_n/nodes.rb
Expand Up @@ -9,15 +9,40 @@ class Nodes
include Enumerable
include Neo4j::Core::ToJava

def initialize(node, decl_rel) # :nodoc:
def initialize(node, decl_rel, cypher_query_hash = nil, &cypher_block) # :nodoc:
@node = node
@decl_rel = decl_rel
@cypher_block = cypher_block
@cypher_query_hash = cypher_query_hash

rule_node = Neo4j::Wrapper::Rule::Rule.rule_node_for(@decl_rel.target_class)

singelton = class << self;
self;
end
rule_node && rule_node.rules.each do |rule|
next if rule.rule_name == :all
singelton.send(:define_method, rule.rule_name) do |*cypher_query_hash, &cypher_block|

proc = Proc.new do |m|
r0 = m.incoming(:dangerous)
if cypher_block
self.instance_exec(m, &cypher_block)
end
end
query(cypher_query_hash.first, &proc)
end
end
end

def to_s
"HasN::Nodes [#{@decl_rel.dir}, id: #{@node.neo_id} type: #{@decl_rel && @decl_rel.rel_type} decl_rel:#{@decl_rel}]"
end

def query(cypher_query_hash = nil, &block)
Neo4j::Core::Traversal::CypherQuery.new(@node.neo_id, @decl_rel.dir, [@decl_rel.rel_type], cypher_query_hash, &block)
end

# Traverse the relationship till the index position
# @return [Neo4j::NodeMixin,Neo4j::Node,nil] the node at the given position
def [](index)
Expand All @@ -35,7 +60,11 @@ def is_a?(type)

# Required by the Enumerable mixin.
def each
@decl_rel.each_node(@node) { |n| yield n } # Should use yield here as passing &block through doesn't always work (why?)
if @cypher_block || @cypher_query_hash
query(@cypher_query_hash, &@cypher_block).each { |i| yield i }
else
@decl_rel.each_node(@node) { |n| yield n } # Should use yield here as passing &block through doesn't always work (why?)
end
end

# returns none wrapped nodes, you may get better performance using this method
Expand Down
3 changes: 3 additions & 0 deletions lib/neo4j-wrapper/node_mixin/delegates.rb
Expand Up @@ -134,6 +134,9 @@ def #{method_name}(*args, &block)

# @macro node.delegate
delegate :getId

# @macro node.delegate
delegate :getRelationships
end
end
end
Expand Down
6 changes: 3 additions & 3 deletions lib/neo4j-wrapper/rule/class_methods.rb
Expand Up @@ -102,9 +102,9 @@ def rule(rule_name, props = {}, &block)
end

# define class methods
singleton.send(:define_method, rule_name) do
singleton.send(:define_method, rule_name) do |*args, &cypher_block|
rule_node = Rule.rule_node_for(self)
rule_node.traversal(rule_name)
rule_node.traversal(rule_name, args.first, &cypher_block)
end unless respond_to?(rule_name)

# define instance methods
Expand All @@ -129,7 +129,7 @@ def ref_node_for_class
end

# Assigns the reference node for a class via a supplied block.
# Example of usage:
# @example usage:
# class Person
# include Neo4j::NodeMixin
# ref_node { Neo4j.default_ref_node }
Expand Down
1 change: 1 addition & 0 deletions lib/neo4j-wrapper/rule/neo4j_core_ext/traverser.rb
Expand Up @@ -12,6 +12,7 @@ def filter_method(name, &proc)
self
end


def functions_method(func, rule_node, rule_name)
singelton = class << self;
self;
Expand Down
20 changes: 12 additions & 8 deletions lib/neo4j-wrapper/rule/rule_node.rb
Expand Up @@ -97,17 +97,21 @@ def remove_rule(rule_name)

# Return a traversal object with methods for each rule and function.
# E.g. Person.all.old or Person.all.sum(:age)
def traversal(rule_name)
def traversal(rule_name, cypher_query_hash = nil, &cypher_block)
traversal = rule_node.outgoing(rule_name)
@rules.each do |rule|
traversal.filter_method(rule.rule_name) do |path|
path.end_node.rel?(:incoming, rule.rule_name)
end
rule.functions && rule.functions.each do |func|
traversal.functions_method(func, self, rule_name)
if cypher_query_hash || cypher_block
traversal.query(cypher_query_hash, &cypher_block)
else
@rules.each do |rule|
traversal.filter_method(rule.rule_name) do |path|
path.end_node.rel?(:incoming, rule.rule_name)
end
rule.functions && rule.functions.each do |func|
traversal.functions_method(func, self, rule_name)
end
end
traversal
end
traversal
end

def find_function(rule_name, function_name, function_id)
Expand Down
2 changes: 1 addition & 1 deletion neo4j-wrapper.gemspec
Expand Up @@ -27,5 +27,5 @@ It comes included with the Apache Lucene document database.
s.extra_rdoc_files = %w( README.rdoc )
s.rdoc_options = ["--quiet", "--title", "Neo4j.rb", "--line-numbers", "--main", "README.rdoc", "--inline-source"]

s.add_dependency("neo4j-core", "0.0.9")
s.add_dependency("neo4j-core", "0.0.10")
end
8 changes: 3 additions & 5 deletions spec/neo4j-wrapper/has_n/nodes_spec.rb
Expand Up @@ -8,11 +8,9 @@
end

let(:target_class) do
Class.new do
def self.to_s
"TargetClass"
end
end
klass = Class.new
TempModel.setup(klass)
klass
end

let(:decl_rel) do
Expand Down
107 changes: 107 additions & 0 deletions spec/neo4j/rule/rule_cypher_integration_spec.rb
@@ -0,0 +1,107 @@
require 'spec_helper'

class Monster
include Neo4j::NodeMixin
property :age
rule :all
rule(:dangerous) { |m| m[:strength] > 15 }
end

class Dungeon
include Neo4j::NodeMixin
has_n(:monsters).to(Monster)
end

class Room
include Neo4j::NodeMixin
has_n(:monsters).to(Monster)
end

describe "cypher queries for and has_n", :type => :integration do

before(:all) do
new_tx
@basilisk = Monster.new(:strength => 17, :name => 'Basilisk')
@bugbear = Monster.new(:strength => 13, :name => 'Bugbear')
@ghost = Monster.new(:strength => 10, :name => 'Ghost')

@treasure_room = Room.new(:name => 'Treasure Room')
@guard_room = Room.new(:name => 'Guard Room')

@dungeon = Dungeon.new
@dungeon.monsters << @basilisk << @bugbear << @ghost

@treasure_room.monsters << @basilisk
@guard_room.monsters << @bugbear << @ghost
finish_tx
end

describe "dungeon.monsters(:name => 'Ghost', :strength => 10)" do
it "uses cypher" do
@dungeon.monsters(:name => 'Ghost', :strength => 10).first[:strength].should == 10
end
end

describe "dungeon.monsters.query(:name => 'Ghost', :strength => 10)" do
it "uses cypher" do
@dungeon.monsters.query(:name => 'Ghost', :strength => 10).first[:strength].should == 10
end
end

describe "dungeon.monsters{|m| m > 8}" do
it "uses cypher" do
@dungeon.monsters { |m| m[:strength] > 16 }.first[:strength].should == 17
end
end

describe "dungeon.monsters{|m| m.incoming(Room.monsters}[:name] == 'Treasure Room'" do
it "uses cypher" do
@dungeon.monsters { |m| (m.incoming(Room.monsters)[:name] == 'Guard Room') & (m[:strength] > 12) }.first.should == @bugbear
# Same as (!)
# START n0=node(6) MATCH (n0)-[:`Dungeon#monsters`]->(default_ret),(default_ret)<-[:`Room#monsters`]-(v1) WHERE (v1.name = "Guard Room") and (default_ret.strength > 12) RETURN default_ret'
end
end

describe "Monster.all(:strength => 17)" do
it "uses cypher " do
Monster.all(:strength => 17).first.should == @basilisk
#puts "RET #{Monster.all(:strength => 17).class}"
#Monster.all(:strength => 17){|x| x.distinct}.to_a.size.should == 1
#Monster.dangerous(:strength => 17).to_a.size.should == 1
#Monster.dangerous.query.count.should == 1
end

it "can explain the cypher query as a String" do
rule_node = Neo4j::Wrapper::Rule::Rule.rule_node_for(Monster)
id = rule_node.rule_node.neo_id
Monster.all.query(:strength => 17).to_s.should == "START n0=node(#{id}) MATCH (n0)-[:`all`]->(default_ret) WHERE default_ret.strength = 17 RETURN default_ret"
end

end

describe "dungeon.monsters.dangerous" do
it "uses cypher" do
@dungeon.monsters.dangerous.to_a.size.should == 1
end
end

describe "dungeon.monsters.dangerous{|m| m[:weapon?] == 'sword']}" do
it "uses cypher" do
@dungeon.monsters.dangerous { |m| m[:weapon?] == 'sword' } == 1
end

it "can be explained" do
id = @dungeon.neo_id
@dungeon.monsters.dangerous { |m| m[:weapon?] == 'sword' }.to_s.should == "START n0=node(#{id}) MATCH (n0)-[:`Dungeon#monsters`]->(default_ret),(default_ret)<-[:`dangerous`]-(v1) WHERE default_ret.weapon? = \"sword\" RETURN default_ret"
end
end


describe "return a different relationship: @dungeon.monsters.dangerous { |m| rooms = m.incoming(Room.monsters); rooms} " do
it "uses cypher" do
# In which rooms are the dangerous monsters ?
@dungeon.monsters.dangerous { |m| rooms = m.incoming(Room.monsters); rooms }.first.should == @treasure_room
end
end

end
4 changes: 2 additions & 2 deletions spec/spec_helper.rb
Expand Up @@ -94,15 +94,15 @@ def new(base_class, mixin, &block)
klass
end

def setup(klass, mixin)
def setup(klass, mixin=nil)
name = "TestClass_#{@@_counter}"
@@_counter += 1
klass.class_eval <<-RUBY
def self.to_s
"#{name}"
end
RUBY
klass.send(:include, mixin) unless klass.kind_of?(mixin)
klass.send(:include, mixin) unless mixin.nil? || klass.kind_of?(mixin)
Kernel.const_set(name, klass)
klass
end
Expand Down

0 comments on commit 662e0ad

Please sign in to comment.