couchbase-model-relationship 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,25 @@
1
+ module Couchbase
2
+ class Model
3
+ module Relationship
4
+ module Child
5
+ extend ::ActiveSupport::Concern
6
+
7
+ def create_with_parent_id(options = {})
8
+ if id.blank? && parent.present?
9
+ @id = prefixed_id(parent.id)
10
+ end
11
+
12
+ create_without_parent_id(options)
13
+ end
14
+
15
+ module ClassMethods
16
+ def has_parent
17
+ attr_accessor :parent
18
+
19
+ alias_method_chain :create, :parent_id
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,193 @@
1
+ # All this behavior assumes the same bucket/connection options are used for the
2
+ # root object and all children.
3
+ #
4
+ # TODO Transparent child load if object not present (cache missing objects to reduce queries)
5
+ # TODO Support for "required" children (if missing, error) ?
6
+ # TODO Use multi-set to batch save parent + children
7
+ module Couchbase
8
+ class Model
9
+ module Relationship
10
+ module Parent
11
+ extend ::ActiveSupport::Concern
12
+
13
+ included do
14
+ alias_method_chain :save, :autosave_children
15
+ alias_method_chain :delete, :autodelete_children
16
+ end
17
+
18
+ # TODO How to handle failures saving children?
19
+ def save_with_children(options = {})
20
+ save(options).tap do |result|
21
+ if result
22
+ children.each do |child|
23
+ child.save_if_changed(options)
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ def save_with_autosave_children(options = {})
30
+ # Don't save if we failed
31
+ save_without_autosave_children(options).tap do |result|
32
+ if result
33
+ self.class.child_associations.select(&:auto_save).each do |association|
34
+ association.fetch(self).try :save_if_changed, options
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ def delete_with_autodelete_children(options = {})
41
+ self.class.child_associations.select(&:auto_delete).each do |association|
42
+ association.fetch(self).try :delete, options
43
+ end
44
+
45
+ delete_without_autodelete_children(options)
46
+ end
47
+
48
+ # FIXME #changed? should include children if any are autosave
49
+
50
+ def delete_with_children(options = {})
51
+ children.each {|child| child.delete options }
52
+
53
+ delete(options)
54
+ end
55
+
56
+ def children
57
+ self.class.child_associations.map do |association|
58
+ association.fetch self
59
+ end.compact
60
+ end
61
+
62
+ def reload_all
63
+ children.each(&:reload)
64
+ reload
65
+ end
66
+
67
+ module ClassMethods
68
+ def child(name, options = {})
69
+ # TODO This may get the full module path for a relationship name,
70
+ # and that will make the keys very long. Is this necessary? see: AR STI
71
+ name = name.to_s.underscore unless name.is_a?(String)
72
+
73
+ (@_children ||= []).push Relationship::Association.new(name, options)
74
+
75
+ define_method("#{name}=") do |object|
76
+ # FIXME Sanity check. If parent and parent != self, error
77
+ object.parent = self if object.respond_to?(:parent)
78
+
79
+ instance_variable_set :"@_child_#{name}", object
80
+ end
81
+
82
+ define_method("#{name}_loaded?") do
83
+ instance_variable_get("@_child_#{name}_loaded")
84
+ end
85
+
86
+ define_method("#{name}_loaded!") do
87
+ instance_variable_set("@_child_#{name}_loaded", true)
88
+ end
89
+ protected "#{name}_loaded!".to_sym
90
+
91
+ define_method("#{name}") do
92
+ # DO NOT USE Association#fetch IN THIS METHOD
93
+ base_var_name = "@_child_#{name}"
94
+
95
+ if (existing = instance_variable_get(base_var_name)).present?
96
+ existing
97
+ else
98
+ if send("#{name}_loaded?")
99
+ send("build_#{name}")
100
+ else
101
+ assoc = self.class.child_association_for(name)
102
+ send("#{name}_loaded!")
103
+
104
+ if (unloaded = assoc.load(self)).present?
105
+ send("#{name}=", unloaded)
106
+ end
107
+
108
+ send(name)
109
+ end
110
+ end
111
+ end
112
+
113
+ define_method("build_#{name}") do |attributes = {}|
114
+ assoc = self.class.child_association_for(name)
115
+ send("#{name}=", assoc.child_class.new(attributes)).tap do |child|
116
+ child.parent = self
117
+ end
118
+ end
119
+ end
120
+
121
+ def children(*names)
122
+ options = names.extract_options!
123
+
124
+ names.each {|name| child name, options }
125
+ end
126
+
127
+ def child_association_names
128
+ children.map(&:name)
129
+ end
130
+
131
+ def child_associations
132
+ @_children || []
133
+ end
134
+
135
+ def child_association_for(name)
136
+ @_children.detect {|association| association.name == name.to_s }
137
+ end
138
+
139
+ def find_with_children(id, *children)
140
+ find_all_with_children(id, *children).first
141
+ end
142
+
143
+ # FIXME This is a horrible abortion of a method
144
+ def find_all_with_children(ids, *children)
145
+ ids = Array(ids)
146
+
147
+ effective_children = if children.blank?
148
+ @_children.select {|child| child.auto_load }
149
+ else
150
+ children = children.map(&:to_s)
151
+ @_children.select {|child| children.include?(child.name) }
152
+ end
153
+
154
+ search_ids = ids.dup
155
+ ids.each do |id|
156
+ search_ids.concat(effective_children.map do |child|
157
+ child.child_class.prefixed_id(id)
158
+ end)
159
+ end
160
+
161
+ results = bucket.get(search_ids, quiet: true, extended: true)
162
+
163
+ parent_objects = ids.map do |id|
164
+ if results.key?(id)
165
+ raw_new(id, results.delete(id))
166
+ else
167
+ raise Couchbase::Error::NotFound.new("failed to get value (key=\"#{id}\"")
168
+ end
169
+ end
170
+
171
+ parent_objects.each do |parent|
172
+ results.each do |child_id, child_attributes|
173
+ if unprefixed_id(parent.id) == unprefixed_id(child_id)
174
+ assoc = effective_children.detect {|assoc| assoc.prefix == prefix_from_id(child_id) }
175
+ parent.send "#{assoc.name}=",
176
+ assoc.child_class.raw_new(child_id, child_attributes)
177
+ end
178
+ end
179
+
180
+ effective_children.each {|assoc| parent.send("#{assoc.name}_loaded!") }
181
+ end
182
+ end
183
+
184
+ def raw_new(id, results)
185
+ obj, flags, cas = results
186
+ obj = {:raw => obj} unless obj.is_a?(Hash)
187
+ new({:id => id, :meta => {'flags' => flags, 'cas' => cas}}.merge(obj))
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,7 @@
1
+ module Couchbase
2
+ class Model
3
+ module Relationship
4
+ VERSION = "0.1"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+
3
+ describe "associations" do
4
+ subject { ::Couchbase::Model::Relationship::Association }
5
+
6
+ it "sets the name" do
7
+ subject.new('abc').name.should eq('abc')
8
+ end
9
+
10
+ it "sets autosave properly" do
11
+ subject.new('abc', auto_save: true).auto_save.should be_true
12
+ subject.new('abc').auto_save.should be_false
13
+ end
14
+
15
+ it "sets autodelete properly" do
16
+ subject.new('abc', auto_delete: true).auto_delete.should be_true
17
+ subject.new('abc').auto_delete.should be_false
18
+ end
19
+
20
+ it "uses the provide class name" do
21
+ subject.new('abc', class_name: "String").child_klass.should eq('String')
22
+ end
23
+
24
+ it "fetches the object from the parent" do
25
+ parent = stub(abc: :object)
26
+
27
+ subject.new("abc").fetch(parent).should eq(:object)
28
+ end
29
+
30
+ it "loads the object from the database" do
31
+ child_class = stub_klass(Couchbase::Model)
32
+ child_class.expects(:prefixed_id).with('core/user:abc123').returns(:id)
33
+ child_class.expects(:find_by_id).with(:id).returns(:child)
34
+
35
+ parent = stub(id: "core/user:abc123")
36
+ instance = subject.new("string")
37
+ instance.stubs(child_class: child_class)
38
+
39
+ instance.load(parent).should eq(:child)
40
+ end
41
+
42
+ it "knows the child class name" do
43
+ subject.new("string").child_klass.should eq("String")
44
+ end
45
+
46
+ it "knows the child class" do
47
+ subject.new("string").child_class.should eq(String)
48
+ end
49
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ require 'couchbase/model'
4
+ require 'couchbase/model/attributes'
5
+
6
+ describe "Attributes" do
7
+ let(:klass) do
8
+ Class.new(Couchbase::Model) do
9
+
10
+ attribute :abc
11
+ end
12
+ end
13
+
14
+ subject { klass.new }
15
+
16
+ it "should have a setter" do
17
+ subject.should respond_to(:abc=)
18
+ end
19
+
20
+ it "should have a getter" do
21
+ subject.should respond_to(:abc)
22
+ end
23
+
24
+ it "should set the variable" do
25
+ subject.abc = 1
26
+ subject.read_attribute('abc').should eq(1)
27
+ end
28
+
29
+ it "should read the variable" do
30
+ subject.write_attribute("abc", 1)
31
+ subject.abc.should eq(1)
32
+ end
33
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ class ChildTest < Couchbase::Model
4
+ has_parent
5
+ end
6
+
7
+ class ChildTestParent < Couchbase::Model
8
+ child :child_test
9
+ end
10
+
11
+ describe "children" do
12
+ let(:parent) { ChildTestParent.new }
13
+ subject { ChildTest.new }
14
+
15
+ it "allows you to set the parent" do
16
+ subject.should respond_to(:parent=)
17
+
18
+ subject.parent = parent
19
+ subject.parent.should eq(parent)
20
+ end
21
+
22
+ describe "creating" do
23
+ it "inherits the UUID of the parent" do
24
+ parent.id = "child_test_parent:1234"
25
+ ChildTest.stubs(bucket: stub(add: true))
26
+
27
+ subject.parent = parent
28
+ subject.create
29
+
30
+ subject.id.should eq("child_test:1234")
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,54 @@
1
+ require 'spec_helper'
2
+
3
+ class ComplexTest < Couchbase::Model
4
+ class Child
5
+ attr_accessor :name
6
+
7
+ def self.json_create(args)
8
+ new(*args['parameters'])
9
+ end
10
+
11
+ def initialize(name)
12
+ self.name = name
13
+ end
14
+
15
+ def to_json(*args)
16
+ {
17
+ 'json_class' => self.class.name,
18
+ 'parameters' => [name]
19
+ }.to_json(*args)
20
+ end
21
+ end
22
+
23
+ array_attribute :array, class_name: ComplexTest::Child.name
24
+ end
25
+
26
+ describe "complex attributes" do
27
+ describe "array attributes" do
28
+ subject { ComplexTest.new }
29
+
30
+ let(:raw) do
31
+ {
32
+ 'json_class' => 'ComplexTest::Child',
33
+ 'parameters' => ['abc']
34
+ }.to_json
35
+ end
36
+
37
+ let(:object) { ComplexTest::Child.new('exist') }
38
+
39
+ it "sets values properly" do
40
+ subject.array = [raw, object]
41
+ subject.array.all? {|c| ComplexTest::Child === c }.should be_true
42
+
43
+ subject.array.map(&:name).should eq(%w(abc exist))
44
+ end
45
+
46
+ it "knows what class it stores" do
47
+ ComplexTest.array_attribute_class(:array).should eq("ComplexTest::Child")
48
+ end
49
+
50
+ it "defaults to an array" do
51
+ ComplexTest.new.array.should be_a(Array)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+
3
+ describe Couchbase::Model::DeepCopier do
4
+ it "hands uncloneable objects" do
5
+ source = 1
6
+ copy = described_class.new(source).copy
7
+
8
+ source.should eq(1)
9
+ copy.should eq(1)
10
+ end
11
+ it "properly clones a normal object" do
12
+ source = "abc"
13
+ copy = described_class.new(source).copy
14
+
15
+ source.should eq(copy)
16
+ source.object_id.should_not eq(copy.object_id)
17
+ end
18
+
19
+ it "properly clones an array" do
20
+ source = []
21
+ copy = described_class.new(source).copy
22
+ source.push :a
23
+
24
+ source.should_not eq(copy)
25
+ copy.should eq([])
26
+ end
27
+
28
+ it "properly clones nested arrays" do
29
+ source = [[]]
30
+ copy = described_class.new(source).copy
31
+ source.last.push :a
32
+
33
+ source.should_not eq(copy)
34
+ copy.should eq([[]])
35
+ end
36
+
37
+ it "properly clones a hash" do
38
+ source = {}
39
+ copy = described_class.new(source).copy
40
+ source[:key] = :val
41
+
42
+ source.should_not eq(copy)
43
+ copy.should eq({})
44
+ end
45
+
46
+ it "properly clones a nested hash" do
47
+ source = {key: []}
48
+ copy = described_class.new(source).copy
49
+
50
+ source[:key1] = 'a'
51
+ source[:key].push 'b'
52
+
53
+ source.should_not eq(copy)
54
+ copy.should eq({key: []})
55
+ end
56
+ end