ripple 0.8.0 → 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|