activejsonmodel 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,699 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ostruct'
4
+
5
+ require 'active_support'
6
+ require_relative './json_attribute'
7
+ require_relative './after_load_callback'
8
+
9
+ if defined?(::ActiveRecord)
10
+ require_relative './active_record_type'
11
+ require_relative './active_record_encrypted_type'
12
+ end
13
+
14
+ module ActiveJsonModel
15
+ module Array
16
+ def self.included(base_class)
17
+ # Add all the class methods to the included class
18
+ base_class.extend(ClassMethods)
19
+
20
+ # Add additional settings into the class
21
+ base_class.class_eval do
22
+ # Make sure the objects will be ActiveModels
23
+ include ::ActiveModel::Model unless include?(::ActiveModel::Model)
24
+
25
+ # This class will be have like a list-like object
26
+ include ::Enumerable unless include?(::Enumerable)
27
+
28
+ # Make sure that it has dirty tracking
29
+ include ::ActiveModel::Dirty unless include?(::ActiveModel::Dirty)
30
+
31
+ # The raw values for the
32
+ attr_accessor :values
33
+
34
+ # Most of the functionality gets delegated to the actual values array. This is almost all possible methods for
35
+ # array, leaving off those that might be problems for equality checking, etc.
36
+ delegate :try_convert, :&, :*, :+, :-, :<<, :<=>, :[], :[]=, :all?, :any?, :append, :assoc, :at, :bsearch,
37
+ :bsearch_index, :clear, :collect, :collect!, :combination, :compact, :compact!, :concat, :count,
38
+ :cycle, :deconstruct, :delete, :delete_at, :delete_if, :difference, :dig, :drop, :drop_while,
39
+ :each, :each_index, :empty?, :eql?, :fetch, :fill, :filter!, :find_index, :first, :flatten,
40
+ :flatten!, :hash, :include?, :index, :initialize_copy, :insert, :inspect, :intersection, :join,
41
+ :keep_if, :last, :length, :map, :map!, :max, :min, :minmax, :none?, :old_to_s, :one?, :pack,
42
+ :permutation, :pop, :prepend, :product, :push, :rassoc, :reject, :reject!, :repeated_combination,
43
+ :repeated_permutation, :replace, :reverse, :reverse!, :reverse_each, :rindex, :rotate, :rotate!,
44
+ :sample, :select!, :shift, :shuffle, :shuffle!, :size, :slice, :slice!, :sort, :sort!,
45
+ :sort_by!, :sum, :take, :take_while, :transpose, :union, :uniq, :uniq!, :unshift, :values_at, :zip, :|,
46
+ to: :values
47
+
48
+ # Has this model changed? Override's <code>ActiveModel::Dirty</code>'s base behavior to properly handle
49
+ # recursive changes.
50
+ #
51
+ # @return [Boolean] true if any attribute has changed, false otherwise
52
+ def changed?
53
+ # Note: this method is implemented here versus in the module overall because if it is implemented in the
54
+ # module overall, it doesn't properly override the implementation for <code>ActiveModel::Dirty</code> that
55
+ # gets dynamically pulled in using the <code>included</code> hook.
56
+ super || values != @_active_json_model_original_values || values&.any?{|val| val&.respond_to?(:changed?) && val.changed? }
57
+ end
58
+
59
+ # For new/loaded tracking
60
+ @_active_json_model_dumped = false
61
+ @_active_json_model_loaded = false
62
+
63
+ # Register model validation to handle recursive validation into the model tree
64
+ validate :active_json_model_validate
65
+
66
+ def initialize(arr=nil, **kwargs)
67
+ if !arr.nil? && !kwargs[:values].nil?
68
+ raise ArgumentError.new('Can only specify either array or values for ActiveJsonModel::Array')
69
+ end
70
+
71
+ # Just repackage as named parameters
72
+ kwargs[:values] = arr unless arr.nil?
73
+
74
+ unless kwargs.key?(:values)
75
+ kwargs[:values] = []
76
+ end
77
+
78
+ # Invoke the superclass constructor to let active model do the work of setting the attributes
79
+ super(**kwargs).tap do |_|
80
+ # Clear out any recorded changes as this object is starting fresh
81
+ clear_changes_information
82
+ @_active_json_model_original_values = self.values
83
+ end
84
+ end
85
+
86
+ # Select certain values based on a condition and generate a new ActiveJsonModel Array
87
+ # @return [ActiveJsonModel] the filtered array
88
+ def select(&block)
89
+ if block
90
+ self.class.new(values: values.select(&block))
91
+ else
92
+ values.select
93
+ end
94
+ end
95
+
96
+ # As in the real implementation, <code>filter</code> is just <code>select</code>
97
+ alias_method :filter, :select
98
+ end
99
+ end
100
+
101
+ # Was this instance loaded from JSON?
102
+ # @return [Boolean] true if loaded from JSON, false otherwise
103
+ def loaded?
104
+ @_active_json_model_loaded
105
+ end
106
+
107
+ # Was this instance dumped to JSON?
108
+ # @return [Boolean] true if dumped to JSON, false otherwise
109
+ def dumped?
110
+ @_active_json_model_dumped
111
+ end
112
+
113
+ # Is this a new instance that was created without loading, and has yet to be dumped?
114
+ # @return [Boolean] true if new, false otherwise
115
+ def new?
116
+ !loaded? && !dumped?
117
+ end
118
+
119
+ # Have the values for this array actually be set, or a defaults coming through?
120
+ # @return [Boolean] true if the values have actually been set
121
+ def values_set?
122
+ !!@_active_json_model_values_set
123
+ end
124
+
125
+ # Load array for this instance from a JSON array
126
+ #
127
+ # @param json_array [Array] array of data to be loaded into a model instance
128
+ def load_from_json(json_array)
129
+ # Record this object was loaded
130
+ @_active_json_model_loaded = true
131
+
132
+ if json_array.nil?
133
+ if self.class.active_json_model_array_serialization_tuple.nil_data_to_empty_array
134
+ self.values = []
135
+ else
136
+ self.values = nil
137
+ end
138
+
139
+ @_active_json_model_values_set = false
140
+
141
+ return
142
+ end
143
+
144
+ if !json_array.respond_to?(:map) || json_array.is_a?(Hash)
145
+ raise ArgumentError.new("Invalid value specified for json_array. Expected array-like object received #{json_array.class}")
146
+ end
147
+
148
+ # Record that we have some sort of values set
149
+ @_active_json_model_values_set = true
150
+
151
+ # Iterate over all the allowed attributes
152
+ self.values = json_array.map do |json_val|
153
+ if self.class.active_json_model_array_serialization_tuple.deserialize_proc
154
+ self.class.active_json_model_array_serialization_tuple.deserialize_proc.call(json_val)
155
+ else
156
+ send(self.class.active_json_model_array_serialization_tuple.deserialize_method, json_val)
157
+ end
158
+ end
159
+
160
+ # Now that the load is complete, mark dirty tracking as clean
161
+ clear_changes_information
162
+ @_active_json_model_original_values = self.values
163
+
164
+ # Invoke any on-load callbacks
165
+ self.class.ancestry_active_json_model_load_callbacks.each do |cb|
166
+ cb.invoke(self)
167
+ end
168
+ end
169
+
170
+ def dump_to_json
171
+ # Record that the data has been dumped
172
+ @_active_json_model_dumped = true
173
+
174
+ unless self.class.active_json_model_array_serialization_tuple
175
+ raise RuntimeError.new('ActiveJsonModel::Array not properly configured')
176
+ end
177
+
178
+ return nil if values.nil?
179
+
180
+ values.map do |val|
181
+ if self.class.active_json_model_array_serialization_tuple.serialize_proc
182
+ self.class.active_json_model_array_serialization_tuple.serialize_proc.call(val)
183
+ else
184
+ send(self.class.active_json_model_array_serialization_tuple.deserialize_method, val)
185
+ end
186
+ end.tap do |vals|
187
+ # All changes are cleared after dump
188
+ clear_changes_information
189
+ @_active_json_model_original_values = vals
190
+ end
191
+ end
192
+
193
+ # Validate method that handles recursive validation into models in the array. Individual validations
194
+ # on attributes for this model will be handled by the standard mechanism.
195
+ def active_json_model_validate
196
+ errors.add(:values, 'ActiveJsonModel::Array values must be an array') unless values.is_a?(::Array)
197
+
198
+ values.each_with_index do |val, i|
199
+ # Check if attribute value is an ActiveJsonModel
200
+ if val && val.respond_to?(:valid?)
201
+ # This call to <code>valid?</code> is important because it will actually trigger recursive validations
202
+ unless val.valid?
203
+ val.errors.each do |error|
204
+ errors.add("[#{i}].#{error.attribute}".to_sym, error.message)
205
+ end
206
+ end
207
+ end
208
+
209
+ if self.class.active_json_model_array_serialization_tuple.validate_proc
210
+
211
+ # It's a proc (likely lambda)
212
+ if self.class.active_json_model_array_serialization_tuple.validate_proc.arity == 4
213
+ # Handle the validator_for_item_type validators that need to take the self as a param
214
+ # for recursive validators
215
+ self.class.active_json_model_array_serialization_tuple.validate_proc.call(val, i, errors, self)
216
+ else
217
+ self.class.active_json_model_array_serialization_tuple.validate_proc.call(val, i, errors)
218
+ end
219
+
220
+ elsif self.class.active_json_model_array_serialization_tuple.validate_method
221
+
222
+ # It's implemented as method on this object
223
+ send(self.class.active_json_model_array_serialization_tuple.validate_method, val, i)
224
+ end
225
+ end
226
+ end
227
+
228
+ module ClassMethods
229
+ if defined?(::ActiveRecord)
230
+ # Allow this model to be used as ActiveRecord attribute type in Rails 5+.
231
+ #
232
+ # E.g.
233
+ # class Credentials < ::ActiveJsonModel; end;
234
+ #
235
+ # class Integration < ActiveRecord::Base
236
+ # attribute :credentials, Credentials.attribute_type
237
+ # end
238
+ #
239
+ # Note that this array_data would be stored as jsonb in the database
240
+ def attribute_type
241
+ @attribute_type ||= ActiveModelJsonSerializableType.new(self)
242
+ end
243
+
244
+ # Allow this model to be used as ActiveRecord attribute type in Rails 5+.
245
+ #
246
+ # E.g.
247
+ # class SecureCredentials < ::ActiveJsonModel; end;
248
+ #
249
+ # class Integration < ActiveRecord::Base
250
+ # attribute :secure_credentials, SecureCredentials.encrypted_attribute_type
251
+ # end
252
+ #
253
+ # Note that this array_data would be stored as a string in the database, encrypted using
254
+ # a symmetric key at the application level.
255
+ def encrypted_attribute_type
256
+ @encrypted_attribute_type ||= ActiveModelJsonSerializableEncryptedType.new(self)
257
+ end
258
+ end
259
+
260
+ # A list of procs that will be executed after array_data has been loaded.
261
+ #
262
+ # @return [Array<Proc>] array of procs executed after array_data is loaded
263
+ def active_json_model_load_callbacks
264
+ @__active_json_model_load_callbacks ||= []
265
+ end
266
+
267
+ # A factory defined via <code>json_polymorphic_via</code> that allows the class to choose different concrete
268
+ # classes based on the array_data in the JSON. Property is for only this class, not the entire class hierarchy.
269
+ #
270
+ # @ return [Proc, nil] proc used to select the concrete base class for the list model class
271
+ def active_json_model_polymorphic_factory
272
+ @__active_json_model_polymorphic_factory
273
+ end
274
+
275
+ # OpenStruct storing the configuration of of this ActiveJsonModel::Array. Properties include:
276
+ # serialize_proc - proc used to translate from objects -> json
277
+ # serialize_method - symbol of method name to call to translate from objects -> json
278
+ # deserialize_proc - proc used to translate from json -> objects
279
+ # deserialize_method - symbol of method name to call to translate from json -> objects
280
+ # keep_nils - boolean flag indicating if nils should be kept in the array after de/serialization
281
+ # errors_go_to_nil - boolean flag if errors should be capture from the de/serialization methods and translated
282
+ # to nil
283
+ def active_json_model_array_serialization_tuple
284
+ @__active_json_model_array_serialization_tuple
285
+ end
286
+
287
+ # Filter the ancestor hierarchy to those built with <code>ActiveJsonModel::Array</code> concerns
288
+ #
289
+ # @return [Array<Class>] reversed array of classes in the hierarchy of this class that include ActiveJsonModel
290
+ def active_json_model_ancestors
291
+ self.ancestors.filter{|o| o.respond_to?(:active_json_model_array_serialization_tuple)}.reverse
292
+ end
293
+
294
+ # Get all active json model after load callbacks for all the class hierarchy tree
295
+ #
296
+ # @return [Array<AfterLoadCallback>] After load callbacks for the ancestry tree
297
+ def ancestry_active_json_model_load_callbacks
298
+ self.active_json_model_ancestors.flat_map(&:active_json_model_load_callbacks)
299
+ end
300
+
301
+ # Get all polymorphic factories in the ancestry chain.
302
+ #
303
+ # @return [Array<Proc>] After load callbacks for the ancestry tree
304
+ def ancestry_active_json_model_polymorphic_factory
305
+ self.active_json_model_ancestors.map(&:active_json_model_polymorphic_factory).filter(&:present?)
306
+ end
307
+
308
+ # Configure this list class to have elements of a specific ActiveJsonModel Model type.
309
+ #
310
+ # Example:
311
+ # class PhoneNumber
312
+ # include ::ActiveJsonModel::Model
313
+ #
314
+ # json_attribute :number, String
315
+ # json_attribute :label, String
316
+ # end
317
+ #
318
+ # class PhoneNumberArray
319
+ # include ::ActiveJsonModel::Array
320
+ #
321
+ # json_array_of PhoneNumber
322
+ # end
323
+ #
324
+ # @param clazz [Clazz] the class to use when loading model elements
325
+ # @param validate [Proc, symbol] Proc to use for validating elements of the array. May be a symbol to a method
326
+ # implemented in the class. Arguments are value, index, errors object (if not method on class). Method
327
+ # should add items to the errors array if there are errors, Note that if the elements of the array
328
+ # implement the <code>valid?</code> and <code>errors</code> methods, those are used in addition to the
329
+ # <code>validate</code> method.
330
+ # @param keep_nils [Boolean] Should the resulting array keep nils? Default is false and the array will be
331
+ # compacted after deserialization.
332
+ # @param errors_go_to_nil [Boolean] Should excepts be trapped and converted to nil values? Default is true.
333
+ # @param nil_data_to_empty_array [Boolean] When deserializing data, should a nil value make the values array empty
334
+ # (versus nil values, which will cause errors)
335
+ def json_array_of(clazz, validate: nil, keep_nils: false, errors_go_to_nil: true, nil_data_to_empty_array: false)
336
+ unless clazz && clazz.is_a?(Class)
337
+ raise ArgumentError.new("json_array_of must be passed a class to use as the type for elements of the array. Received '#{clazz}'")
338
+ end
339
+
340
+ unless [Integer, Float, String, Symbol, DateTime, Date].any?{|c| c == clazz} || clazz.include?(::ActiveJsonModel::Model)
341
+ raise ArgumentError.new("Class used with json_array_of must include ActiveJsonModel::Model or be of type Integer, Float, String, Symbol, DateTime, or Date")
342
+ end
343
+
344
+ if @__active_json_model_array_serialization_tuple
345
+ raise ArgumentError.new("json_array_of, json_polymorphic_array_by, and json_array are exclusive. Exactly one of them must be specified.")
346
+ end
347
+
348
+ # Delegate the real work to a serialize/deserialize approach.
349
+ if clazz == Integer
350
+ json_array(serialize: ->(o){ o }, deserialize: ->(d){ d&.to_i },
351
+ validate: validator_for_item_type(Integer, validate),
352
+ keep_nils: keep_nils, errors_go_to_nil: errors_go_to_nil,
353
+ nil_data_to_empty_array: nil_data_to_empty_array)
354
+ elsif clazz == Float
355
+ json_array(serialize: ->(o){ o }, deserialize: ->(d){ d&.to_f },
356
+ validate: validator_for_item_type(Float, validate),
357
+ keep_nils: keep_nils, errors_go_to_nil: errors_go_to_nil,
358
+ nil_data_to_empty_array: nil_data_to_empty_array)
359
+ elsif clazz == String
360
+ json_array(serialize: ->(o){ o }, deserialize: ->(d){ d&.to_s },
361
+ validate: validator_for_item_type(String, validate),
362
+ keep_nils: keep_nils, errors_go_to_nil: errors_go_to_nil,
363
+ nil_data_to_empty_array: nil_data_to_empty_array)
364
+ elsif clazz == Symbol
365
+ json_array(serialize: ->(o){ o&.to_s }, deserialize: ->(d){ d&.to_sym },
366
+ validate: validator_for_item_type(Symbol, validate),
367
+ keep_nils: keep_nils, errors_go_to_nil: errors_go_to_nil,
368
+ nil_data_to_empty_array: nil_data_to_empty_array)
369
+ elsif clazz == DateTime
370
+ json_array(serialize: ->(o){ o&.iso8601 }, deserialize: ->(d){ DateTime.iso8601(d) },
371
+ validate: validator_for_item_type(DateTime, validate),
372
+ keep_nils: keep_nils, errors_go_to_nil: errors_go_to_nil,
373
+ nil_data_to_empty_array: nil_data_to_empty_array)
374
+ elsif clazz == Date
375
+ json_array(serialize: ->(o){ o&.iso8601 }, deserialize: ->(d){ Date.iso8601(d) },
376
+ validate: validator_for_item_type(Date, validate),
377
+ keep_nils: keep_nils, errors_go_to_nil: errors_go_to_nil,
378
+ nil_data_to_empty_array: nil_data_to_empty_array)
379
+ else
380
+ # This is the case where this is a Active JSON Model
381
+ json_array(
382
+ serialize: ->(o) {
383
+ if o && o.respond_to?(:dump_to_json)
384
+ o.dump_to_json
385
+ else
386
+ o
387
+ end
388
+ }, deserialize: ->(d) {
389
+ c = if clazz&.respond_to?(:active_json_model_concrete_class_from_ancestry_polymorphic)
390
+ clazz.active_json_model_concrete_class_from_ancestry_polymorphic(d) || clazz
391
+ else
392
+ clazz
393
+ end
394
+
395
+ if c
396
+ c.new.tap do |m|
397
+ m.load_from_json(d)
398
+ end
399
+ else
400
+ nil
401
+ end
402
+ },
403
+ validate: validator_for_item_type(clazz, validate),
404
+ keep_nils: keep_nils,
405
+ errors_go_to_nil: errors_go_to_nil,
406
+ nil_data_to_empty_array: nil_data_to_empty_array)
407
+ end
408
+ end
409
+
410
+ # The factory for generating instances of the array when hydrating from JSON. The factory must return the
411
+ # ActiveJsonModel::Model implementing class chosen.
412
+ #
413
+ # Example:
414
+ # class PhoneNumber
415
+ # include ::ActiveJsonModel::Model
416
+ #
417
+ # json_attribute :number, String
418
+ # json_attribute :label, String
419
+ # end
420
+ #
421
+ # class Email
422
+ # include ::ActiveJsonModel::Model
423
+ #
424
+ # json_attribute :address, String
425
+ # json_attribute :label, String
426
+ # end
427
+ #
428
+ # class ContactInfoArray
429
+ # include ::ActiveJsonModel::Array
430
+ #
431
+ # json_polymorphic_array_by do |item_data|
432
+ # if item_data.key?(:address)
433
+ # Email
434
+ # else
435
+ # PhoneNumber
436
+ # end
437
+ # end
438
+ # end
439
+ # @param factory [Proc, String] that factory method to choose the appropriate class for each element.
440
+ # @param validate [Proc, symbol] Proc to use for validating elements of the array. May be a symbol to a method
441
+ # implemented in the class. Arguments are value, index, errors object (if not method on class). Method
442
+ # should add items to the errors array if there are errors, Note that if the elements of the array
443
+ # implement the <code>valid?</code> and <code>errors</code> methods, those are used in addition to the
444
+ # <code>validate</code> method.
445
+ # @param keep_nils [Boolean] Should the resulting array keep nils? Default is false and the array will be
446
+ # compacted after deserialization.
447
+ # @param errors_go_to_nil [Boolean] Should excepts be trapped and converted to nil values? Default is true.
448
+ # @param nil_data_to_empty_array [Boolean] When deserializing data, should a nil value make the values array empty
449
+ # (versus nil values, which will cause errors)
450
+ def json_polymorphic_array_by(validate: nil, keep_nils: false, errors_go_to_nil: false, nil_data_to_empty_array: false, &factory)
451
+ unless factory && factory.arity == 1
452
+ raise ArgumentError.new("Must pass block taking one argument to json_polymorphic_array_by")
453
+ end
454
+
455
+ if @__active_json_model_array_serialization_tuple
456
+ raise ArgumentError.new("json_array_of, json_polymorphic_array_by, and json_array are exclusive. Exactly one of them must be specified.")
457
+ end
458
+
459
+ # Delegate the real work to a serialize/deserialize approach.
460
+ json_array(
461
+ serialize: ->(o) {
462
+ if o && o.respond_to?(:dump_to_json)
463
+ o.dump_to_json
464
+ else
465
+ o
466
+ end
467
+ }, deserialize: ->(d) {
468
+ clazz = factory.call(d)
469
+
470
+ if clazz&.respond_to?(:active_json_model_concrete_class_from_ancestry_polymorphic)
471
+ clazz = clazz.active_json_model_concrete_class_from_ancestry_polymorphic(d) || clazz
472
+ end
473
+
474
+ if clazz
475
+ clazz.new.tap do |m|
476
+ m.load_from_json(d)
477
+ end
478
+ else
479
+ nil
480
+ end
481
+ },
482
+ validate: validate,
483
+ keep_nils: keep_nils,
484
+ errors_go_to_nil: errors_go_to_nil,
485
+ nil_data_to_empty_array: nil_data_to_empty_array)
486
+ end
487
+
488
+ # A JSON array that uses arbitrary serialization/deserialization.
489
+ #
490
+ # Example:
491
+ # class DateTimeArray
492
+ # include ::ActiveJsonModel::Array
493
+ #
494
+ # json_array serialize: ->(dt){ dt.iso8601 }
495
+ # deserialize: ->(s){ DateTime.iso8601(s) }
496
+ # end
497
+ # @param serialize [Proc, symbol] Proc to use for serialization. May be a symbol to a method implemented
498
+ # in the class.
499
+ # @param deserialize [Proc, symbol] Proc to use for deserialization. May be a symbol to a method implemented
500
+ # in the class.
501
+ # @param validate [Proc, symbol] Proc to use for validating elements of the array. May be a symbol to a method
502
+ # implemented in the class. Arguments are value, index, errors object (if not method on class). Method
503
+ # should add items to the errors array if there are errors, Note that if the elements of the array
504
+ # implement the <code>valid?</code> and <code>errors</code> methods, those are used in addition to the
505
+ # <code>validate</code> method.
506
+ # @param keep_nils [Boolean] Should the resulting array keep nils? Default is false and the array will be
507
+ # compacted after deserialization.
508
+ # @param errors_go_to_nil [Boolean] Should excepts be trapped and converted to nil values? Default is true.
509
+ # @param nil_data_to_empty_array [Boolean] When deserializing data, should a nil value make the values array empty
510
+ # (versus nil values, which will cause errors)
511
+ def json_array(serialize:, deserialize:, validate: nil,
512
+ keep_nils: false, errors_go_to_nil: true, nil_data_to_empty_array: false)
513
+ unless serialize && (serialize.is_a?(Proc) || serialize.is_a?(Symbol))
514
+ 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")
515
+ end
516
+
517
+ if serialize.is_a?(Proc) && serialize.arity != 1
518
+ raise ArgumentError.new("Serialize proc must take exactly one argument.")
519
+ end
520
+
521
+ unless deserialize && (deserialize.is_a?(Proc) || deserialize.is_a?(Symbol))
522
+ 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")
523
+ end
524
+
525
+ if deserialize.is_a?(Proc) && deserialize.arity != 1
526
+ raise ArgumentError.new("Deserialize proc must take exactly one argument.")
527
+ end
528
+
529
+ if @__active_json_model_array_serialization_tuple
530
+ raise ArgumentError.new("json_array_of, json_polymorphic_array_by, and json_array are exclusive. Exactly one of them must be specified.")
531
+ end
532
+
533
+ @__active_json_model_array_serialization_tuple = OpenStruct.new.tap do |t|
534
+ if serialize.is_a?(Proc)
535
+ t.serialize_proc = serialize
536
+ else
537
+ t.serialize_method = serialize
538
+ end
539
+
540
+ if deserialize.is_a?(Proc)
541
+ t.deserialize_proc = deserialize
542
+ else
543
+ t.deserialize_method = deserialize
544
+ end
545
+
546
+ if validate
547
+ if validate.is_a?(Proc)
548
+ t.validate_proc = validate
549
+ else
550
+ t.validate_method = validate
551
+ end
552
+ end
553
+
554
+ t.keep_nils = keep_nils
555
+ t.errors_go_to_nil = errors_go_to_nil
556
+ t.nil_data_to_empty_array = nil_data_to_empty_array
557
+ end
558
+ end
559
+
560
+ # Crate a validator that can be used to check that items of the array of a specified type.
561
+ #
562
+ # @param clazz [Class] the type to check against
563
+ # @param recursive_validator [Proc, Symbol] an optional validator to be called in addition to this one
564
+ # @return [Proc] a proc to do the validation
565
+ def validator_for_item_type(clazz, recursive_validator=nil)
566
+ ->(val, i, errors, me) do
567
+ unless val&.is_a?(clazz)
568
+ errors.add(:values, "Element #{i} must be of type #{clazz} but is of type #{val&.class}")
569
+ end
570
+
571
+ if recursive_validator
572
+ if recursive_validator.is_a?(Proc)
573
+ if recursive_validator.arity == 4
574
+ recursive_validator.call(val, i, errors, me)
575
+ else
576
+ recursive_validator.call(val, i, errors)
577
+ end
578
+ else
579
+ me.send(recursive_validator, val, i)
580
+ end
581
+ end
582
+ end
583
+ end
584
+
585
+ # Computes the concrete class that should be used to load the data based on the ancestry tree's
586
+ # <code>json_polymorphic_via</code>. Also handles potential recursion at the leaf nodes of the tree.
587
+ #
588
+ # @param array_data [Array] the array_data being loaded from JSON
589
+ # @return [Class] the class to be used to load the JSON
590
+ def active_json_model_concrete_class_from_ancestry_polymorphic(array_data)
591
+ clazz = nil
592
+ ancestry_active_json_model_polymorphic_factory.each do |proc|
593
+ clazz = proc.call(array_data)
594
+ break if clazz
595
+ end
596
+
597
+ if clazz
598
+ if clazz != self && clazz.respond_to?(:active_json_model_concrete_class_from_ancestry_polymorphic)
599
+ clazz.active_json_model_concrete_class_from_ancestry_polymorphic(array_data) || clazz
600
+ else
601
+ clazz
602
+ end
603
+ else
604
+ self
605
+ end
606
+ end
607
+
608
+ # Define a polymorphic factory to choose the concrete class for the list model. Note that because the array_data passed
609
+ # to the block is an array of models, you must account for what the behavior is if there are no elements.
610
+ #
611
+ # Example:
612
+ #
613
+ # class BaseWorkflowArray
614
+ # include ::ActiveJsonModel::List
615
+ #
616
+ # json_polymorphic_via do |array_data|
617
+ # if array_data[0]
618
+ # if array_data[0][:type] == 'email'
619
+ # EmailWorkflow
620
+ # else
621
+ # WebhookWorkflow
622
+ # end
623
+ # else
624
+ # BaseWorkflowArray
625
+ # end
626
+ # end
627
+ # end
628
+ #
629
+ # class EmailWorkflow < BaseWorkflow
630
+ # def home_emails
631
+ # filter{|e| e.label == 'home'}
632
+ # end
633
+ # end
634
+ #
635
+ # class WebhookWorkflow < BaseWorkflow
636
+ # def secure_webhooks
637
+ # filter{|wh| wh.secure }
638
+ # end
639
+ # end
640
+ def json_polymorphic_via(&block)
641
+ @__active_json_model_polymorphic_factory = block
642
+ end
643
+
644
+ # Register a new after load callback which is invoked after the instance is loaded from JSON
645
+ #
646
+ # @param method_name [Symbol, String] the name of the method to be invoked
647
+ # @param block [Proc] block to be executed after load. Will optionally be passed an instance of the loaded object.
648
+ def json_after_load(method_name=nil, &block)
649
+ raise ArgumentError.new("Must specify method or block for ActiveJsonModel after load") unless method_name || block
650
+ raise ArgumentError.new("Can only specify method or block for ActiveJsonModel after load") if method_name && block
651
+
652
+ active_json_model_load_callbacks.push(
653
+ AfterLoadCallback.new(
654
+ method_name: method_name,
655
+ block: block
656
+ )
657
+ )
658
+ end
659
+
660
+ # Load an instance of the class from JSON
661
+ #
662
+ # @param json_array_data [String, Array] the data to be loaded into the instance. May be an array or a string.
663
+ # @return Instance of the list class
664
+ def load(json_array_data)
665
+ if json_array_data.nil? || (json_array_data.is_a?(String) && json_array_data.blank?)
666
+ clazz = active_json_model_concrete_class_from_ancestry_polymorphic([])
667
+ if clazz&.active_json_model_array_serialization_tuple&.nil_data_to_empty_array
668
+ return clazz.new.tap do |instance|
669
+ instance.load_from_json(nil)
670
+ end
671
+ else
672
+ return nil
673
+ end
674
+ end
675
+
676
+ # Get the array_data to a hash, regardless of the starting array_data type
677
+ array_data = json_array_data.is_a?(String) ? JSON.parse(json_array_data) : json_array_data
678
+
679
+ # Recursively make the value have indifferent access
680
+ array_data = ::ActiveJsonModel::Utils.recursively_make_indifferent(array_data)
681
+
682
+ # Get the concrete class from the ancestry tree's potential polymorphic behavior. Note this needs to be done
683
+ # for each sub property as well. This just covers the outermost case.
684
+ clazz = active_json_model_concrete_class_from_ancestry_polymorphic(array_data)
685
+ clazz.new.tap do |instance|
686
+ instance.load_from_json(array_data)
687
+ end
688
+ end
689
+
690
+ # Dump the specified object to JSON
691
+ #
692
+ # @param obj [self] object to dump to json
693
+ def dump(obj)
694
+ raise ArgumentError.new("Expected #{self} got #{obj.class} to dump to JSON") unless obj.is_a?(self)
695
+ obj.dump_to_json
696
+ end
697
+ end
698
+ end
699
+ end