module Neo4j
module Rails
# Defines class methods, see {ClassMethods}
module HasN
extend ActiveSupport::Concern
module ClassMethods
# Create a number of methods similar to active record has_many.
# The first one returns an {Neo4j::Rails::Relationships::NodesDSL}
# the second generate method (with the _rels postfix) returns a
# {Neo4j::Rails::Relationships::RelsDSL}
#
# See also Neo4j::NodeMixin#has_n which only work with persisted relationships.
#
# @example
# class Thing < Neo4j::Rails::Model
# has_n(:things)
# end
#
# t = Thing.new
# t.things << Thing.new << OtherClass.new
# t.save # saves all nodes and relationships
#
# @example declare a to relationship
# class Company
# has_n(:employees).to(Person)
# # alternative - has_n(:employees).to("Person")
# end
#
# c = Company.new
# c.employees << Person.new << Person.new(:name => 'kalle')
#
# @example creates a new person and relationship
# c.employees.build(:name => 'sune')
#
# @example creates a new person and relationship and persist it
# c.employees.create(:name => 'sune')
#
# @example delete all nodes and relationships
# c.employees.destroy_all
#
# @example access the relationships and destroy them
# c.employees_rels.destroy_all
#
# @example advanced traversal, using Neo4j::Core::Traversal
# c._outgoing(Company.employees).outgoing(:friends).depth.each{ }
#
# @example declare incoming
# class Person < Neo4j::Rails::Model
# has_n(:likes).to(Song)
# end
#
# class Song < Neo4j::Rails::Model
# has_n(:people_liking_this_song).from(Person.likes) # or from(Person, likes)
# end
#
# Song.people_liking_this_song.build # creates a Person
#
# @see Neo4j::Rails::Relationships::NodesDSL
def has_n(*args)
options = args.extract_options!
define_has_n_methods_for(args.first, options)
end
alias_method :has_many, :has_n
# Declares ONE incoming or outgoing relationship
#
# @example
# class Person
# has_one(:friend).to(OtherClass)
# has_one(:root).from("SomeOtherClassAsString")
# end
#
# @example
# person.best_friend = my_friend
#
# @example
# person.best_friend # => my_friend
#
# @example
# person.build_best_friend(:name => 'foo')
#
# @example
# person.create_best_friend(:name => 'foo')
#
# @example
# person.create_best_friend!(:name => 'foo')
#
# @notice when using the build_ and create_ methods you must specify a to relationship as done above in the Person example
def has_one(*args)
options = args.extract_options!
define_has_one_methods_for(args.first, options)
end
protected
def define_has_one_methods_for(rel_type, options)
unless method_defined?(rel_type)
class_eval <<-RUBY, __FILE__, __LINE__
def #{rel_type}
dsl = _decl_rels_for(:'#{rel_type}')
storage = _create_or_get_storage_for_decl_rels(dsl)
storage.single_node(dsl.dir)
end
RUBY
end
unless method_defined?("#{rel_type}_rel")
class_eval <<-RUBY, __FILE__, __LINE__
def #{rel_type}_rel
dsl = _decl_rels_for(:'#{rel_type}')
storage = _create_or_get_storage_for_decl_rels(dsl)
storage.single_relationship(dsl.dir)
end
RUBY
end
unless method_defined?("#{rel_type}=")
class_eval <<-RUBY, __FILE__, __LINE__
def #{rel_type}=(other)
dsl = _decl_rels_for(:'#{rel_type}')
storage = _create_or_get_storage_for_decl_rels(dsl)
storage.destroy_single_relationship(dsl.dir)
storage.create_relationship_to(other, dsl.dir) if other
end
RUBY
end
unless method_defined?("build_#{rel_type}".to_sym)
class_eval <<-RUBY, __FILE__, __LINE__
def build_#{rel_type}(attr)
dsl = _decl_rels_for(:'#{rel_type}')
storage = _create_or_get_storage_for_decl_rels(dsl)
NodesDSL.new(storage, dsl.dir).build(attr)
end
RUBY
end
unless method_defined?("create_#{rel_type}".to_sym)
class_eval <<-RUBY, __FILE__, __LINE__
def create_#{rel_type}(attr)
dsl = _decl_rels_for(:'#{rel_type}')
storage = _create_or_get_storage_for_decl_rels(dsl)
NodesDSL.new(storage, dsl.dir).create(attr)
end
RUBY
end
unless method_defined?("create_#{rel_type}!".to_sym)
class_eval <<-RUBY, __FILE__, __LINE__
def create_#{rel_type}!(attr)
dsl = _decl_rels_for(:'#{rel_type}')
storage = _create_or_get_storage_for_decl_rels(dsl)
NodesDSL.new(storage, dsl.dir).create!(attr)
end
RUBY
end
instance_eval <<-RUBY, __FILE__, __LINE__
def #{rel_type}
_decl_rels[:'#{rel_type}'].rel_type.to_sym
end
RUBY
_decl_rels[rel_type.to_sym] = Neo4j::Wrapper::HasN::DeclRel.new(rel_type, true, self)
end
def define_has_n_methods_for(rel_type, options) #:nodoc:
unless method_defined?(rel_type)
class_eval <<-RUBY, __FILE__, __LINE__
def #{rel_type}(cypher_hash_query = nil, &cypher_block)
dsl = _decl_rels_for(:'#{rel_type}')
if cypher_hash_query || cypher_block
raise "Expected a hash, can't translated to cypher where statements" if cypher_hash_query && !cypher_hash_query.is_a?(Hash)
Neo4j::Wrapper::HasN::Nodes.new(self, dsl, cypher_hash_query, &cypher_block)
else
storage = _create_or_get_storage_for_decl_rels(dsl)
NodesDSL.new(storage, dsl.dir).tap do |n|
Neo4j::Wrapper::HasN::Nodes.define_rule_methods_on(n, dsl)
end
end
end
RUBY
end
unless method_defined?("#{rel_type}=".to_sym)
# TODO: This is a temporary fix for allowing running neo4j with Formtastic, issue 109
# A better solution might be to implement accept_ids for has_n relationship and
# make sure (somehow) that Formtastic uses the _ids methods.
class_eval <<-RUBY, __FILE__, __LINE__
def #{rel_type}=(nodes)
if nodes.is_a?(Array) && nodes.first.is_a?(String)
if nodes.first.blank?
self.#{rel_type}_rels.destroy_all
nodes.shift
end
else
self.#{rel_type}_rels.destroy_all
end
association = self.#{rel_type}
nodes.each { |node| association << node }
end
RUBY
end
unless method_defined?("#{rel_type}_rels".to_sym)
class_eval <<-RUBY, __FILE__, __LINE__
def #{rel_type}_rels
dsl = _decl_rels_for(:'#{rel_type}')
storage = _create_or_get_storage_for_decl_rels(dsl)
RelsDSL.new(storage, dsl.dir)
end
RUBY
end
instance_eval <<-RUBY, __FILE__, __LINE__
def #{rel_type}
_decl_rels[:'#{rel_type}'].rel_type.to_sym
end
RUBY
_decl_rels[rel_type.to_sym] = Neo4j::Wrapper::HasN::DeclRel.new(rel_type, false, self)
end
end
end
end
end