attr_json 1.5.0 → 2.0.0.rc1

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