Class: AttrJson::Type::PolymorphicModel

Inherits:
ActiveModel::Type::Value
  • Object
show all
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

Instance Method Summary collapse

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)
  options = { type_key: "type", unrecognized_type: :raise}.merge(
    args.extract_options!.assert_valid_keys(:type_key, :unrecognized_type)
  )
  @type_key = options[:type_key]
  @unrecognized_type = options[: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_lookupObject (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_keyObject (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_typeObject (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_namesObject



91
92
93
# File 'lib/attr_json/type/polymorphic_model.rb', line 91

def model_names
  model_type_lookup.keys
end

#model_typesObject



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

#typeObject

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