Skip to content

Commit

Permalink
Merge pull request #406 from andreasronge/query_experiment
Browse files Browse the repository at this point in the history
An experiment no more
  • Loading branch information
andreasronge committed Aug 14, 2014
2 parents 624936b + 1dfbc5e commit 1209891
Show file tree
Hide file tree
Showing 24 changed files with 1,308 additions and 1,157 deletions.
3 changes: 1 addition & 2 deletions lib/neo4j.rb
Expand Up @@ -28,11 +28,10 @@
require 'neo4j/active_node/validations'
require 'neo4j/active_node/rels'
require 'neo4j/active_node/has_n'
require 'neo4j/active_node/has_n/decl_rel'
require 'neo4j/active_node/has_n/association'
require 'neo4j/active_node/has_n/nodes'
require 'neo4j/active_node/query/query_proxy'
require 'neo4j/active_node/query'
require 'neo4j/active_node/quick_query'
require 'neo4j/active_node/serialized_properties'
require 'neo4j/paginated'
require 'neo4j/active_node'
Expand Down
2 changes: 1 addition & 1 deletion lib/neo4j/active_node.rb
Expand Up @@ -35,12 +35,12 @@ module ActiveNode
include Neo4j::ActiveNode::Persistence
include Neo4j::ActiveNode::SerializedProperties
include Neo4j::ActiveNode::Property
include Neo4j::ActiveNode::Query
include Neo4j::ActiveNode::Labels
include Neo4j::ActiveNode::Validations
include Neo4j::ActiveNode::Callbacks
include Neo4j::ActiveNode::Rels
include Neo4j::ActiveNode::HasN
include Neo4j::ActiveNode::Query

def wrapper
self
Expand Down
188 changes: 64 additions & 124 deletions lib/neo4j/active_node/has_n.rb
Expand Up @@ -2,155 +2,95 @@ module Neo4j::ActiveNode
module HasN
extend ActiveSupport::Concern

def _decl_rels_for(rel_type)
self.class._decl_rels[rel_type]
end

module ClassMethods


def has_relationship?(rel_type)
!!_decl_rels[rel_type]
end

def has_one_relationship?(rel_type)
has_relationship?(rel_type) && _decl_rels[rel_type].has_one?
end

def relationship_dir(rel_type)
has_relationship?(rel_type) && _decl_rels[rel_type].dir
def has_association?(name)
!!associations[name]
end

def _decl_rels
@_decl_rels ||= {}
def associations
@associations || {}
end

# make sure the inherited classes inherit the <tt>_decl_rels</tt> hash
def inherited(klass)
copy = _decl_rels.clone
copy.each_pair { |k, v| copy[k] = v.inherit_new }
klass.instance_variable_set(:@_decl_rels, copy)
klass.instance_variable_set(:@associations, associations.clone)

super
end

def has_many(direction, name, options = {})
name = name.to_sym

# Specifies a relationship between two node active node classes.
# Generates assignment and accessor methods for the given relationship.
# Both incoming and outgoing relationships can be declared, see {Neo4j::ActiveNode::HasN::DeclRel}
#
# @example has_n(:files)
#
# class FolderNode
# include Neo4j::ActiveNode
# has_n(:files)
# end
#
# folder = FolderNode.new
# folder.files << Neo4j::Node.new << Neo4j::Node.new
# folder.files.inject {...}
#
# FolderNode.files #=> 'files' the name of the relationship
#
# @example has_n(x).to(...)
#
# # You can declare which class it has relationship to.
# # The generated relationships will be prefixed with the name of that class.
# class FolderNode
# include Neo4j::ActiveNode
# has_n(:files).to(File)
# # Same as has_n(:files).to("File")
# end
#
# FolderNode.files #=> 'File#files' the name of the relationship
#
# @example has_one(x).from(class, has_one_name)
#
# # generate accessor method for traversing and adding relationship on incoming nodes.
# class FileNode
# include Neo4j::ActiveNode
# has_one(:folder).from(FolderNode.files)
# # or same as
# has_one(:folder).from(FolderNode, :files)
# end
#
#
# @return [Neo4j::ActiveNode::HasN::DeclRel] a DSL object where the has_n relationship can be further specified
def has_n(rel_type, *callbacks)
clazz = self
module_eval(%Q{def #{rel_type}=(values)
#{rel_type}_rels.each {|rel| rel.del }
dsl = _decl_rels_for('#{rel_type}'.to_sym)
values.each do |value|
dsl.create_relationship_to(self, value)
end
end}, __FILE__, __LINE__)
association = Neo4j::ActiveNode::HasN::Association.new(:has_many, direction, name, options)
name = name.to_sym

module_eval(%Q{
def #{rel_type}()
dsl = _decl_rels_for('#{rel_type}'.to_sym)
Neo4j::ActiveNode::HasN::Nodes.new(self, dsl)
end}, __FILE__, __LINE__)
@associations ||= {}
@associations[name] = association

target_class_name = association.target_class_name || 'nil'

# TODO: Make assignment more efficient? (don't delete nodes when they are being assigned)
module_eval(%Q{
def #{rel_type}_rels
dsl = _decl_rels_for('#{rel_type}'.to_sym)
dsl.all_relationships(self)
end}, __FILE__, __LINE__)
def #{name}(node = nil, rel = nil)
Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class_name}, self.class.associations[#{name.inspect}], session: self.class.neo4j_session, start_object: self, node: node, rel: rel)
end
def #{name}=(other_nodes)
#{name}(nil, :r).query_as(:n).delete(:r).exec
other_nodes.each do |node|
#{name} << node
end
end
def #{name}_rels
#{name}(nil, :r).pluck(:r)
end}, __FILE__, __LINE__)

instance_eval(%Q{
def #{rel_type}
_decl_rels[:#{rel_type}].rel_type
def #{name}(node = nil, rel = nil)
Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class_name}, @associations[#{name.inspect}], session: self.neo4j_session, query_proxy: self.query_proxy, node: node, rel: rel)
end}, __FILE__, __LINE__)
_decl_rels[rel_type.to_sym] = DeclRel.new(rel_type, false, clazz, *callbacks)
end

def has_one(direction, name, options = {})
name = name.to_sym

# Specifies a relationship between two node classes.
# Generates assignment and accessor methods for the given relationship
# Old relationship is deleted when a new relationship is assigned.
# Both incoming and outgoing relationships can be declared, see {Neo4j::Wrapper::HasN::DeclRel}
#
# @example
#
# class FileNode
# include Neo4j::ActiveNode
# has_one(:folder)
# end
#
# file = FileNode.create
# file.folder = Neo4j::Node.create
# file.folder # => the node above
# file.folder_rel # => the relationship object between those nodes
#
# @return [Neo4j::ActiveNode::HasN::DeclRel] a DSL object where the has_one relationship can be futher specified
def has_one(rel_type, *callbacks)
clazz = self
module_eval(%Q{def #{rel_type}=(value)
return if !value
dsl = _decl_rels_for(:#{rel_type})
rel = dsl.single_relationship(self)
rel && rel.del
dsl.create_relationship_to(self, value)
end}, __FILE__, __LINE__)

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

module_eval(%Q{def #{rel_type}_rel
dsl = _decl_rels_for(:#{rel_type})
dsl.single_relationship(self)
end}, __FILE__, __LINE__)
association = Neo4j::ActiveNode::HasN::Association.new(:has_one, direction, name, options)
name = name.to_sym

instance_eval(%Q{
def #{rel_type}
_decl_rels[:#{rel_type}].rel_type
@associations ||= {}
@associations[name] = association

target_class_name = association.target_class_name || 'nil'

module_eval(%Q{
def #{name}=(other_node)
#{name}_query_proxy(rel: :r).query_as(:n).delete(:r).exec
#{name}_query_proxy << other_node
end
def #{name}_query_proxy(options = {})
self.class.#{name}_query_proxy({start_object: self}.merge(options))
end
def #{name}_rel
#{name}_query_proxy(rel: :r).pluck(:r).first
end
def #{name}(node = nil, rel = nil)
#{name}_query_proxy(node: node, rel: rel).first
end}, __FILE__, __LINE__)

_decl_rels[rel_type.to_sym] = DeclRel.new(rel_type, true, clazz, *callbacks)
instance_eval(%Q{
def #{name}_query_proxy(options = {})
Neo4j::ActiveNode::Query::QueryProxy.new(#{target_class_name}, @associations[#{name.inspect}], {session: self.neo4j_session}.merge(options))
end
def #{name}(node = nil, rel = nil)
#{name}_query_proxy(query_proxy: self.query_proxy, node: node, rel: rel)
end}, __FILE__, __LINE__)
end


Expand Down
142 changes: 142 additions & 0 deletions lib/neo4j/active_node/has_n/association.rb
@@ -0,0 +1,142 @@
require 'active_support/inflector/inflections'

module Neo4j
module ActiveNode
module HasN
class Association
attr_reader :type, :name, :relationship, :direction

def initialize(type, direction, name, options = {})
raise ArgumentError, "Invalid association type: #{type.inspect}" if not [:has_many, :has_one].include?(type.to_sym)
raise ArgumentError, "Invalid direction: #{direction.inspect}" if not [:out, :in, :both].include?(direction.to_sym)
@type = type.to_sym
@name = name
@direction = direction.to_sym
raise ArgumentError, "Cannot specify both :type and :origin (#{base_declaration})" if options[:type] && options[:origin]

@target_class_name_from_name = name.to_s.classify
@target_class_option = target_class_option(options)
@callbacks = {before: options[:before], after: options[:after]}
@relationship_type = options[:type] && options[:type].to_sym
@origin = options[:origin] && options[:origin].to_sym
end

def target_class_option(options)
if options[:model_class].nil?
@target_class_name_from_name
elsif options[:model_class]
options[:model_class]
end
end

# Return cypher partial query string for the relationship part of a MATCH (arrow / relationship definition)
def arrow_cypher(var = nil, properties = {}, create = false)
validate_origin!

relationship_type = relationship_type(create)
relationship_name_cypher = ":`#{relationship_type}`" if relationship_type

properties_string = get_properties_string(properties)
relationship_cypher = get_relationship_cypher(var, relationship_name_cypher, properties_string)
get_direction(relationship_cypher, create)
end

def target_class_name
@target_class_option.to_s if @target_class_option
end

def target_class
return @target_class if @target_class

@target_class = target_class_name.constantize if target_class_name
rescue NameError
raise ArgumentError, "Could not find `#{@target_class}` class and no :model_class specified"
end

def callback(type)
@callbacks[type]
end

def perform_callback(caller, other_node, type)
return if callback(type).nil?
caller.send(callback(type), other_node)
end

def relationship_type(create = false)
if @relationship_type
@relationship_type
elsif @origin
origin_type
else
(create || exceptional_target_class?) && "##{@name}"
end
end

private

def get_direction(relationship_cypher, create)
dir = (create && @direction == :both) ? :out : @direction
case dir
when :out
"-#{relationship_cypher}->"
when :in
"<-#{relationship_cypher}-"
when :both
"-#{relationship_cypher}-"
end
end

def get_relationship_cypher(var, relationship_name_cypher, properties_string)
"[#{var}#{relationship_name_cypher}#{properties_string}]"
end

def get_properties_string(properties)
p = properties.map do |key, value|
"#{key}: #{value.inspect}"
end.join(', ')
p.size == 0 ? '' : " {#{p}}"
end

def origin_type
target_class.associations[@origin].relationship_type
end

# Return basic details about association as declared in the model
# @example
# has_many :in, :bands
def base_declaration
"#{type} #{direction.inspect}, #{name.inspect}"
end


# Determine if model class as derived from the association name would be different than the one specified via the model_class key
# @example
# has_many :friends # Would return false
# has_many :friends, model_class: Friend # Would return false
# has_many :friends, model_class: Person # Would return true
def exceptional_target_class?
# TODO: Exceptional if target_class.nil?? (when model_class false)

target_class && target_class.name != @target_class_name_from_name
end

def validate_origin!
if @origin
if target_class
if association = target_class.associations[@origin]
if @direction == association.direction
raise ArgumentError, "Origin `#{@origin.inspect}` (specified in #{base_declaration}) has same direction `#{@direction}`)"
end
else
raise ArgumentError, "Origin `#{@origin.inspect}` association not found for #{target_class} (specified in #{base_declaration})"
end
else
raise ArgumentError, "Cannot use :origin without a model_class (implied or explicit)"
end
end
end
end
end
end
end

0 comments on commit 1209891

Please sign in to comment.