activejsonmodel 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|