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