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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +79 -0
- data/lib/active_json_model.rb +8 -0
- data/lib/activejsonmodel/active_record_encrypted_type.rb +72 -0
- data/lib/activejsonmodel/active_record_type.rb +84 -0
- data/lib/activejsonmodel/after_load_callback.rb +35 -0
- data/lib/activejsonmodel/array.rb +699 -0
- data/lib/activejsonmodel/json_attribute.rb +64 -0
- data/lib/activejsonmodel/model.rb +597 -0
- data/lib/activejsonmodel/utils.rb +23 -0
- data/lib/activejsonmodel/version.rb +5 -0
- metadata +99 -0
@@ -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
|