daylight 0.9.0.rc2 → 0.9.0.rc3

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.
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