Class: AttrJson::Type::PolymorphicModel
- Inherits:
-
ActiveModel::Type::Value
- Object
- ActiveModel::Type::Value
- AttrJson::Type::PolymorphicModel
- Defined in:
- lib/attr_json/type/polymorphic_model.rb
Overview
AttrJson::Type::PolymorphicModel can be used to create attr_json attributes that can hold any of various specified AttrJson::Model models. It is a somewhat experimental feature.
"polymorphic" may not be quite the right word, but we use it out of analogy with ActiveRecord polymorphic assocications, which it resembles, as well as ActiveRecord Single-Table Inheritance.
Similar to these AR features, a PolymorphicModel-typed attribute will serialize the
model name of a given value in a type
json hash key, so it can deserialize
to the same correct model class.
It can be used for single-model attributes, or arrays (which can be hetereogenous),
in either AttrJson::Record or nested AttrJson::Models. If CD
, Book
, Person
,
and Corporation
are all AttrJson::Model classes:
attr_json :favorite, AttrJson::Type::PolymorphicAttribute.new(CD, Book)
attr_json :authors, AttrJson::Type::PolymorphicAttribute.new(Person, Corporation), array: true
Currently, you need a specific enumerated list of allowed types, and they all need to be AttrJson::Model classes. You can't at the moment have an "open" polymorphic type that can accept any AttrJson::Model.
You can change the json key that the "type" (class name) for a value is stored to, when creating the type:
attr_json, :author, AttrJson::Type::PolymorphicAttribute.new(Person, Corporation, type_key: "__type__")
But if you already have existing data in the db, that's gonna be problematic to change on the fly.
You can set attributes with a hash, but it needs to have an appropriate type
key
(or other as set by type_key
arg). If it does not, or you try to set a non-hash
value, you will get a AttrJson::Type::PolymorphicModel::TypeError. (maybe a validation
error would be better? but it's not what it does now.)
Note this also applies to loading non-compliant data from the database. If you have non-compliant data in the db, the only way to look at it will be as a serialized json string in top-level #json_attributes_before_cast (or other relevant container attribute.)
There is no built-in form support for PolymorphicModels, you'll have to work it out.
jsonb_contains support
There is basic jsonb_contains support, but no sophisticated type-casting like normal, beyond the polymorphic attribute. But you can do:
MyRecord.jsonb_contains(author: { name: "foo"})
MyRecord.jsonb_contains(author: { name: "foo", type: "Corporation"})
MyRecord.jsonb_contains(author: Corporation.new(name: "foo"))
Additionally, there is not_jsonb_contains, which creates the same query terms like jsonb_contains, but negated.
Defined Under Namespace
Classes: TypeError
Instance Attribute Summary collapse
-
#model_type_lookup ⇒ Object
readonly
Returns the value of attribute model_type_lookup.
-
#type_key ⇒ Object
readonly
Returns the value of attribute type_key.
-
#unrecognized_type ⇒ Object
readonly
Returns the value of attribute unrecognized_type.
Instance Method Summary collapse
- #cast(v) ⇒ Object
- #deserialize(v) ⇒ Object
-
#initialize(*args) ⇒ PolymorphicModel
constructor
A new instance of PolymorphicModel.
- #model_names ⇒ Object
- #model_types ⇒ Object
- #serialize(v) ⇒ Object
-
#type ⇒ Object
ActiveModel method, symbol type label.
- #type_for_model_name(model_name) ⇒ Object
-
#value_for_contains_query(key_path_arr, value) ⇒ Object
This is used only by our own keypath-chaining query stuff.
Constructor Details
#initialize(*args) ⇒ PolymorphicModel
Returns a new instance of PolymorphicModel.
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/attr_json/type/polymorphic_model.rb', line 62 def initialize(*args) = { type_key: "type", unrecognized_type: :raise}.merge( args..assert_valid_keys(:type_key, :unrecognized_type) ) @type_key = [:type_key] @unrecognized_type = [:unrecognized_type] model_types = args model_types.collect! do |m| if m.respond_to?(:ancestors) && m.ancestors.include?(AttrJson::Model) m.to_type else m end end if bad_arg = model_types.find { |m| !m.is_a? AttrJson::Type::Model } raise ArgumentError, "#{self.class.name} only works with AttrJson::Model / AttrJson::Type::Model, not '#{bad_arg.inspect}'" end if type_key_conflict = model_types.find { |m| m.model.attr_json_registry.has_attribute?(@type_key) } raise ArgumentError, "conflict between type_key '#{@type_key}' and an existing attr_json in #{type_key_conflict.model}" end @model_type_lookup = model_types.collect do |type| [type.model.name, type] end.to_h end |
Instance Attribute Details
#model_type_lookup ⇒ Object (readonly)
Returns the value of attribute model_type_lookup.
61 62 63 |
# File 'lib/attr_json/type/polymorphic_model.rb', line 61 def model_type_lookup @model_type_lookup end |
#type_key ⇒ Object (readonly)
Returns the value of attribute type_key.
61 62 63 |
# File 'lib/attr_json/type/polymorphic_model.rb', line 61 def type_key @type_key end |
#unrecognized_type ⇒ Object (readonly)
Returns the value of attribute unrecognized_type.
61 62 63 |
# File 'lib/attr_json/type/polymorphic_model.rb', line 61 def unrecognized_type @unrecognized_type end |
Instance Method Details
#cast(v) ⇒ Object
104 105 106 |
# File 'lib/attr_json/type/polymorphic_model.rb', line 104 def cast(v) cast_or_deserialize(v, :cast) end |
#deserialize(v) ⇒ Object
108 109 110 |
# File 'lib/attr_json/type/polymorphic_model.rb', line 108 def deserialize(v) cast_or_deserialize(v, :deserialize) end |
#model_names ⇒ Object
91 92 93 |
# File 'lib/attr_json/type/polymorphic_model.rb', line 91 def model_names model_type_lookup.keys end |
#model_types ⇒ Object
95 96 97 |
# File 'lib/attr_json/type/polymorphic_model.rb', line 95 def model_types model_type_lookup.values end |
#serialize(v) ⇒ Object
112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/attr_json/type/polymorphic_model.rb', line 112 def serialize(v) return nil if v.nil? # if it's not already a model cast it to a model if possible (eg it's a hash) v = cast(v) model_name = v.class.name type = type_for_model_name(model_name) raise_bad_model_name(model_name, v) if type.nil? type.serialize(v).merge(type_key => model_name) end |
#type ⇒ Object
ActiveModel method, symbol type label
100 101 102 |
# File 'lib/attr_json/type/polymorphic_model.rb', line 100 def type @type ||= "any_of_#{model_types.collect(&:type).collect(&:to_s).join('_')}".to_sym end |
#type_for_model_name(model_name) ⇒ Object
126 127 128 |
# File 'lib/attr_json/type/polymorphic_model.rb', line 126 def type_for_model_name(model_name) model_type_lookup[model_name] end |
#value_for_contains_query(key_path_arr, value) ⇒ Object
This is used only by our own keypath-chaining query stuff. For PolymorphicModel type, it does no type casting, just sticks whatever you gave it in, which needs to be json-compat values.
134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/attr_json/type/polymorphic_model.rb', line 134 def value_for_contains_query(key_path_arr, value) hash_arg = {} key_path_arr.each.with_index.inject(hash_arg) do |hash, (n, i)| if i == key_path_arr.length - 1 hash[n] = value else hash[n] = {} end end hash_arg end |