attr_json 1.4.0 → 2.6.0

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +22 -22
  3. data/.github/workflows/future_rails_ci.yml +3 -3
  4. data/Appraisals +40 -29
  5. data/CHANGELOG.md +144 -1
  6. data/Gemfile +8 -2
  7. data/README.md +83 -63
  8. data/attr_json.gemspec +8 -7
  9. data/doc_src/forms.md +3 -14
  10. data/gemfiles/rails_6_0.gemfile +3 -2
  11. data/gemfiles/rails_6_1.gemfile +2 -2
  12. data/gemfiles/rails_7_0.gemfile +2 -3
  13. data/gemfiles/{rails_5_1.gemfile → rails_7_1.gemfile} +4 -4
  14. data/gemfiles/{rails_5_2.gemfile → rails_7_2.gemfile} +4 -4
  15. data/gemfiles/{rails_5_0.gemfile → rails_8_0.gemfile} +5 -6
  16. data/gemfiles/rails_8_1.gemfile +18 -0
  17. data/gemfiles/rails_edge.gemfile +4 -4
  18. data/lib/attr_json/attribute_definition/registry.rb +43 -9
  19. data/lib/attr_json/attribute_definition.rb +51 -14
  20. data/lib/attr_json/config.rb +1 -2
  21. data/lib/attr_json/model/nested_model_validator.rb +27 -0
  22. data/lib/attr_json/model.rb +185 -53
  23. data/lib/attr_json/nested_attributes/builder.rb +2 -0
  24. data/lib/attr_json/nested_attributes/multiparameter_attribute_writer.rb +2 -0
  25. data/lib/attr_json/nested_attributes/writer.rb +6 -6
  26. data/lib/attr_json/nested_attributes.rb +7 -1
  27. data/lib/attr_json/record/query_builder.rb +2 -0
  28. data/lib/attr_json/record/query_scopes.rb +2 -0
  29. data/lib/attr_json/record.rb +113 -88
  30. data/lib/attr_json/serialization_coder_from_type.rb +2 -0
  31. data/lib/attr_json/type/array.rb +12 -3
  32. data/lib/attr_json/type/container_attribute.rb +2 -0
  33. data/lib/attr_json/type/model.rb +15 -4
  34. data/lib/attr_json/type/polymorphic_model.rb +43 -23
  35. data/lib/attr_json/version.rb +1 -1
  36. data/lib/attr_json.rb +21 -6
  37. data/playground_models.rb +2 -2
  38. metadata +15 -32
  39. data/doc_src/dirty_tracking.md +0 -155
  40. data/lib/attr_json/record/dirty.rb +0 -281
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'attr_json/type/array'
2
4
 
3
5
  module AttrJson
@@ -34,23 +36,14 @@
34
36
 
35
37
  @default = if options.has_key?(:default)
36
38
  options[:default]
39
+ elsif options[:array] == true
40
+ -> { [] }
37
41
  else
38
42
  NO_DEFAULT_PROVIDED
39
43
  end
40
44
 
41
- if type.is_a? Symbol
42
- # ActiveModel::Type.lookup may make more sense, but ActiveModel::Type::Date
43
- # seems to have a bug with multi-param assignment. Mostly they return
44
- # the same types, but ActiveRecord::Type::Date works with multi-param assignment.
45
- #
46
- # We pass `adapter: nil` to avoid triggering a db connection.
47
- # See: https://github.com/jrochkind/attr_json/issues/41
48
- # This is at the "cost" of not using any adapter-specific types... which
49
- # maybe preferable anyway?
50
- type = ActiveRecord::Type.lookup(type, adapter: nil)
51
- elsif ! type.is_a? ActiveModel::Type::Value
52
- raise ArgumentError, "Second argument (#{type}) must be a symbol or instance of an ActiveModel::Type::Value subclass"
53
- end
45
+ type = self.class.lookup_type(type)
46
+
54
47
  @type = (options[:array] == true ? AttrJson::Type::Array.new(type) : type)
55
48
  end
56
49
 
@@ -71,7 +64,7 @@
71
64
  end
72
65
 
73
66
  def store_key
74
- (@store_key || name).to_s
67
+ AttrJson.efficient_to_s(@store_key || name)
75
68
  end
76
69
 
77
70
  def has_default?
@@ -102,5 +95,49 @@
102
95
  def array_type?
103
96
  type.is_a? AttrJson::Type::Array
104
97
  end
98
+
99
+ # Used for figuring out appropriate behavior for nested attribute implementation among
100
+ # other places. true if type is a nested model, either straight or polymorphic
101
+ def single_model_type?
102
+ self.class.single_model_type?(type)
103
+ end
104
+
105
+ def self.single_model_type?(arg_type)
106
+ arg_type.is_a?(AttrJson::Type::Model) || arg_type.is_a?(AttrJson::Type::PolymorphicModel)
107
+ end
108
+
109
+ # Used for figuring out appropriate behavior in nested attribute implementation among
110
+ # other places. true if type is an array of things that are not nested models.
111
+ def array_of_primitive_type?
112
+ array_type? && !self.class.single_model_type?(type.base_type)
113
+ end
114
+
115
+ # Can look up a symbol to a type, or leave a type alone, or raise if it's neither.
116
+ # Extracted into a method, so it can be called from AttrJson::Model#attr_json, for
117
+ # some timezone aware shenanigans.
118
+ def self.lookup_type(type)
119
+ if type.is_a? Symbol
120
+ # ActiveModel::Type.lookup may make more sense, but ActiveModel::Type::Date
121
+ # seems to have a bug with multi-param assignment. Mostly they return
122
+ # the same types, but ActiveRecord::Type::Date works with multi-param assignment.
123
+ #
124
+ # We pass `adapter: nil` to avoid triggering a db connection.
125
+ # See: https://github.com/jrochkind/attr_json/issues/41
126
+ # This is at the "cost" of not using any adapter-specific types... which
127
+ # maybe preferable anyway?
128
+ #
129
+ # AND we add precision for datetime/time types... since we're using Rails json
130
+ # serializing, we're kind of stuck with this precision in current implementation.
131
+ lookup_kwargs = { adapter: nil }
132
+ if type == :datetime || type == :time
133
+ lookup_kwargs[:precision] = ActiveSupport::JSON::Encoding.time_precision
134
+ end
135
+ type = ActiveRecord::Type.lookup(type, **lookup_kwargs)
136
+ elsif !(type.is_a?(ActiveModel::Type::Value) || type.is_a?(ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter))
137
+ raise ArgumentError, "Second argument (#{type}) must be a symbol or instance of an ActiveModel::Type::Value subclass"
138
+ end
139
+ type
140
+ end
141
+
105
142
  end
106
143
  end
@@ -5,18 +5,17 @@ module AttrJson
5
5
  class Config
6
6
  RECORD_ALLOWED_KEYS = %i{
7
7
  default_container_attribute
8
- default_rails_attribute
9
8
  default_accepts_nested_attributes
10
9
  }
11
10
 
12
11
  MODEL_ALLOWED_KEYS = %i{
13
12
  unknown_key
14
13
  bad_cast
14
+ time_zone_aware_attributes
15
15
  }
16
16
 
17
17
  DEFAULTS = {
18
18
  default_container_attribute: "json_attributes",
19
- default_rails_attribute: false,
20
19
  unknown_key: :raise
21
20
  }
22
21
 
@@ -0,0 +1,27 @@
1
+ module AttrJson
2
+ module Model
3
+ # Used to validate an attribute in an AttrJson::Model whose values are other models, when
4
+ # you want validation errors on the nested models to post up.
5
+ #
6
+ # This is based on ActiveRecord's own ActiveRecord::Validations::AssociatedValidator, and actually forked
7
+ # from it at https://github.com/rails/rails/blob/e37adfed4eff3b43350ec87222a922e9c72d9c1b/activerecord/lib/active_record/validations/associated.rb
8
+ #
9
+ # We used to simply use an ActiveRecord::Validations::AssociatedValidator, but as of https://github.com/jrochkind/attr_json/pull/220 (e1e798142d)
10
+ # it got ActiveRecord-specific functionality that no longer worked with our use case.
11
+ #
12
+ # No problem, the implementation is simple, we can provide it here, based on the last version that did work.
13
+ class NestedModelValidator < ActiveModel::EachValidator
14
+ def validate_each(record, attribute, value)
15
+ if Array(value).reject { |r| valid_object?(r) }.any?
16
+ record.errors.add(attribute, :invalid, **options.merge(value: value))
17
+ end
18
+ end
19
+
20
+ private
21
+ def valid_object?(record)
22
+ #(record.respond_to?(:marked_for_destruction?) && record.marked_for_destruction?) || record.valid?
23
+ record.valid?
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/concern'
2
4
  require 'active_model/type'
3
5
 
@@ -6,6 +8,7 @@ require 'attr_json/attribute_definition/registry'
6
8
 
7
9
  require 'attr_json/type/model'
8
10
  require 'attr_json/model/cocoon_compat'
11
+ require 'attr_json/model/nested_model_validator'
9
12
 
10
13
  require 'attr_json/serialization_coder_from_type'
11
14
 
@@ -20,7 +23,7 @@ module AttrJson
20
23
  # Meant for use as an attribute of a AttrJson::Record. Can be nested,
21
24
  # AttrJson::Models can have attributes that are other AttrJson::Models.
22
25
  #
23
- # @note Includes ActiveModel::Model whether you like it or not. TODO, should it?
26
+ # @note Includes ActiveModel::Model whether you like it or not.
24
27
  #
25
28
  # You can control what happens if you set an unknown key (one that you didn't
26
29
  # register with `attr_json`) with the config attribute `attr_json_config(unknown_key:)`.
@@ -47,6 +50,21 @@ module AttrJson
47
50
  # #...
48
51
  # end
49
52
  #
53
+ # ## Date-type timezone conversion
54
+ #
55
+ # By default, AttrJson::Model date/time attributes will be
56
+ # [ActiveRecord timezone-aware](https://api.rubyonrails.org/classes/ActiveRecord/Timestamp.html)
57
+ # based on settings of `config.active_record.time_zone_aware_attributes` and
58
+ # `ActiveRecord::Base.time_zone_aware_types`.
59
+ #
60
+ # If you'd like to override this, you can set:
61
+ #
62
+ # ```
63
+ # attr_json_config(time_zone_aware_attributes: true)
64
+ # attr_json_config(time_zone_aware_attributes: false)
65
+ # attr_json_config(time_zone_aware_attributes: [:datetime, :time]) # custom list of types
66
+ # ```
67
+ #
50
68
  # ## ActiveRecord `serialize`
51
69
  #
52
70
  # If you want to map a single AttrJson::Model to a json/jsonb column, you
@@ -65,6 +83,15 @@ module AttrJson
65
83
  # serialize :some_json_column, ValueModel.to_serialize_coder
66
84
  # end
67
85
  #
86
+ # # Strip nils
87
+ #
88
+ # When embedded in an `attr_json` attribute, models are normally serialized with `nil` values
89
+ # stripped from hash where possible, for a more compact representation.
90
+ # This can be set differently in the type.
91
+ #
92
+ # attr_json :lang_and_value, LangAndValue.to_type(strip_nils: false)
93
+ #
94
+ # See #serializable_hash docs for possible values.
68
95
  module Model
69
96
  extend ActiveSupport::Concern
70
97
 
@@ -106,14 +133,14 @@ module AttrJson
106
133
  # The inverse of model#serializable_hash -- re-hydrates a serialized hash to a model.
107
134
  #
108
135
  # Similar to `.new`, but translates things that need to be translated in deserialization,
109
- # like store_keys, and properly calling deserialize on the underlying types.
136
+ # like store_keys, and properly calling deserialize (rather than cast) on the underlying types.
110
137
  #
111
138
  # @example Model.new_from_serializable(hash)
112
139
  def new_from_serializable(attributes = {})
113
140
  attributes = attributes.collect do |key, value|
114
141
  # store keys in arguments get translated to attribute names on initialize.
115
- if attribute_def = self.attr_json_registry.store_key_lookup("", key.to_s)
116
- key = attribute_def.name.to_s
142
+ if attribute_def = self.attr_json_registry.store_key_lookup("".freeze, AttrJson.efficient_to_s(key))
143
+ key = AttrJson.efficient_to_s(attribute_def.name)
117
144
  end
118
145
 
119
146
  attr_type = self.attr_json_registry.has_attribute?(key) && self.attr_json_registry.type_for_attribute(key)
@@ -127,14 +154,43 @@ module AttrJson
127
154
  self.new(attributes)
128
155
  end
129
156
 
130
- def to_type
131
- @type ||= AttrJson::Type::Model.new(self)
157
+ # @returns ActiveModel::Type suitable for including this model in
158
+ # an AttrJson::Record or ::Model attribute
159
+ #
160
+ # @param strip_nils [Boolean,Symbol] [true,false,:safely] as a type,
161
+ # should we strip nils when serializing? By default this type strips
162
+ # nils in :safely mode. See AttrJson::Model#serializable_hash
163
+ def to_type(strip_nils: :safely)
164
+ @type ||= AttrJson::Type::Model.new(self, strip_nils: strip_nils)
132
165
  end
133
166
 
167
+ # An ActiveModel::Type that can be used to serialize this model
168
+ # across an entire JSON(b) column.
169
+ #
170
+ # @example using standard ActiveRecord `serialize` feature.
171
+ #
172
+ # class MyTable < ApplicationRecord
173
+ # serialize :some_json_column, MyModel.to_serialization_coder
174
+ #
175
+ # # In Rails 7.1+:
176
+ # # serialize :some_json_column, coder: MyModel.to_serialization_coder
177
+ # end
178
+ #
134
179
  def to_serialization_coder
135
180
  @serialization_coder ||= AttrJson::SerializationCoderFromType.new(to_type)
136
181
  end
137
182
 
183
+ # like the ActiveModel::Attributes method
184
+ def attribute_names
185
+ attr_json_registry.attribute_names
186
+ end
187
+
188
+ # like the ActiveModel::Attributes method, hash with name keys, and ActiveModel::Type values
189
+ def attribute_types
190
+ attribute_names.collect { |name| [AttrJson.efficient_to_s(name), attr_json_registry.type_for_attribute(name)]}.to_h
191
+ end
192
+
193
+
138
194
  # Type can be an instance of an ActiveModel::Type::Value subclass, or a symbol that will
139
195
  # be looked up in `ActiveModel::Type.lookup`
140
196
  #
@@ -143,6 +199,8 @@ module AttrJson
143
199
  # @param type [ActiveModel::Type::Value] An instance of an ActiveModel::Type::Value (or subclass)
144
200
  #
145
201
  # @option options [Boolean] :array (false) Make this attribute an array of given type.
202
+ # Array types default to an empty array. If you want to turn that off, you can add
203
+ # `default: AttrJson::AttributeDefinition::NO_DEFAULT_PROVIDED`
146
204
  #
147
205
  # @option options [Object] :default (nil) Default value, if a Proc object it will be #call'd
148
206
  # for default.
@@ -150,54 +208,34 @@ module AttrJson
150
208
  # @option options [String,Symbol] :store_key (nil) Serialize to JSON using
151
209
  # given store_key, rather than name as would be usual.
152
210
  #
153
- # @option options [Boolean] :validate (true) Create an ActiveRecord::Validations::AssociatedValidator so
154
- # validation errors on the attributes post up to self.
211
+ # @option options [Boolean] :validate (true) Mak validation errors on the attributes post up
212
+ # to self, using something similar to an ActiveRecord::Validations::AssociatedValidator
155
213
  def attr_json(name, type, **options)
156
214
  options.assert_valid_keys(*(AttributeDefinition::VALID_OPTIONS - [:container_attribute] + [:validate]))
157
215
 
216
+ type = _attr_json_maybe_wrap_timezone_aware(type)
217
+
158
218
  self.attr_json_registry = attr_json_registry.with(
159
219
  AttributeDefinition.new(name.to_sym, type, options.except(:validate))
160
220
  )
161
221
 
162
222
  # By default, automatically validate nested models
163
- if type.kind_of?(AttrJson::Type::Model) && options[:validate] != false
164
- # Yes. we're passing an ActiveRecord::Validations validator, but
165
- # it works fine for ActiveModel. If this changes in the future, tests will catch.
166
- self.validates_with ActiveRecord::Validations::AssociatedValidator, attributes: [name.to_sym]
223
+ if (type.kind_of?(AttrJson::Type::Model) || type.kind_of?(AttrJson::Type::PolymorphicModel)) && options[:validate] != false
224
+ # Post validations up with something based on ActiveRecord::Validations::AssociatedValidator
225
+ self.validates_with ::AttrJson::Model::NestedModelValidator, attributes: [name.to_sym]
167
226
  end
168
227
 
169
228
  _attr_jsons_module.module_eval do
170
229
  define_method("#{name}=") do |value|
171
- _attr_json_write(name.to_s, value)
230
+ _attr_json_write(AttrJson.efficient_to_s(name), value)
172
231
  end
173
232
 
174
233
  define_method("#{name}") do
175
- attributes[name.to_s]
234
+ attributes[AttrJson.efficient_to_s(name)]
176
235
  end
177
236
  end
178
237
  end
179
238
 
180
- # This should kind of be considered 'protected', but the semantics
181
- # of how we want to call it don't give us a visibility modifier that works.
182
- # Prob means refactoring called for. TODO?
183
- def fill_in_defaults(hash)
184
- # Only if we need to mutate it to add defaults, we'll dup it first. deep_dup not neccesary
185
- # since we're only modifying top-level here.
186
- duped = false
187
- attr_json_registry.definitions.each do |definition|
188
- if definition.has_default? && ! (hash.has_key?(definition.store_key.to_s) || hash.has_key?(definition.store_key.to_sym))
189
- unless duped
190
- hash = hash.dup
191
- duped = true
192
- end
193
-
194
- hash[definition.store_key] = definition.provide_default!
195
- end
196
- end
197
-
198
- hash
199
- end
200
-
201
239
  private
202
240
 
203
241
  # Define an anonymous module and include it, so can still be easily
@@ -210,14 +248,54 @@ module AttrJson
210
248
  mod
211
249
  end
212
250
  end
251
+
252
+ # wrap in ActiveRecord type for timezone-awareness/conversion, if needed
253
+ # We at present only need this in AttrJson::Model cause in AttrJson::Record,
254
+ # our sync with ActiveRecord attributes takes care of it for us.
255
+ # See https://github.com/rails/rails/blob/v7.0.4/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb#L78
256
+ #
257
+ # For now we use ActiveRecord::Base state to decide.
258
+ #
259
+ # We have to turn a symbol type into a real object type if we want to wrap it -- we will
260
+ # return an actual ActiveModel::Value::Type either way, converting from symbol!
261
+ #
262
+ # That *wrapped* type will be the new type, registered with AttributeDefinition
263
+ # and then with Rails `attribute`, to provide timezone conversion.
264
+ def _attr_json_maybe_wrap_timezone_aware(type)
265
+ type = AttributeDefinition.lookup_type(type)
266
+
267
+ if self.attr_json_config.time_zone_aware_attributes.nil?
268
+ # nil config means use ActiveRecord::Base
269
+ aware = ActiveRecord::Base.time_zone_aware_attributes
270
+ aware_types = ActiveRecord::Base.time_zone_aware_types
271
+ elsif self.attr_json_config.time_zone_aware_attributes.kind_of?(Array)
272
+ # Array config means we're doing it, and these are the types
273
+ aware = true
274
+ aware_types = self.attr_json_config.time_zone_aware_attributes
275
+ else
276
+ # boolean config, types from ActiveRecord::Base
277
+ aware = !!self.attr_json_config.time_zone_aware_attributes
278
+ aware_types = ActiveRecord::Base.time_zone_aware_types
279
+ end
280
+
281
+ if aware && aware_types.include?(type.type)
282
+ type = ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter.new(type)
283
+ end
284
+
285
+ type
286
+ end
213
287
  end
214
288
 
215
289
  def initialize(attributes = {})
216
- if !attributes.respond_to?(:transform_keys)
217
- raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
218
- end
290
+ super
291
+
292
+ fill_in_defaults!
293
+ end
219
294
 
220
- super(self.class.fill_in_defaults(attributes))
295
+ # inspired by https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/activemodel/lib/active_model/attributes.rb
296
+ def initialize_dup(other) # :nodoc:
297
+ @attributes = @attributes.deep_dup
298
+ super
221
299
  end
222
300
 
223
301
  def attributes
@@ -255,32 +333,69 @@ module AttrJson
255
333
  self.class.attr_json_registry.has_attribute?(str)
256
334
  end
257
335
 
258
- # Override from ActiveModel::Serialization to #serialize
259
- # by type to make sure any values set directly on hash still
336
+ # like the ActiveModel::Attributes method
337
+ def attribute_names
338
+ self.class.attribute_names
339
+ end
340
+
341
+ # Override from ActiveModel::Serialization to:
342
+ #
343
+ # * handle store_key settings
344
+ #
345
+ # * #serialize by type to make sure any values set directly on hash still
260
346
  # get properly type-serialized.
261
- def serializable_hash(*options)
262
- super.collect do |key, value|
347
+ #
348
+ # * custom logic for keeping nil values out of serialization to be more compact
349
+ #
350
+ # @param strip_nils [:symbol, Boolean] (default false) Should we keep keys with
351
+ # `nil` values out of the serialization entirely? You might want to to keep
352
+ # your in-database serialization compact. By default this method does not -- but
353
+ # by default AttrJson::Type::Model sends `:safely` when serializing.
354
+ # * false => do not strip nils
355
+ # * :safely => strip nils only when there is no default value for the attribute,
356
+ # so `nil` can still override the default value
357
+ # * true => strip nils even if there is a default value -- in AttrJson
358
+ # context, this means the default will be reapplied over nil on
359
+ # every de-serialization!
360
+ def serializable_hash(options=nil)
361
+ strip_nils = options&.has_key?(:strip_nils) ? options.delete(:strip_nils) : false
362
+
363
+ unless [true, false, :safely].include?(strip_nils)
364
+ raise ArgumentError, ":strip_nils must be true, false, or :safely"
365
+ end
366
+
367
+ super(options).collect do |key, value|
263
368
  if attribute_def = self.class.attr_json_registry[key.to_sym]
264
369
  key = attribute_def.store_key
265
- if value.kind_of?(Time) || value.kind_of?(DateTime)
266
- value = value.utc.change(usec: 0)
267
- end
268
370
 
269
371
  value = attribute_def.serialize(value)
270
372
  end
271
- # Do we need unknown key handling here? Apparently not?
272
- [key, value]
273
- end.to_h
373
+
374
+ # strip_nils handling
375
+ if value.nil? && (
376
+ (strip_nils == :safely && !attribute_def&.has_default?) ||
377
+ strip_nils == true )
378
+ then
379
+ # do not include in serializable_hash
380
+ nil
381
+ else
382
+ [key, value]
383
+ end
384
+ end.compact.to_h
274
385
  end
275
386
 
276
387
  # ActiveRecord JSON serialization will insist on calling
277
388
  # this, instead of the specified type's #serialize, at least in some cases.
278
389
  # So it's important we define it -- the default #as_json added by ActiveSupport
279
390
  # will serialize all instance variables, which is not what we want.
280
- def as_json(*options)
281
- serializable_hash(*options)
391
+ #
392
+ # @param strip_nils [:symbol, Boolean] (default false) [true, false, :safely],
393
+ # see #serializable_hash
394
+ def as_json(options=nil)
395
+ serializable_hash(options)
282
396
  end
283
397
 
398
+
284
399
  # We deep_dup on #to_h, you want attributes unduped, ask for #attributes.
285
400
  def to_h
286
401
  attributes.deep_dup
@@ -301,14 +416,31 @@ module AttrJson
301
416
  false
302
417
  end
303
418
 
419
+ # like ActiveModel::Attributes at
420
+ # https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/activemodel/lib/active_model/attributes.rb#L120
421
+ #
422
+ # is not a full deep freeze
423
+ def freeze
424
+ attributes.freeze unless frozen?
425
+ super
426
+ end
427
+
304
428
  private
305
429
 
430
+ def fill_in_defaults!
431
+ self.class.attr_json_registry.definitions.each do |definition|
432
+ if definition.has_default? && !attributes.has_key?(AttrJson.efficient_to_s(definition.name))
433
+ self.send("#{definition.name.to_s}=", definition.provide_default!)
434
+ end
435
+ end
436
+ end
437
+
306
438
  def _attr_json_write(key, value)
307
439
  if attribute_def = self.class.attr_json_registry[key.to_sym]
308
- attributes[key.to_s] = attribute_def.cast(value)
440
+ attributes[AttrJson.efficient_to_s(key)] = attribute_def.cast(value)
309
441
  else
310
442
  # TODO, strict mode, ignore, raise, allow.
311
- attributes[key.to_s] = value
443
+ attributes[AttrJson.efficient_to_s(key)] = value
312
444
  end
313
445
  end
314
446
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AttrJson
2
4
  module NestedAttributes
3
5
  # Implementation of `build_` methods, called by the `build_` methods
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AttrJson
2
4
  module NestedAttributes
3
5
  # Rails has a weird "multiparameter attribute" thing, that is used for simple_form's
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'attr_json/nested_attributes/multiparameter_attribute_writer'
2
4
 
3
5
  module AttrJson
@@ -15,12 +17,10 @@ module AttrJson
15
17
  delegate :nested_attributes_options, to: :model
16
18
 
17
19
  def assign_nested_attributes(attributes)
18
- if attr_def.array_type?
19
- if attr_def.type.base_type_primitive?
20
- assign_nested_attributes_for_primitive_array(attributes)
21
- else
22
- assign_nested_attributes_for_model_array(attributes)
23
- end
20
+ if attr_def.array_of_primitive_type?
21
+ assign_nested_attributes_for_primitive_array(attributes)
22
+ elsif attr_def.array_type?
23
+ assign_nested_attributes_for_model_array(attributes)
24
24
  else
25
25
  assign_nested_attributes_for_single_model(attributes)
26
26
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'attr_json/nested_attributes/builder'
2
4
  require 'attr_json/nested_attributes/writer'
3
5
 
@@ -65,6 +67,10 @@ module AttrJson
65
67
  raise ArgumentError, "No attr_json found for name '#{attr_name}'. Has it been defined yet?"
66
68
  end
67
69
 
70
+ unless attr_def.array_type? || attr_def.single_model_type?
71
+ raise TypeError, "attr_json_accepts_nested_attributes_for is only for array or nested model types; `#{attr_name}` is type #{attr_def.type.type.inspect}"
72
+ end
73
+
68
74
  # We're sharing AR class attr in an AR, or using our own in a Model.
69
75
  nested_attributes_options = self.nested_attributes_options.dup
70
76
  nested_attributes_options[attr_name.to_sym] = options
@@ -80,7 +86,7 @@ module AttrJson
80
86
  end
81
87
 
82
88
  # No build method for our wacky array of primitive type.
83
- if options[:define_build_method] && !(attr_def.array_type? && attr_def.type.base_type_primitive?)
89
+ if options[:define_build_method] && !attr_def.array_of_primitive_type?
84
90
  _attr_jsons_module.module_eval do
85
91
  build_method_name = "build_#{attr_name.to_s.singularize}"
86
92
  if method_defined?(build_method_name)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AttrJson
2
4
  module Record
3
5
  # Implementation class called by #jsonb_contains scope method. Ordinarily
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'attr_json/record/query_builder'
2
4
 
3
5
  module AttrJson