couchbase-model-relationship 0.1

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.
@@ -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