module Neo4j
module ActiveNode
module HasN
# A DSL for declared relationships has_n and has_one
# This DSL will be used to create accessor methods for relationships.
# Instead of using the 'raw' Neo4j::ActiveNode#rels method where one needs to know
# the name of relationship and direction one can use the generated accessor methods.
#
# @example
#
# class Folder
# include Neo4j::ActiveNode
# property :name
# # Declaring a Many relationship to any other node
# has_n(:files)
# end
#
# class File
# include Neo4j::ActiveNode
# # declaring a incoming relationship from Folder's relationship files
# has_one(:folder).from(Folder, :files)
# end
#
# The following methods will be generated:
# Folder#files :: returns an Enumerable of outgoing nodes for relationship 'files'
# Folder#files_rels :: returns an Enumerable of outgoing relationships for relationship 'files'
# File#folder :: for adding one node for the relationship 'files' from the outgoing Folder node
# File#folder_rel :: for accessing relationship 'files' from the outgoing Folder node
# File#folder :: for accessing nodes from relationship 'files' from the outgoing Folder node
#
class DeclRel
attr_reader :source_class, :dir, :rel_type, :method_id
def initialize(method_id, has_one, source_class)
@method_id = method_id
@has_one = has_one
@dir = :outgoing
@rel_type = method_id.to_sym
@source_class = source_class
end
def inherit_new
base = self
dr = DeclRel.new(@method_id, @has_one, @source_class)
dr.instance_eval do
@dir = base.dir
@rel_type = base.rel_type
@target_name = base.target_name if base.target_name
@source_class = base.source_class
end
dr
end
def to_s
"DeclRel one #{has_one?}, dir: #{@dir}, rel_id: #{@method_id}, rel_type: #{@rel_type}, target_class:#{@target_name}"
end
def inspect
to_s
end
# @return [true, false]
def has_one?
@has_one
end
# @return [true, false]
def has_n?
!@has_one
end
# @return [true,false]
def incoming? #:nodoc:
@dir == :incoming
end
# Declares an outgoing relationship type.
# It is possible to prefix relationship types so that it's possible to distinguish different incoming relationships.
# There is no validation that the added node is of the specified class.
#
# @example Example
# class FolderNode
# include Neo4j::ActiveNode
# has_n(:files).to(FileNode)
# has_one(:root).to("FileSystem") # also possible, if the class is not defined yet
# end
#
# folder = FolderNode.new
# # generate a relationship between folder and file of type 'FileNode#files'
# folder.files << FileNode.new
#
# @example relationship with a hash, user defined relationship
#
# class FolderNode
# include Neo4j::ActiveNode
# has_n(:files).to('FolderNode#files')
# end
#
# @example without prefix
#
# class FolderNode
# include Neo4j::ActiveNode
# has_n(:files).to(:contains)
# end
#
# file = FileNode.new
# # create an outgoing relationship of type 'contains' from folder node to file
# folder.files << FolderNode.new
#
# @param [Class, String, Symbol] target the other class to which this relationship goes (if String or Class) or the relationship (if Symbol)
# @param [String, Symbol] rel_type the rel_type postfix for the relationships, which defaults to the same as the has_n/one method id
# @return self
def to(target, rel_type = @method_id)
@dir = :outgoing
case target
when /#/
@target_name, _ = target.to_s.split("#")
@rel_type = target.to_sym
when Class, String
@target_name = target.to_s
@rel_type = "#{@source_class}##{rel_type}".to_sym
when Symbol
@target_name = nil
@rel_type = target.to_sym
else
raise "Expected a class or a symbol for, got #{target}/#{target.class}"
end
self
end
# Specifies an incoming relationship.
# Will use the outgoing relationship given by the from class.
#
# @example with prefix FileNode
# class FolderNode
# include Neo4j::NodeMixin
# has_n(:files).to(FileNode)
# end
#
# class FileNode
# include Neo4j::NodeMixin
# # will only traverse any incoming relationship of type files from node FileNode
# has_one(:folder).from(FolderNode.files)
# # alternative: has_one(:folder).from(FolderNode, :files)
# end
#
# file = FileNode.new
# # create an outgoing relationship of type 'FileNode#files' from folder node to file (FileNode is the prefix).
# file.folder = FolderNode.new
#
# @example without prefix
#
# class FolderNode
# include Neo4j::NodeMixin
# has_n(:files)
# end
#
# class FileNode
# include Neo4j::NodeMixin
# has_one(:folder).from(:files) # will traverse any incoming relationship of type files
# end
#
# file = FileNode.new
# # create an outgoing relationship of type 'files' from folder node to file
# file.folder = FolderNode.new
#
#
def from(target, rel_type=@method_id)
@dir = :incoming
case target
when /#/
@target_name, _ = target.to_s.split("#")
@rel_type = target
when Class, String
@target_name = target.to_s
@rel_type = "#{@target_name}##{rel_type}".to_sym
when Symbol
@target_name = nil
@rel_type = target.to_sym
else
raise "Expected a class or a symbol for, got #{target}/#{target.class}"
end
self
end
# @private
def target_name
@target_name
end
def target_class
@target_name && @target_name.split("::").inject(Kernel) { |container, name| container.const_get(name.to_s) }
end
# @private
def each_node(node, &block)
node.nodes(dir: dir, type: rel_type).each { |n| block.call(n) }
end
def all_relationships(node)
to_enum(:each_rel, node)
end
def each_rel(node, &block) #:nodoc:
node.rels(dir: dir, type: rel_type).each { |rel| block.call(rel) }
end
def single_relationship(node)
node.rel(dir: dir, type: rel_type)
end
def single_node(node)
node.node(dir: dir, type: rel_type)
end
# @private
def create_relationship_to(node, other, relationship_props={}) # :nodoc:
from, to = incoming? ? [other, node] : [node, other]
from.create_rel(@rel_type, to, relationship_props)
end
end
end
end
end