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
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
Class Method Summary collapse
-
.attr_json(name, type, **options) ⇒ Object
Type can be an instance of an ActiveModel::Type::Value subclass, or a symbol that will be looked up in
ActiveModel::Type.lookup
. - .attr_json_config(new_values = {}) ⇒ Object
-
.attribute_names ⇒ Object
like the ActiveModel::Attributes method.
-
.attribute_types ⇒ Object
like the ActiveModel::Attributes method, hash with name keys, and ActiveModel::Type values.
-
.new_from_serializable(attributes = {}) ⇒ Object
The inverse of model#serializable_hash -- re-hydrates a serialized hash to a model.
-
.to_serialization_coder ⇒ Object
An ActiveModel::Type that can be used to serialize this model across an entire JSON(b) column.
-
.to_type(strip_nils: :safely) ⇒ Object
an AttrJson::Record or ::Model attribute.
Instance Method Summary collapse
-
#==(other_object) ⇒ Object
Two AttrJson::Model objects are equal if they are the same class AND their #attributes are equal.
-
#_destroy ⇒ Object
ActiveRecord objects have a
_destroy
, related tomarked_for_destruction?
functionality used with AR nested attributes. -
#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.
-
#assign_attributes(new_attributes) ⇒ Object
ActiveModel method, called in initialize.
-
#attribute_names ⇒ Object
like the ActiveModel::Attributes method.
- #attributes ⇒ Object
-
#freeze ⇒ Object
like ActiveModel::Attributes at https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/activemodel/lib/active_model/attributes.rb#L120.
-
#has_attribute?(str) ⇒ Boolean
This attribute from ActiveRecord make SimpleForm happy, and able to detect type.
- #initialize(attributes = {}) ⇒ Object
- #initialize_dup(other) ⇒ Object
-
#serializable_hash(options = nil) ⇒ Object
Override from ActiveModel::Serialization to:.
-
#to_h ⇒ Object
We deep_dup on #to_h, you want attributes unduped, ask for #attributes.
-
#type_for_attribute(attr_name) ⇒ Object
This attribute from ActiveRecord makes SimpleForm happy, and able to detect type.
Class Method Details
.attr_json(name, type, **options) ⇒ Object
Type can be an instance of an ActiveModel::Type::Value subclass, or a symbol that will
be looked up in ActiveModel::Type.lookup
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 |
# File 'lib/attr_json/model.rb', line 213 def attr_json(name, type, **) .assert_valid_keys(*(AttributeDefinition::VALID_OPTIONS - [:container_attribute] + [:validate])) type = _attr_json_maybe_wrap_timezone_aware(type) self.attr_json_registry = attr_json_registry.with( AttributeDefinition.new(name.to_sym, type, .except(:validate)) ) # By default, automatically validate nested models if (type.kind_of?(AttrJson::Type::Model) || type.kind_of?(AttrJson::Type::PolymorphicModel)) && [:validate] != false # Post validations up with something based on ActiveRecord::Validations::AssociatedValidator self.validates_with ::AttrJson::Model::NestedModelValidator, attributes: [name.to_sym] end _attr_jsons_module.module_eval do define_method("#{name}=") do |value| _attr_json_write(AttrJson.efficient_to_s(name), value) end define_method("#{name}") do attributes[AttrJson.efficient_to_s(name)] end end end |
.attr_json_config(new_values = {}) ⇒ Object
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/attr_json/model.rb', line 112 def attr_json_config(new_values = {}) if new_values.present? # get one without new values, then merge new values into it, and # set it locally for this class. @attr_json_config = attr_json_config.merge(new_values) else if instance_variable_defined?("@attr_json_config") # we have a custom one for this class, return it. @attr_json_config elsif superclass.respond_to?(:attr_json_config) # return superclass without setting it locally, so changes in superclass # will continue effecting us. superclass.attr_json_config else # no superclass, no nothing, set it to blank one. @attr_json_config = Config.new(mode: :model) end end end |
.attribute_names ⇒ Object
like the ActiveModel::Attributes method
184 185 186 |
# File 'lib/attr_json/model.rb', line 184 def attribute_names attr_json_registry.attribute_names end |
.attribute_types ⇒ Object
like the ActiveModel::Attributes method, hash with name keys, and ActiveModel::Type values
189 190 191 |
# File 'lib/attr_json/model.rb', line 189 def attribute_types attribute_names.collect { |name| [AttrJson.efficient_to_s(name), attr_json_registry.type_for_attribute(name)]}.to_h end |
.new_from_serializable(attributes = {}) ⇒ Object
The inverse of model#serializable_hash -- re-hydrates a serialized hash to a model.
Similar to .new
, but translates things that need to be translated in deserialization,
like store_keys, and properly calling deserialize (rather than cast) on the underlying types.
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/attr_json/model.rb', line 139 def new_from_serializable(attributes = {}) attributes = attributes.collect do |key, value| # store keys in arguments get translated to attribute names on initialize. if attribute_def = self.attr_json_registry.store_key_lookup("".freeze, AttrJson.efficient_to_s(key)) key = AttrJson.efficient_to_s(attribute_def.name) end attr_type = self.attr_json_registry.has_attribute?(key) && self.attr_json_registry.type_for_attribute(key) if attr_type value = attr_type.deserialize(value) end [key, value] end.to_h self.new(attributes) end |
.to_serialization_coder ⇒ Object
An ActiveModel::Type that can be used to serialize this model across an entire JSON(b) column.
179 180 181 |
# File 'lib/attr_json/model.rb', line 179 def to_serialization_coder @serialization_coder ||= AttrJson::SerializationCoderFromType.new(to_type) end |
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 |
#_destroy ⇒ Object
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.
394 395 396 |
# File 'lib/attr_json/model.rb', line 394 def as_json(=nil) serializable_hash() end |
#assign_attributes(new_attributes) ⇒ Object
ActiveModel method, called in initialize. overridden. from https://github.com/rails/rails/blob/42a16a4d6514f28e05f1c22a5f9125d194d9c7cb/activemodel/lib/active_model/attribute_assignment.rb
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_names ⇒ Object
like the ActiveModel::Attributes method
337 338 339 |
# File 'lib/attr_json/model.rb', line 337 def attribute_names self.class.attribute_names end |
#attributes ⇒ Object
301 302 303 |
# File 'lib/attr_json/model.rb', line 301 def attributes @attributes ||= {} end |
#freeze ⇒ Object
like ActiveModel::Attributes at https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/activemodel/lib/active_model/attributes.rb#L120
is not a full deep freeze
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.
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
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(=nil) strip_nils = &.has_key?(:strip_nils) ? .delete(:strip_nils) : false unless [true, false, :safely].include?(strip_nils) raise ArgumentError, ":strip_nils must be true, false, or :safely" end super().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_h ⇒ Object
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 |