ripple 0.8.0 → 0.8.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/lib/ripple/attribute_methods.rb +1 -1
- data/lib/ripple/document/key.rb +7 -0
- data/lib/ripple/document.rb +1 -0
- data/lib/ripple/embedded_document.rb +1 -0
- data/lib/ripple/nested_attributes.rb +263 -0
- data/lib/ripple/properties.rb +4 -3
- data/lib/ripple.rb +1 -0
- data/spec/integration/ripple/associations_spec.rb +9 -7
- data/spec/integration/ripple/nested_attributes_spec.rb +260 -0
- data/spec/integration/ripple/persistence_spec.rb +2 -0
- data/spec/ripple/attribute_methods_spec.rb +10 -9
- data/spec/ripple/properties_spec.rb +7 -2
- data/spec/spec_helper.rb +4 -1
- data/spec/support/models/car.rb +15 -0
- data/spec/support/models/driver.rb +5 -0
- data/spec/support/models/engine.rb +4 -0
- data/spec/support/models/passenger.rb +5 -0
- data/spec/support/models/seat.rb +4 -0
- data/spec/support/models/wheel.rb +5 -0
- data/spec/support/models/widget.rb +1 -0
- data/spec/support/test_server.rb +17 -0
- data/spec/support/test_server.yml +2 -0
- metadata +23 -5
@@ -102,7 +102,7 @@ module Ripple
|
|
102
102
|
|
103
103
|
def attributes_from_property_defaults
|
104
104
|
self.class.properties.values.inject({}) do |hash, prop|
|
105
|
-
hash[prop.key] = prop.default
|
105
|
+
hash[prop.key] = prop.default unless prop.default.nil?
|
106
106
|
hash
|
107
107
|
end.with_indifferent_access
|
108
108
|
end
|
data/lib/ripple/document/key.rb
CHANGED
@@ -29,6 +29,9 @@ module Ripple
|
|
29
29
|
def key=(value)
|
30
30
|
self.#{prop} = value
|
31
31
|
end
|
32
|
+
def key_attr
|
33
|
+
:#{prop}
|
34
|
+
end
|
32
35
|
CODE
|
33
36
|
end
|
34
37
|
end
|
@@ -43,6 +46,10 @@ module Ripple
|
|
43
46
|
def key=(value)
|
44
47
|
@key = value.to_s
|
45
48
|
end
|
49
|
+
|
50
|
+
def key_attr
|
51
|
+
:key
|
52
|
+
end
|
46
53
|
end
|
47
54
|
end
|
48
55
|
end
|
data/lib/ripple/document.rb
CHANGED
@@ -0,0 +1,263 @@
|
|
1
|
+
require 'ripple'
|
2
|
+
module Ripple
|
3
|
+
module NestedAttributes #:nodoc:
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
UNASSIGNABLE_KEYS = %w{ _destroy }
|
7
|
+
TRUE_VALUES = [ true, "true", 1, "1", "yes", "ok", "y" ]
|
8
|
+
|
9
|
+
included do
|
10
|
+
class_inheritable_accessor :nested_attributes_options, :instance_writer => false
|
11
|
+
self.nested_attributes_options = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
# = Nested Attributes
|
15
|
+
#
|
16
|
+
# This is similar to the `accepts_nested_attributes` functionality
|
17
|
+
# as found in AR. This allows the use update attributes and create
|
18
|
+
# new child records through the parent. It also allows the use of
|
19
|
+
# the `fields_for` form view helper, using a presenter pattern.
|
20
|
+
#
|
21
|
+
# To enable in the model, call the class method, using the same
|
22
|
+
# relationship as defined in the `one` or `many`.
|
23
|
+
#
|
24
|
+
# class Shipment
|
25
|
+
# include Ripple::Document
|
26
|
+
# one :box
|
27
|
+
# many :addresses
|
28
|
+
# accepts_nested_attributes_for :box, :addresses
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# == One
|
32
|
+
#
|
33
|
+
# Given this model:
|
34
|
+
#
|
35
|
+
# class Shipment
|
36
|
+
# include Ripple::Document
|
37
|
+
# one :box
|
38
|
+
# accepts_nested_attributes_for :box
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# This allows creating a box child during creation:
|
42
|
+
#
|
43
|
+
# shipment = Shipment.create(:box_attributes => { :shape => 'square' })
|
44
|
+
# shipment.box.shape # => 'square'
|
45
|
+
#
|
46
|
+
# This also allows updating box attributes:
|
47
|
+
#
|
48
|
+
# shipment.update_attributes(:box_attributes => { :key => 'xxx', :shape => 'triangle' })
|
49
|
+
# shipment.box.shape # => 'triangle'
|
50
|
+
#
|
51
|
+
# == Many
|
52
|
+
#
|
53
|
+
# Given this model
|
54
|
+
#
|
55
|
+
# class Manifest
|
56
|
+
# include Ripple::Document
|
57
|
+
# many :shipments
|
58
|
+
# accepts_nested_attributes_for :shipments
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# This allows creating several shipments during manifest creation:
|
62
|
+
#
|
63
|
+
# manifest = Manifest.create(:shipments_attributes => [ { :reference => "foo1" }, { :reference => "foo2" } ])
|
64
|
+
# manifest.shipments.size # => 2
|
65
|
+
# manifest.shipments.first.reference # => foo1
|
66
|
+
# manifest.shipments.second.reference # => foo2
|
67
|
+
#
|
68
|
+
# And updating shipment attributes:
|
69
|
+
#
|
70
|
+
# manifest.update_attributes(:shipment_attributes => [ { :key => 'xxx', :reference => 'updated foo1' },
|
71
|
+
# { :key => 'yyy', :reference => 'updated foo2' } ])
|
72
|
+
# manifest.shipments.first.reference # => updated foo1
|
73
|
+
# manifest.shipments.second.reference # => updated foo2
|
74
|
+
#
|
75
|
+
# NOTE: On many embedded, then entire collection of embedded documents is replaced, as there
|
76
|
+
# is no key to specifically update.
|
77
|
+
#
|
78
|
+
# Given
|
79
|
+
#
|
80
|
+
# class Manifest
|
81
|
+
# include Ripple::Documnet
|
82
|
+
# many :signatures
|
83
|
+
# accepts_nested_attributes_for :signatures
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# class Signature
|
87
|
+
# include Ripple::EmbeddedDocument
|
88
|
+
# property :esignature, String
|
89
|
+
# end
|
90
|
+
#
|
91
|
+
# The assigning of attributes replaces existing:
|
92
|
+
#
|
93
|
+
# manifest = Manifest.create(:signature_attributes => [ { :esig => 'a00001' }, { :esig => 'b00001' } ]
|
94
|
+
# manifest.signatures # => [<Signature esig="a00001">, <Signature esig="b00001">]
|
95
|
+
#
|
96
|
+
# manifest.signature_attributes = [ { :esig => 'c00001' } ]
|
97
|
+
# manifest.signatures # => [<Signature esig="c00001">]
|
98
|
+
#
|
99
|
+
module ClassMethods
|
100
|
+
|
101
|
+
def accepts_nested_attributes_for(*attr_names)
|
102
|
+
options = { :allow_destroy => false }
|
103
|
+
options.update(attr_names.extract_options!)
|
104
|
+
|
105
|
+
attr_names.each do |association_name|
|
106
|
+
if association = self.associations[association_name]
|
107
|
+
nested_attributes_options[association_name.to_sym] = options
|
108
|
+
|
109
|
+
class_eval %{
|
110
|
+
def #{association_name}_attributes=(attributes)
|
111
|
+
assign_nested_attributes_for_#{association.type}_association(:#{association_name}, attributes)
|
112
|
+
end
|
113
|
+
|
114
|
+
before_save :autosave_nested_attributes_for_#{association_name}
|
115
|
+
before_save :destroy_marked_for_destruction
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def autosave_nested_attributes_for_#{association_name}
|
120
|
+
save_nested_attributes_for_#{association.type}_association(:#{association_name}) if self.autosave[:#{association_name}]
|
121
|
+
end
|
122
|
+
}, __FILE__, __LINE__
|
123
|
+
else
|
124
|
+
raise ArgumentError, "Association #{association_name} not found!"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
module InstanceMethods
|
131
|
+
|
132
|
+
protected
|
133
|
+
|
134
|
+
def autosave
|
135
|
+
@autosave_nested_attributes_for ||= {}
|
136
|
+
end
|
137
|
+
|
138
|
+
def marked_for_destruction
|
139
|
+
@marked_for_destruction ||= {}
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
def save_nested_attributes_for_one_association(association_name)
|
145
|
+
send(association_name).save
|
146
|
+
end
|
147
|
+
|
148
|
+
def save_nested_attributes_for_many_association(association_name)
|
149
|
+
send(association_name).map(&:save)
|
150
|
+
end
|
151
|
+
|
152
|
+
def destroy_marked_for_destruction
|
153
|
+
self.marked_for_destruction.each_pair do |association_name, resources|
|
154
|
+
resources.map(&:destroy)
|
155
|
+
send(association_name).reload
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def destroy_nested_many_association(association_name)
|
160
|
+
send(association_name).map(&:destroy)
|
161
|
+
end
|
162
|
+
|
163
|
+
def assign_nested_attributes_for_one_association(association_name, attributes)
|
164
|
+
association = self.class.associations[association_name]
|
165
|
+
if association.embeddable?
|
166
|
+
assign_nested_attributes_for_one_embedded_association(association_name, attributes)
|
167
|
+
else
|
168
|
+
self.autosave[association_name] = true
|
169
|
+
assign_nested_attributes_for_one_linked_association(association_name, attributes)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def assign_nested_attributes_for_one_embedded_association(association_name, attributes)
|
174
|
+
send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
|
175
|
+
end
|
176
|
+
|
177
|
+
def assign_nested_attributes_for_one_linked_association(association_name, attributes)
|
178
|
+
attributes = attributes.stringify_keys
|
179
|
+
options = nested_attributes_options[association_name]
|
180
|
+
|
181
|
+
if attributes[key_attr.to_s].blank? && !reject_new_record?(association_name, attributes)
|
182
|
+
send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
|
183
|
+
else
|
184
|
+
if ((existing_record = send(association_name)).key.to_s == attributes[key_attr.to_s].to_s)
|
185
|
+
assign_to_or_mark_for_destruction(existing_record, attributes, association_name, options[:allow_destroy])
|
186
|
+
else
|
187
|
+
raise ArgumentError, "Attempting to update a child that isn't already associated to the parent."
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def assign_nested_attributes_for_many_association(association_name, attributes_collection)
|
193
|
+
unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
|
194
|
+
raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
|
195
|
+
end
|
196
|
+
|
197
|
+
if attributes_collection.is_a? Hash
|
198
|
+
attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
|
199
|
+
end
|
200
|
+
|
201
|
+
association = self.class.associations[association_name]
|
202
|
+
if association.embeddable?
|
203
|
+
assign_nested_attributes_for_many_embedded_association(association_name, attributes_collection)
|
204
|
+
else
|
205
|
+
self.autosave[association_name] = true
|
206
|
+
assign_nested_attributes_for_many_linked_association(association_name, attributes_collection)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def assign_nested_attributes_for_many_embedded_association(association_name, attributes_collection)
|
211
|
+
options = nested_attributes_options[association_name]
|
212
|
+
send(:"#{association_name}=", []) # Clobber existing
|
213
|
+
attributes_collection.each do |attributes|
|
214
|
+
attributes = attributes.stringify_keys
|
215
|
+
if !reject_new_record?(association_name, attributes)
|
216
|
+
send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def assign_nested_attributes_for_many_linked_association(association_name, attributes_collection)
|
222
|
+
options = nested_attributes_options[association_name]
|
223
|
+
attributes_collection.each do |attributes|
|
224
|
+
attributes = attributes.stringify_keys
|
225
|
+
|
226
|
+
if attributes[key_attr.to_s].blank? && !reject_new_record?(association_name, attributes)
|
227
|
+
send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
|
228
|
+
elsif existing_record = send(association_name).detect { |record| record.key.to_s == attributes[key_attr.to_s].to_s }
|
229
|
+
assign_to_or_mark_for_destruction(existing_record, attributes, association_name, options[:allow_destroy])
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def assign_to_or_mark_for_destruction(record, attributes, association_name, allow_destroy)
|
236
|
+
if has_destroy_flag?(attributes) && allow_destroy
|
237
|
+
(self.marked_for_destruction[association_name] ||= []) << record
|
238
|
+
else
|
239
|
+
record.attributes = attributes.except(*UNASSIGNABLE_KEYS)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def has_destroy_flag?(hash)
|
244
|
+
TRUE_VALUES.include?(hash.stringify_keys['_destroy'])
|
245
|
+
end
|
246
|
+
|
247
|
+
def reject_new_record?(association_name, attributes)
|
248
|
+
has_destroy_flag?(attributes) || call_reject_if(association_name, attributes)
|
249
|
+
end
|
250
|
+
|
251
|
+
def call_reject_if(association_name, attributes)
|
252
|
+
attributes = attributes.stringify_keys
|
253
|
+
case callback = nested_attributes_options[association_name][:reject_if]
|
254
|
+
when Symbol
|
255
|
+
method(callback).arity == 0 ? send(callback) : send(callback, attributes)
|
256
|
+
when Proc
|
257
|
+
callback.call(attributes)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
end
|
262
|
+
|
263
|
+
end
|
data/lib/ripple/properties.rb
CHANGED
@@ -58,9 +58,10 @@ module Ripple
|
|
58
58
|
|
59
59
|
# @return [Object] The default value for this property if defined, or nil.
|
60
60
|
def default
|
61
|
-
|
62
|
-
|
63
|
-
|
61
|
+
default = options[:default]
|
62
|
+
|
63
|
+
return nil if default.nil?
|
64
|
+
type_cast(default.respond_to?(:call) ? default.call : default)
|
64
65
|
end
|
65
66
|
|
66
67
|
# @return [Hash] options appropriate for the validates class method
|
data/lib/ripple.rb
CHANGED
@@ -14,8 +14,10 @@
|
|
14
14
|
require File.expand_path("../../../spec_helper", __FILE__)
|
15
15
|
|
16
16
|
describe "Ripple Associations" do
|
17
|
+
require 'support/test_server'
|
18
|
+
|
17
19
|
before :all do
|
18
|
-
Object.module_eval do
|
20
|
+
Object.module_eval do
|
19
21
|
class User
|
20
22
|
include Ripple::Document
|
21
23
|
one :profile
|
@@ -37,7 +39,7 @@ describe "Ripple Associations" do
|
|
37
39
|
end
|
38
40
|
end
|
39
41
|
end
|
40
|
-
|
42
|
+
|
41
43
|
before :each do
|
42
44
|
@user = User.new(:email => 'riak@ripple.com')
|
43
45
|
@profile = Profile.new(:name => 'Ripple')
|
@@ -46,7 +48,7 @@ describe "Ripple Associations" do
|
|
46
48
|
@friend1 = User.create(:email => "friend@ripple.com")
|
47
49
|
@friend2 = User.create(:email => "friend2@ripple.com")
|
48
50
|
end
|
49
|
-
|
51
|
+
|
50
52
|
it "should save one embedded associations" do
|
51
53
|
@user.profile = @profile
|
52
54
|
@user.save
|
@@ -55,7 +57,7 @@ describe "Ripple Associations" do
|
|
55
57
|
@found.profile.should be_a(Profile)
|
56
58
|
@found.profile.user.should == @found
|
57
59
|
end
|
58
|
-
|
60
|
+
|
59
61
|
it "should save many embedded associations" do
|
60
62
|
@user.addresses << @billing << @shipping
|
61
63
|
@user.save
|
@@ -71,7 +73,7 @@ describe "Ripple Associations" do
|
|
71
73
|
@ship.should be_a(Address)
|
72
74
|
end
|
73
75
|
|
74
|
-
it "should save a many linked association" do
|
76
|
+
it "should save a many linked association" do
|
75
77
|
@user.friends << @friend1 << @friend2
|
76
78
|
@user.save
|
77
79
|
@user.should_not be_new_record
|
@@ -87,7 +89,7 @@ describe "Ripple Associations" do
|
|
87
89
|
@found = User.find(@user.key)
|
88
90
|
@found.emergency_contact.key.should == @friend1.key
|
89
91
|
end
|
90
|
-
|
92
|
+
|
91
93
|
after :each do
|
92
94
|
User.destroy_all
|
93
95
|
end
|
@@ -97,5 +99,5 @@ describe "Ripple Associations" do
|
|
97
99
|
Object.send(:remove_const, :Profile)
|
98
100
|
Object.send(:remove_const, :Address)
|
99
101
|
end
|
100
|
-
|
102
|
+
|
101
103
|
end
|
@@ -0,0 +1,260 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
describe Ripple::NestedAttributes do
|
4
|
+
require 'support/models/car'
|
5
|
+
require 'support/models/driver'
|
6
|
+
require 'support/models/passenger'
|
7
|
+
require 'support/models/engine'
|
8
|
+
require 'support/models/seat'
|
9
|
+
require 'support/models/wheel'
|
10
|
+
require 'support/test_server'
|
11
|
+
|
12
|
+
context "one :driver (link)" do
|
13
|
+
subject { Car.new }
|
14
|
+
|
15
|
+
it { should respond_to(:driver_attributes=) }
|
16
|
+
|
17
|
+
it "should not have a driver" do
|
18
|
+
subject.driver.should be_nil
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "creation" do
|
22
|
+
subject { Car.new(:make => 'VW', :model => 'Rabbit', :driver_attributes => { :name => 'Speed Racer' }) }
|
23
|
+
|
24
|
+
it "should have a driver of class Driver" do
|
25
|
+
subject.driver.should be_a(Driver)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should have a driver with name 'Speed Racer'" do
|
29
|
+
subject.driver.name.should == 'Speed Racer'
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should save the child when saving the parent" do
|
33
|
+
subject.driver.should_receive(:save)
|
34
|
+
subject.save
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "update" do
|
39
|
+
let(:driver) { Driver.create(:name => 'Slow Racer') }
|
40
|
+
|
41
|
+
before do
|
42
|
+
subject.driver = driver
|
43
|
+
subject.save
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should have a driver" do
|
47
|
+
subject.driver.should == driver
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should update attributes" do
|
51
|
+
subject.driver_attributes = { :name => 'Speed Racer' }
|
52
|
+
subject.driver.name.should == 'Speed Racer'
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should not save the child if attributes haven't been updated" do
|
56
|
+
subject.driver.should_not_receive(:save)
|
57
|
+
subject.save
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should save the child when saving the parent" do
|
61
|
+
subject.driver_attributes = { :name => 'Speed Racer' }
|
62
|
+
subject.driver.should_receive(:save)
|
63
|
+
subject.save
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context "many :passengers (link)" do
|
69
|
+
subject { Car.new }
|
70
|
+
|
71
|
+
it { should respond_to(:passengers_attributes=) }
|
72
|
+
|
73
|
+
it "should not have passengers" do
|
74
|
+
subject.passengers.should == []
|
75
|
+
end
|
76
|
+
|
77
|
+
describe "creation" do
|
78
|
+
subject { Car.new(:make => 'VW',
|
79
|
+
:model => 'Rabbit',
|
80
|
+
:passengers_attributes => [ { :name => 'Joe' },
|
81
|
+
{ :name => 'Sue' },
|
82
|
+
{ :name => 'Pat' } ] ) }
|
83
|
+
|
84
|
+
it "should have 3 passengers" do
|
85
|
+
subject.passengers.size.should == 3
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should have 3 passengers with specified names" do
|
89
|
+
subject.passengers.first.name.should == 'Joe'
|
90
|
+
subject.passengers.second.name.should == 'Sue'
|
91
|
+
subject.passengers.third.name.should == 'Pat'
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should save the children when saving the parent" do
|
95
|
+
subject.passengers.each do |passenger|
|
96
|
+
passenger.should_receive(:save)
|
97
|
+
end
|
98
|
+
subject.save
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe "update" do
|
103
|
+
let(:passenger1) { Passenger.create(:name => 'One') }
|
104
|
+
let(:passenger2) { Passenger.create(:name => 'Two') }
|
105
|
+
let(:passenger3) { Passenger.create(:name => 'Three') }
|
106
|
+
|
107
|
+
before do
|
108
|
+
subject.passengers << passenger1
|
109
|
+
subject.passengers << passenger2
|
110
|
+
subject.passengers << passenger3
|
111
|
+
subject.save
|
112
|
+
end
|
113
|
+
|
114
|
+
it "should have 3 passengers" do
|
115
|
+
subject.passengers.size.should == 3
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should update attributes" do
|
119
|
+
subject.passengers_attributes = [ { :key => passenger1.key, :name => 'UPDATED One' },
|
120
|
+
{ :key => passenger2.key, :name => 'UPDATED Two' },
|
121
|
+
{ :key => passenger3.key, :name => 'UPDATED Three' } ]
|
122
|
+
subject.passengers.first.name.should == 'UPDATED One'
|
123
|
+
subject.passengers.second.name.should == 'UPDATED Two'
|
124
|
+
subject.passengers.third.name.should == 'UPDATED Three'
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should not save the child if attributes haven't been updated" do
|
128
|
+
subject.passengers.each do |passenger|
|
129
|
+
passenger.should_not_receive(:save)
|
130
|
+
end
|
131
|
+
subject.save
|
132
|
+
end
|
133
|
+
|
134
|
+
it "should save the child when saving the parent" do
|
135
|
+
subject.passengers_attributes = [ { :key => passenger1.key, :name => 'UPDATED One' },
|
136
|
+
{ :key => passenger1.key, :name => 'UPDATED Two' },
|
137
|
+
{ :key => passenger1.key, :name => 'UPDATED Three' } ]
|
138
|
+
subject.passengers.each do |passenger|
|
139
|
+
passenger.should_receive(:save)
|
140
|
+
end
|
141
|
+
subject.save
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
context "one :engine (embedded)" do
|
147
|
+
subject { Car.new }
|
148
|
+
|
149
|
+
it { should respond_to(:engine_attributes=) }
|
150
|
+
|
151
|
+
it "should not have an engine" do
|
152
|
+
subject.engine.should be_nil
|
153
|
+
end
|
154
|
+
|
155
|
+
describe "creation" do
|
156
|
+
subject { Car.new(:make => 'VW', :model => 'Rabbit', :engine_attributes => { :displacement => '2.5L' }) }
|
157
|
+
|
158
|
+
it "should have an engine of class Engine" do
|
159
|
+
subject.engine.should be_a(Engine)
|
160
|
+
end
|
161
|
+
|
162
|
+
it "should have a engine with displacement '2.5L'" do
|
163
|
+
subject.engine.displacement.should == '2.5L'
|
164
|
+
end
|
165
|
+
|
166
|
+
it "should save the child when saving the parent" do
|
167
|
+
subject.engine.should_not_receive(:save)
|
168
|
+
subject.save
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
describe "update" do
|
173
|
+
before do
|
174
|
+
subject.engine.build(:displacement => '3.6L')
|
175
|
+
subject.save
|
176
|
+
end
|
177
|
+
|
178
|
+
it "should have a specified engine" do
|
179
|
+
subject.engine.displacement.should == '3.6L'
|
180
|
+
end
|
181
|
+
|
182
|
+
it "should update attributes" do
|
183
|
+
subject.engine_attributes = { :displacement => 'UPDATED 3.6L' }
|
184
|
+
subject.engine.displacement.should == 'UPDATED 3.6L'
|
185
|
+
end
|
186
|
+
|
187
|
+
it "should not save the child if attributes haven't been updated" do
|
188
|
+
subject.engine.should_not_receive(:save)
|
189
|
+
subject.save
|
190
|
+
end
|
191
|
+
|
192
|
+
it "should not save the child when saving the parent" do
|
193
|
+
subject.engine_attributes = { :displacement => 'UPDATED 3.6L' }
|
194
|
+
subject.engine.should_not_receive(:save)
|
195
|
+
subject.save
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
context "many :seats (embedded)" do
|
201
|
+
subject { Car.new }
|
202
|
+
|
203
|
+
it { should respond_to(:seats_attributes=) }
|
204
|
+
|
205
|
+
it "should not have passengers" do
|
206
|
+
subject.seats.should == []
|
207
|
+
end
|
208
|
+
|
209
|
+
describe "creation" do
|
210
|
+
subject { Car.new(:make => 'VW',
|
211
|
+
:model => 'Rabbit',
|
212
|
+
:seats_attributes => [ { :color => 'red' },
|
213
|
+
{ :color => 'blue' },
|
214
|
+
{ :color => 'brown' } ] ) }
|
215
|
+
|
216
|
+
it "should have 3 seats" do
|
217
|
+
subject.seats.size.should == 3
|
218
|
+
end
|
219
|
+
|
220
|
+
it "should have 3 passengers with specified names" do
|
221
|
+
subject.seats.first.color.should == 'red'
|
222
|
+
subject.seats.second.color.should == 'blue'
|
223
|
+
subject.seats.third.color.should == 'brown'
|
224
|
+
end
|
225
|
+
|
226
|
+
specify "replace/clobber" do
|
227
|
+
subject.seats_attributes = [ { :color => 'orange' } ]
|
228
|
+
subject.seats.size.should == 1
|
229
|
+
subject.seats.first.color.should == 'orange'
|
230
|
+
end
|
231
|
+
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
context ":reject_if" do
|
236
|
+
it "should not create a wheel" do
|
237
|
+
car = Car.new(:wheels_attributes => [ { :diameter => 10 } ])
|
238
|
+
car.wheels.should == []
|
239
|
+
end
|
240
|
+
|
241
|
+
it "should create a wheel" do
|
242
|
+
car = Car.new(:wheels_attributes => [ { :diameter => 16 } ])
|
243
|
+
car.wheels.size.should == 1
|
244
|
+
car.wheels.first.diameter.should == 16
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
context ":allow_delete" do
|
249
|
+
let(:wheel) { Wheel.create(:diameter => 17) }
|
250
|
+
subject { Car.create(:wheels => [ wheel ] ) }
|
251
|
+
|
252
|
+
it "should allow us to delete the wheel" do
|
253
|
+
subject.wheels_attributes = [ { :key => wheel.key, :_destroy => "1" } ]
|
254
|
+
subject.save
|
255
|
+
subject.wheels.should == []
|
256
|
+
end
|
257
|
+
|
258
|
+
end
|
259
|
+
|
260
|
+
end
|
@@ -15,7 +15,7 @@ require File.expand_path("../../spec_helper", __FILE__)
|
|
15
15
|
|
16
16
|
describe Ripple::AttributeMethods do
|
17
17
|
require 'support/models/widget'
|
18
|
-
|
18
|
+
|
19
19
|
before :each do
|
20
20
|
@widget = Widget.new
|
21
21
|
end
|
@@ -37,7 +37,7 @@ describe Ripple::AttributeMethods do
|
|
37
37
|
@widget.attributes = {'key' => 'new-key'}
|
38
38
|
@widget.key.should == 'widget-key'
|
39
39
|
end
|
40
|
-
|
40
|
+
|
41
41
|
it "should typecast the key to a string" do
|
42
42
|
@widget.key = 10
|
43
43
|
@widget.key.should == "10"
|
@@ -56,8 +56,9 @@ describe Ripple::AttributeMethods do
|
|
56
56
|
|
57
57
|
it "should return the property default if defined and not set" do
|
58
58
|
@widget.name.should == "widget"
|
59
|
+
@widget.manufactured.should == false
|
59
60
|
end
|
60
|
-
|
61
|
+
|
61
62
|
it "should allow raw attribute access when accessing the document with []" do
|
62
63
|
@widget['name'].should == 'widget'
|
63
64
|
end
|
@@ -78,7 +79,7 @@ describe Ripple::AttributeMethods do
|
|
78
79
|
@widget.size = 10
|
79
80
|
@widget.size.should == 10
|
80
81
|
end
|
81
|
-
|
82
|
+
|
82
83
|
it "should allow assignment of undefined attributes when assigning to the document with []=" do
|
83
84
|
@widget['name'] = 'sprocket'
|
84
85
|
@widget.name.should == 'sprocket'
|
@@ -136,7 +137,7 @@ describe Ripple::AttributeMethods do
|
|
136
137
|
end
|
137
138
|
|
138
139
|
it "should provide a hash representation of all of the attributes" do
|
139
|
-
@widget.attributes.should == {"name" => "widget", "size" => nil}
|
140
|
+
@widget.attributes.should == {"name" => "widget", "size" => nil, "manufactured" => false}
|
140
141
|
end
|
141
142
|
|
142
143
|
it "should load attributes from mass assignment" do
|
@@ -154,24 +155,24 @@ describe Ripple::AttributeMethods do
|
|
154
155
|
@widget = Widget.new(:name => "Riak")
|
155
156
|
@widget.changes.should be_blank
|
156
157
|
end
|
157
|
-
|
158
|
+
|
158
159
|
it "should allow adding to the @attributes hash for attributes that do not exist" do
|
159
160
|
@widget = Widget.new
|
160
161
|
@widget['foo'] = 'bar'
|
161
162
|
@widget.instance_eval { @attributes['foo'] }.should == 'bar'
|
162
163
|
end
|
163
|
-
|
164
|
+
|
164
165
|
it "should allow reading from the @attributes hash for attributes that do not exist" do
|
165
166
|
@widget = Widget.new
|
166
167
|
@widget['foo'] = 'bar'
|
167
168
|
@widget['foo'].should == 'bar'
|
168
169
|
end
|
169
|
-
|
170
|
+
|
170
171
|
it "should allow a block upon initialization to set attributes protected from mass assignment" do
|
171
172
|
@widget = Widget.new { |w| w.key = 'some-key' }
|
172
173
|
@widget.key.should == 'some-key'
|
173
174
|
end
|
174
|
-
|
175
|
+
|
175
176
|
it "should raise an argument error when assigning a non hash to attributes" do
|
176
177
|
@widget = Widget.new
|
177
178
|
lambda { @widget.attributes = nil }.should raise_error(ArgumentError)
|
@@ -77,6 +77,11 @@ describe Ripple::Property do
|
|
77
77
|
prop.default.should == "bar"
|
78
78
|
end
|
79
79
|
|
80
|
+
it "should allow false for a Boolean" do
|
81
|
+
prop = Ripple::Property.new('foo', Boolean, :default => false)
|
82
|
+
prop.default.should == false
|
83
|
+
end
|
84
|
+
|
80
85
|
it "should allow lambdas for deferred evaluation" do
|
81
86
|
prop = Ripple::Property.new('foo', String, :default => lambda { "bar" })
|
82
87
|
prop.default.should == "bar"
|
@@ -122,11 +127,11 @@ describe Ripple::Property do
|
|
122
127
|
@prop.type_cast([]).should == "[]"
|
123
128
|
end
|
124
129
|
end
|
125
|
-
|
130
|
+
|
126
131
|
it "should not cast nil" do
|
127
132
|
@prop.type_cast(nil).should be_nil
|
128
133
|
end
|
129
|
-
|
134
|
+
|
130
135
|
end
|
131
136
|
|
132
137
|
describe "when type is an Integer type" do
|
data/spec/spec_helper.rb
CHANGED
@@ -17,10 +17,13 @@ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'riak-client','
|
|
17
17
|
|
18
18
|
require 'rubygems' # Use the gems path only for the spec suite
|
19
19
|
require 'ripple'
|
20
|
-
require 'rspec
|
20
|
+
require 'rspec'
|
21
21
|
|
22
22
|
Dir[File.join(File.dirname(__FILE__), "support", "*.rb")].each {|f| require f }
|
23
23
|
|
24
24
|
Rspec.configure do |config|
|
25
25
|
config.mock_with :rspec
|
26
|
+
config.after(:each) do
|
27
|
+
$test_server.recycle if $test_server
|
28
|
+
end
|
26
29
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Car
|
2
|
+
include Ripple::Document
|
3
|
+
|
4
|
+
property :make, String
|
5
|
+
property :model, String
|
6
|
+
|
7
|
+
one :driver # linked, key_on :name
|
8
|
+
many :passengers # linked, standard :key
|
9
|
+
one :engine # embedded
|
10
|
+
many :seats # embedded
|
11
|
+
many :wheels
|
12
|
+
|
13
|
+
accepts_nested_attributes_for :driver, :passengers, :engine, :seats
|
14
|
+
accepts_nested_attributes_for :wheels, :reject_if => proc{|attrs| attrs['diameter'] < 12 }, :allow_destroy => true
|
15
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'riak/test_server'
|
2
|
+
|
3
|
+
unless $test_server
|
4
|
+
begin
|
5
|
+
require 'yaml'
|
6
|
+
config = YAML.load_file("spec/support/test_server.yml")
|
7
|
+
$test_server = Riak::TestServer.new(config.symbolize_keys)
|
8
|
+
$test_server.prepare!
|
9
|
+
$test_server.start
|
10
|
+
Ripple.config = {:port => 9000 }
|
11
|
+
at_exit { $test_server.cleanup }
|
12
|
+
rescue => e
|
13
|
+
warn "Can't run Riak::TestServer specs. Specify the location of your Riak installation in spec/support/test_server.yml. See Riak::TestServer docs for more info."
|
14
|
+
warn e.inspect
|
15
|
+
$test_server = nil
|
16
|
+
end
|
17
|
+
end
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 8
|
8
|
-
-
|
9
|
-
version: 0.8.
|
8
|
+
- 1
|
9
|
+
version: 0.8.1
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Sean Cribbs
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-
|
17
|
+
date: 2010-10-11 00:00:00 -04:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -45,8 +45,8 @@ dependencies:
|
|
45
45
|
segments:
|
46
46
|
- 0
|
47
47
|
- 8
|
48
|
-
-
|
49
|
-
version: 0.8.
|
48
|
+
- 1
|
49
|
+
version: 0.8.1
|
50
50
|
type: :runtime
|
51
51
|
version_requirements: *id002
|
52
52
|
- !ruby/object:Gem::Dependency
|
@@ -122,6 +122,7 @@ files:
|
|
122
122
|
- lib/ripple/i18n.rb
|
123
123
|
- lib/ripple/inspection.rb
|
124
124
|
- lib/ripple/locale/en.yml
|
125
|
+
- lib/ripple/nested_attributes.rb
|
125
126
|
- lib/ripple/properties.rb
|
126
127
|
- lib/ripple/property_type_mismatch.rb
|
127
128
|
- lib/ripple/railtie.rb
|
@@ -133,6 +134,7 @@ files:
|
|
133
134
|
- Rakefile
|
134
135
|
- spec/fixtures/config.yml
|
135
136
|
- spec/integration/ripple/associations_spec.rb
|
137
|
+
- spec/integration/ripple/nested_attributes_spec.rb
|
136
138
|
- spec/integration/ripple/persistence_spec.rb
|
137
139
|
- spec/ripple/associations/many_embedded_proxy_spec.rb
|
138
140
|
- spec/ripple/associations/many_linked_proxy_spec.rb
|
@@ -162,10 +164,13 @@ files:
|
|
162
164
|
- spec/support/mocks.rb
|
163
165
|
- spec/support/models/address.rb
|
164
166
|
- spec/support/models/box.rb
|
167
|
+
- spec/support/models/car.rb
|
165
168
|
- spec/support/models/cardboard_box.rb
|
166
169
|
- spec/support/models/clock.rb
|
167
170
|
- spec/support/models/customer.rb
|
171
|
+
- spec/support/models/driver.rb
|
168
172
|
- spec/support/models/email.rb
|
173
|
+
- spec/support/models/engine.rb
|
169
174
|
- spec/support/models/family.rb
|
170
175
|
- spec/support/models/favorite.rb
|
171
176
|
- spec/support/models/invoice.rb
|
@@ -173,10 +178,15 @@ files:
|
|
173
178
|
- spec/support/models/note.rb
|
174
179
|
- spec/support/models/page.rb
|
175
180
|
- spec/support/models/paid_invoice.rb
|
181
|
+
- spec/support/models/passenger.rb
|
182
|
+
- spec/support/models/seat.rb
|
176
183
|
- spec/support/models/tasks.rb
|
177
184
|
- spec/support/models/tree.rb
|
178
185
|
- spec/support/models/user.rb
|
186
|
+
- spec/support/models/wheel.rb
|
179
187
|
- spec/support/models/widget.rb
|
188
|
+
- spec/support/test_server.rb
|
189
|
+
- spec/support/test_server.yml
|
180
190
|
has_rdoc: true
|
181
191
|
homepage: http://seancribbs.github.com/ripple
|
182
192
|
licenses: []
|
@@ -211,6 +221,7 @@ specification_version: 3
|
|
211
221
|
summary: ripple is an object-mapper library for Riak, the distributed database by Basho.
|
212
222
|
test_files:
|
213
223
|
- spec/integration/ripple/associations_spec.rb
|
224
|
+
- spec/integration/ripple/nested_attributes_spec.rb
|
214
225
|
- spec/integration/ripple/persistence_spec.rb
|
215
226
|
- spec/ripple/associations/many_embedded_proxy_spec.rb
|
216
227
|
- spec/ripple/associations/many_linked_proxy_spec.rb
|
@@ -240,10 +251,13 @@ test_files:
|
|
240
251
|
- spec/support/mocks.rb
|
241
252
|
- spec/support/models/address.rb
|
242
253
|
- spec/support/models/box.rb
|
254
|
+
- spec/support/models/car.rb
|
243
255
|
- spec/support/models/cardboard_box.rb
|
244
256
|
- spec/support/models/clock.rb
|
245
257
|
- spec/support/models/customer.rb
|
258
|
+
- spec/support/models/driver.rb
|
246
259
|
- spec/support/models/email.rb
|
260
|
+
- spec/support/models/engine.rb
|
247
261
|
- spec/support/models/family.rb
|
248
262
|
- spec/support/models/favorite.rb
|
249
263
|
- spec/support/models/invoice.rb
|
@@ -251,7 +265,11 @@ test_files:
|
|
251
265
|
- spec/support/models/note.rb
|
252
266
|
- spec/support/models/page.rb
|
253
267
|
- spec/support/models/paid_invoice.rb
|
268
|
+
- spec/support/models/passenger.rb
|
269
|
+
- spec/support/models/seat.rb
|
254
270
|
- spec/support/models/tasks.rb
|
255
271
|
- spec/support/models/tree.rb
|
256
272
|
- spec/support/models/user.rb
|
273
|
+
- spec/support/models/wheel.rb
|
257
274
|
- spec/support/models/widget.rb
|
275
|
+
- spec/support/test_server.rb
|