Module: AttrJson::Model

Extended by:
ActiveSupport::Concern
Includes:
ActiveModel::Model, ActiveModel::Serialization
Defined in:
lib/attr_json/model.rb,
lib/attr_json/model/cocoon_compat.rb,
lib/attr_json/model/nested_model_validator.rb

Overview

Note:

Includes ActiveModel::Model whether you like it or not.

Meant for use in a plain class, turns it into an ActiveModel::Model with attr_json support. NOT for use in an ActiveRecord::Base model, see Record for ActiveRecord use.

Creates an ActiveModel object with typed attributes, easily serializable to json, and with a corresponding ActiveModel::Type representing the class. Meant for use as an attribute of a AttrJson::Record. Can be nested, AttrJson::Models can have attributes that are other AttrJson::Models.

You can control what happens if you set an unknown key (one that you didn't register with attr_json) with the config attribute attr_json_config(unknown_key:).

  • :raise (default) raise ActiveModel::UnknownAttributeError
  • :strip Ignore the unknown key and do not include it, without raising.
  • :allow Allow the unknown key and it's value to be in the serialized hash, and written to the database. May be useful for legacy data or columns that other software touches, to let unknown keys just flow through.

    class Something include AttrJson::Model attr_json_config(unknown_key: :allow) #... end

Similarly, trying to set a Model-valued attribute with an object that can't be cast to a Hash or Model at all will normally raise a AttrJson::Type::Model::BadCast error, but you can set config bad_cast: :as_nil to make it cast to nil, more like typical ActiveRecord cast.

   class Something
     include AttrJson::Model
     attr_json_config(bad_cast: :as_nil)
     #...
   end

Date-type timezone conversion

By default, AttrJson::Model date/time attributes will be ActiveRecord timezone-aware based on settings of config.active_record.time_zone_aware_attributes and ActiveRecord::Base.time_zone_aware_types.

If you'd like to override this, you can set:

attr_json_config(time_zone_aware_attributes: true)
attr_json_config(time_zone_aware_attributes: false)
attr_json_config(time_zone_aware_attributes: [:datetime, :time]) # custom list of types

ActiveRecord serialize

If you want to map a single AttrJson::Model to a json/jsonb column, you can use ActiveRecord serialize feature.

https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html

We provide a simple shim to give you the right API for a "coder" for AR serialize:

class ValueModel include AttrJson::Model attr_json :some_string, :string end

class SomeModel < ApplicationRecord serialize :some_json_column, ValueModel.to_serialize_coder end

Strip nils

When embedded in an attr_json attribute, models are normally serialized with nil values stripped from hash where possible, for a more compact representation. This can be set differently in the type.

attr_json :lang_and_value, LangAndValue.to_type(strip_nils: false)

See #serializable_hash docs for possible values.

Defined Under Namespace

Modules: CocoonCompat Classes: NestedModelValidator

Instance Method Summary collapse

Instance Method Details

#==(other_object) ⇒ Object

Two AttrJson::Model objects are equal if they are the same class AND their #attributes are equal.



406
407
408
# File 'lib/attr_json/model.rb', line 406

def ==(other_object)
  other_object.class == self.class && other_object.attributes == self.attributes
end

#_destroyObject

ActiveRecord objects have a _destroy, related to marked_for_destruction? functionality used with AR nested attributes. We don't mark for destruction, our nested attributes implementation just deletes immediately, but having this simple method always returning false makes things work more compatibly and smoothly with standard code for nested attributes deletion in form builders.



415
416
417
# File 'lib/attr_json/model.rb', line 415

def _destroy
  false
end

#as_json(options = nil) ⇒ Object

ActiveRecord JSON serialization will insist on calling this, instead of the specified type's #serialize, at least in some cases. So it's important we define it -- the default #as_json added by ActiveSupport will serialize all instance variables, which is not what we want.

Parameters:

  • strip_nils (:symbol, Boolean)

    (default false) [true, false, :safely], see #serializable_hash



394
395
396
# File 'lib/attr_json/model.rb', line 394

def as_json(options=nil)
  serializable_hash(options)
end

#assign_attributes(new_attributes) ⇒ Object



307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# File 'lib/attr_json/model.rb', line 307

def assign_attributes(new_attributes)
  if !new_attributes.respond_to?(:stringify_keys)
    raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
  end
  return if new_attributes.empty?

  # stringify keys just like https://github.com/rails/rails/blob/4f99a2186479d5f77460622f2c0f37708b3ec1bc/activemodel/lib/active_model/attribute_assignment.rb#L34
  new_attributes.stringify_keys.each do |k, v|
    setter = :"#{k}="
    if respond_to?(setter)
      public_send(setter, v)
    else
      _attr_json_write_unknown_attribute(k, v)
    end
  end
end

#attribute_namesObject

like the ActiveModel::Attributes method



337
338
339
# File 'lib/attr_json/model.rb', line 337

def attribute_names
  self.class.attribute_names
end

#attributesObject



301
302
303
# File 'lib/attr_json/model.rb', line 301

def attributes
  @attributes ||= {}
end

#freezeObject



423
424
425
426
# File 'lib/attr_json/model.rb', line 423

def freeze
  attributes.freeze unless frozen?
  super
end

#has_attribute?(str) ⇒ Boolean

This attribute from ActiveRecord make SimpleForm happy, and able to detect type.

Returns:

  • (Boolean)


332
333
334
# File 'lib/attr_json/model.rb', line 332

def has_attribute?(str)
  self.class.attr_json_registry.has_attribute?(str)
end

#initialize(attributes = {}) ⇒ Object



289
290
291
292
293
# File 'lib/attr_json/model.rb', line 289

def initialize(attributes = {})
  super

  fill_in_defaults!
end

#initialize_dup(other) ⇒ Object



296
297
298
299
# File 'lib/attr_json/model.rb', line 296

def initialize_dup(other) # :nodoc:
  @attributes = @attributes.deep_dup
  super
end

#serializable_hash(options = nil) ⇒ Object

Override from ActiveModel::Serialization to:

  • handle store_key settings

  • serialize by type to make sure any values set directly on hash still

    get properly type-serialized.

  • custom logic for keeping nil values out of serialization to be more compact

Parameters:

  • strip_nils (:symbol, Boolean)

    (default false) Should we keep keys with nil values out of the serialization entirely? You might want to to keep your in-database serialization compact. By default this method does not -- but by default AttrJson::Type::Model sends :safely when serializing.

    • false => do not strip nils
    • :safely => strip nils only when there is no default value for the attribute, so nil can still override the default value
    • true => strip nils even if there is a default value -- in AttrJson context, this means the default will be reapplied over nil on every de-serialization!


360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
# File 'lib/attr_json/model.rb', line 360

def serializable_hash(options=nil)
  strip_nils = options&.has_key?(:strip_nils) ? options.delete(:strip_nils) : false

  unless [true, false, :safely].include?(strip_nils)
    raise ArgumentError, ":strip_nils must be true, false, or :safely"
  end

  super(options).collect do |key, value|
    if attribute_def = self.class.attr_json_registry[key.to_sym]
      key = attribute_def.store_key

      value = attribute_def.serialize(value)
    end

    # strip_nils handling
    if value.nil? && (
       (strip_nils == :safely && !attribute_def&.has_default?) ||
        strip_nils == true )
    then
      # do not include in serializable_hash
      nil
    else
      [key, value]
    end
  end.compact.to_h
end

#to_hObject

We deep_dup on #to_h, you want attributes unduped, ask for #attributes.



400
401
402
# File 'lib/attr_json/model.rb', line 400

def to_h
  attributes.deep_dup
end

#type_for_attribute(attr_name) ⇒ Object

This attribute from ActiveRecord makes SimpleForm happy, and able to detect type.



326
327
328
# File 'lib/attr_json/model.rb', line 326

def type_for_attribute(attr_name)
  self.class.attr_json_registry.type_for_attribute(attr_name)
end