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.

[View source]

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

[View source]

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

[View source]

108
109
110
# File 'lib/attr_json/type/polymorphic_model.rb', line 108

def deserialize(v)
  cast_or_deserialize(v, :deserialize)
end

#model_namesObject

[View source]

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

def model_names
  model_type_lookup.keys
end

#model_typesObject

[View source]

95
96
97
# File 'lib/attr_json/type/polymorphic_model.rb', line 95

def model_types
  model_type_lookup.values
end

#serialize(v) ⇒ Object

[View source]

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

[View source]

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

[View source]

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.

[View source]

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