Module: ActiveJsonModel::Model::ClassMethods

Defined in:
lib/active_json_model/model.rb

Instance Method Summary collapse

Instance Method Details

#active_json_model_ancestorsArray<Class>

Filter the ancestor hierarchy to those built with ActiveJsonModel::Model concerns

Returns:

  • (Array<Class>)

    reversed array of classes in the hierarchy of this class that include ActiveJsonModel



334
335
336
# File 'lib/active_json_model/model.rb', line 334

def active_json_model_ancestors
  self.ancestors.filter{|o| o.respond_to?(:active_json_model_attributes)}.reverse
end

#active_json_model_attributesArray<JsonAttribute>

Attributes that have been defined for this class using json_attribute.

Returns:



312
313
314
# File 'lib/active_json_model/model.rb', line 312

def active_json_model_attributes
  @__active_json_model_attributes ||= []
end

#active_json_model_cast(val) ⇒ Object

Convert a value that might already be an instance of this class from underlying data. Used to delegate potential loading from ActiveRecord attributes

Parameters:

  • val

    either an instance of this model or a Hash like object



566
567
568
569
570
571
572
# File 'lib/active_json_model/model.rb', line 566

def active_json_model_cast(val)
  if val.is_a?(self)
    val
  elsif val.is_a?(::Hash) || val.is_a?(::HashWithIndifferentAccess)
    self.load(val)
  end
end

#active_json_model_concrete_class_from_ancestry_polymorphic(data) ⇒ Class

Computes the concrete class that should be used to load the data based on the ancestry tree’s json_polymorphic_via. Also handles potential recursion at the leaf nodes of the tree.

Parameters:

  • data (Hash)

    the data being loaded from JSON

Returns:

  • (Class)

    the class to be used to load the JSON



544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
# File 'lib/active_json_model/model.rb', line 544

def active_json_model_concrete_class_from_ancestry_polymorphic(data)
  clazz = nil
  ancestry_active_json_model_polymorphic_factory.each do |proc|
    clazz = proc.call(data)
    break if clazz
  end

  if clazz
    if clazz != self && clazz.respond_to?(:active_json_model_concrete_class_from_ancestry_polymorphic)
      clazz.active_json_model_concrete_class_from_ancestry_polymorphic(data) || clazz
    else
      clazz
    end
  else
    self
  end
end

#active_json_model_fixed_attributesHash

Get the hash of key-value pairs that are fixed for this class. Fixed attributes render to the JSON payload but cannot be set directly.

Returns:

  • (Hash)

    set of fixed attributes for this class



363
364
365
# File 'lib/active_json_model/model.rb', line 363

def active_json_model_fixed_attributes
  @__active_json_fixed_attributes ||= {}
end

#active_json_model_load_callbacksArray<Proc>

A list of procs that will be executed after data has been loaded.

Returns:

  • (Array<Proc>)

    array of procs executed after data is loaded



319
320
321
# File 'lib/active_json_model/model.rb', line 319

def active_json_model_load_callbacks
  @__active_json_model_load_callbacks ||= []
end

#active_json_model_polymorphic_factoryObject

A factory defined via json_polymorphic_via that allows the class to choose different concrete classes based on the data in the JSON. Property is for only this class, not the entire class hierarchy.

@ return [Proc, nil] proc used to select the concrete base class for the model class



327
328
329
# File 'lib/active_json_model/model.rb', line 327

def active_json_model_polymorphic_factory
  @__active_json_model_polymorphic_factory
end

#ancestry_active_json_model_attributesArray<JsonAttribute>

Get all active json model attributes for all the class hierarchy tree

Returns:



341
342
343
# File 'lib/active_json_model/model.rb', line 341

def ancestry_active_json_model_attributes
  self.active_json_model_ancestors.flat_map(&:active_json_model_attributes)
end

#ancestry_active_json_model_fixed_attributesHash

Get the hash of key-value pairs that are fixed for this class hierarchy. Fixed attributes render to the JSON payload but cannot be set directly.

Returns:

  • (Hash)

    set of fixed attributes for this class hierarchy



371
372
373
374
375
376
# File 'lib/active_json_model/model.rb', line 371

def ancestry_active_json_model_fixed_attributes
  self
    .active_json_model_ancestors
    .map{|a| a.active_json_model_fixed_attributes}
    .reduce({}, :merge)
end

#ancestry_active_json_model_load_callbacksArray<AfterLoadCallback>

Get all active json model after load callbacks for all the class hierarchy tree

Returns:



348
349
350
# File 'lib/active_json_model/model.rb', line 348

def ancestry_active_json_model_load_callbacks
  self.active_json_model_ancestors.flat_map(&:active_json_model_load_callbacks)
end

#ancestry_active_json_model_polymorphic_factoryArray<Proc>

Get all polymorphic factories in the ancestry chain.

Returns:

  • (Array<Proc>)

    After load callbacks for the ancestry tree



355
356
357
# File 'lib/active_json_model/model.rb', line 355

def ancestry_active_json_model_polymorphic_factory
  self.active_json_model_ancestors.map(&:active_json_model_polymorphic_factory).filter(&:present?)
end

#attribute_typeObject

Allow this model to be used as ActiveRecord attribute type in Rails 5+.

E.g.

class Credentials < ::ActiveJsonModel; end;

class Integration < ActiveRecord::Base
  attribute :credentials, Credentials.attribute_type
end

Note that this data would be stored as jsonb in the database



277
278
279
280
281
282
283
# File 'lib/active_json_model/model.rb', line 277

def attribute_type
  if Gem.find_files("active_record").any?
    @attribute_type ||= ::ActiveJsonModel::ActiveRecordType.new(self)
  else
    raise RuntimeError.new('ActiveRecord must be installed to use attribute_type')
  end
end

#dump(obj) ⇒ Object

Dump the specified object to JSON

Parameters:

  • obj (self)

    object to dump to json

Raises:

  • (ArgumentError)


616
617
618
619
# File 'lib/active_json_model/model.rb', line 616

def dump(obj)
  raise ArgumentError.new("Expected #{self} got #{obj.class} to dump to JSON") unless obj.is_a?(self)
  obj.dump_to_json
end

#encrypted_attribute_typeObject

Allow this model to be used as ActiveRecord attribute type in Rails 5+.

E.g.

class SecureCredentials < ::ActiveJsonModel; end;

class Integration < ActiveRecord::Base
  attribute :secure_credentials, SecureCredentials.encrypted_attribute_type
end

Note that this data would be stored as a string in the database, encrypted using a symmetric key at the application level.



296
297
298
299
300
301
302
303
304
305
306
# File 'lib/active_json_model/model.rb', line 296

def encrypted_attribute_type
  if Gem.find_files("active_record").any?
    if Gem.find_files("symmetric-encryption").any?
      @encrypted_attribute_type ||= ::ActiveJsonModel::ActiveRecordEncryptedType.new(self)
    else
      raise RuntimeError.new('symmetric-encryption must be installed to use attribute_type')
    end
  else
    raise RuntimeError.new('active_record must be installed to use attribute_type')
  end
end

#json_after_load(method_name = nil, &block) ⇒ Object

Register a new after load callback which is invoked after the instance is loaded from JSON

Parameters:

  • method_name (Symbol, String) (defaults to: nil)

    the name of the method to be invoked

  • block (Proc)

    block to be executed after load. Will optionally be passed an instance of the loaded object.

Raises:

  • (ArgumentError)


578
579
580
581
582
583
584
585
586
587
588
# File 'lib/active_json_model/model.rb', line 578

def json_after_load(method_name=nil, &block)
  raise ArgumentError.new("Must specify method or block for ActiveJsonModel after load") unless method_name || block
  raise ArgumentError.new("Can only specify method or block for ActiveJsonModel after load") if method_name && block

  active_json_model_load_callbacks.push(
    AfterLoadCallback.new(
      method_name: method_name,
      block: block
    )
  )
end

#json_attribute(name, clazz = nil, default: nil, render_default: true, validation: nil, serialize_with: nil, deserialize_with: nil, &load_proc) ⇒ Object

Define a new attribute for the model that will be backed by a JSON attribute

Parameters:

  • name (Symbol, String)

    the name of the attribute

  • clazz (Class) (defaults to: nil)

    the Class to use to initialize the object type

  • default (Object) (defaults to: nil)

    the default value for the attribute

  • render_default (Boolean) (defaults to: true)

    should the default value be rendered to JSON? Default is true. Note this only applies if the value has not be explicitly set. If explicitly set, the value renders, regardless of if the value is the same as the default value.

  • validation (Object) (defaults to: nil)

    object whose properties correspond to settings for active model validators

  • serialize_with (Proc) (defaults to: nil)

    proc to generate a value from the value to be rendered to JSON. Given value and parent_model (optional parameter) values. The value returned is assumed to be a valid JSON value.

  • deserialize_with (Proc) (defaults to: nil)

    proc to deserialize a value from JSON. This is an alternative to passing a block (load_proc) to the method and has the same semantics.

  • load_proc (Proc)

    proc that allows the model to customize the value generated. The proc is passed value_json and parent_json. value_json is the value for this sub-property, and parent_json (optional parameter) is the json for the parent object. This proc can either return a class or a concrete instance. If a class is returned, a new instance of the class will be created on JSON load, and if supported, the sub-JSON will be loaded into it. If a concrete value is returned, it is assumed this is the reconstructed value. This proc allows for simplified polymorphic load behavior as well as custom deserialization.



452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
# File 'lib/active_json_model/model.rb', line 452

def json_attribute(name, clazz = nil, default: nil, render_default: true, validation: nil,
                   serialize_with: nil, deserialize_with: nil, &load_proc)
  if deserialize_with && load_proc
    raise ArgumentError.new("Cannot specify both deserialize_with and block to json_attribute")
  end

  name = name.to_sym

  # Add the attribute to the collection of json attributes defined for this class
  active_json_model_attributes.push(
    JsonAttribute.new(
      name: name,
      clazz: clazz,
      default: default,
      render_default: render_default,
      validation: validation,
      dump_proc: serialize_with,
      load_proc: load_proc || deserialize_with
    )
  )

  # Define ActiveModel attribute methods (https://api.rubyonrails.org/classes/ActiveModel/AttributeMethods.html)
  # for this class. E.g. reset_<name>
  #
  # Used for dirty tracking for the model.
  #
  # @see https://api.rubyonrails.org/classes/ActiveModel/AttributeMethods/ClassMethods.html#method-i-define_attribute_methods
  define_attribute_methods name

  # Define the getter for this attribute
  attr_reader name

  # Define the setter for this attribute with proper change tracking
  #
  # @param value [...] the value to set the attribute to
  define_method "#{name}=" do |value|
    # Trigger ActiveModle's change tracking system if the value is actually changing
    # @see https://stackoverflow.com/questions/23958170/understanding-attribute-will-change-method
    send("#{name}_will_change!") unless value == instance_variable_get("@#{name}")

    # Set the value as a direct instance variable
    instance_variable_set("@#{name}", value)

    # Record that the value is not a default
    instance_variable_set("@#{name}_is_default", false)
  end

  # Check if the attribute is set to the default value. This implies this value has never been set.
  # @return [Boolean] true if the value has been explicitly set or loaded, false otherwise
  define_method "#{name}_is_default?" do
    !!instance_variable_get("@#{name}_is_default")
  end

  if validation
    validates name, validation
  end
end

#json_fixed_attribute(name, value:) ⇒ Object

Set a fixed attribute for the current class. A fixed attribute is a constant value that is set at the class level that still renders to the underlying JSON structure. This is useful when you have a hierarchy of classes which may have certain properties set that differentiate them in the rendered json. E.g. a type attribute.

Example:

class BaseWorkflow
  include ::ActiveJsonModel::Model
  json_attribute :name
end

class EmailWorkflow < BaseWorkflow
  include ::ActiveJsonModel::Model
  json_fixed_attribute :type, 'email'
end

class WebhookWorkflow < BaseWorkflow
  include ::ActiveJsonModel::Model
  json_fixed_attribute :type, 'webhook'
end

workflows = [EmailWorkflow.new(name: 'wf1'), WebhookWorkflow.new(name: 'wf2')].map(&:dump_to_json)
# [{"name": "wf1", "type": "email"}, {"name": "wf2", "type": "webhook"}]

Parameters:

  • name (Symbol)

    the name of the attribute

  • value (Object)

    the value to set the attribute to



405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
# File 'lib/active_json_model/model.rb', line 405

def json_fixed_attribute(name, value:)
  active_json_model_fixed_attributes[name.to_sym] = value

  # We could handle fixed attributes as just a get method, but this approach keeps them consistent with the
  # other attributes for things like changed tracking.
  instance_variable_set("@#{name}", value)

  # Define ActiveModel attribute methods (https://api.rubyonrails.org/classes/ActiveModel/AttributeMethods.html)
  # for this class. E.g. reset_<name>
  #
  # Used for dirty tracking for the model.
  #
  # @see https://api.rubyonrails.org/classes/ActiveModel/AttributeMethods/ClassMethods.html#method-i-define_attribute_methods
  define_attribute_methods name

  # Define the getter for this attribute
  attr_reader name

  # Define the setter method to prevent the value from being changed.
  define_method "#{name}=" do |v|
    unless value == v
      raise RuntimeError.new("#{self.class}.#{name} is an Active JSON Model fixed attribute with a value of '#{value}'. It's value cannot be set to '#{v}''.")
    end
  end
end

#json_polymorphic_via(&block) ⇒ Object

Define a polymorphic factory to choose the concrete class for the model.

Example:

class BaseWorkflow
  include ::ActiveJsonModel::Model

  json_polymorphic_via do |data|
    if data[:type] == 'email'
      EmailWorkflow
    else
      WebhookWorkflow
    end
  end
end

class EmailWorkflow < BaseWorkflow
  include ::ActiveJsonModel::Model
  json_fixed_attribute :type, 'email'
end

class WebhookWorkflow < BaseWorkflow
  include ::ActiveJsonModel::Model
  json_fixed_attribute :type, 'webhook'
end


535
536
537
# File 'lib/active_json_model/model.rb', line 535

def json_polymorphic_via(&block)
  @__active_json_model_polymorphic_factory = block
end

#load(json_data) ⇒ Object

Load an instance of the class from JSON

Parameters:

  • json_data (String, Hash)

    the data to be loaded into the instance. May be a hash or a string.

Returns:

  • Instance of the class



594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
# File 'lib/active_json_model/model.rb', line 594

def load(json_data)
  if json_data.nil? || (json_data.is_a?(String) && json_data.blank?)
    return nil
  end

  # Get the data to a hash, regardless of the starting data type
  data = json_data.is_a?(String) ? ::JSON.parse(json_data) : json_data

  # Recursively make the value have indifferent access
  data = ::ActiveJsonModel::Utils.recursively_make_indifferent(data)

  # Get the concrete class from the ancestry tree's potential polymorphic behavior. Note this needs to be done
  # for each sub property as well. This just covers the outermost case.
  clazz = active_json_model_concrete_class_from_ancestry_polymorphic(data)
  clazz.new.tap do |instance|
    instance.load_from_json(data)
  end
end