active_node 0.0.2.alpha

Sign up to get free protection for your applications and to get access to all the features.
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