activejsonmodel 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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