active_node 0.0.2.alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ *.iml
6
+ .idea/*
7
+ neo4j
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ script: "bundle exec rake neo4j:install['enterprise','2.0.0-M03'] neo4j:start spec --trace"
2
+ language: ruby
3
+ rvm:
4
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in active_node.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2013 Heinrich Klobuczek
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rspec/core/rake_task'
4
+ require 'neography/tasks'
5
+
6
+ RSpec::Core::RakeTask.new(:spec) do |t|
7
+ t.rspec_opts = "--color"
8
+ t.pattern = "spec/**/*_spec.rb"
9
+ end
10
+
11
+ desc "Run Tests"
12
+ task :default => :spec
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "active_node/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "active_node"
7
+ s.version = ActiveNode::VERSION
8
+ s.authors = ["Heinrich Klobuczek"]
9
+ s.email = ["heinrich@mail.com"]
10
+ s.homepage = ""
11
+ s.summary = "ActiveRecord style Object Graph Mapping for neo4j"
12
+ s.description = "ActiveRecord style Object Graph Mapping for neo4j"
13
+
14
+ s.rubyforge_project = "active_node"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_dependency "active_attr"
22
+ s.add_dependency "neography"
23
+ s.add_dependency "activesupport"
24
+ s.add_dependency "activemodel"
25
+ s.add_development_dependency "rspec", ">= 2.11"
26
+ s.add_development_dependency "net-http-spy", "0.2.1"
27
+ #s.add_development_dependency "rake", ">= 0.8.7"
28
+ s.add_development_dependency "coveralls"
29
+
30
+ end
@@ -0,0 +1,44 @@
1
+ require 'active_support/core_ext/array/wrap'
2
+
3
+ module ActiveNode
4
+ module Associations
5
+ # = Active Record Associations
6
+ #
7
+ # This is the root class of all associations ('+ Foo' signifies an included module Foo):
8
+ #
9
+ # Association
10
+ # SingularAssociation
11
+ # HasOneAssociation
12
+ # HasOneThroughAssociation + ThroughAssociation
13
+ # BelongsToAssociation
14
+ # BelongsToPolymorphicAssociation
15
+ # CollectionAssociation
16
+ # HasAndBelongsToManyAssociation
17
+ # HasManyAssociation
18
+ # HasManyThroughAssociation + ThroughAssociation
19
+ class Association #:nodoc:
20
+ attr_reader :owner, :target, :reflection
21
+
22
+ delegate :options, :to => :reflection
23
+
24
+ def initialize(owner, reflection)
25
+ @owner, @reflection = owner, reflection
26
+
27
+ reset
28
+ end
29
+
30
+ # Resets the \loaded flag to +false+ and sets the \target to +nil+.
31
+ def reset
32
+ @loaded = false
33
+ @target = nil
34
+ @stale_state = nil
35
+ end
36
+
37
+ # Returns the class of the target. belongs_to polymorphic overrides this to look at the
38
+ # polymorphic_type field on the owner.
39
+ def klass
40
+ reflection.klass
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,70 @@
1
+ module ActiveNode::Associations::Builder
2
+ class Association #:nodoc:
3
+ class << self
4
+ attr_accessor :valid_options
5
+ end
6
+
7
+ self.valid_options = [:class_name]
8
+
9
+ attr_reader :model, :name, :options, :reflection
10
+
11
+ def self.build(*args)
12
+ new(*args).build
13
+ end
14
+
15
+ def initialize(model, name, options)
16
+ raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)
17
+
18
+ @model = model
19
+ @name = name
20
+ @options = options
21
+ end
22
+
23
+ def mixin
24
+ @model
25
+ end
26
+
27
+ include Module.new { def build; end }
28
+
29
+ def build
30
+ validate_options
31
+ define_accessors
32
+ @reflection = model.create_reflection(macro, name, options, model)
33
+ super # provides an extension point
34
+ @reflection
35
+ end
36
+
37
+ def macro
38
+ raise NotImplementedError
39
+ end
40
+
41
+ def valid_options
42
+ Association.valid_options
43
+ end
44
+
45
+ def validate_options
46
+ options.assert_valid_keys(valid_options)
47
+ end
48
+
49
+ def define_accessors
50
+ define_readers
51
+ define_writers
52
+ end
53
+
54
+ def define_readers
55
+ mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
56
+ def #{name}(*args)
57
+ association(:#{name}).reader(*args)
58
+ end
59
+ CODE
60
+ end
61
+
62
+ def define_writers
63
+ mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
64
+ def #{name}=(value)
65
+ association(:#{name}).writer(value)
66
+ end
67
+ CODE
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,25 @@
1
+ require 'active_node/associations'
2
+
3
+ module ActiveNode::Associations::Builder
4
+ class CollectionAssociation < Association #:nodoc:
5
+ def define_readers
6
+ super
7
+
8
+ mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
9
+ def #{name.to_s.singularize}_ids
10
+ association(:#{name}).ids_reader
11
+ end
12
+ CODE
13
+ end
14
+
15
+ def define_writers
16
+ super
17
+
18
+ mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
19
+ def #{name.to_s.singularize}_ids=(ids)
20
+ association(:#{name}).ids_writer(ids)
21
+ end
22
+ CODE
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ module ActiveNode::Associations::Builder
2
+ class HasMany < CollectionAssociation #:nodoc:
3
+ def macro
4
+ :has_many
5
+ end
6
+
7
+ def valid_options
8
+ super + [:direction, :type]
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,73 @@
1
+ module ActiveNode
2
+ module Associations
3
+ # = Active Record Association Collection
4
+ #
5
+ # CollectionAssociation is an abstract class that provides common stuff to
6
+ # ease the implementation of association proxies that represent
7
+ # collections. See the class hierarchy in AssociationProxy.
8
+ #
9
+ # CollectionAssociation:
10
+ # HasAndBelongsToManyAssociation => has_and_belongs_to_many
11
+ # HasManyAssociation => has_many
12
+ # HasManyThroughAssociation + ThroughAssociation => has_many :through
13
+ #
14
+ # CollectionAssociation class provides common methods to the collections
15
+ # defined by +has_and_belongs_to_many+, +has_many+ or +has_many+ with
16
+ # +:through association+ option.
17
+ #
18
+ # You need to be careful with assumptions regarding the target: The proxy
19
+ # does not fetch records from the database until it needs them, but new
20
+ # ones created with +build+ are added to the target. So, the target may be
21
+ # non-empty and still lack children waiting to be read from the database.
22
+ # If you look directly to the database you cannot assume that's the entire
23
+ # collection because new records may have been added to the target, etc.
24
+ #
25
+ # If you need to work on all current children, new and existing records,
26
+ # +load_target+ and the +loaded+ flag are your friends.
27
+ class CollectionAssociation < Association #:nodoc:
28
+
29
+ # Implements the reader method, e.g. foo.items for Foo.has_many :items
30
+ def reader(force_reload = false)
31
+ @target ||= load_target
32
+ end
33
+
34
+ # Implements the writer method, e.g. foo.items= for Foo.has_many :items
35
+ def writer(records)
36
+ @dirty = true
37
+ @target = records
38
+ end
39
+
40
+ # Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items
41
+ def ids_reader
42
+ reader.map(&:id)
43
+ end
44
+
45
+ # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
46
+ def ids_writer(ids)
47
+ writer klass.find(ids.reject(&:blank?).map!(&:to_i))
48
+ end
49
+
50
+ def load_target
51
+ owner.send(reflection.direction, reflection.type, reflection.klass)
52
+ end
53
+
54
+ def save
55
+ return unless @dirty
56
+ #delete all relations missing in new target
57
+ owner.node.rels(reflection.type).send(reflection.direction).each do |rel|
58
+ rel.del unless ids_reader.include? rel.other_node(owner.node).neo_id.to_i
59
+ end
60
+ original_target = owner.node.send(reflection.direction, reflection.type)
61
+ original_target_ids = original_target.map(&:neo_id).map(&:to_i)
62
+ #add relations missing in old target
63
+ @target.each { |n| original_target << n.node unless original_target_ids.include? n.id }
64
+ end
65
+
66
+ def reset
67
+ super
68
+ @target = owner.new_record? ? [] : nil
69
+ @dirty = false
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,11 @@
1
+ module ActiveNode
2
+ # = Active Record Has Many Association
3
+ module Associations
4
+ # This is the proxy that handles a has many association.
5
+ #
6
+ # If the association has a <tt>:through</tt> option further specialization
7
+ # is provided by its child HasManyThroughAssociation.
8
+ class HasManyAssociation < CollectionAssociation #:nodoc:
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,60 @@
1
+ require 'active_support/core_ext/enumerable'
2
+ require 'active_support/core_ext/string/conversions'
3
+ require 'active_support/core_ext/module/remove_method'
4
+
5
+ module ActiveNode
6
+ module Associations # :nodoc:
7
+ extend ActiveSupport::Autoload
8
+ extend ActiveSupport::Concern
9
+
10
+ autoload :Association, 'active_node/associations/association'
11
+ autoload :CollectionAssociation, 'active_node/associations/collection_association'
12
+ autoload :HasManyAssociation, 'active_node/associations/has_many_association'
13
+
14
+ module Builder #:nodoc:
15
+ autoload :Association, 'active_node/associations/builder/association'
16
+ autoload :CollectionAssociation, 'active_node/associations/builder/collection_association'
17
+
18
+ autoload :HasMany, 'active_node/associations/builder/has_many'
19
+ end
20
+
21
+
22
+ # Clears out the association cache.
23
+ def clear_association_cache #:nodoc:
24
+ @association_cache.clear if persisted?
25
+ end
26
+
27
+ # :nodoc:
28
+ attr_reader :association_cache
29
+
30
+ # Returns the association instance for the given name, instantiating it if it doesn't already exist
31
+ def association(name) #:nodoc:
32
+ association = association_instance_get(name)
33
+
34
+ if association.nil?
35
+ reflection = self.class.reflect_on_association(name)
36
+ association = reflection.association_class.new(self, reflection)
37
+ association_instance_set(name, association)
38
+ end
39
+
40
+ association
41
+ end
42
+
43
+ private
44
+ # Returns the specified association instance if it responds to :loaded?, nil otherwise.
45
+ def association_instance_get(name)
46
+ @association_cache[name.to_sym]
47
+ end
48
+
49
+ # Set the specified association instance.
50
+ def association_instance_set(name, association)
51
+ @association_cache[name] = association
52
+ end
53
+
54
+ module ClassMethods
55
+ def has_many(name, options = {})
56
+ Builder::HasMany.build(self, name, options)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,14 @@
1
+ require 'active_attr'
2
+ require 'active_node/errors'
3
+
4
+ module ActiveNode
5
+ class Base
6
+ include ActiveAttr::Model
7
+ include Persistence
8
+ include Validations
9
+ include Callbacks
10
+ include Associations
11
+ include Reflection
12
+ include Core
13
+ end
14
+ end
@@ -0,0 +1,45 @@
1
+ module ActiveNode
2
+ module Callbacks
3
+ extend ActiveSupport::Concern
4
+
5
+ CALLBACKS = [
6
+ :after_initialize, :after_find, :after_touch, :before_validation, :after_validation,
7
+ :before_save, :around_save, :after_save, :before_create, :around_create,
8
+ :after_create, :before_update, :around_update, :after_update,
9
+ :before_destroy, :around_destroy, :after_destroy, :after_commit, :after_rollback
10
+ ]
11
+
12
+ module ClassMethods
13
+ include ActiveModel::Callbacks
14
+ end
15
+
16
+ included do
17
+ include ActiveModel::Validations::Callbacks
18
+
19
+ define_model_callbacks :initialize, :find, :touch, :only => :after
20
+ define_model_callbacks :save, :create, :update, :destroy
21
+ end
22
+
23
+ def destroy include_relationships=false
24
+ run_callbacks(:destroy) { super include_relationships }
25
+ end
26
+
27
+ def touch(*) #:nodoc:
28
+ run_callbacks(:touch) { super }
29
+ end
30
+
31
+ private
32
+
33
+ def create_or_update #:nodoc:
34
+ run_callbacks(:save) { super }
35
+ end
36
+
37
+ def create_record #:nodoc:
38
+ run_callbacks(:create) { super }
39
+ end
40
+
41
+ def update_record(*) #:nodoc:
42
+ run_callbacks(:update) { super }
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,10 @@
1
+ module ActiveNode
2
+ module Core
3
+ extend ActiveSupport::Concern
4
+
5
+ def initialize(attributes = nil)
6
+ @association_cache = {}
7
+ super attributes
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,13 @@
1
+ module ActiveNode
2
+
3
+ # = Active Node Errors
4
+ #
5
+ # Generic Active Node exception class.
6
+ class ActiveNodeError < StandardError
7
+ end
8
+
9
+ # Raised by ActiveRecord::Base.save! and ActiveRecord::Base.create! methods when record cannot be
10
+ # saved because record is invalid.
11
+ class RecordNotSaved < ActiveNodeError
12
+ end
13
+ end
@@ -0,0 +1,106 @@
1
+ module ActiveNode
2
+ module Persistence
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def find ids
7
+ ids.is_a?(Enumerable) ? ids.map { |id| find(id) } : new_instance(Neography::Node.load(ids))
8
+ end
9
+
10
+ def all
11
+ result = Neography::Node.find(:node_auto_index, :type, type)
12
+ (result.is_a?(Enumerable) ? result : [result]).map { |node| new_instance(node) }.compact
13
+ end
14
+
15
+ def type
16
+ name.underscore
17
+ end
18
+
19
+ def wrap node, klass=nil
20
+ node.is_a?(Enumerable) ?
21
+ node.map { |n| wrap(n, klass) } :
22
+ node.is_a?(Neography::Node) && (active_node_class(node.type.camelize, klass)).try(:new, node) || node
23
+ end
24
+
25
+ def active_node_class(class_name, default_klass=nil)
26
+ klass = Module.const_get(class_name) rescue nil
27
+ klass && klass < ActiveNode::Base && klass || default_klass
28
+ end
29
+
30
+ private
31
+ def new_instance node
32
+ node && new(node)
33
+ end
34
+ end
35
+
36
+ attr_reader :node
37
+ #protected :node
38
+
39
+ delegate :neo_id, to: :node, allow_nil: true
40
+ def id
41
+ neo_id && neo_id.to_i
42
+ end
43
+
44
+ alias :to_param :id
45
+ alias :persisted? :id
46
+ alias :[] :send
47
+
48
+ def initialize hash={}
49
+ @node, hash = hash, hash.send(:table) if hash.is_a? Neography::Node
50
+ super hash
51
+ end
52
+
53
+ def new_record?
54
+ !id
55
+ end
56
+
57
+ def save(*)
58
+ create_or_update
59
+ end
60
+
61
+ alias save! save
62
+
63
+ def destroy include_relationships=false
64
+ destroyable = destroy_associations include_relationships
65
+ node.del if destroyable
66
+ @destroyed = destroyable
67
+ end
68
+
69
+ def destroy!
70
+ destroy true
71
+ end
72
+
73
+ def incoming(types=nil, klass=nil)
74
+ node && self.class.wrap(node.incoming(types), klass)
75
+ end
76
+
77
+ def outgoing(types=nil, klass=nil)
78
+ node && self.class.wrap(node.outgoing(types), klass)
79
+ end
80
+
81
+ private
82
+ def destroy_associations include_associations
83
+ rels = node.rels
84
+ return false unless rels.empty? || include_associations
85
+ rels.each { |rel| rel.del }
86
+ true
87
+ end
88
+
89
+ def nullify_blanks! attrs
90
+ attrs.each { |k, v| attrs[k]=nil if attrs[k].blank? }
91
+ end
92
+
93
+ def create_or_update
94
+ write; true
95
+ association_cache.values.each &:save
96
+ end
97
+
98
+ def write
99
+ if @node
100
+ nullify_blanks!(attributes).each { |k, v| @node[k]=v }
101
+ else
102
+ @node = Neography::Node.create nullify_blanks!(attributes).merge(type: self.class.type)
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,235 @@
1
+ module ActiveNode
2
+ # = Active Record Reflection
3
+ module Reflection # :nodoc:
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class_attribute :reflections
8
+ self.reflections = {}
9
+ end
10
+
11
+ # Reflection enables to interrogate Active Record classes and objects
12
+ # about their associations and aggregations. This information can,
13
+ # for example, be used in a form builder that takes an Active Record object
14
+ # and creates input fields for all of the attributes depending on their type
15
+ # and displays the associations to other objects.
16
+ #
17
+ # MacroReflection class has info for AggregateReflection and AssociationReflection
18
+ # classes.
19
+ module ClassMethods
20
+ def create_reflection(macro, name, options, model)
21
+ case macro
22
+ when :has_many, :has_one
23
+ klass = options[:through] ? ThroughReflection : AssociationReflection
24
+ reflection = klass.new(macro, name, options, model)
25
+ end
26
+
27
+ self.reflections = self.reflections.merge(name => reflection)
28
+ reflection
29
+ end
30
+
31
+ # Returns an array of AssociationReflection objects for all the
32
+ # associations in the class. If you only want to reflect on a certain
33
+ # association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>,
34
+ # <tt>:belongs_to</tt>) as the first parameter.
35
+ #
36
+ # Example:
37
+ #
38
+ # Account.reflect_on_all_associations # returns an array of all associations
39
+ # Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
40
+ #
41
+ def reflect_on_all_associations(macro = nil)
42
+ association_reflections = reflections.values.grep(AssociationReflection)
43
+ macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
44
+ end
45
+
46
+ # Returns the AssociationReflection object for the +association+ (use the symbol).
47
+ #
48
+ # Account.reflect_on_association(:owner) # returns the owner AssociationReflection
49
+ # Invoice.reflect_on_association(:line_items).macro # returns :has_many
50
+ #
51
+ def reflect_on_association(association)
52
+ reflection = reflections[association]
53
+ reflection if reflection.is_a?(AssociationReflection)
54
+ end
55
+
56
+ # Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
57
+ def reflect_on_all_autosave_associations
58
+ reflections.values.select { |reflection| reflection.options[:autosave] }
59
+ end
60
+ end
61
+
62
+ # Base class for AggregateReflection and AssociationReflection. Objects of
63
+ # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
64
+ #
65
+ # MacroReflection
66
+ # AggregateReflection
67
+ # AssociationReflection
68
+ # ThroughReflection
69
+ class MacroReflection
70
+ # Returns the name of the macro.
71
+ #
72
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:balance</tt>
73
+ # <tt>has_many :clients</tt> returns <tt>:clients</tt>
74
+ attr_reader :name
75
+
76
+ # Returns the macro type.
77
+ #
78
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:composed_of</tt>
79
+ # <tt>has_many :clients</tt> returns <tt>:has_many</tt>
80
+ attr_reader :macro
81
+
82
+ attr_reader :scope
83
+
84
+ # Returns the hash of options used for the macro.
85
+ #
86
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>{ class_name: "Money" }</tt>
87
+ # <tt>has_many :clients</tt> returns +{}+
88
+ attr_reader :options
89
+
90
+ attr_reader :model
91
+
92
+
93
+ def initialize(macro, name, options, model)
94
+ @macro = macro
95
+ @name = name
96
+ @options = options
97
+ @model = model
98
+ end
99
+
100
+ # Returns the class for the macro.
101
+ #
102
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class
103
+ # <tt>has_many :clients</tt> returns the Client class
104
+ def klass
105
+ @klass ||= class_name.constantize
106
+ end
107
+
108
+ # Returns the class name for the macro.
109
+ #
110
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt>
111
+ # <tt>has_many :clients</tt> returns <tt>'Client'</tt>
112
+ def class_name
113
+ @class_name ||= (options[:class_name] || derive_class_name).to_s
114
+ end
115
+
116
+ def direction
117
+ @direction ||= (options[:direction] || :outgoing)
118
+ end
119
+
120
+ def type
121
+ @type ||= (options[:type] || name.to_s.singularize)
122
+ end
123
+
124
+ # Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +model+ attribute,
125
+ # and +other_aggregation+ has an options hash assigned to it.
126
+ def ==(other_aggregation)
127
+ super ||
128
+ other_aggregation.kind_of?(self.class) &&
129
+ name == other_aggregation.name &&
130
+ other_aggregation.options &&
131
+ model == other_aggregation.model
132
+ end
133
+
134
+ private
135
+ def derive_class_name
136
+ name.to_s.camelize
137
+ end
138
+ end
139
+
140
+
141
+ # Holds all the meta-data about an association as it was specified in the
142
+ # Active Record class.
143
+ class AssociationReflection < MacroReflection #:nodoc:
144
+ # Returns the target association's class.
145
+ #
146
+ # class Author < ActiveRecord::Base
147
+ # has_many :books
148
+ # end
149
+ #
150
+ # Author.reflect_on_association(:books).klass
151
+ # # => Book
152
+ #
153
+ # <b>Note:</b> Do not call +klass.new+ or +klass.create+ to instantiate
154
+ # a new association object. Use +build_association+ or +create_association+
155
+ # instead. This allows plugins to hook into association object creation.
156
+ #def klass
157
+ # @klass ||= model.send(:compute_type, class_name)
158
+ #end
159
+
160
+ def initialize(*args)
161
+ super
162
+ @collection = [:has_many].include?(macro)
163
+ end
164
+
165
+ # Returns a new, unsaved instance of the associated class. +attributes+ will
166
+ # be passed to the class's constructor.
167
+ def build_association(attributes, &block)
168
+ klass.new(attributes, &block)
169
+ end
170
+
171
+ def through_reflection
172
+ nil
173
+ end
174
+
175
+ def source_reflection
176
+ nil
177
+ end
178
+
179
+ # A chain of reflections from this one back to the owner. For more see the explanation in
180
+ # ThroughReflection.
181
+ def chain
182
+ [self]
183
+ end
184
+
185
+ # Returns whether or not this association reflection is for a collection
186
+ # association. Returns +true+ if the +macro+ is either +has_many+ or
187
+ # +has_and_belongs_to_many+, +false+ otherwise.
188
+ def collection?
189
+ @collection
190
+ end
191
+
192
+ # Returns whether or not the association should be validated as part of
193
+ # the parent's validation.
194
+ #
195
+ # Unless you explicitly disable validation with
196
+ # <tt>validate: false</tt>, validation will take place when:
197
+ #
198
+ # * you explicitly enable validation; <tt>validate: true</tt>
199
+ # * you use autosave; <tt>autosave: true</tt>
200
+ # * the association is a +has_many+ association
201
+ def validate?
202
+ !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many)
203
+ end
204
+
205
+ def association_class
206
+ case macro
207
+ when :has_many
208
+ if options[:through]
209
+ Associations::HasManyThroughAssociation
210
+ else
211
+ Associations::HasManyAssociation
212
+ end
213
+ when :has_one
214
+ if options[:through]
215
+ Associations::HasOneThroughAssociation
216
+ else
217
+ Associations::HasOneAssociation
218
+ end
219
+ end
220
+ end
221
+
222
+ private
223
+ def derive_class_name
224
+ class_name = name.to_s.camelize
225
+ class_name = class_name.singularize if collection?
226
+ class_name
227
+ end
228
+ end
229
+
230
+ # Holds all the meta-data about a :through association as it was specified
231
+ # in the Active Record class.
232
+ class ThroughReflection < AssociationReflection #:nodoc:
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,81 @@
1
+ module ActiveNode
2
+ # = Active Node RecordInvalid
3
+ #
4
+ # Raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. Use the
5
+ # +record+ method to retrieve the record which did not validate.
6
+ #
7
+ # begin
8
+ # complex_operation_that_calls_save!_internally
9
+ # rescue ActiveRecord::RecordInvalid => invalid
10
+ # puts invalid.record.errors
11
+ # end
12
+ class RecordInvalid < ActiveNodeError
13
+ attr_reader :record # :nodoc:
14
+ def initialize(record) # :nodoc:
15
+ @record = record
16
+ errors = @record.errors.full_messages.join(", ")
17
+ super(I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", :errors => errors, :default => :"errors.messages.record_invalid"))
18
+ end
19
+ end
20
+
21
+ # = Active Node Validations
22
+ #
23
+ # Active Node includes the majority of its validations from <tt>ActiveModel::Validations</tt>
24
+ # all of which accept the <tt>:on</tt> argument to define the context where the
25
+ # validations are active. Active Node will always supply either the context of
26
+ # <tt>:create</tt> or <tt>:update</tt> dependent on whether the model is a
27
+ # <tt>new_record?</tt>.
28
+
29
+ module Validations
30
+ extend ActiveSupport::Concern
31
+ include ActiveModel::Validations
32
+
33
+ module ClassMethods
34
+ # Creates an object just like Base.create but calls <tt>save!</tt> instead of +save+
35
+ # so an exception is raised if the record is invalid.
36
+ def create!(attributes = nil, &block)
37
+ if attributes.is_a?(Array)
38
+ attributes.collect { |attr| create!(attr, &block) }
39
+ else
40
+ object = new(attributes)
41
+ yield(object) if block_given?
42
+ object.save!
43
+ object
44
+ end
45
+ end
46
+ end
47
+
48
+ # The validation process on save can be skipped by passing <tt>validate: false</tt>.
49
+ # The regular Base#save method is replaced with this when the validations
50
+ # module is mixed in, which it is by default.
51
+ def save(options={})
52
+ perform_validations(options) ? super : false
53
+ end
54
+
55
+ # Attempts to save the record just like Base#save but will raise a +RecordInvalid+
56
+ # exception instead of returning +false+ if the record is not valid.
57
+ def save!(options={})
58
+ perform_validations(options) ? super : raise(RecordInvalid.new(self))
59
+ end
60
+
61
+ # Runs all the validations within the specified context. Returns +true+ if
62
+ # no errors are found, +false+ otherwise.
63
+ #
64
+ # If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if
65
+ # <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not.
66
+ #
67
+ # Validations with no <tt>:on</tt> option will run no matter the context. Validations with
68
+ # some <tt>:on</tt> option will only run in the specified context.
69
+ def valid?(context = nil)
70
+ context ||= (new_record? ? :create : :update)
71
+ output = super(context)
72
+ errors.empty? && output
73
+ end
74
+
75
+ protected
76
+
77
+ def perform_validations(options={}) # :nodoc:
78
+ options[:validate] == false || valid?(options[:context])
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,3 @@
1
+ module ActiveNode
2
+ VERSION = "0.0.2.alpha"
3
+ end
@@ -0,0 +1,17 @@
1
+ require "active_support/dependencies/autoload"
2
+
3
+ module ActiveNode
4
+ extend ActiveSupport::Autoload
5
+ autoload :Base
6
+ autoload :Callbacks
7
+ autoload :Core
8
+ autoload :Persistence
9
+ autoload :Validations
10
+ autoload :Reflection
11
+ autoload :VERSION
12
+
13
+ eager_autoload do
14
+ autoload :ActiveNodeError, 'active_node/errors'
15
+ autoload :Associations
16
+ end
17
+ end
@@ -0,0 +1,77 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveNode::Associations do
4
+ describe "#save" do
5
+ it "should have empty association" do
6
+ client = Client.new(name: 'a')
7
+ client.save
8
+ client.users.should be_empty
9
+ end
10
+
11
+ it "should not have empty association" do
12
+ user = NeoUser.new(name: 'Heinrich')
13
+ user.save
14
+ client = Client.new(name: 'a', users: [user])
15
+ client.save
16
+ client.users.should == [user]
17
+ end
18
+
19
+ it "can set association by id" do
20
+ user = NeoUser.new(name: 'Heinrich')
21
+ user.save
22
+ client = Client.new(name: 'a', user_ids: [user.id])
23
+ client.save
24
+ client.users.should == [user]
25
+ client.user_ids.should == [user.id]
26
+ client.users.first.clients.first.should == client
27
+ end
28
+
29
+ it "can remove associated objects" do
30
+ user = NeoUser.new(name: 'Heinrich')
31
+ user.save
32
+ client = Client.new(name: 'a', user_ids: [user.id])
33
+ client.save
34
+ client.user_ids = []
35
+ client.save
36
+ client.users.should be_empty
37
+ client.user_ids.should be_empty
38
+ end
39
+
40
+ it "can remove some of the associated objects" do
41
+ child1 = Person.create!
42
+ child2 = Person.create!
43
+ person = Person.create! child_ids: [child1.id, child2.id]
44
+ person = Person.find(person.id)
45
+ person.children.count.should == 2
46
+ person.child_ids = [child2.id]
47
+ person.save
48
+ Person.find(person.id).children.should == [child2]
49
+ end
50
+
51
+ it "can remove and add some of the associated objects" do
52
+ child1 = Person.create!
53
+ child2 = Person.create!
54
+ person = Person.create! child_ids: [child1.id, child2.id]
55
+ person = Person.find(person.id)
56
+ person.children.count.should == 2
57
+ child3 = Person.create!
58
+ person.child_ids = [child2.id, child3.id]
59
+ person.save
60
+ Person.find(person.id).children.should == [child2, child3]
61
+ end
62
+
63
+ it 'can handle self referencing' do
64
+ person = Person.new
65
+ person.save
66
+ person.people = [person]
67
+ person.save
68
+ person.people.first == person
69
+ Person.all.count.should == 1
70
+ end
71
+
72
+ it 'can handle reference to the same class' do
73
+ id = Person.create!(children: [Person.create!, Person.create!]).id
74
+ Person.find(id).children.size.should == 2
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveNode::Persistence do
4
+ describe "#save" do
5
+ it "should save not conventionally named object" do
6
+ NeoUser.new(name: 'Heinrich').save.should be_true
7
+ NeoUser.all.map(&:name).should == ['Heinrich']
8
+ end
9
+
10
+ it 'should destroy node' do
11
+ user = NeoUser.create!(name: 'abc')
12
+ NeoUser.all.count.should == 1
13
+ user.destroy.should be_true
14
+ NeoUser.all.count.should == 0
15
+ end
16
+
17
+ it 'should not destroy node with relationships' do
18
+ person = Person.create! children: [Person.create!, Person.create!]
19
+ person.destroy.should be_false
20
+ Person.all.count.should == 3
21
+ end
22
+
23
+ it 'should destroy! node with relationships' do
24
+ person = Person.create! children: [Person.create!, Person.create!]
25
+ person.destroy!.should be_true
26
+ Person.all.count.should == 2
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveNode::Validations do
4
+ describe "#save" do
5
+ it "should not save invalid object" do
6
+ Client.new.save.should be_false
7
+ end
8
+
9
+ it "should save invalid object" do
10
+ Client.new(name: 'abc7').save.should be_true
11
+ Client.all.first.name.should == 'abc7'
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ class Client < ActiveNode::Base
2
+ attribute :name, type: String
3
+
4
+ #has_many :phases
5
+ has_many :users, type: :client, direction: :incoming, class_name: 'NeoUser'
6
+
7
+ validates :name, presence: true
8
+ end
@@ -0,0 +1,9 @@
1
+ class NeoUser < ActiveNode::Base
2
+ attribute :name, type: String
3
+ has_many :clients
4
+ validates :name, presence: true
5
+
6
+ def self.type
7
+ 'user'
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ class Person < ActiveNode::Base
2
+ has_many :people
3
+ has_many :children, class_name: "Person"
4
+ #has_one :father, type: :child, direction: :incoming, class_name: "Person"
5
+ end
@@ -0,0 +1,67 @@
1
+ #require 'rubygems'
2
+ #require 'bundler/setup'
3
+ require 'active_node'
4
+ #
5
+ #require "active_node/version"
6
+ #require "active_model/version"
7
+ #require 'active_support'
8
+ #require 'active_model/validator'
9
+ #require 'active_model/validations'
10
+ #require 'active_model'
11
+ #require 'active_attr'
12
+ require 'neography'
13
+ require 'benchmark'
14
+ #require 'matchers'
15
+ require 'coveralls'
16
+ Coveralls.wear!
17
+
18
+ # If you want to see more, uncomment the next few lines
19
+ # require 'net-http-spy'
20
+ # Net::HTTP.http_logger_options = {:body => true} # just the body
21
+ # Net::HTTP.http_logger_options = {:verbose => true} # see everything
22
+
23
+ Dir[File.dirname(__FILE__) + '/models/*.rb'].each {|file| require file }
24
+
25
+ def generate_text(length=8)
26
+ chars = 'abcdefghjkmnpqrstuvwxyz'
27
+ key = ''
28
+ length.times { |i| key << chars[rand(chars.length)] }
29
+ key
30
+ end
31
+
32
+ RSpec.configure do |c|
33
+ c.filter_run_excluding :slow => true, :gremlin => true
34
+ #c.around(:each) do
35
+ # Neography::Rest.new.execute_query("START n0=node(0),nx=node(*) MATCH n0-[r0?]-(),nx-[rx?]-() WHERE nx <> n0 DELETE r0,rx,nx")
36
+ #end
37
+ c.before(:each) do
38
+ @neo=Neography::Rest.new
39
+ @neo.execute_query("START n0=node(0),nx=node(*) MATCH n0-[r0?]-(),nx-[rx?]-() WHERE nx <> n0 DELETE r0,rx,nx")
40
+ @neo.set_node_auto_index_status(true)
41
+ @neo.add_node_auto_index_property('type')
42
+ end
43
+ end
44
+
45
+
46
+ def json_content_type
47
+ {"Content-Type"=>"application/json"}
48
+ end
49
+
50
+ def error_response(attributes)
51
+ request_uri = double()
52
+ request_uri.stub(:request_uri).and_return("")
53
+
54
+ http_header = double()
55
+ http_header.stub(:request_uri).and_return(request_uri)
56
+
57
+ stub(
58
+ http_header: http_header,
59
+ code: attributes[:code],
60
+ body: {
61
+ message: attributes[:message],
62
+ exception: attributes[:exception],
63
+ stacktrace: attributes[:stacktrace]
64
+ }.reject { |k,v| v.nil? }.to_json
65
+ )
66
+ end
67
+
metadata ADDED
@@ -0,0 +1,193 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_node
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2.alpha
5
+ prerelease: 6
6
+ platform: ruby
7
+ authors:
8
+ - Heinrich Klobuczek
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-09-06 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: active_attr
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: neography
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: activesupport
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: activemodel
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rspec
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '2.11'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '2.11'
94
+ - !ruby/object:Gem::Dependency
95
+ name: net-http-spy
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - '='
100
+ - !ruby/object:Gem::Version
101
+ version: 0.2.1
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - '='
108
+ - !ruby/object:Gem::Version
109
+ version: 0.2.1
110
+ - !ruby/object:Gem::Dependency
111
+ name: coveralls
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ description: ActiveRecord style Object Graph Mapping for neo4j
127
+ email:
128
+ - heinrich@mail.com
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - .gitignore
134
+ - .travis.yml
135
+ - Gemfile
136
+ - LICENSE
137
+ - Rakefile
138
+ - active_node.gemspec
139
+ - lib/active_node.rb
140
+ - lib/active_node/associations.rb
141
+ - lib/active_node/associations/association.rb
142
+ - lib/active_node/associations/builder/association.rb
143
+ - lib/active_node/associations/builder/collection_association.rb
144
+ - lib/active_node/associations/builder/has_many.rb
145
+ - lib/active_node/associations/collection_association.rb
146
+ - lib/active_node/associations/has_many_association.rb
147
+ - lib/active_node/base.rb
148
+ - lib/active_node/callbacks.rb
149
+ - lib/active_node/core.rb
150
+ - lib/active_node/errors.rb
151
+ - lib/active_node/persistence.rb
152
+ - lib/active_node/reflection.rb
153
+ - lib/active_node/validations.rb
154
+ - lib/active_node/version.rb
155
+ - spec/functional/associations_spec.rb
156
+ - spec/functional/persistence_spec.rb
157
+ - spec/functional/validations_spec.rb
158
+ - spec/models/client.rb
159
+ - spec/models/neo_user.rb
160
+ - spec/models/person.rb
161
+ - spec/spec_helper.rb
162
+ homepage: ''
163
+ licenses: []
164
+ post_install_message:
165
+ rdoc_options: []
166
+ require_paths:
167
+ - lib
168
+ required_ruby_version: !ruby/object:Gem::Requirement
169
+ none: false
170
+ requirements:
171
+ - - ! '>='
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ required_rubygems_version: !ruby/object:Gem::Requirement
175
+ none: false
176
+ requirements:
177
+ - - ! '>'
178
+ - !ruby/object:Gem::Version
179
+ version: 1.3.1
180
+ requirements: []
181
+ rubyforge_project: active_node
182
+ rubygems_version: 1.8.25
183
+ signing_key:
184
+ specification_version: 3
185
+ summary: ActiveRecord style Object Graph Mapping for neo4j
186
+ test_files:
187
+ - spec/functional/associations_spec.rb
188
+ - spec/functional/persistence_spec.rb
189
+ - spec/functional/validations_spec.rb
190
+ - spec/models/client.rb
191
+ - spec/models/neo_user.rb
192
+ - spec/models/person.rb
193
+ - spec/spec_helper.rb