Module: ActiveJsonModel::Array::ClassMethods

Defined in:
lib/active_json_model/array.rb

Instance Method Summary collapse

Instance Method Details

#active_json_model_ancestorsArray<Class>

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

Returns:

  • (Array<Class>)

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



302
303
304
# File 'lib/active_json_model/array.rb', line 302

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

#active_json_model_array_serialization_tupleObject

OpenStruct storing the configuration of of this ActiveJsonModel::Array. Properties include:

serialize_proc - proc used to translate from objects -> json
serialize_method - symbol of method name to call to translate from objects -> json
deserialize_proc - proc used to translate from json -> objects
deserialize_method - symbol of method name to call to translate from json -> objects
keep_nils - boolean flag indicating if nils should be kept in the array after de/serialization
errors_go_to_nil - boolean flag if errors should be capture from the de/serialization methods and translated
to nil


295
296
297
# File 'lib/active_json_model/array.rb', line 295

def active_json_model_array_serialization_tuple
  @__active_json_model_array_serialization_tuple
end

#active_json_model_cast(vals) ⇒ 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:

  • vals

    either an instance of this model or an array-like object



624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
# File 'lib/active_json_model/array.rb', line 624

def active_json_model_cast(vals)
  if vals.is_a?(self)
    vals
  elsif vals.is_a?(::Array)
    if vals.length == 0
      self.new(values: vals)
    elsif vals[0].respond_to?(:dump_to_json)
      self.new(values: vals)
    else
      self.load(vals)
    end
  elsif vals.nil?
    self.new(values: [])
  end
end

#active_json_model_concrete_class_from_ancestry_polymorphic(array_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:

  • array_data (Array)

    the array_data being loaded from JSON

Returns:

  • (Class)

    the class to be used to load the JSON



602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
# File 'lib/active_json_model/array.rb', line 602

def active_json_model_concrete_class_from_ancestry_polymorphic(array_data)
  clazz = nil
  ancestry_active_json_model_polymorphic_factory.each do |proc|
    clazz = proc.call(array_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(array_data) || clazz
    else
      clazz
    end
  else
    self
  end
end

#active_json_model_load_callbacksArray<Proc>

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

Returns:

  • (Array<Proc>)

    array of procs executed after array_data is loaded



275
276
277
# File 'lib/active_json_model/array.rb', line 275

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 array_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 list model class



283
284
285
# File 'lib/active_json_model/array.rb', line 283

def active_json_model_polymorphic_factory
  @__active_json_model_polymorphic_factory
end

#ancestry_active_json_model_load_callbacksArray<AfterLoadCallback>

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

Returns:



309
310
311
# File 'lib/active_json_model/array.rb', line 309

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



316
317
318
# File 'lib/active_json_model/array.rb', line 316

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 array_data would be stored as jsonb in the database



240
241
242
243
244
245
246
# File 'lib/active_json_model/array.rb', line 240

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)


725
726
727
728
# File 'lib/active_json_model/array.rb', line 725

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 array_data would be stored as a string in the database, encrypted using a symmetric key at the application level.



259
260
261
262
263
264
265
266
267
268
269
# File 'lib/active_json_model/array.rb', line 259

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)


680
681
682
683
684
685
686
687
688
689
690
# File 'lib/active_json_model/array.rb', line 680

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_array(serialize:, deserialize:, validate: nil, keep_nils: false, errors_go_to_nil: true, nil_data_to_empty_array: false) ⇒ Object

A JSON array that uses arbitrary serialization/deserialization.

Example:

class DateTimeArray
  include ::ActiveJsonModel::Array

  json_array serialize: ->(dt){ dt.iso8601 }
           deserialize: ->(s){ DateTime.iso8601(s) }
end

Parameters:

  • serialize (Proc, symbol)

    Proc to use for serialization. May be a symbol to a method implemented in the class.

  • deserialize (Proc, symbol)

    Proc to use for deserialization. May be a symbol to a method implemented in the class.

  • validate (Proc, symbol) (defaults to: nil)

    Proc to use for validating elements of the array. May be a symbol to a method implemented in the class. Arguments are value, index, errors object (if not method on class). Method should add items to the errors array if there are errors, Note that if the elements of the array implement the valid? and errors methods, those are used in addition to the validate method.

  • keep_nils (Boolean) (defaults to: false)

    Should the resulting array keep nils? Default is false and the array will be compacted after deserialization.

  • errors_go_to_nil (Boolean) (defaults to: true)

    Should excepts be trapped and converted to nil values? Default is true.

  • nil_data_to_empty_array (Boolean) (defaults to: false)

    When deserializing data, should a nil value make the values array empty (versus nil values, which will cause errors)



523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
# File 'lib/active_json_model/array.rb', line 523

def json_array(serialize:, deserialize:, validate: nil,
               keep_nils: false, errors_go_to_nil: true, nil_data_to_empty_array: false)
  unless serialize && (serialize.is_a?(Proc) || serialize.is_a?(Symbol))
    raise ArgumentError.new("Must specify serialize to json_array and it must be either a proc or a symbol to refer to a method in the class")
  end

  if serialize.is_a?(Proc) && serialize.arity != 1
    raise ArgumentError.new("Serialize proc must take exactly one argument.")
  end

  unless deserialize && (deserialize.is_a?(Proc) || deserialize.is_a?(Symbol))
    raise ArgumentError.new("Must specify deserialize to json_array and it must be either a proc or a symbol to refer to a method in the class")
  end

  if deserialize.is_a?(Proc) && deserialize.arity != 1
    raise ArgumentError.new("Deserialize proc must take exactly one argument.")
  end

  if @__active_json_model_array_serialization_tuple
    raise ArgumentError.new("json_array_of, json_polymorphic_array_by, and json_array are exclusive. Exactly one of them must be specified.")
  end

  @__active_json_model_array_serialization_tuple = OpenStruct.new.tap do |t|
    if serialize.is_a?(Proc)
      t.serialize_proc = serialize
    else
      t.serialize_method = serialize
    end

    if deserialize.is_a?(Proc)
      t.deserialize_proc = deserialize
    else
      t.deserialize_method = deserialize
    end

    if validate
      if validate.is_a?(Proc)
        t.validate_proc = validate
      else
        t.validate_method = validate
      end
    end

    t.keep_nils = keep_nils
    t.errors_go_to_nil = errors_go_to_nil
    t.nil_data_to_empty_array = nil_data_to_empty_array
  end
end

#json_array_of(clazz, validate: nil, keep_nils: false, errors_go_to_nil: true, nil_data_to_empty_array: false) ⇒ Object

Configure this list class to have elements of a specific ActiveJsonModel Model type.

Example:

class PhoneNumber
  include ::ActiveJsonModel::Model

  json_attribute :number, String
  json_attribute :label, String
end

class PhoneNumberArray
  include ::ActiveJsonModel::Array

  json_array_of PhoneNumber
end

Parameters:

  • clazz (Clazz)

    the class to use when loading model elements

  • validate (Proc, symbol) (defaults to: nil)

    Proc to use for validating elements of the array. May be a symbol to a method implemented in the class. Arguments are value, index, errors object (if not method on class). Method should add items to the errors array if there are errors, Note that if the elements of the array implement the valid? and errors methods, those are used in addition to the validate method.

  • keep_nils (Boolean) (defaults to: false)

    Should the resulting array keep nils? Default is false and the array will be compacted after deserialization.

  • errors_go_to_nil (Boolean) (defaults to: true)

    Should excepts be trapped and converted to nil values? Default is true.

  • nil_data_to_empty_array (Boolean) (defaults to: false)

    When deserializing data, should a nil value make the values array empty (versus nil values, which will cause errors)



347
348
349
350
351
352
353
354
355
356
357
358
359
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
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
# File 'lib/active_json_model/array.rb', line 347

def json_array_of(clazz, validate: nil, keep_nils: false, errors_go_to_nil: true, nil_data_to_empty_array: false)
  unless clazz && clazz.is_a?(Class)
    raise ArgumentError.new("json_array_of must be passed a class to use as the type for elements of the array. Received '#{clazz}'")
  end

  unless [Integer, Float, String, Symbol, DateTime, Date].any?{|c| c == clazz} || clazz.include?(::ActiveJsonModel::Model)
    raise ArgumentError.new("Class used with json_array_of must include ActiveJsonModel::Model or be of type Integer, Float, String, Symbol, DateTime, or Date")
  end

  if @__active_json_model_array_serialization_tuple
    raise ArgumentError.new("json_array_of, json_polymorphic_array_by, and json_array are exclusive. Exactly one of them must be specified.")
  end

  # Delegate the real work to a serialize/deserialize approach.
  if clazz == Integer
    json_array(serialize: ->(o){ o }, deserialize: ->(d){ d&.to_i },
               validate: validator_for_item_type(Integer, validate),
               keep_nils: keep_nils, errors_go_to_nil: errors_go_to_nil,
               nil_data_to_empty_array: nil_data_to_empty_array)
  elsif clazz == Float
    json_array(serialize: ->(o){ o }, deserialize: ->(d){ d&.to_f },
               validate: validator_for_item_type(Float, validate),
               keep_nils: keep_nils, errors_go_to_nil: errors_go_to_nil,
               nil_data_to_empty_array: nil_data_to_empty_array)
  elsif clazz == String
    json_array(serialize: ->(o){ o }, deserialize: ->(d){ d&.to_s },
               validate: validator_for_item_type(String, validate),
               keep_nils: keep_nils, errors_go_to_nil: errors_go_to_nil,
               nil_data_to_empty_array: nil_data_to_empty_array)
  elsif clazz == Symbol
    json_array(serialize: ->(o){ o&.to_s }, deserialize: ->(d){ d&.to_sym },
               validate: validator_for_item_type(Symbol, validate),
               keep_nils: keep_nils, errors_go_to_nil: errors_go_to_nil,
               nil_data_to_empty_array: nil_data_to_empty_array)
  elsif clazz == DateTime
    json_array(serialize: ->(o){ o&.iso8601 }, deserialize: ->(d){ DateTime.iso8601(d) },
               validate: validator_for_item_type(DateTime, validate),
               keep_nils: keep_nils, errors_go_to_nil: errors_go_to_nil,
               nil_data_to_empty_array: nil_data_to_empty_array)
  elsif clazz == Date
    json_array(serialize: ->(o){ o&.iso8601 }, deserialize: ->(d){ Date.iso8601(d) },
               validate: validator_for_item_type(Date, validate),
               keep_nils: keep_nils, errors_go_to_nil: errors_go_to_nil,
               nil_data_to_empty_array: nil_data_to_empty_array)
  else
    # This is the case where this is a Active JSON Model
    json_array(
      serialize: ->(o) {
        if o && o.respond_to?(:dump_to_json)
          o.dump_to_json
        else
          o
        end
      }, deserialize: ->(d) {
      c = if clazz&.respond_to?(:active_json_model_concrete_class_from_ancestry_polymorphic)
            clazz.active_json_model_concrete_class_from_ancestry_polymorphic(d) || clazz
          else
            clazz
          end

      if c
        c.new.tap do |m|
          m.load_from_json(d)
        end
      else
        nil
      end
    },
      validate: validator_for_item_type(clazz, validate),
      keep_nils: keep_nils,
      errors_go_to_nil: errors_go_to_nil,
      nil_data_to_empty_array: nil_data_to_empty_array)
  end
end

#json_polymorphic_array_by(validate: nil, keep_nils: false, errors_go_to_nil: false, nil_data_to_empty_array: false, &factory) ⇒ Object

The factory for generating instances of the array when hydrating from JSON. The factory must return the ActiveJsonModel::Model implementing class chosen.

Example:

class PhoneNumber
  include ::ActiveJsonModel::Model

  json_attribute :number, String
  json_attribute :label, String
end

class Email
  include ::ActiveJsonModel::Model

  json_attribute :address, String
  json_attribute :label, String
end

class ContactInfoArray
  include ::ActiveJsonModel::Array

  json_polymorphic_array_by do |item_data|
    if item_data.key?(:address)
      Email
    else
      PhoneNumber
    end
  end
end

Parameters:

  • factory (Proc, String)

    that factory method to choose the appropriate class for each element.

  • validate (Proc, symbol) (defaults to: nil)

    Proc to use for validating elements of the array. May be a symbol to a method implemented in the class. Arguments are value, index, errors object (if not method on class). Method should add items to the errors array if there are errors, Note that if the elements of the array implement the valid? and errors methods, those are used in addition to the validate method.

  • keep_nils (Boolean) (defaults to: false)

    Should the resulting array keep nils? Default is false and the array will be compacted after deserialization.

  • errors_go_to_nil (Boolean) (defaults to: false)

    Should excepts be trapped and converted to nil values? Default is true.

  • nil_data_to_empty_array (Boolean) (defaults to: false)

    When deserializing data, should a nil value make the values array empty (versus nil values, which will cause errors)



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
# File 'lib/active_json_model/array.rb', line 462

def json_polymorphic_array_by(validate: nil, keep_nils: false, errors_go_to_nil: false, nil_data_to_empty_array: false, &factory)
  unless factory && factory.arity == 1
    raise ArgumentError.new("Must pass block taking one argument to json_polymorphic_array_by")
  end

  if @__active_json_model_array_serialization_tuple
    raise ArgumentError.new("json_array_of, json_polymorphic_array_by, and json_array are exclusive. Exactly one of them must be specified.")
  end

  # Delegate the real work to a serialize/deserialize approach.
  json_array(
    serialize: ->(o) {
      if o && o.respond_to?(:dump_to_json)
        o.dump_to_json
      else
        o
      end
    }, deserialize: ->(d) {
    clazz = factory.call(d)

    if clazz&.respond_to?(:active_json_model_concrete_class_from_ancestry_polymorphic)
      clazz = clazz.active_json_model_concrete_class_from_ancestry_polymorphic(d) || clazz
    end

    if clazz
      clazz.new.tap do |m|
        m.load_from_json(d)
      end
    else
      nil
    end
  },
    validate: validate,
    keep_nils: keep_nils,
    errors_go_to_nil: errors_go_to_nil,
    nil_data_to_empty_array: nil_data_to_empty_array)
end

#json_polymorphic_via(&block) ⇒ Object

Define a polymorphic factory to choose the concrete class for the list model. Note that because the array_data passed to the block is an array of models, you must account for what the behavior is if there are no elements.

Example:

class BaseWorkflowArray
  include ::ActiveJsonModel::List

  json_polymorphic_via do |array_data|
    if array_data[0]
      if array_data[0][:type] == 'email'
        EmailWorkflow
      else
        WebhookWorkflow
      end
    else
      BaseWorkflowArray
    end
  end
end

class EmailWorkflow < BaseWorkflow
  def home_emails
    filter{|e| e.label == 'home'}
  end
end

class WebhookWorkflow < BaseWorkflow
  def secure_webhooks
    filter{|wh| wh.secure }
  end
end


672
673
674
# File 'lib/active_json_model/array.rb', line 672

def json_polymorphic_via(&block)
  @__active_json_model_polymorphic_factory = block
end

#load(json_array_data) ⇒ Object

Load an instance of the class from JSON

Parameters:

  • json_array_data (String, Array)

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

Returns:

  • Instance of the list class



696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
# File 'lib/active_json_model/array.rb', line 696

def load(json_array_data)
  if json_array_data.nil? || (json_array_data.is_a?(String) && json_array_data.blank?)
    clazz = active_json_model_concrete_class_from_ancestry_polymorphic([])
    if clazz&.active_json_model_array_serialization_tuple&.nil_data_to_empty_array
      return clazz.new.tap do |instance|
        instance.load_from_json(nil)
      end
    else
      return nil
    end
  end

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

  # Recursively make the value have indifferent access
  array_data = ::ActiveJsonModel::Utils.recursively_make_indifferent(array_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(array_data)
  clazz.new.tap do |instance|
    instance.load_from_json(array_data)
  end
end

#validator_for_item_type(clazz, recursive_validator = nil) ⇒ Proc

Crate a validator that can be used to check that items of the array of a specified type.

Parameters:

  • clazz (Class)

    the type to check against

  • recursive_validator (Proc, Symbol) (defaults to: nil)

    an optional validator to be called in addition to this one

Returns:

  • (Proc)

    a proc to do the validation



577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
# File 'lib/active_json_model/array.rb', line 577

def validator_for_item_type(clazz, recursive_validator=nil)
  ->(val, i, errors, me) do
    unless val&.is_a?(clazz)
      errors.add(:values, "Element #{i} must be of type #{clazz} but is of type #{val&.class}")
    end

    if recursive_validator
      if recursive_validator.is_a?(Proc)
        if recursive_validator.arity == 4
          recursive_validator.call(val, i, errors, me)
        else
          recursive_validator.call(val, i, errors)
        end
      else
        me.send(recursive_validator, val, i)
      end
    end
  end
end