daylight 0.9.0.rc2 → 0.9.0.rc3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6bf59fd5ce024afa3fc59ddb2d66363f8f5d5209
4
- data.tar.gz: 7dfddabc42f86705bf30f51cdbe41cd879e58bb6
3
+ metadata.gz: bb3cda8546b99d74c72bf1af1ca14aabb51f3d8d
4
+ data.tar.gz: 41ad2d678d9aa293cc766179e2441789b7b351be
5
5
  SHA512:
6
- metadata.gz: 97b17af7fc52458bf4f0b255aaf404fa55821ca6b28b2be6885ecadd3618a22ccbc613b8c64ef011aebc4b12e905164ce29619598fd6b63ed19a0c169d28d5c9
7
- data.tar.gz: 757ffe41b8d52bc4fd6e373a3a2041541c1549e8f7878d88fa9c45d5d5ccdca7b546a58164588e59c30c52a62433adf5321bb4b889efe543bcc20dd1f4aed39a
6
+ metadata.gz: 75780f4b059adb79463f13b0b41e01ae105ecdcad262ca59f133d4624c0fe5557bcc927f9f8a341961d6be8bba5a1720fbd70af0e749202ee8d2aca704b95a99
7
+ data.tar.gz: 438f45ccd0e69581e5c2217d989bfe0b9fa238c8665d184632a76cd9055c6a83cb3df5257c7376c4a968b1904b5e61685f2dc2eb38ac7f97776437222f16edbd
data/doc/usage.md CHANGED
@@ -365,8 +365,9 @@ server-side.
365
365
  Daylight adds additional functionality directly on the association:
366
366
  * add new resources
367
367
  * update existing resources
368
- * add a new resource to a collection
368
+ * add a resource to a collection
369
369
  * associate two existing resources
370
+ * delete from an association
370
371
 
371
372
  Currently, `ActiveResource` will only let you associate a resource by setting
372
373
  the `foreign_key` directly on a model.
@@ -485,48 +486,27 @@ You can also add a nested object to an existing collection:
485
486
 
486
487
  #### Updating a Nested Resource
487
488
 
488
- Updates to nested resources are not saved by saving the parent resource.
489
- You must save the nested resources directly:
489
+ Updates to nested resources are saved by saving the parent resource.
490
490
 
491
491
  ````ruby
492
492
  post = API::Post.first
493
493
  post.author.full_name = "Reid MacDonald"
494
- post.author.save #=> true
495
-
496
- post = API::Post.first
497
- post.author.full_name #=> "Reid MacDonald"
498
- ````
499
-
500
- This is the same as saying:
501
-
502
- ````ruby
503
- post = API::Post.first
504
-
505
- author = post.author
506
- author.full_name = "Reid MacDonald"
507
- author.save #=> true
494
+ post.save #=> true
508
495
 
509
496
  post = API::Post.first
510
497
  post.author.full_name #=> "Reid MacDonald"
511
498
  ````
512
-
513
499
  The same is true of nested objects in collections:
514
500
 
515
501
  ````ruby
516
502
  post = API::Post.first
517
-
518
- first_comment = post.comments.first
519
- first_comment.message = "First!"
520
- first_comment.save #=> true
503
+ post.comments.first.message = "First!"
504
+ post.save #=> true
521
505
 
522
506
  post = API::Post.first
523
507
  post.comments.first.message #=> "First!"
524
508
  ````
525
509
 
526
- > FUTURE [#5](https://github.com/att-cloud/daylight/issues/5):
527
- > Updates to the associated nested resource do not get saved when the parent
528
- > resources are saved and they should be.
529
-
530
510
  #### Associating an Existing Nested Resources
531
511
 
532
512
  Associating using an existing nested records is possible with Daylight. The
@@ -557,8 +537,31 @@ This also will work to add to a collection on a new or existing resource:
557
537
  post.commenters.find {|c| c.username == 'reidmix'} # #<API::V1::User:0x007fe2cfc45ce8 ..>
558
538
  ````
559
539
 
560
- > FUTURE [#15](https://github.com/att-cloud/daylight/issues/15):
561
- > There is no way to remove an nested resource from a collection nor empty the collection.
540
+ #### Deleting Nested Resources
541
+
542
+ A nested resource can be removed from a collection:
543
+
544
+ ````ruby
545
+ post = API::Post.first
546
+ post.comments.count #=> 4
547
+ post.comments.shift
548
+ post.save #=> true
549
+
550
+ post = API::Post.first
551
+ post.comments.count #=> 3
552
+ ````
553
+
554
+ A collection can also be reset:
555
+
556
+ ````ruby
557
+ post = API::Post.first
558
+ post.comments.count #=> 4
559
+ post.comments = []
560
+ post.save #=> true
561
+
562
+ post = API::Post.first
563
+ post.comments.count #=> 0
564
+ ````
562
565
 
563
566
  ### More Chaining
564
567
 
@@ -788,17 +791,18 @@ When saving this model from the client errors will be exposed similar to
788
791
 
789
792
  With the introduction of and use of
790
793
  [Strong Parameters](http://guides.rubyonrails.org/action_controller_overview.html#strong-parameters)
791
- unpermitted or missing attributes will be detected.
794
+ unpermitted or missing attributes can be detected if `action_on_unpermitted_parameters` is set to `:raise`
795
+ in configuration:
792
796
 
793
- > FUTURE [#8](https://github.com/att-cloud/daylight/issues/8):
794
- > Would be nice to know which parameter is raising the error and if it was a
795
- > _required_ parameter or an _unpermitted_ one.
797
+ ````ruby
798
+ config.action_controller.action_on_unpermitted_parameters = :raise
799
+ ````
796
800
 
797
801
  Lets say `created_at` is not permitted on the `PostController`:
798
802
  ````ruby
799
803
  post = API::Post.new(created_at: Time.now)
800
804
  post.save # => false
801
- post.errors.messages # => {:base=>["Unpermitted or missing attribute"]}
805
+ post.errors.messages # => {'created_at'=>['unpermitted parameter']}
802
806
  ````
803
807
 
804
808
  ### Bad Requests
data/lib/daylight.rb CHANGED
@@ -19,6 +19,7 @@ module Daylight
19
19
  autoload :ReadOnly
20
20
  autoload :ResourceProxy
21
21
  autoload :ReflectionExt
22
+ autoload :AssociationPersistance
22
23
  autoload :API
23
24
  end
24
25
  end
data/lib/daylight/api.rb CHANGED
@@ -44,6 +44,7 @@ class Daylight::API < ActiveResource::Base
44
44
  include Daylight::ReadOnly
45
45
  include Daylight::Refinements
46
46
  include Daylight::Associations
47
+ prepend Daylight::AssociationPersistance
47
48
 
48
49
  class << self
49
50
  attr_reader :version, :versions, :namespace
@@ -186,7 +187,7 @@ class Daylight::API < ActiveResource::Base
186
187
  end
187
188
  end
188
189
 
189
- attr_reader :metadata
190
+ attr_reader :metadata, :hashcode, :association_hashcodes
190
191
 
191
192
  ##
192
193
  # Extends ActiveResource to allow for saving metadata from the responses on
@@ -199,6 +200,9 @@ class Daylight::API < ActiveResource::Base
199
200
  extract_metadata!(attributes)
200
201
 
201
202
  super
203
+
204
+ @association_hashcodes = {}.with_indifferent_access
205
+ @hashcode = self.attributes.hash
202
206
  end
203
207
 
204
208
  ##
@@ -269,6 +273,30 @@ class Daylight::API < ActiveResource::Base
269
273
  end
270
274
  end
271
275
 
276
+ ##
277
+ # Load object(s) for a reflection name from the given values which could be
278
+ # an Array or a Hash
279
+
280
+ def load_attributes_for(name, value)
281
+ case value
282
+ when Array
283
+ resource = nil
284
+ value.map do |attrs|
285
+ if attrs.is_a?(Hash)
286
+ resource ||= find_or_create_resource_for_collection(name)
287
+ resource.new(attrs, true)
288
+ else
289
+ attrs.duplicable? ? attrs.dup : attrs
290
+ end
291
+ end
292
+ when Hash
293
+ resource = find_or_create_resource_for(name)
294
+ resource.new(value, true)
295
+ else
296
+ value.duplicable? ? value.dup : value
297
+ end
298
+ end
299
+
272
300
  private
273
301
 
274
302
  ##
@@ -0,0 +1,57 @@
1
+ module Daylight::AssociationPersistance
2
+
3
+ # has our attributes changed since we were loaded?
4
+ def changed?
5
+ new? || hashcode != attributes.hash
6
+ end
7
+
8
+ def serializable_hash(options=nil)
9
+ super((options || {}).reverse_merge(include: association_includes))
10
+ end
11
+
12
+ protected
13
+
14
+ ##
15
+ # returns nil if no changes (ourself or our children)
16
+ # returns empty hash if we've changed, but our children haven't
17
+ # returns include key if some of our children have changed
18
+ #
19
+ # { include: { post: { include: { comment: {} } } }
20
+
21
+ def association_includes
22
+ include_hash = {}
23
+
24
+ self.class.reflection_names.each do |reflection_name|
25
+ association = instance_variable_get("@#{reflection_name}")
26
+ reflection_attribute_name = "#{reflection_name}_attributes"
27
+ # ignore associations that have not been set
28
+ next unless association
29
+
30
+ # recurse into the child(ren)
31
+ child_include_hash =
32
+ if association.respond_to?(:to_ary)
33
+ # merge all the includes from all the children
34
+ children_includes = association.to_ary.map {|child| child.association_includes }.compact
35
+ children_includes.reduce(:merge) if children_includes.present?
36
+ else
37
+ association.association_includes
38
+ end
39
+
40
+ if child_include_hash.present?
41
+ include_hash[reflection_attribute_name] = {include: child_include_hash}
42
+ elsif child_include_hash || changed_associations.include?(reflection_name)
43
+ include_hash[reflection_attribute_name] = {}
44
+ end
45
+ end
46
+
47
+ include_hash if changed? || include_hash.present?
48
+ end
49
+
50
+ ##
51
+ # list of associations that have been modified
52
+ #
53
+ def changed_associations
54
+ association_hashcodes.select {|association, code| send(association).hash != code }
55
+ end
56
+
57
+ end
@@ -73,7 +73,6 @@ module Daylight::Associations
73
73
  # define setter that places the value directly in the attributes using
74
74
  # the nested_attributes functionality server-side
75
75
  define_method "#{reflection.name}=" do |value|
76
- self.attributes[nested_attribute_key] = value
77
76
  instance_variable_set(:"@#{reflection.name}", value)
78
77
  end
79
78
 
@@ -93,15 +92,20 @@ module Daylight::Associations
93
92
  # ActiveResource::Associations#belongs_to
94
93
 
95
94
  def belongs_to name, options={}
96
- # continue to let the original do all the work.
97
- super.tap do |reflection|
95
+ create_reflection(:belongs_to, name, options).tap do |reflection|
96
+
97
+ nested_attribute_key = "#{reflection.name}_attributes"
98
+
99
+ # setup the resource_proxy to fetch the results
100
+ define_cached_method reflection.name, cache_key: nested_attribute_key do
101
+ reflection.klass.find(send(reflection.foreign_key))
102
+ end
98
103
 
99
104
  # Defines a setter caching the value in an instance variable for later
100
105
  # retrieval. Stash value directly in the attributes using the
101
106
  # nested_attributes functionality server-side.
102
107
  define_method "#{reflection.name}=" do |value|
103
108
  attributes[reflection.foreign_key] = value.id # set the foreign key
104
- attributes["#{reflection.name}_attributes"] = value # set the nested_attributes
105
109
  instance_variable_set(:"@#{reflection.name}", value) # set the cached value
106
110
  end
107
111
  end
@@ -198,7 +202,6 @@ module Daylight::Associations
198
202
  end
199
203
 
200
204
  define_method "#{reflection.name}=" do |value|
201
- attributes["#{reflection.name}_attributes"] = value # set the nested_attributes
202
205
  value.attributes[:"#{self.class.element_name}_id"] = self.id
203
206
  instance_variable_set(:"@#{reflection.name}", value) # set the cached value
204
207
  end
@@ -233,14 +236,23 @@ module Daylight::Associations
233
236
  cache_key = options[:cache_key] || method_name
234
237
  attributes = options.has_key?(:index) ? @attributes[options[:index]] : @attributes
235
238
 
236
- if instance_variable_defined?(ivar_name)
237
- instance_variable_get(ivar_name)
238
- elsif attributes.include?(cache_key)
239
- attributes[cache_key]
240
- else
241
- instance_variable_set ivar_name, send(uncached_method_name)
242
- end
239
+ return instance_variable_get(ivar_name) if instance_variable_defined?(ivar_name)
240
+
241
+ value =
242
+ if attributes.include?(cache_key)
243
+ load_attributes_for(method_name, attributes[cache_key])
244
+ else
245
+ send(uncached_method_name)
246
+ end
247
+
248
+ # Track of the association hashcode for changes
249
+ association_hashcodes[method_name] = value.hash
250
+
251
+ instance_variable_set ivar_name, value
243
252
  end
253
+
254
+ # alias our wrapper so calls to the attributes work
255
+ alias_method "#{method_name}_attributes", method_name
244
256
  end
245
257
 
246
258
  end
@@ -23,14 +23,14 @@ module Daylight::ReadOnly
23
23
  end
24
24
 
25
25
  ##
26
- # Adds API specific options when generating json.
26
+ # Adds API specific options when generating a serializable hash.
27
27
  # Removes read_only attributes for requests.
28
28
  #
29
29
  # See
30
30
  # except_read_only
31
31
 
32
- def as_json(options={})
33
- super(except_read_only(options))
32
+ def serializable_hash(options=nil)
33
+ super(except_read_only(options || {}))
34
34
  end
35
35
 
36
36
  ##
@@ -44,6 +44,7 @@ module Daylight::ReadOnly
44
44
  super(except_read_only(options))
45
45
  end
46
46
 
47
+
47
48
  ##
48
49
  # Writers for read_only attributes are not included as methods
49
50
  #--
@@ -75,7 +76,7 @@ module Daylight::ReadOnly
75
76
  ##
76
77
  # Ensures that read_only attributes are merged in with `:except` options.
77
78
 
78
- def except_read_only options
79
+ def except_read_only options={}
79
80
  options.merge(except: (options[:except]||[]).push(*read_only))
80
81
  end
81
82
 
@@ -18,17 +18,6 @@ class Daylight::ResourceProxy
18
18
  @association_name, @association_resource = association.first
19
19
  end
20
20
 
21
- ##
22
- # Delegates appending to the association resource when available
23
-
24
- def << value
25
- if association_resource
26
- association_resource.send("#{association_name}=", records.elements << value)
27
- else
28
- raise NoMethodError, "undefined method `<<' for #{self}"
29
- end
30
- end
31
-
32
21
  ##
33
22
  # Sets `from` URL on a request
34
23
  def from from
@@ -118,11 +107,16 @@ class Daylight::ResourceProxy
118
107
  end
119
108
 
120
109
  ##
121
- # Sets the limit to the current parameters, and fetches the first result.
110
+ # If loaded, return the first element, otherwise -
111
+ # sets the limit to the current parameters, and fetches the first result.
122
112
  # Immediately issues the request to the API.
123
113
 
124
114
  def first
125
- limit(1).to_a.first
115
+ if loaded?
116
+ to_a.first
117
+ else
118
+ limit(1).to_a.first
119
+ end
126
120
  end
127
121
 
128
122
  ##
@@ -183,8 +177,7 @@ class Daylight::ResourceProxy
183
177
 
184
178
  ##
185
179
  # Will attempt to fulfill the method if it exists on the resource or if it
186
- # exists on an Array. Defines or delegates the method on for subsequent
187
- # execution.
180
+ # exists on an Array. Delegates the method on for subsequent execution.
188
181
 
189
182
  def method_missing(method_name, *args, &block)
190
183
  if resource_class.respond_to?(method_name)
@@ -193,8 +186,9 @@ class Daylight::ResourceProxy
193
186
  end
194
187
  resource_class.send(method_name, *args, &block)
195
188
  elsif Array.method_defined?(method_name)
196
- self.class.delegate method_name, :to => :to_a
197
- to_a.send(method_name, *args, &block)
189
+ wrap_array_method(method_name)
190
+ # resend call to newly wrapped method
191
+ send(method_name, *args, &block)
198
192
  else
199
193
  super
200
194
  end
@@ -223,4 +217,29 @@ class Daylight::ResourceProxy
223
217
  @records = nil
224
218
  self
225
219
  end
220
+
221
+ private
222
+
223
+ ##
224
+ # Create a wrapper method around a called array method that updates the assocation
225
+ # resource if the array has changed.
226
+ # This way we can propagate those changes to the server when the client model is saved.
227
+
228
+ def wrap_array_method(method_name)
229
+ self.class.send(:define_method, method_name) do |*args, &block|
230
+ array = records.elements
231
+ count_before = array.count
232
+ response = array.send(method_name, *args, &block)
233
+ # update the association if the array has changed
234
+ association_resource.send("#{association_name}=", array) if association_name && count_before != array.count
235
+ response
236
+ end
237
+ end
238
+
239
+ ##
240
+ # has this proxy loaded its data yet?
241
+
242
+ def loaded?
243
+ @records.present?
244
+ end
226
245
  end
@@ -3,7 +3,7 @@ module Daylight
3
3
  MAJOR = 0
4
4
  MINOR = 9
5
5
  PATCH = 0
6
- BUILD = 'rc2'
6
+ BUILD = 'rc3'
7
7
 
8
8
  Daylight::VERSION = STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join('.')
9
9
  end
@@ -49,6 +49,8 @@ class Daylight::APIController < ApplicationController
49
49
  include Daylight::Helpers
50
50
  include VersionedUrlFor
51
51
 
52
+ ALLOWED_WHERE_PARAMS = [:id, :order, :limit, :offset, :associated, :remoted, :format].freeze
53
+
52
54
  API_ACTIONS = [:index, :create, :show, :update, :destroy, :associated, :remoted].freeze
53
55
  class_attribute :record_name, :collection_name, :model_name, instance_predicate: false
54
56
 
@@ -199,6 +201,16 @@ class Daylight::APIController < ApplicationController
199
201
  respond_to?(model_params_name, true) ? send(model_params_name) : params[model_key]
200
202
  end
201
203
 
204
+ ##
205
+ # Permits known parameters for quering
206
+ # This has become necessary as of Rails 4.0.9 and 4.1.5 because of this security fix:
207
+ # https://groups.google.com/forum/#!topic/rubyonrails-security/M4chq5Sb540
208
+ def where_params
209
+ params.permit(*ALLOWED_WHERE_PARAMS,
210
+ filters: params[:filters].try(:keys),
211
+ scopes: [])
212
+ end
213
+
202
214
  ##
203
215
  # Instance-level delegate of the `primary_key` method
204
216
  #
@@ -227,7 +239,7 @@ class Daylight::APIController < ApplicationController
227
239
  # See:
228
240
  # Daylight::Refiners.refine_by
229
241
  def index
230
- render json: self.collection = model.refine_by(params)
242
+ render json: self.collection = model.refine_by(where_params)
231
243
  end
232
244
 
233
245
  ##
@@ -330,7 +342,7 @@ class Daylight::APIController < ApplicationController
330
342
  # Daylight::Helpers.associated_params
331
343
  # RouteOptions
332
344
  def associated
333
- render json: self.collection = model.associated(params), root: associated_params
345
+ render json: self.collection = model.associated(where_params), root: associated_params
334
346
  end
335
347
 
336
348
  ##
@@ -350,6 +362,6 @@ class Daylight::APIController < ApplicationController
350
362
  # Daylight::Helpers.remoted_params
351
363
  # RouteOptions
352
364
  def remoted
353
- render json: self.collection = model.remoted(params), root: remoted_params
365
+ render json: self.collection = model.remoted(where_params), root: remoted_params
354
366
  end
355
367
  end
@@ -19,7 +19,10 @@ module NestedAttributesExt
19
19
  def assign_nested_attributes_for_collection_association association_name, attributes_collection
20
20
  return if attributes_collection.nil?
21
21
 
22
+ return if is_collection_multilevel?(association_name)
23
+
22
24
  associate_existing_records(association_name, attributes_collection)
25
+ unassociate_missing_records(association_name, attributes_collection)
23
26
 
24
27
  super
25
28
  end
@@ -63,30 +66,73 @@ module NestedAttributesExt
63
66
  return if attribute_ids.empty?
64
67
 
65
68
  association = association(association_name)
66
- primary_key = association.klass.primary_key.to_sym
67
-
68
- # get known existing ids on the association
69
- existing_record_ids = if association.loaded?
70
- association.target.map(&primary_key)
71
- else
72
- association.scope.where(primary_key => attribute_ids).pluck(primary_key)
73
- end
69
+ foreign_key = association.reflection.foreign_key
74
70
 
75
- # unassociated records are those that are not part of existing in the association
76
- unassociated_record_ids = attribute_ids.map(&:to_s) - existing_record_ids.map(&:to_s)
71
+ # unassociated records ids are those not existing in the association ids
72
+ unassociated_record_ids = attribute_ids.map(&:to_s) - association.ids_reader.map(&:to_s)
77
73
 
78
74
  # we are about to set all foreign_keys, remove any foreign_key references in
79
75
  # unassigned records attributes so they don't get clobbered
80
76
  attributes_collection.map do |a|
81
77
  if unassociated_record_ids.include?((a['id'] || a[:id]).to_s)
82
- key = association.reflection.foreign_key
83
- a.delete(key) || a.delete(key.to_sym)
78
+ a.delete(foreign_key) || a.delete(foreign_key.to_sym)
84
79
  end
85
80
  end
86
81
 
87
82
  # concat the unassociated records to the association
88
83
  association.concat(association.klass.find(unassociated_record_ids))
89
84
  end
85
+
86
+ ##
87
+ # Determines removed records from existing records on the association and sets their
88
+ # foreign keys to NULL
89
+ def unassociate_missing_records(association_name, attributes_collection)
90
+ # determine existing records, bail if there are none specified by 'id'
91
+ attribute_ids = attributes_collection.map {|a| (a['id'] || a[:id]) }.compact
92
+
93
+ association = association(association_name)
94
+
95
+ # removed records are those that are not part of existing in the association
96
+ removed_record_ids = association.ids_reader.map(&:to_s) - attribute_ids.map(&:to_s)
97
+
98
+ # remove the records from the association
99
+ association.delete(*removed_record_ids) unless removed_record_ids.empty?
100
+ end
101
+
102
+ ##
103
+ # returns true if the collection is a has_many :through or has_and_belongs_to_many
104
+ # association.
105
+ #
106
+
107
+ def is_collection_multilevel?(association_name)
108
+ association = association(association_name)
109
+
110
+ type = has_many_type(association)
111
+ return false unless type
112
+
113
+ logger.error <<-ERROR
114
+ Attempt to modify "#{association_name}" collection on #{self.class.name}.
115
+ Ignoring modification for #{type} used with
116
+ accepts_nested_attributes_for because it causes unexpected results.
117
+ ERROR
118
+ end
119
+
120
+ ##
121
+ # Return a description of the association if it is a has_and_belongs_to_many
122
+ # or a has_many :through.
123
+ #
124
+ # Takes differences between Rails 4.0 and 4.1 into account.
125
+
126
+ def has_many_type(association)
127
+ reflection = association.reflection
128
+ if reflection.try(:has_and_belongs_to_many?) ||
129
+ (reflection.try(:parent_reflection) &&
130
+ reflection.parent_reflection.last.try(:macro) == :has_and_belongs_to_many)
131
+ 'has_and_belongs_to_many'
132
+ elsif reflection.options.has_key?(:through)
133
+ 'has_many :through'
134
+ end
135
+ end
90
136
  end
91
137
 
92
138
  ActiveRecord::Base.class_eval do
@@ -119,13 +119,14 @@ describe Daylight::API do
119
119
 
120
120
  it "does not objectify a known reflection's attributes" do
121
121
  test = TestDescendant.find(1)
122
- test.child_attributes['id'].should == 2
122
+ test.attributes['child_attributes']['id'].should == 2
123
123
  end
124
124
 
125
125
  it "objectifies hashes within a known reflection's attributes" do
126
126
  test = TestDescendant.find(1)
127
- test.child_attributes['toy'].should be_kind_of(ActiveResource::Base)
128
- test.child_attributes['toy'].attributes.should == {'id' => 5, 'name' => 'slinky'}
127
+ toy = test.attributes['child_attributes']['toy']
128
+ toy.should be_kind_of(ActiveResource::Base)
129
+ toy.attributes.should == {'id' => 5, 'name' => 'slinky'}
129
130
  end
130
131
 
131
132
  it "still objectifies other attributes" do
@@ -188,4 +189,38 @@ describe Daylight::API do
188
189
  test.metadata.should be_present
189
190
  end
190
191
  end
192
+
193
+ describe :load_attributes_for do
194
+ let(:test) { TestDescendant.new }
195
+
196
+ it 'creates a resource from a Hash based on the name' do
197
+ obj = test.send(:load_attributes_for, :test_descendant, {name: 'foo'})
198
+ obj.should be_instance_of(TestDescendant)
199
+ obj.name.should == 'foo'
200
+ end
201
+
202
+ it 'creates a resource for each Hash in an array' do
203
+ obj = test.send(:load_attributes_for, :test_descendants, [{name: 'foo'}, {name: 'bar'}])
204
+ obj.size.should == 2
205
+ obj.first.should be_instance_of(TestDescendant)
206
+ obj.first.name.should == 'foo'
207
+ end
208
+
209
+ it 'dups each object in a given array if it is not a Hash' do
210
+ data = %w[one two three]
211
+ obj = test.send(:load_attributes_for, :test_descendants, data)
212
+ obj.size.should == 3
213
+ obj.first.should be_instance_of(String)
214
+ obj.first.should == 'one'
215
+ obj.object_id.should_not == data.first.object_id
216
+ end
217
+
218
+ it 'otherwise creates a dup of the given value' do
219
+ my_string = 'my string'
220
+ obj = test.send(:load_attributes_for, :test_descendant, my_string)
221
+ obj.should be_instance_of(String)
222
+ obj.should == 'my string'
223
+ obj.object_id.should_not == my_string.object_id
224
+ end
225
+ end
191
226
  end
@@ -0,0 +1,130 @@
1
+ require 'spec_helper'
2
+
3
+ describe Daylight::AssociationPersistance do
4
+
5
+ class GrandchildPersistanceTestClass < Daylight::API
6
+ end
7
+
8
+ class RelatedPersistanceTestClass < Daylight::API
9
+ has_many :grandchildren, class_name: GrandchildPersistanceTestClass
10
+ end
11
+
12
+ class PersistanceTestClass < Daylight::API
13
+ has_many :children, class_name: 'RelatedPersistanceTestClass'
14
+ belongs_to :parent, class_name: 'RelatedPersistanceTestClass'
15
+
16
+ def parent_id ; 1 ; end
17
+ end
18
+
19
+ before do
20
+ data = {id: 1, name: 'test'}
21
+ stub_request(:get, %r{#{PersistanceTestClass.element_path(1)}}).to_return(body: data.to_json)
22
+ stub_request(:get, %r{#{RelatedPersistanceTestClass.element_path(1)}}).to_return(body: data.to_json)
23
+ stub_request(:get, %r{#{RelatedPersistanceTestClass.element_path(2)}}).to_return(body: data.merge(id:2).to_json)
24
+ stub_request(:get, %r{/persistance_test_classes/1/children\.json}).to_return(body: [data, data].to_json)
25
+ stub_request(:get, %r{/related_persistance_test_classes/1/grandchildren\.json}).to_return(body: [data, data].to_json)
26
+ end
27
+
28
+ let(:object) { PersistanceTestClass.find(1) }
29
+
30
+ describe :changed? do
31
+
32
+ it 'returns false if the object is not modified' do
33
+ object.changed?.should be_false
34
+ end
35
+
36
+ it 'returns true if the object has a field modified' do
37
+ object.name = 'this is a change'
38
+
39
+ object.changed?.should be_true
40
+ end
41
+
42
+ it 'returns true if the object has a modified association' do
43
+ object.parent = RelatedPersistanceTestClass.new
44
+
45
+ object.changed?.should be_true
46
+ end
47
+
48
+ it 'returns true if the object is new' do
49
+ PersistanceTestClass.new.changed?.should be_true
50
+ end
51
+ end
52
+
53
+ describe :include_child_updates do
54
+ it 'does not include associations if has not been loaded' do
55
+ hash = object.serializable_hash
56
+ hash['parent_attributes'].should be_nil
57
+ hash['children_attributes'].should be_nil
58
+ end
59
+
60
+ it 'includes the single belongs_to/has_one assocation if it has changed' do
61
+ object.parent.should_not be_nil
62
+ object.parent.name = 'updated name'
63
+ hash = object.serializable_hash
64
+ hash['parent_attributes'].should be_present
65
+ end
66
+
67
+ it 'includes all children in a has_many type relation if any of them have changed' do
68
+ object.children.should be_present
69
+ object.children[0].name = 'updated name'
70
+
71
+ hash = object.serializable_hash
72
+ hash['children_attributes'].count.should == 2
73
+ end
74
+
75
+ it 'does not include the association attribute if nothing has changed in the collection' do
76
+ object.children.should be_present
77
+
78
+ hash = object.serializable_hash
79
+ hash.should_not have_key('children_attributes')
80
+ end
81
+
82
+ it 'does not include the single child if it has not changed' do
83
+ object.parent.should be_present
84
+
85
+ hash = object.serializable_hash
86
+ hash.should_not have_key('parent_attributes')
87
+ end
88
+
89
+ it 'still includes associations when there is a new child object' do
90
+ object.children.should be_present
91
+ object.children << RelatedPersistanceTestClass.new
92
+ object.children.count.should == 3
93
+
94
+ hash = object.serializable_hash
95
+ hash['children_attributes'].count.should == 3
96
+ end
97
+
98
+ it 'still includes associations when an additional persisted child object is added' do
99
+ object.children.should be_present
100
+ object.children << RelatedPersistanceTestClass.find(1)
101
+ object.children.count.should == 3
102
+
103
+ hash = object.serializable_hash
104
+ hash['children_attributes'].count.should == 3
105
+ end
106
+
107
+ it 'still includes has_one associations when persisted child object is set' do
108
+ object.parent.should be_present
109
+ object.parent = RelatedPersistanceTestClass.find(2)
110
+
111
+ hash = object.serializable_hash
112
+ hash['parent_attributes'].should be_present
113
+ end
114
+
115
+ it 'does not include has_one associations when it is replaced with the same object' do
116
+ object.parent.should be_present
117
+ object.parent = RelatedPersistanceTestClass.find(1)
118
+
119
+ hash = object.serializable_hash
120
+ hash['parent_attributes'].should_not be_present
121
+ end
122
+
123
+ it 'handles grandchild modifications' do
124
+ object.children[0].grandchildren[0].name = 'updated name'
125
+ hash = object.serializable_hash
126
+ hash['children_attributes'][0]['grandchildren_attributes'][0]['name'].should == 'updated name'
127
+ end
128
+ end
129
+
130
+ end
@@ -53,12 +53,6 @@ describe Daylight::Associations do
53
53
  collection.should be_a Array
54
54
  end
55
55
 
56
- it "sets the associations directly the attributes hash" do
57
- new_resource.related_test_classes = ["associated instances"]
58
-
59
- new_resource.attributes['related_test_classes_attributes'].should == ["associated instances"]
60
- end
61
-
62
56
  it "fetches the stored associations out of the attributes when they are set" do
63
57
  new_resource.related_test_classes = ["associated instances"]
64
58
 
@@ -107,12 +101,6 @@ describe Daylight::Associations do
107
101
  proxy.to_params[:filters].should == {wibble: 'wobble'}
108
102
  end
109
103
 
110
- it "sets the associations directly the attributes hash" do
111
- existing_resource.related_test_classes = ["associated instances"]
112
-
113
- existing_resource.attributes['related_test_classes_attributes'].should == ["associated instances"]
114
- end
115
-
116
104
  it "fetches the stored associations out of the attributes when they exist" do
117
105
  existing_resource.related_test_classes = ["associated instances"]
118
106
 
@@ -150,13 +138,6 @@ describe Daylight::Associations do
150
138
 
151
139
  resource.attributes['parent_id'].should == 789
152
140
  end
153
-
154
- it 'sets the parent directly in the nested attributes hash' do
155
- resource = AssociationsTestClass.find(1)
156
- resource.parent = RelatedTestClass.new(id: 789, name: 'new parent')
157
-
158
- resource.attributes['parent_attributes'].should == resource.parent
159
- end
160
141
  end
161
142
 
162
143
  describe :belongs_to_through do
@@ -167,6 +148,7 @@ describe Daylight::Associations do
167
148
  parent_id: 456, # ignored because of parent_id method
168
149
  parent_attributes: {
169
150
  id: 456,
151
+ name: 'nested',
170
152
  grandparent_id: 3
171
153
  }
172
154
  }
@@ -190,12 +172,12 @@ describe Daylight::Associations do
190
172
  stub_request(:get, %r{#{RelatedTestClass.element_path(3)}}).to_return(body: related_data.merge(id: 3).to_json)
191
173
  end
192
174
 
193
- it 'still fetches the parent object' do
175
+ it 'loads the parent object from the original response' do
194
176
  resource = AssociationsTestClass.find(1)
195
177
 
196
178
  resource.parent.should_not be_nil
197
179
  resource.parent.id.should == 456
198
- resource.parent.name.should == 'related'
180
+ resource.parent.name.should == 'nested'
199
181
  end
200
182
 
201
183
  it 'fetches the "through" object' do
@@ -260,13 +242,6 @@ describe Daylight::Associations do
260
242
 
261
243
  resource.associate.associations_test_class_id.should == resource.id
262
244
  end
263
-
264
- it 'sets the associate directly in the nested attributes hash' do
265
- resource = AssociationsTestClass.find(1)
266
- resource.associate = RelatedTestClass.new(id: 333, name: 'Rik Mayall')
267
-
268
- resource.attributes['associate_attributes'].should == resource.associate
269
- end
270
245
  end
271
246
 
272
247
  describe :remote do
@@ -50,16 +50,10 @@ describe Daylight::ResourceProxy do
50
50
  it "still thrown when chaining" do
51
51
  expect { ProxyTestClass.foo.not_a_method }.to raise_error(NoMethodError)
52
52
  end
53
-
54
- it "still thrown when appending to scopes" do
55
- ProxyTestClass.foo.association_resource.should be_nil
56
-
57
- expect { ProxyTestClass.foo << ['foo class'] }.to raise_error(NoMethodError)
58
- end
59
53
  end
60
54
 
61
55
  describe "Array methods" do
62
- it "supported as a generated method" do
56
+ it "supports through generated methods" do
63
57
  mock = ProxyTestClass.new(name: 'three')
64
58
  results = ProxyTestClass.foo
65
59
 
@@ -83,6 +77,19 @@ describe Daylight::ResourceProxy do
83
77
  it "supported through Enumerable methods" do
84
78
  ProxyTestClass.foo.map {|f| f.name}.should == %w[one two]
85
79
  end
80
+
81
+ it "pulls the first off of the array if it's already loaded" do
82
+ result = ProxyTestClass.foo
83
+ result.instance_variable_set('@records', [:yay, :boo])
84
+
85
+ result.first.should == :yay
86
+ end
87
+
88
+ it "does a request for first if it hasn't been loaded" do
89
+ result = ProxyTestClass.foo
90
+
91
+ result.first.name.should == 'one'
92
+ end
86
93
  end
87
94
 
88
95
  describe "results fetch" do
@@ -14,7 +14,7 @@ class SuitesController < Daylight::APIController
14
14
 
15
15
  private
16
16
  def suite_params
17
- params.require(:suite).permit(:name, :switch)
17
+ params.fetch(:suite, {}).permit(:name, :switch)
18
18
  end
19
19
  end
20
20
 
@@ -6,8 +6,9 @@ class NoNestedAttributesTest < ActiveRecord::Base; end
6
6
  # test cyclic autosave problem that AutosaveAssociataionFix resolves
7
7
  class NestedAttributeTest < ActiveRecord::Base
8
8
  has_many :collection, class_name: 'AssocNestedAttributeTest', inverse_of: :nested_attribute_test
9
+ has_many :through_collection, class_name: 'SingleNestedAttributeTest', through: :collection, source: :single_nested_attribute_test
9
10
 
10
- accepts_nested_attributes_for :collection
11
+ accepts_nested_attributes_for :collection, :through_collection
11
12
  end
12
13
 
13
14
  class SingleNestedAttributeTest < ActiveRecord::Base
@@ -23,6 +24,16 @@ class AssocNestedAttributeTest < ActiveRecord::Base
23
24
  accepts_nested_attributes_for :nested_attribute_test, :single_nested_attribute_test
24
25
  end
25
26
 
27
+ class HabtmParentNestedAttributeTest < ActiveRecord::Base
28
+ has_and_belongs_to_many :foo, class_name: 'HabtmChildNestedAttributeTest'
29
+
30
+ accepts_nested_attributes_for :foo
31
+ end
32
+
33
+ class HabtmChildNestedAttributeTest < ActiveRecord::Base
34
+ has_and_belongs_to_many :foo, class_name: 'HabtmParentNestedAttributeTest'
35
+ end
36
+
26
37
  describe NestedAttributesExt, type: [:model] do
27
38
 
28
39
  migrate do
@@ -39,6 +50,19 @@ describe NestedAttributesExt, type: [:model] do
39
50
  t.references :nested_attribute_test
40
51
  t.references :single_nested_attribute_test
41
52
  end
53
+
54
+ create_table :habtm_parent_nested_attribute_tests do |t|
55
+ t.string :name
56
+ end
57
+
58
+ create_table :habtm_child_nested_attribute_tests do |t|
59
+ t.string :name
60
+ end
61
+
62
+ create_table :habtm_child_nested_attribute_tests_parent_nested_attribute_tests do |t|
63
+ t.belongs_to :habtm_parent_nested_attribute_test
64
+ t.belongs_to :habtm_child_nested_attribute_test
65
+ end
42
66
  end
43
67
 
44
68
  before(:all) do
@@ -54,6 +78,14 @@ describe NestedAttributesExt, type: [:model] do
54
78
  factory :assoc_nested_attribute_test do
55
79
  name { Faker::Name.name }
56
80
  end
81
+
82
+ factory :habtm_child_nested_attribute_test do
83
+ name { Faker::Name.name }
84
+ end
85
+
86
+ factory :habtm_parent_nested_attribute_test do
87
+ name { Faker::Name.name }
88
+ end
57
89
  end
58
90
  end
59
91
 
@@ -77,101 +109,171 @@ describe NestedAttributesExt, type: [:model] do
77
109
  end
78
110
 
79
111
 
80
- describe 'has_many' do
81
- let(:record) { create(:nested_attribute_test) }
82
- let(:member) { create(:assoc_nested_attribute_test) }
112
+ describe 'associate' do
83
113
 
84
- it "ignores nil values" do
85
- record.collection_attributes = nil
114
+ describe 'has_many' do
115
+ let(:record) { create(:nested_attribute_test) }
116
+ let(:member) { create(:assoc_nested_attribute_test) }
86
117
 
87
- lambda { record.save! }.should_not raise_error
88
- record.collection.should == []
89
- end
118
+ it "ignores nil values" do
119
+ record.collection_attributes = nil
90
120
 
91
- it "continues to create and associate new records" do
92
- record.collection_attributes = [{name: 'foo'}, {name: 'bar'}]
121
+ lambda { record.save! }.should_not raise_error
122
+ record.collection.should == []
123
+ end
93
124
 
94
- lambda { record.save! }.should_not raise_error
125
+ it "continues to create and associate new records" do
126
+ record.collection_attributes = [{name: 'foo'}, {name: 'bar'}]
95
127
 
96
- names = record.collection.map(&:name)
97
- names.should include('foo')
98
- names.should include('bar')
99
- end
128
+ lambda { record.save! }.should_not raise_error
100
129
 
101
- it "continues to update assoicated records" do
102
- record.collection << member
103
- record.collection_attributes = [{id: member.id, name: 'Foozle Cumberbunch'}]
130
+ names = record.collection.map(&:name)
131
+ names.should include('foo')
132
+ names.should include('bar')
133
+ end
104
134
 
105
- lambda { record.save! }.should_not raise_error
106
- record.collection.size.should == 1
107
- record.collection.first.name.should == 'Foozle Cumberbunch'
108
- end
135
+ it "continues to update assoicated records" do
136
+ record.collection << member
137
+ record.collection_attributes = [{id: member.id, name: 'Foozle Cumberbunch'}]
138
+
139
+ lambda { record.save! }.should_not raise_error
140
+ record.collection.size.should == 1
141
+ record.collection.first.name.should == 'Foozle Cumberbunch'
142
+ end
109
143
 
110
- # this also tests cyclic autosave problem that AutosaveAssociataionFix resolves
111
- it "associates existing records" do
112
- record.collection_attributes = [{id: member.id}]
144
+ # this also tests cyclic autosave problem that AutosaveAssociataionFix resolves
145
+ it "associates existing records" do
146
+ record.collection_attributes = [{id: member.id}]
113
147
 
114
- lambda { record.save! }.should_not raise_error
115
- record.collection.size.should == 1
116
- record.collection.first.id.should == member.id
148
+ lambda { record.save! }.should_not raise_error
149
+ record.collection.size.should == 1
150
+ record.collection.first.id.should == member.id
151
+ end
152
+
153
+ # this also tests cyclic autosave problem that AutosaveAssociataionFix resolves
154
+ it "keeps association for existing records that are already assoicated" do
155
+ record.collection << member
156
+ record.collection_attributes = [{id: member.id}]
157
+
158
+ lambda { record.save! }.should_not raise_error
159
+ record.collection.size.should == 1
160
+ record.collection.first.id.should == member.id
161
+ end
162
+
163
+ it "updates records that were just associated" do
164
+ record.collection_attributes = [{id: member.id, name: 'Foozle Cumberbunch'}]
165
+
166
+ lambda { record.save! }.should_not raise_error
167
+ record.collection.size.should == 1
168
+ record.collection.first.name.should == 'Foozle Cumberbunch'
169
+ end
170
+
171
+ it "ignores foreign key updates" do
172
+ different_foreign_key = record.id + 1
173
+ record.collection_attributes = [{id: member.id, name: 'Foozle Cumberbunch', nested_attribute_test_id: different_foreign_key}]
174
+
175
+ lambda { record.save! }.should_not raise_error
176
+
177
+ record.collection.size.should == 1
178
+ record.collection.first.nested_attribute_test_id.should == record.id
179
+ end
180
+
181
+ # this also tests cyclic autosave problem that AutosaveAssociataionFix resolves
182
+ it "updates previsoulsy associated records" do
183
+ record.collection << member
184
+
185
+ lambda { record.update!(collection_attributes: [{id: member.id, name: 'Foozle Cumberbunch'}]) }.should_not raise_error
186
+
187
+ record.collection.size.should == 1
188
+ record.collection.first.name.should == 'Foozle Cumberbunch'
189
+ end
117
190
  end
118
191
 
119
- # this also tests cyclic autosave problem that AutosaveAssociataionFix resolves
120
- it "keeps association for existing records that are already assoicated" do
121
- record.collection << member
122
- record.collection_attributes = [{id: member.id}]
192
+ describe 'has_one' do
193
+ let(:record) { create(:single_nested_attribute_test) }
194
+ let(:member) { create(:assoc_nested_attribute_test) }
123
195
 
124
- lambda { record.save! }.should_not raise_error
125
- record.collection.size.should == 1
126
- record.collection.first.id.should == member.id
196
+ it "ignores nil values" do
197
+ record.single = nil
198
+
199
+ lambda { record.save! }.should_not raise_error
200
+ record.single.should be_nil
201
+ end
202
+
203
+ it "continues to create and associate new records" do
204
+ record.single_attributes = {name: 'wibble'}
205
+
206
+ lambda { record.save! }.should_not raise_error
207
+
208
+ record.single.name.should == 'wibble'
209
+ end
127
210
  end
211
+ end
128
212
 
129
- it "updates records that were just associated" do
130
- record.collection_attributes = [{id: member.id, name: 'Foozle Cumberbunch'}]
213
+ describe 'unassociating records missing from the attributes collection' do
131
214
 
132
- lambda { record.save! }.should_not raise_error
133
- record.collection.size.should == 1
134
- record.collection.first.name.should == 'Foozle Cumberbunch'
215
+ let(:single1) { create(:single_nested_attribute_test) }
216
+ let(:single2) { create(:single_nested_attribute_test) }
217
+ let(:assoc1) do
218
+ create(:assoc_nested_attribute_test) {|record| record.single_nested_attribute_test = single1 }
219
+ end
220
+ let(:assoc2) do
221
+ create(:assoc_nested_attribute_test) {|record| record.single_nested_attribute_test = single2 }
135
222
  end
136
223
 
137
- it "ignores foreign key updates" do
138
- different_foreign_key = record.id + 1
139
- record.collection_attributes = [{id: member.id, name: 'Foozle Cumberbunch', nested_attribute_test_id: different_foreign_key}]
224
+ let(:record) do
225
+ record = create(:nested_attribute_test) {|r| r.collection = [assoc1, assoc2] }
226
+ end
227
+
228
+ it 'handles has_many relationships' do
229
+ record.collection_attributes = [assoc2.as_json]
140
230
 
141
231
  lambda { record.save! }.should_not raise_error
142
232
 
143
- record.collection.size.should == 1
144
- record.collection.first.nested_attribute_test_id.should == record.id
233
+ record.reload.collection.map(&:id).should == [assoc2.id]
234
+
235
+ assoc1.reload.nested_attribute_test.should be_nil
236
+ assoc2.reload.nested_attribute_test.should == record
145
237
  end
146
238
 
147
- # this also tests cyclic autosave problem that AutosaveAssociataionFix resolves
148
- it "updates previsoulsy associated records" do
149
- record.collection << member
239
+ it 'ignores has_many through' do
240
+ assoc2.single_nested_attribute_test.id.should == single2.id
241
+
242
+ record.through_collection.count.should == 2
150
243
 
151
- lambda { record.update!(collection_attributes: [{id: member.id, name: 'Foozle Cumberbunch'}]) }.should_not raise_error
244
+ record.through_collection_attributes = [single1.as_json]
152
245
 
153
- record.collection.size.should == 1
154
- record.collection.first.name.should == 'Foozle Cumberbunch'
246
+ lambda { record.save! }.should_not raise_error
247
+
248
+ record.reload.through_collection.count.should == 2
155
249
  end
156
- end
157
250
 
158
- describe 'has_one' do
159
- let(:record) { create(:single_nested_attribute_test) }
160
- let(:member) { create(:assoc_nested_attribute_test) }
251
+ it 'ignores habtm' do
252
+ test = create(:habtm_parent_nested_attribute_test)
253
+ test.foo << create(:habtm_child_nested_attribute_test)
254
+ test.foo << create(:habtm_child_nested_attribute_test)
255
+ test.save!
256
+
257
+ test.reload.foo.count.should == 2
161
258
 
162
- it "ignores nil values" do
163
- record.single = nil
259
+ test.foo_attributes = [test.foo.first.as_json]
164
260
 
165
261
  lambda { record.save! }.should_not raise_error
166
- record.single.should be_nil
262
+
263
+ test.reload.foo.count.should == 2
167
264
  end
168
265
 
169
- it "continues to create and associate new records" do
170
- record.single_attributes = {name: 'wibble'}
266
+ it 'allows removing all things from a collection' do
267
+ record.collection_attributes = []
171
268
 
172
269
  lambda { record.save! }.should_not raise_error
173
270
 
174
- record.single.name.should == 'wibble'
271
+ record.reload.collection.should be_empty
272
+
273
+ assoc1.reload.nested_attribute_test.should be_nil
274
+ assoc2.reload.nested_attribute_test.should be_nil
175
275
  end
276
+
176
277
  end
278
+
177
279
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: daylight
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0.rc2
4
+ version: 0.9.0.rc3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Reid MacDonald
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-07-30 00:00:00.000000000 Z
12
+ date: 2014-08-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activeresource
@@ -224,6 +224,7 @@ files:
224
224
  - config/routes.rb
225
225
  - lib/daylight.rb
226
226
  - lib/daylight/api.rb
227
+ - lib/daylight/association_persistance.rb
227
228
  - lib/daylight/associations.rb
228
229
  - lib/daylight/client_reloader.rb
229
230
  - lib/daylight/collection.rb
@@ -308,6 +309,7 @@ files:
308
309
  - spec/dummy/public/favicon.ico
309
310
  - spec/helpers/documentation_helper_spec.rb
310
311
  - spec/lib/daylight/api_spec.rb
312
+ - spec/lib/daylight/association_persistance_spec.rb
311
313
  - spec/lib/daylight/associations_spec.rb
312
314
  - spec/lib/daylight/collection_spec.rb
313
315
  - spec/lib/daylight/errors_spec.rb
@@ -398,6 +400,7 @@ test_files:
398
400
  - spec/dummy/public/favicon.ico
399
401
  - spec/helpers/documentation_helper_spec.rb
400
402
  - spec/lib/daylight/api_spec.rb
403
+ - spec/lib/daylight/association_persistance_spec.rb
401
404
  - spec/lib/daylight/associations_spec.rb
402
405
  - spec/lib/daylight/collection_spec.rb
403
406
  - spec/lib/daylight/errors_spec.rb