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.
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +19 -0
- data/couchbase-model-relationship.gemspec +33 -0
- data/lib/couchbase/model/attributes.rb +36 -0
- data/lib/couchbase/model/complex_attributes.rb +38 -0
- data/lib/couchbase/model/deep_copier.rb +45 -0
- data/lib/couchbase/model/dirty.rb +81 -0
- data/lib/couchbase/model/id_prefix.rb +53 -0
- data/lib/couchbase/model/relationship.rb +34 -0
- data/lib/couchbase/model/relationship/association.rb +40 -0
- data/lib/couchbase/model/relationship/child.rb +25 -0
- data/lib/couchbase/model/relationship/parent.rb +193 -0
- data/lib/couchbase/model/relationship/version.rb +7 -0
- data/spec/association_spec.rb +49 -0
- data/spec/attributes_spec.rb +33 -0
- data/spec/child_spec.rb +33 -0
- data/spec/complex_attributes_spec.rb +54 -0
- data/spec/deep_copier_spec.rb +56 -0
- data/spec/dirty_spec.rb +139 -0
- data/spec/id_prefix_spec.rb +45 -0
- data/spec/parent_spec.rb +282 -0
- data/spec/spec_helper.rb +197 -0
- metadata +246 -0
@@ -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,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
|
data/spec/child_spec.rb
ADDED
@@ -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
|