attr_json 1.5.0 → 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -20,7 +20,7 @@ module AttrJson
20
20
  # Meant for use as an attribute of a AttrJson::Record. Can be nested,
21
21
  # AttrJson::Models can have attributes that are other AttrJson::Models.
22
22
  #
23
- # @note Includes ActiveModel::Model whether you like it or not. TODO, should it?
23
+ # @note Includes ActiveModel::Model whether you like it or not.
24
24
  #
25
25
  # You can control what happens if you set an unknown key (one that you didn't
26
26
  # register with `attr_json`) with the config attribute `attr_json_config(unknown_key:)`.
@@ -47,6 +47,21 @@ module AttrJson
47
47
  # #...
48
48
  # end
49
49
  #
50
+ # ## Date-type timezone conversion
51
+ #
52
+ # By default, AttrJson::Model date/time attributes will be
53
+ # [ActiveRecord timezone-aware](https://api.rubyonrails.org/classes/ActiveRecord/Timestamp.html)
54
+ # based on settings of `config.active_record.time_zone_aware_attributes` and
55
+ # `ActiveRecord::Base.time_zone_aware_types`.
56
+ #
57
+ # If you'd like to override this, you can set:
58
+ #
59
+ # ```
60
+ # attr_json_config(time_zone_aware_attributes: true)
61
+ # attr_json_config(time_zone_aware_attributes: false)
62
+ # attr_json_config(time_zone_aware_attributes: [:datetime, :time]) # custom list of types
63
+ # ```
64
+ #
50
65
  # ## ActiveRecord `serialize`
51
66
  #
52
67
  # If you want to map a single AttrJson::Model to a json/jsonb column, you
@@ -65,6 +80,15 @@ module AttrJson
65
80
  # serialize :some_json_column, ValueModel.to_serialize_coder
66
81
  # end
67
82
  #
83
+ # # Strip nils
84
+ #
85
+ # When embedded in an `attr_json` attribute, models are normally serialized with `nil` values
86
+ # stripped from hash where possible, for a more compact representation.
87
+ # This can be set differently in the type.
88
+ #
89
+ # attr_json :lang_and_value, LangAndValue.to_type(strip_nils: false)
90
+ #
91
+ # See #serializable_hash docs for possible values.
68
92
  module Model
69
93
  extend ActiveSupport::Concern
70
94
 
@@ -106,7 +130,7 @@ module AttrJson
106
130
  # The inverse of model#serializable_hash -- re-hydrates a serialized hash to a model.
107
131
  #
108
132
  # 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.
133
+ # like store_keys, and properly calling deserialize (rather than cast) on the underlying types.
110
134
  #
111
135
  # @example Model.new_from_serializable(hash)
112
136
  def new_from_serializable(attributes = {})
@@ -127,10 +151,25 @@ module AttrJson
127
151
  self.new(attributes)
128
152
  end
129
153
 
130
- def to_type
131
- @type ||= AttrJson::Type::Model.new(self)
154
+ # @returns ActiveModel::Type suitable for including this model in
155
+ # an AttrJson::Record or ::Model attribute
156
+ #
157
+ # @param strip_nils [Boolean,Symbol] [true,false,:safely] as a type,
158
+ # should we strip nils when serializing? By default this type strips
159
+ # nils in :safely mode. See AttrJson::Model#serializable_hash
160
+ def to_type(strip_nils: :safely)
161
+ @type ||= AttrJson::Type::Model.new(self, strip_nils: strip_nils)
132
162
  end
133
163
 
164
+ # An ActiveModel::Type that can be used to serialize this model
165
+ # across an entire JSON(b) column.
166
+ #
167
+ # @example using standard ActiveRecord `serialize` feature.
168
+ #
169
+ # class MyTable < ApplicationRecord
170
+ # serialize :some_json_column, MyModel.to_serialization_coder
171
+ # end
172
+ #
134
173
  def to_serialization_coder
135
174
  @serialization_coder ||= AttrJson::SerializationCoderFromType.new(to_type)
136
175
  end
@@ -154,6 +193,8 @@ module AttrJson
154
193
  # @param type [ActiveModel::Type::Value] An instance of an ActiveModel::Type::Value (or subclass)
155
194
  #
156
195
  # @option options [Boolean] :array (false) Make this attribute an array of given type.
196
+ # Array types default to an empty array. If you want to turn that off, you can add
197
+ # `default: AttrJson::AttributeDefinition::NO_DEFAULT_PROVIDED`
157
198
  #
158
199
  # @option options [Object] :default (nil) Default value, if a Proc object it will be #call'd
159
200
  # for default.
@@ -166,6 +207,8 @@ module AttrJson
166
207
  def attr_json(name, type, **options)
167
208
  options.assert_valid_keys(*(AttributeDefinition::VALID_OPTIONS - [:container_attribute] + [:validate]))
168
209
 
210
+ type = _attr_json_maybe_wrap_timezone_aware(type)
211
+
169
212
  self.attr_json_registry = attr_json_registry.with(
170
213
  AttributeDefinition.new(name.to_sym, type, options.except(:validate))
171
214
  )
@@ -200,6 +243,42 @@ module AttrJson
200
243
  mod
201
244
  end
202
245
  end
246
+
247
+ # wrap in ActiveRecord type for timezone-awareness/conversion, if needed
248
+ # We at present only need this in AttrJson::Model cause in AttrJson::Record,
249
+ # our sync with ActiveRecord attributes takes care of it for us.
250
+ # See https://github.com/rails/rails/blob/v7.0.4/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb#L78
251
+ #
252
+ # For now we use ActiveRecord::Base state to decide.
253
+ #
254
+ # We have to turn a symbol type into a real object type if we want to wrap it -- we will
255
+ # return an actual ActiveModel::Value::Type either way, converting from symbol!
256
+ #
257
+ # That *wrapped* type will be the new type, registered with AttributeDefinition
258
+ # and then with Rails `attribute`, to provide timezone conversion.
259
+ def _attr_json_maybe_wrap_timezone_aware(type)
260
+ type = AttributeDefinition.lookup_type(type)
261
+
262
+ if self.attr_json_config.time_zone_aware_attributes.nil?
263
+ # nil config means use ActiveRecord::Base
264
+ aware = ActiveRecord::Base.time_zone_aware_attributes
265
+ aware_types = ActiveRecord::Base.time_zone_aware_types
266
+ elsif self.attr_json_config.time_zone_aware_attributes.kind_of?(Array)
267
+ # Array config means we're doing it, and these are the types
268
+ aware = true
269
+ aware_types = self.attr_json_config.time_zone_aware_attributes
270
+ else
271
+ # boolean config, types from ActiveRecord::Base
272
+ aware = !!self.attr_json_config.time_zone_aware_attributes
273
+ aware_types = ActiveRecord::Base.time_zone_aware_types
274
+ end
275
+
276
+ if aware && aware_types.include?(type.type)
277
+ type = ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter.new(type)
278
+ end
279
+
280
+ type
281
+ end
203
282
  end
204
283
 
205
284
  def initialize(attributes = {})
@@ -254,32 +333,64 @@ module AttrJson
254
333
  self.class.attribute_names
255
334
  end
256
335
 
257
- # Override from ActiveModel::Serialization to #serialize
258
- # by type to make sure any values set directly on hash still
336
+ # Override from ActiveModel::Serialization to:
337
+ #
338
+ # * handle store_key settings
339
+ #
340
+ # * #serialize by type to make sure any values set directly on hash still
259
341
  # get properly type-serialized.
260
- def serializable_hash(*options)
261
- super.collect do |key, value|
342
+ #
343
+ # * custom logic for keeping nil values out of serialization to be more compact
344
+ #
345
+ # @param strip_nils [:symbol, Boolean] (default false) Should we keep keys with
346
+ # `nil` values out of the serialization entirely? You might want to to keep
347
+ # your in-database serialization compact. By default this method does not -- but
348
+ # by default AttrJson::Type::Model sends `:safely` when serializing.
349
+ # * false => do not strip nils
350
+ # * :safely => strip nils only when there is no default value for the attribute,
351
+ # so `nil` can still override the default value
352
+ # * true => strip nils even if there is a default value -- in AttrJson
353
+ # context, this means the default will be reapplied over nil on
354
+ # every de-serialization!
355
+ def serializable_hash(options=nil)
356
+ strip_nils = options&.has_key?(:strip_nils) ? options.delete(:strip_nils) : false
357
+
358
+ unless [true, false, :safely].include?(strip_nils)
359
+ raise ArgumentError, ":strip_nils must be true, false, or :safely"
360
+ end
361
+
362
+ super(options).collect do |key, value|
262
363
  if attribute_def = self.class.attr_json_registry[key.to_sym]
263
364
  key = attribute_def.store_key
264
- if value.kind_of?(Time) || value.kind_of?(DateTime)
265
- value = value.utc.change(usec: 0)
266
- end
267
365
 
268
366
  value = attribute_def.serialize(value)
269
367
  end
270
- # Do we need unknown key handling here? Apparently not?
271
- [key, value]
272
- end.to_h
368
+
369
+ # strip_nils handling
370
+ if value.nil? && (
371
+ (strip_nils == :safely && !attribute_def&.has_default?) ||
372
+ strip_nils == true )
373
+ then
374
+ # do not include in serializable_hash
375
+ nil
376
+ else
377
+ [key, value]
378
+ end
379
+ end.compact.to_h
273
380
  end
274
381
 
275
382
  # ActiveRecord JSON serialization will insist on calling
276
383
  # this, instead of the specified type's #serialize, at least in some cases.
277
384
  # So it's important we define it -- the default #as_json added by ActiveSupport
278
385
  # will serialize all instance variables, which is not what we want.
279
- def as_json(*options)
280
- serializable_hash(*options)
386
+ #
387
+ # @param strip_nils [:symbol, Boolean] (default false) [true, false, :safely],
388
+ # see #serializable_hash
389
+ def as_json(options=nil)
390
+ serializable_hash(options)
281
391
  end
282
392
 
393
+
283
394
  # We deep_dup on #to_h, you want attributes unduped, ask for #attributes.
284
395
  def to_h
285
396
  attributes.deep_dup
@@ -15,12 +15,10 @@ module AttrJson
15
15
  delegate :nested_attributes_options, to: :model
16
16
 
17
17
  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
18
+ if attr_def.array_of_primitive_type?
19
+ assign_nested_attributes_for_primitive_array(attributes)
20
+ elsif attr_def.array_type?
21
+ assign_nested_attributes_for_model_array(attributes)
24
22
  else
25
23
  assign_nested_attributes_for_single_model(attributes)
26
24
  end
@@ -65,6 +65,10 @@ module AttrJson
65
65
  raise ArgumentError, "No attr_json found for name '#{attr_name}'. Has it been defined yet?"
66
66
  end
67
67
 
68
+ unless attr_def.array_type? || attr_def.single_model_type?
69
+ 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}"
70
+ end
71
+
68
72
  # We're sharing AR class attr in an AR, or using our own in a Model.
69
73
  nested_attributes_options = self.nested_attributes_options.dup
70
74
  nested_attributes_options[attr_name.to_sym] = options
@@ -80,7 +84,7 @@ module AttrJson
80
84
  end
81
85
 
82
86
  # 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?)
87
+ if options[:define_build_method] && !attr_def.array_of_primitive_type?
84
88
  _attr_jsons_module.module_eval do
85
89
  build_method_name = "build_#{attr_name.to_s.singularize}"
86
90
  if method_defined?(build_method_name)
@@ -24,35 +24,51 @@ module AttrJson
24
24
 
25
25
  class_attribute :attr_json_registry, instance_accessor: false
26
26
  self.attr_json_registry = AttrJson::AttributeDefinition::Registry.new
27
- end
28
27
 
29
- protected
28
+ # Ensure that rails attributes tracker knows about values we just fetched
29
+ after_initialize do
30
+ attr_json_sync_to_rails_attributes
31
+ end
32
+
33
+ # After a safe, rails attribute dirty tracking ends up re-creating
34
+ # new objects for attribute values, so we need to sync again
35
+ # so mutation effects both.
36
+ after_save do
37
+ attr_json_sync_to_rails_attributes
38
+ end
39
+ end
30
40
 
31
- # adapted from ActiveRecord query_attribute method
32
- # https://github.com/rails/rails/blob/v5.2.3/activerecord/lib/active_record/attribute_methods/query.rb#L12
41
+ # Sync all values FROM the json_attributes json column TO rails attributes
33
42
  #
34
- # Sadly we could not re-use Rails code here, becuase the built-in method assumes attribute
35
- # can be obtained with `self[attr_name]`, which you can not with attr_json (is that bad?), as
36
- # well as `self.class.columns_hash[attr_name]` which you definitely can not (which is probably not bad),
37
- # and has no way to use the value-translation semantics independently of that. May be a problem if
38
- # ActiveRecord changes it's query method semantics in the future, will have to be sync'd here.
43
+ # If values have for some reason gotten out of sync this will make them the
44
+ # identical objects again, with the container hash value being the source.
39
45
  #
40
- # Used to implement query methods on attr_json attributes, like `attr_json :foo, :string`, method `#foo?`
41
- def self.attr_json_query_method(record, attribute)
42
- value = record.send(attribute)
43
-
44
- case value
45
- when true
46
- true
47
- when false, nil, ActiveModel::Type::Boolean::FALSE_VALUES
48
- false
49
- else
50
- if value.respond_to?(:to_i) && ( Numeric === value || value.to_s !~ /[^0-9]/ )
51
- !value.to_i.zero?
52
- elsif value.respond_to?(:zero?)
53
- !value.zero?
54
- else
55
- !value.blank?
46
+ # In some cases, the values may already be equivalent but different objects --
47
+ # This is meant to ensure they are the _same object_ in both places, so
48
+ # mutation of mutable object will effect both places, for instance for dirty
49
+ # tracking.
50
+ def attr_json_sync_to_rails_attributes
51
+ self.class.attr_json_registry.attribute_names.each do |attr_name|
52
+ begin
53
+ attribute_def = self.class.attr_json_registry.fetch(attr_name.to_sym)
54
+ json_value = public_send(attribute_def.container_attribute)
55
+ value = json_value[attribute_def.store_key]
56
+
57
+ if value
58
+ # TODO, can we just make this use the setter?
59
+ write_attribute(attr_name, value)
60
+
61
+ clear_attribute_change(attr_name) if persisted?
62
+
63
+ # writing and clearning will result in a new object stored in
64
+ # rails attributes, we want
65
+ # to make sure the exact same object is in the json attribute,
66
+ # so in-place mutation changes to it are reflected in both places.
67
+ json_value[attribute_def.store_key] = read_attribute(attr_name)
68
+ end
69
+ rescue AttrJson::Type::Model::BadCast, AttrJson::Type::PolymorphicModel::TypeError => e
70
+ # There was bad data in the DB, we're just going to skip the Rails attribute sync.
71
+ # Should we log?
56
72
  end
57
73
  end
58
74
  end
@@ -93,7 +109,8 @@ module AttrJson
93
109
  end
94
110
 
95
111
 
96
-
112
+ # Registers an attr_json attribute, and a Rails attribute covering it.
113
+ #
97
114
  # Type can be a symbol that will be looked up in `ActiveModel::Type.lookup`,
98
115
  # or an ActiveModel:::Type::Value).
99
116
  #
@@ -102,6 +119,8 @@ module AttrJson
102
119
  # @param type [ActiveModel::Type::Value] An instance of an ActiveModel::Type::Value (or subclass)
103
120
  #
104
121
  # @option options [Boolean] :array (false) Make this attribute an array of given type.
122
+ # Array types default to an empty array. If you want to turn that off, you can add
123
+ # `default: AttrJson::AttributeDefinition::NO_DEFAULT_PROVIDED`
105
124
  #
106
125
  # @option options [Object] :default (nil) Default value, if a Proc object it will be #call'd
107
126
  # for default.
@@ -113,22 +132,22 @@ module AttrJson
113
132
  # json(b) ActiveRecord attribute/column to serialize as a key in. Defaults to
114
133
  # `attr_json_config.default_container_attribute`, which defaults to `:json_attributes`
115
134
  #
116
- # @option options [Boolean] :validate (true) Create an ActiveRecord::Validations::AssociatedValidator so
117
- # validation errors on the attributes post up to self.
135
+ # @option options [Boolean] :validate (true) validation errors on nested models in the attributes
136
+ # should post up to self similar to Rails ActiveRecord::Validations::AssociatedValidator on
137
+ # associated objects.
138
+ #
139
+ # @option options [Boolean,Hash] :accepts_nested_attributes. If true, equivalent
140
+ # of writing `attr_json_accepts_nested_attributes :attribute_name`. If value is a hash,
141
+ # then same, but with hash as options to `attr_json_accepts_nested_attributes`.
142
+ # Default taken from `attr_json_config.default_accepts_nested_attributes`, for
143
+ # array or model types where it is applicable.
118
144
  #
119
- # @option options [Boolean] :rails_attribute (false) Create an actual ActiveRecord
120
- # `attribute` for name param. A Rails attribute isn't needed for our functionality,
121
- # but registering thusly will let the type be picked up by simple_form and
122
- # other tools that may look for it via Rails attribute APIs. Default can be changed
123
- # with `attr_json_config(default_rails_attribute: true)`
124
145
  def attr_json(name, type, **options)
125
146
  options = {
126
- rails_attribute: self.attr_json_config.default_rails_attribute,
127
147
  validate: true,
128
148
  container_attribute: self.attr_json_config.default_container_attribute,
129
- accepts_nested_attributes: self.attr_json_config.default_accepts_nested_attributes
130
149
  }.merge!(options)
131
- options.assert_valid_keys(AttributeDefinition::VALID_OPTIONS + [:validate, :rails_attribute, :accepts_nested_attributes])
150
+ options.assert_valid_keys(AttributeDefinition::VALID_OPTIONS + [:validate, :accepts_nested_attributes])
132
151
  container_attribute = options[:container_attribute]
133
152
 
134
153
  # TODO arg check container_attribute make sure it exists. Hard cause
@@ -150,63 +169,65 @@ module AttrJson
150
169
  end
151
170
 
152
171
  self.attr_json_registry = attr_json_registry.with(
153
- AttributeDefinition.new(name.to_sym, type, options.except(:rails_attribute, :validate, :accepts_nested_attributes))
172
+ AttributeDefinition.new(name.to_sym, type, options.except(:validate, :accepts_nested_attributes))
154
173
  )
155
174
 
156
- # By default, automatically validate nested models
175
+ # By default, automatically validate nested models, allowing nils.
157
176
  if type.kind_of?(AttrJson::Type::Model) && options[:validate]
158
- self.validates_with ActiveRecord::Validations::AssociatedValidator, attributes: [name.to_sym]
159
- end
160
-
161
- # We don't actually use this for anything, we provide our own covers. But registering
162
- # it with usual system will let simple_form and maybe others find it.
163
- if options[:rails_attribute]
164
- attr_json_definition = attr_json_registry[name]
165
-
166
- attribute_args = attr_json_definition.has_default? ? { default: attr_json_definition.default_argument } : {}
167
- self.attribute name.to_sym, attr_json_definition.type, **attribute_args
168
-
169
- # Ensure that rails attributes tracker knows about value we just fetched
170
- # for this particular attribute. Yes, we are registering an after_find for each
171
- # attr_json registered with rails_attribute:true, using the `name` from above under closure. .
172
- after_find do
173
- value = public_send(name)
174
- if value && has_attribute?(name.to_sym)
175
- write_attribute(name.to_sym, value)
176
- self.send(:clear_attribute_changes, [name.to_sym])
177
+ # implementation adopted from:
178
+ # https://github.com/rails/rails/blob/v7.0.4.1/activerecord/lib/active_record/validations/associated.rb#L6-L10
179
+ #
180
+ # but had to customize to allow nils in an array through
181
+ validates_each name.to_sym do |record, attr, value|
182
+ if Array(value).reject { |element| element.nil? || element.valid? }.any?
183
+ record.errors.add(attr, :invalid, value: value)
177
184
  end
178
185
  end
179
186
  end
180
187
 
181
- _attr_jsons_module.module_eval do
182
- # For getter and setter, we used to use read_store_attribute/write_store_attribute
183
- # copied from Rails store_accessor implementation.
184
- # https://github.com/rails/rails/blob/74c3e43fba458b9b863d27f0c45fd2d8dc603cbc/activerecord/lib/active_record/store.rb#L90-L96
185
- #
186
- # But in fact just getting/setting in the hash provided to us by ActiveRecord json type
187
- # container works BETTER for dirty tracking. We had a test that only passed doing it
188
- # this simple way.
188
+ # Register as a Rails attribute
189
+ attr_json_definition = attr_json_registry[name]
190
+ attribute_args = attr_json_definition.has_default? ? { default: attr_json_definition.default_argument } : {}
191
+ self.attribute name.to_sym, attr_json_definition.type, **attribute_args
189
192
 
193
+ # For getter and setter, we consider the container has the "canonical" data location.
194
+ # But setter also writes to rails attribute, and tries to keep them in sync with the
195
+ # *same object*, so mutations happen to both places.
196
+ #
197
+ # This began roughly modelled on approach of Rail sstore_accessor implementation:
198
+ # https://github.com/rails/rails/blob/74c3e43fba458b9b863d27f0c45fd2d8dc603cbc/activerecord/lib/active_record/store.rb#L90-L96
199
+ #
200
+ # ...But wound up with lots of evolution to try to get dirty tracking working as well
201
+ # as we could -- without a completely custom separate dirty tracking implementation
202
+ # like store_accessor tries!
203
+ _attr_jsons_module.module_eval do
190
204
  define_method("#{name}=") do |value|
191
- super(value) if defined?(super)
205
+ super(value) # should write to rails attribute
206
+
207
+ # write to container hash, with value read from attribute to try to keep objects
208
+ # sync'd to exact same object in rails attribute and container hash.
192
209
  attribute_def = self.class.attr_json_registry.fetch(name.to_sym)
193
- public_send(attribute_def.container_attribute)[attribute_def.store_key] = attribute_def.cast(value)
210
+ public_send(attribute_def.container_attribute)[attribute_def.store_key] = read_attribute(name)
194
211
  end
195
212
 
196
213
  define_method("#{name}") do
214
+ # read from container hash -- we consider that the canonical location.
197
215
  attribute_def = self.class.attr_json_registry.fetch(name.to_sym)
198
216
  public_send(attribute_def.container_attribute)[attribute_def.store_key]
199
217
  end
218
+ end
200
219
 
201
- define_method("#{name}?") do
202
- # implementation of `query_store_attribute` is based on Rails `query_attribute` implementation
203
- AttrJson::Record.attr_json_query_method(self, name)
204
- end
220
+ accepts_nested_attributes = if options.has_key?(:accepts_nested_attributes)
221
+ options[:accepts_nested_attributes]
222
+ elsif attr_json_definition.single_model_type? || attr_json_definition.array_type?
223
+ # use configured default only if we have a type appropriate for it!
224
+ self.attr_json_config.default_accepts_nested_attributes
225
+ else
226
+ false
205
227
  end
206
228
 
207
- # Default attr_json_accepts_nested_attributes_for values
208
- if options[:accepts_nested_attributes]
209
- options = options[:accepts_nested_attributes] == true ? {} : options[:accepts_nested_attributes]
229
+ if accepts_nested_attributes
230
+ options = accepts_nested_attributes == true ? {} : accepts_nested_attributes
210
231
  self.attr_json_accepts_nested_attributes_for name, **options
211
232
  end
212
233
  end
@@ -30,6 +30,10 @@ module AttrJson
30
30
  convert_to_array(value).collect { |v| base_type.deserialize(v) }
31
31
  end
32
32
 
33
+ def changed_in_place?(raw_old_value, new_value)
34
+ serialize(new_value) != raw_old_value
35
+ end
36
+
33
37
  # This is used only by our own keypath-chaining query stuff.
34
38
  def value_for_contains_query(key_path_arr, value)
35
39
  [
@@ -41,10 +45,13 @@ module AttrJson
41
45
  ]
42
46
  end
43
47
 
44
- # Hacky, hard-coded classes, but used in our weird primitive implementation in NestedAttributes,
45
- # better than putting the conditionals there
48
+ # Soft-deprecated. You probably want to use
49
+ #
50
+ # AttrJson::AttributeDefinition#array_of_primitive_type?
51
+ #
52
+ # instead where possible.
46
53
  def base_type_primitive?
47
- !(base_type.is_a?(AttrJson::Type::Model) || base_type.is_a?(AttrJson::Type::PolymorphicModel))
54
+ ! AttrJson::AttributeDefinition.single_model_type?(base_type)
48
55
  end
49
56
 
50
57
  protected
@@ -8,13 +8,22 @@ module AttrJson
8
8
  # but normally that's only done in AttrJson::Model.to_type, there isn't
9
9
  # an anticipated need to create from any other place.
10
10
  #
11
+ #
11
12
  class Model < ::ActiveModel::Type::Value
12
13
  class BadCast < ArgumentError ; end
13
14
 
14
- attr_accessor :model
15
- def initialize(model)
15
+ attr_accessor :model, :strip_nils
16
+
17
+ # @param model [AttrJson::Model] the model _class_ object
18
+ # @param strip_nils [Symbol,Boolean] [true, false, or :safely]
19
+ # (default :safely), As a type, should we strip nils when serialiing?
20
+ # This value passed to AttrJson::Model#serialized_hash(strip_nils).
21
+ # by default it's :safely, we strip nils when it can be done safely
22
+ # to preserve default overrides.
23
+ def initialize(model, strip_nils: :safely)
16
24
  #TODO type check, it really better be a AttrJson::Model. maybe?
17
25
  @model = model
26
+ @strip_nils = strip_nils
18
27
  end
19
28
 
20
29
  def type
@@ -51,9 +60,9 @@ module AttrJson
51
60
  if v.nil?
52
61
  nil
53
62
  elsif v.kind_of?(model)
54
- v.serializable_hash
63
+ v.serializable_hash(strip_nils: strip_nils)
55
64
  else
56
- (cast_v = cast(v)) && cast_v.serializable_hash
65
+ (cast_v = cast(v)) && cast_v.serializable_hash(strip_nils: strip_nils)
57
66
  end
58
67
  end
59
68
 
@@ -1,3 +1,3 @@
1
1
  module AttrJson
2
- VERSION = "1.5.0"
2
+ VERSION = "2.0.0.rc1"
3
3
  end
data/lib/attr_json.rb CHANGED
@@ -9,11 +9,6 @@ require 'attr_json/nested_attributes'
9
9
  require 'attr_json/record/query_scopes'
10
10
  require 'attr_json/type/polymorphic_model'
11
11
 
12
- # Dirty not supported on Rails 5.0
13
- if Gem.loaded_specs["activerecord"].version.release >= Gem::Version.new('5.1')
14
- require 'attr_json/record/dirty'
15
- end
16
-
17
12
  module AttrJson
18
13
 
19
14
  end
data/playground_models.rb CHANGED
@@ -61,7 +61,7 @@ class MyModel < ActiveRecord::Base
61
61
  self.table_name = "products"
62
62
 
63
63
  include AttrJson::Record
64
- include AttrJson::Record::Dirty
64
+ #include AttrJson::Record::Dirty
65
65
 
66
66
  attr_json :str, :string
67
67
  attr_json :str_array, :string, array: true
@@ -76,7 +76,7 @@ end
76
76
  class Product < StaticProduct
77
77
  include AttrJson::Record
78
78
  include AttrJson::Record::QueryScopes
79
- include AttrJson::Record::Dirty
79
+ #include AttrJson::Record::Dirty
80
80
 
81
81
  attr_json :title, :string
82
82
  attr_json :rank, :integer