diff --git a/Gemfile.lock b/Gemfile.lock index 3d467a8..20412dd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - neography (0.0.4) + neography (0.0.5) httparty (~> 0.6.1) json diff --git a/README.rdoc b/README.rdoc index 329cb55..e46350f 100644 --- a/README.rdoc +++ b/README.rdoc @@ -157,6 +157,8 @@ The Neo4j ID is available by using node.neo_id . new_rel = Neography::Relationship.create(:family, n1, n2) # Create a relationship from my_node to node2 new_rel.start_node # Get the start/from node of a relationship new_rel.end_node # Get the end/to node of a relationship + new_rel.other_node(n2) # Get the other node of a relationship + existing_rel = Neography::Relationship.load(12) # Get an existing relationship by id existing_rel.del # Delete a relationship @@ -173,6 +175,14 @@ The Neo4j ID is available by using node.neo_id . n1.incoming(:friends) # Get nodes related by incoming friends relationship n1.both(:friends) # Get nodes related by friends relationship + n1.outgoing(:friends).incoming(:enemies) # Get nodes related by one of multiple relationships + n1.outgoing(:friends).depth(2) # Get nodes related by friends and friends of friends + n1.outgoing(:friends).depth(:all) # Get nodes related by friends until the end of the graph + n1.outgoing(:friends).depth(2).include_start_node # Get n1 and nodes related by friends and friends of friends + + n1.outgoing(:friends).prune("position.endNode().getProperty('name') == 'Tom';") + n1.outgoing(:friends).filter("position.length() == 2;") + n1.rel?(:friends) # Has a friends relationship n1.rel?(:outgoing, :friends) # Has outgoing friends relationship n1.rel?(:friends, :outgoing) # same, just the other way @@ -181,8 +191,12 @@ The Neo4j ID is available by using node.neo_id . n1.rel?(:all) # same as above n1.rel? # same as above - - + n1.rels # Get node relationships + n1.rels(:friends) # Get friends relationships + n1.rels(:friends).outgoing # Get outgoing friends relationships + n1.rels(:friends).incoming # Get incoming friends relationships + n1.rels(:friends,:work) # Get friends and work relationships + n1.rels(:friends,:work).outgoing # Get outgoing friends and work relationships See Neo4j API for: * {Order}[http://components.neo4j.org/neo4j-examples/1.2.M04/apidocs/org/neo4j/graphdb/Traverser.Order.html] @@ -199,12 +213,11 @@ A couple of examples borrowed from Matthew Deiters's Neo4jr-social === To Do -* More tests -* More examples -* batch import with typhoeus ? -* create proper objects for Node and Relationship +* More Tests +* More Examples +* Mixins ? -=== License +=== Licenses * Neography - MIT, see the LICENSE file http://github.com/maxdemarzi/neography/tree/master/LICENSE. * Lucene - Apache, see http://lucene.apache.org/java/docs/features.html diff --git a/lib/neography/node_relationship.rb b/lib/neography/node_relationship.rb index 4938f80..955f78a 100644 --- a/lib/neography/node_relationship.rb +++ b/lib/neography/node_relationship.rb @@ -2,27 +2,15 @@ module Neography module NodeRelationship def outgoing(types=nil) - if types - NodeTraverser.new(self).outgoing(types) - else - NodeTraverser.new(self).outgoing(types).collect {|n| n} - end + NodeTraverser.new(self).outgoing(types) end def incoming(types=nil) - if types - NodeTraverser.new(self).incoming(types) - else - NodeTraverser.new(self).incoming(types).collect {|n| n} - end + NodeTraverser.new(self).incoming(types) end def both(types=nil) - if types - NodeTraverser.new(self).both(types) - else - NodeTraverser.new(self).both(types).collect {|n| n} - end + NodeTraverser.new(self).both(types) end def rels(*types) @@ -30,7 +18,9 @@ def rels(*types) end def rel(dir, type) - Neography::RelationshipTraverser.new(self, type, dir).first + rel = Neography::RelationshipTraverser.new(self, type, dir) + rel = rel.first unless rel.nil? + rel end def rel?(dir=nil, type=nil) diff --git a/lib/neography/node_traverser.rb b/lib/neography/node_traverser.rb index 3a620a1..cb31176 100644 --- a/lib/neography/node_traverser.rb +++ b/lib/neography/node_traverser.rb @@ -48,28 +48,28 @@ def incoming(type) self end - def filter(language, name) - @filter["language"] = language - if language == "builtin" - @filter["name"] = name - else - @filter["body"] = name - end + def filter(body) + @filter = Hash.new + @filter["language"] = "javascript" + @filter["body"] = body self end - def prune(language, body) - @prune["language"] = language + def prune(body) + @prune = Hash.new + @prune["language"] = "javascript" @prune["body"] = body self end def depth(d) + d = 2147483647 if d == :all @depth = d self end def include_start_node + @filter = Hash.new @filter["language"] = "builtin" @filter["name"] = "all" self @@ -110,9 +110,9 @@ def iterator rels = @from.neo_server.get_node_relationships(@from, @relationships[0]["direction"]) case @relationships[0]["direction"] when "in" - rels.collect { |r| @from.neo_server.get_node(r["start"]) }.uniq + rels.collect { |r| @from.neo_server.get_node(r["start"]) } #.uniq when "out" - rels.collect { |r| @from.neo_server.get_node(r["end"]) }.uniq + rels.collect { |r| @from.neo_server.get_node(r["end"]) } #.uniq else rels.collect { |r| if @from.neo_id == r["start"].split('/').last @@ -120,7 +120,7 @@ def iterator else @from.neo_server.get_node(r["start"]) end - }.uniq + } #.uniq end else @from.neo_server.traverse(@from, "nodes", options) diff --git a/lib/neography/relationship.rb b/lib/neography/relationship.rb index 005f1af..32c6fd7 100644 --- a/lib/neography/relationship.rb +++ b/lib/neography/relationship.rb @@ -42,8 +42,12 @@ def del self.start_node.neo_server.delete_relationship(self.neo_id) end + def exist? + !self.start_node.neo_server.get_relationship(self.neo_id).nil? + end + def other_node(node) - if node = @start_node + if node == @start_node @end_node else @start_node diff --git a/lib/neography/relationship_traverser.rb b/lib/neography/relationship_traverser.rb index 4baa89a..448cb51 100644 --- a/lib/neography/relationship_traverser.rb +++ b/lib/neography/relationship_traverser.rb @@ -19,15 +19,38 @@ def to_s end def each - iterator.each { |i| yield Neography::Relationship.new(i, @node.neo_server) } + iterator.each do |i| + rel = Neography::Relationship.new(i, @node.neo_server) + rel.start_node = Neography::Node.load(rel.start_node) + rel.end_node = Neography::Node.load(rel.end_node) + + yield rel if match_to_other?(rel) + end end def empty? first == nil end - + def iterator - @node.neo_server.get_node_relationships(@node, @direction, @types) + Array(@node.neo_server.get_node_relationships(@node, @direction, @types)) + end + + def match_to_other?(rel) + if @to_other.nil? + true + elsif @direction == :outgoing + rel.end_node == @to_other + elsif @direction == :incoming + rel.start_node == @to_other + else + rel.start_node == @to_other || rel.end_node == @to_other + end + end + + def to_other(to_other) + @to_other = to_other + self end def del @@ -44,13 +67,11 @@ def both end def incoming - raise "Not allowed calling incoming when finding several relationships types" if @types @direction = :incoming self end def outgoing - raise "Not allowed calling outgoing when finding several relationships types" if @types @direction = :outgoing self end diff --git a/spec/integration/node_relationship_spec.rb b/spec/integration/node_relationship_spec.rb index 6cee78d..38e1fd8 100644 --- a/spec/integration/node_relationship_spec.rb +++ b/spec/integration/node_relationship_spec.rb @@ -57,7 +57,39 @@ def create_nodes a,b,c,d,e,f = create_nodes b.outgoing.should include(c,f,d) - [*b.outgoing].size.should == 3 + [*b.outgoing].size.should == 4 #c is related by both work and friends + end + + it "#outgoing(type) should only return outgoing nodes of the given type of depth one" do + a,b,c,d = create_nodes + b.outgoing(:work).should include(c,d) + [*b.outgoing(:work)].size.should == 2 + end + + it "#outgoing(type1).outgoing(type2) should return outgoing nodes of the given types" do + a,b,c,d,e,f = create_nodes + nodes = b.outgoing(:work).outgoing(:friends) + + nodes.should include(c,d,f) + nodes.size.should == 4 #c is related by both work and friends + end + + it "#outgoing(type).depth(4) should only return outgoing nodes of the given type and depth" do + a,b,c,d,e = create_nodes + [*b.outgoing(:work).depth(4)].size.should == 3 + b.outgoing(:work).depth(4).should include(c,d,e) + end + + it "#outgoing(type).depth(4).include_start_node should also include the start node" do + a,b,c,d,e = create_nodes + [*b.outgoing(:work).depth(4).include_start_node].size.should == 4 + b.outgoing(:work).depth(4).include_start_node.should include(b,c,d,e) + end + + it "#outgoing(type).depth(:all) should traverse at any depth" do + a,b,c,d,e = create_nodes + [*b.outgoing(:work).depth(:all)].size.should == 3 + b.outgoing(:work).depth(:all).should include(c,d,e) end end @@ -91,6 +123,18 @@ def create_nodes b.incoming.should include(a) [*b.incoming].size.should == 1 end + + it "#incoming(type).depth(2) should only return outgoing nodes of the given type and depth" do + a,b,c,d,e = create_nodes + [*e.incoming(:work).depth(2)].size.should == 2 + e.incoming(:work).depth(2).should include(b,d) + end + + it "#incoming(type) should only return incoming nodes of the given type of depth one" do + a,b,c,d = create_nodes + c.incoming(:work).should include(b) + [*c.incoming(:work)].size.should == 1 + end end describe "both" do @@ -108,29 +152,192 @@ def create_nodes a,b,c,d,e,f = create_nodes b.both.should include(a,c,d,f) - - [*b.both].size.should == 4 + [*b.both].size.should == 5 #c is related by both work and friends b.incoming.should include(a) b.outgoing.should include(c) end + it "#both(type) should return both incoming and outgoing nodes of the given type of depth one" do + a,b,c,d,e,f = create_nodes + + b.both(:friends).should include(a,c,f) + [*b.both(:friends)].size.should == 3 + end + + it "#outgoing and #incoming can be combined to traverse several relationship types" do + a,b,c,d,e = create_nodes + nodes = [*b.incoming(:friends).outgoing(:work)] + + nodes.should include(a,c,d) + nodes.should_not include(b,e) + end + end + + + describe "prune" do + it "#prune, if it returns true the traversal will be stop for that path" do + a, b, c, d, e = create_nodes + [*b.outgoing(:work).depth(4)].size.should == 3 + b.outgoing(:work).depth(4).should include(c,d,e) + + [*b.outgoing(:work).prune("position.endNode().getProperty('name') == 'd';")].size.should == 2 + b.outgoing(:work).prune("position.endNode().getProperty('name') == 'd';").should include(c,d) + end end -# b.both.each { |n| puts n.inspect } + describe "filter" do + it "#filter, if it returns true the node will be included in the return results" do + a, b, c, d, e = create_nodes + [*b.outgoing(:work).depth(4)].size.should == 3 + b.outgoing(:work).depth(4).should include(c,d,e) + [*b.outgoing(:work).depth(4).filter("position.length() == 2;")].size.should == 1 + b.outgoing(:work).depth(4).filter("position.length() == 2;").should include(e) + end + end describe "rels" do - it "" do - pending + it "#rels returns a RelationshipTraverser which can filter which relationship it should return by specifying #to_other" do + a = Neography::Node.create + b = Neography::Node.create + c = Neography::Node.create + r1 = Neography::Relationship.create(:friend, a, b) + Neography::Relationship.create(:friend, a, c) + + a.rels.to_other(b).size.should == 1 + a.rels.to_other(b).should include(r1) + end + + it "#rels returns an RelationshipTraverser which provides a method for deleting all the relationships" do + a = Neography::Node.create + b = Neography::Node.create + c = Neography::Node.create + r1 = Neography::Relationship.create(:friend, a, b) + r2 = Neography::Relationship.create(:friend, a, c) + + a.rel?(:friend).should be_true + a.rels.del + a.rel?(:friend).should be_false + r1.exist?.should be_false + r2.exist?.should be_false + end + + it "#rels returns an RelationshipTraverser with methods #del and #to_other which can be combined to only delete a subset of the relationships" do + a = Neography::Node.create + b = Neography::Node.create + c = Neography::Node.create + r1 = Neography::Relationship.create(:friend, a, b) + r2 = Neography::Relationship.create(:friend, a, c) + r1.exist?.should be_true + r2.exist?.should be_true + a.rels.to_other(c).del + r1.exist?.should be_true + r2.exist?.should be_false + end + + it "#rels should return both incoming and outgoing relationship of any type of depth one" do + a,b,c,d,e,f = create_nodes + b.rels.size.should == 5 + nodes = b.rels.collect{|r| r.other_node(b)} + nodes.should include(a,c,d,f) + nodes.should_not include(e) + end + + it "#rels(:friends) should return both incoming and outgoing relationships of given type of depth one" do + # given + a,b,c,d,e,f = create_nodes + + # when + rels = [*b.rels(:friends)] + + # then + rels.size.should == 3 + nodes = rels.collect{|r| r.end_node} + nodes.should include(b,c,f) + nodes.should_not include(a,d,e) + end + + it "#rels(:friends).outgoing should return only outgoing relationships of given type of depth one" do + # given + a,b,c,d,e,f = create_nodes + + # when + rels = [*b.rels(:friends).outgoing] + + # then + rels.size.should == 2 + nodes = rels.collect{|r| r.end_node} + nodes.should include(c,f) + nodes.should_not include(a,b,d,e) + end + + + it "#rels(:friends).incoming should return only outgoing relationships of given type of depth one" do + # given + a,b,c,d,e = create_nodes + + # when + rels = [*b.rels(:friends).incoming] + + # then + rels.size.should == 1 + nodes = rels.collect{|r| r.start_node} + nodes.should include(a) + nodes.should_not include(b,c,d,e) + end + + it "#rels(:friends,:work) should return both incoming and outgoing relationships of given types of depth one" do + # given + a,b,c,d,e,f = create_nodes + + # when + rels = [*b.rels(:friends,:work)] + + # then + rels.size.should == 5 + nodes = rels.collect{|r| r.other_node(b)} + nodes.should include(a,c,d,f) + nodes.should_not include(b,e) + end + + it "#rels(:friends,:work).outgoing should return outgoing relationships of given types of depth one" do + # given + a,b,c,d,e,f = create_nodes + + # when + rels = [*b.rels(:friends,:work).outgoing] + + # then + rels.size.should == 4 + nodes = rels.collect{|r| r.other_node(b)} + nodes.should include(c,d,f) + nodes.should_not include(a,b,e) end end describe "rel" do - it "" do - pending + it "#rel returns a single relationship if there is only one relationship" do + a = Neography::Node.create + b = Neography::Node.create + rel = Neography::Relationship.create(:friend, a, b) + a.rel(:outgoing, :friend).should == rel + end + + it "#rel returns nil if there is no relationship" do + a = Neography::Node.create + b = Neography::Node.create + a.rel(:outgoing, :friend).should be_nil end - end + it "#rel should only return one relationship even if there are more" do + a = Neography::Node.create + b = Neography::Node.create + c = Neography::Node.create + Neography::Relationship.create(:friend, a, b) + Neography::Relationship.create(:friend, a, c) + [*a.rel(:outgoing, :friend)].size == 1 + end + end describe "rel?" do it "#rel? returns true if there are any relationships" do