reform 2.0.5 → 2.1.0.rc1
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 +4 -4
- data/.travis.yml +3 -1
- data/CHANGES.md +12 -0
- data/Gemfile +12 -2
- data/README.md +9 -14
- data/Rakefile +1 -1
- data/database.sqlite3 +0 -0
- data/lib/reform.rb +1 -0
- data/lib/reform/contract.rb +13 -20
- data/lib/reform/contract/validate.rb +9 -7
- data/lib/reform/form.rb +45 -31
- data/lib/reform/form/active_model.rb +10 -10
- data/lib/reform/form/active_model/form_builder_methods.rb +5 -4
- data/lib/reform/form/active_model/model_reflections.rb +2 -2
- data/lib/reform/form/active_model/model_validations.rb +3 -3
- data/lib/reform/form/active_model/validations.rb +49 -32
- data/lib/reform/form/dry.rb +55 -0
- data/lib/reform/form/lotus.rb +4 -1
- data/lib/reform/form/module.rb +3 -17
- data/lib/reform/form/multi_parameter_attributes.rb +0 -9
- data/lib/reform/form/populator.rb +72 -30
- data/lib/reform/form/validate.rb +19 -43
- data/lib/reform/form/validation/unique_validator.rb +39 -6
- data/lib/reform/validation.rb +40 -0
- data/lib/reform/validation/groups.rb +73 -0
- data/lib/reform/version.rb +1 -1
- data/reform.gemspec +3 -1
- data/test/active_record_test.rb +2 -0
- data/test/contract_test.rb +2 -2
- data/test/deprecation_test.rb +27 -0
- data/test/deserialize_test.rb +29 -8
- data/test/dummy/config/locales/en.yml +4 -1
- data/test/errors_test.rb +4 -4
- data/test/feature_test.rb +2 -2
- data/test/fixtures/dry_error_messages.yml +43 -0
- data/test/form_builder_test.rb +10 -8
- data/test/form_test.rb +1 -36
- data/test/inherit_test.rb +20 -8
- data/test/module_test.rb +2 -30
- data/test/parse_pipeline_test.rb +15 -0
- data/test/populate_test.rb +41 -12
- data/test/populator_skip_test.rb +28 -0
- data/test/reform_test.rb +1 -1
- data/test/skip_if_test.rb +10 -3
- data/test/test_helper.rb +11 -2
- data/test/unique_test.rb +72 -1
- data/test/validate_test.rb +6 -7
- data/test/validation/activemodel_validation_test.rb +252 -0
- data/test/validation/dry_validation_test.rb +330 -0
- metadata +63 -10
- data/lib/reform/schema.rb +0 -13
@@ -4,23 +4,34 @@ require "uber/delegates"
|
|
4
4
|
|
5
5
|
module Reform::Form::ActiveModel
|
6
6
|
# AM::Validations for your form.
|
7
|
-
#
|
8
7
|
# Provides ::validates, ::validate, #validate, and #valid?.
|
8
|
+
#
|
9
|
+
# Most of this file contains unnecessary wiring to make ActiveModel's error message magic work.
|
10
|
+
# Since Rails still thinks it's a good idea to do things like object.class.human_attribute_name,
|
11
|
+
# we have some hacks in here to provide that. If it doesn't work for you, don't blame us.
|
9
12
|
module Validations
|
10
13
|
def self.included(includer)
|
11
14
|
includer.instance_eval do
|
12
15
|
include Reform::Form::ActiveModel
|
13
|
-
inheritable_attr :validator
|
14
|
-
self.validator = Class.new(Validator) # the actual validations happen in this instance.
|
15
16
|
|
16
17
|
class << self
|
17
18
|
extend Uber::Delegates
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
19
|
+
# # Hooray! Delegate translation back to Reform's Validator class which contains AM::Validations.
|
20
|
+
delegates :active_model_really_sucks, :human_attribute_name, :lookup_ancestors, :i18n_scope # Rails 3.1.
|
21
|
+
|
22
|
+
def validation_group_class
|
23
|
+
Group
|
24
|
+
end
|
25
|
+
|
26
|
+
# this is to allow calls like Form::human_attribute_name (note that this is on the CLASS level) to be resolved.
|
27
|
+
# those calls happen when adding errors in a custom validation method, which is defined on the form (as an instance method).
|
28
|
+
def active_model_really_sucks
|
29
|
+
Class.new(Validator).tap do |v|
|
30
|
+
v.model_name = model_name
|
31
|
+
end
|
32
|
+
end
|
22
33
|
end
|
23
|
-
end
|
34
|
+
end # ::included
|
24
35
|
end
|
25
36
|
|
26
37
|
def build_errors
|
@@ -33,6 +44,24 @@ module Reform::Form::ActiveModel
|
|
33
44
|
send(name)
|
34
45
|
end
|
35
46
|
|
47
|
+
class Group
|
48
|
+
def initialize
|
49
|
+
@validations = Class.new(Reform::Form::ActiveModel::Validations::Validator)
|
50
|
+
end
|
51
|
+
|
52
|
+
extend Uber::Delegates
|
53
|
+
delegates :@validations, :validates, :validate, :validates_with, :validate_with
|
54
|
+
|
55
|
+
def call(fields, errors, form) # FIXME.
|
56
|
+
validator = @validations.new(form)
|
57
|
+
validator.valid?
|
58
|
+
|
59
|
+
validator.errors.each do |name, error| # TODO: handle with proper merge, or something. validator.errors is ALWAYS AM::Errors.
|
60
|
+
errors.add(name, error)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
36
65
|
|
37
66
|
# Validator is the validatable object. On the class level, we define validations,
|
38
67
|
# on instance, it exposes #valid?.
|
@@ -43,45 +72,33 @@ module Reform::Form::ActiveModel
|
|
43
72
|
|
44
73
|
class << self
|
45
74
|
def model_name
|
46
|
-
@_active_model_sucks
|
75
|
+
@_active_model_sucks ||= ActiveModel::Name.new(Reform::Form, nil, "Reform::Form")
|
47
76
|
end
|
48
77
|
|
49
78
|
def model_name=(name)
|
50
79
|
@_active_model_sucks = name
|
51
80
|
end
|
52
81
|
|
53
|
-
def
|
54
|
-
|
82
|
+
def validates(*args, &block)
|
83
|
+
super(*Declarative::DeepDup.(args), &block)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Prevent AM:V from mutating the validator class
|
87
|
+
def attr_reader(*)
|
88
|
+
end
|
89
|
+
|
90
|
+
def attr_writer(*)
|
55
91
|
end
|
56
92
|
end
|
57
93
|
|
58
|
-
def initialize(form
|
94
|
+
def initialize(form)
|
59
95
|
super(form)
|
60
|
-
self.class.model_name =
|
96
|
+
self.class.model_name = form.model_name # one of the many reasons why i will drop support for AM::V in 2.1. or maybe a bit later.
|
61
97
|
end
|
62
98
|
|
63
99
|
def method_missing(m, *args, &block)
|
64
100
|
__getobj__.send(m, *args, &block) # send all methods to the form, even privates.
|
65
101
|
end
|
66
102
|
end
|
67
|
-
|
68
|
-
private
|
69
|
-
|
70
|
-
# Needs to be implemented by every validation backend and implements the
|
71
|
-
# actual validation. See Reform::Form::Lotus, too!
|
72
|
-
def valid?
|
73
|
-
# we always pass the model_name into the validator now, so AM:V can do its magic. problem is that
|
74
|
-
# AM does validator.class.model_name so we have to hack the dynamic model name into the
|
75
|
-
# Validator class.
|
76
|
-
validator = self.class.validator.new(self, model_name)
|
77
|
-
validator.valid? # run the Validations object's validator with the form as context. this won't pollute anything in the form.
|
78
|
-
|
79
|
-
#errors.merge!(validator.errors, "")
|
80
|
-
validator.errors.each do |name, error| # TODO: handle with proper merge, or something. validator.errors is ALWAYS AM::Errors.
|
81
|
-
errors.add(name, error)
|
82
|
-
end
|
83
|
-
|
84
|
-
errors.empty?
|
85
|
-
end
|
86
103
|
end
|
87
104
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require "dry-validation"
|
2
|
+
require "reform/validation"
|
3
|
+
require "dry/validation/schema/form"
|
4
|
+
|
5
|
+
module Reform::Form::Dry
|
6
|
+
module Validations
|
7
|
+
|
8
|
+
def build_errors
|
9
|
+
Reform::Contract::Errors.new(self)
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def validation_group_class
|
14
|
+
Group
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.included(includer)
|
19
|
+
includer.extend(ClassMethods)
|
20
|
+
end
|
21
|
+
|
22
|
+
class Group
|
23
|
+
def initialize
|
24
|
+
@validator = Class.new(ValidatorSchema)
|
25
|
+
end
|
26
|
+
|
27
|
+
def instance_exec(&block)
|
28
|
+
@validator.class_eval(&block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def call(fields, reform_errors, form)
|
32
|
+
validator = @validator.new(form)
|
33
|
+
|
34
|
+
validator.call(fields).messages.each do |dry_error|
|
35
|
+
# a dry error message looks like this:
|
36
|
+
# [:email, [['Please provide your email', '']]]
|
37
|
+
dry_error[1].each do |attr_error|
|
38
|
+
reform_errors.add(dry_error[0], attr_error[0])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class ValidatorSchema < Dry::Validation::Schema::Form
|
45
|
+
def initialize(form)
|
46
|
+
@form = form
|
47
|
+
super()
|
48
|
+
end
|
49
|
+
|
50
|
+
def form
|
51
|
+
@form
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/reform/form/lotus.rb
CHANGED
@@ -4,11 +4,14 @@ require "lotus/validations"
|
|
4
4
|
module Reform::Form::Lotus
|
5
5
|
class Errors < Lotus::Validations::Errors
|
6
6
|
def merge!(errors, prefix)
|
7
|
+
new_errors = {}
|
7
8
|
errors.instance_variable_get(:@errors).each do |name, err|
|
8
9
|
field = (prefix+[name]).join(".")
|
9
|
-
|
10
|
+
new_errors[field] = err
|
10
11
|
end
|
11
12
|
# next if messages[field] and messages[field].include?(msg)
|
13
|
+
|
14
|
+
new_errors.each { |field, err| add(field, *err) } # TODO: use namespace feature in Lotus here!
|
12
15
|
end
|
13
16
|
|
14
17
|
def inspect
|
data/lib/reform/form/module.rb
CHANGED
@@ -1,29 +1,15 @@
|
|
1
1
|
# Include this in every module that gets further included.
|
2
|
-
# TODO: this could be implemented in Declarable, as we can use that everywhere.
|
3
2
|
module Reform::Form::Module
|
4
3
|
def self.included(base)
|
5
4
|
base.extend ClassMethods
|
6
|
-
base.extend Included
|
7
|
-
end
|
8
|
-
|
9
|
-
module Included # TODO: use representable's inheritance mechanism.
|
10
|
-
def included(base)
|
11
|
-
super
|
12
|
-
instructions.each { |cfg|
|
13
|
-
args = cfg[1].dup
|
14
|
-
options = args.extract_options!.dup # we need to duplicate options has as AM::Validations messes it up later.
|
15
5
|
|
16
|
-
|
17
|
-
|
6
|
+
base.extend Declarative::Heritage::DSL # ::heritage
|
7
|
+
base.extend Declarative::Heritage::Included # ::included
|
18
8
|
end
|
19
9
|
|
20
10
|
module ClassMethods
|
21
11
|
def method_missing(method, *args, &block)
|
22
|
-
|
23
|
-
end
|
24
|
-
|
25
|
-
def instructions
|
26
|
-
@instructions ||= []
|
12
|
+
heritage.record(method, *args, &block)
|
27
13
|
end
|
28
14
|
end
|
29
15
|
end
|
@@ -45,13 +45,4 @@ module Reform::Form::MultiParameterAttributes
|
|
45
45
|
def deserialize!(params)
|
46
46
|
super DateTimeParamsFilter.new.call(params) # if params.is_a?(Hash) # this currently works for hash, only.
|
47
47
|
end
|
48
|
-
|
49
|
-
# module BuildDefinition
|
50
|
-
# def build_definition(name, options, &block)
|
51
|
-
# return super unless options[:multi_params]
|
52
|
-
|
53
|
-
# options[:parse_filter] << DateParamsFilter.new
|
54
|
-
# super
|
55
|
-
# end
|
56
|
-
# end
|
57
48
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# Implements the :populator option.
|
2
2
|
#
|
3
|
-
# populator: -> (fragment
|
4
|
-
# populator: -> (fragment
|
3
|
+
# populator: -> (fragment:, model:, :binding)
|
4
|
+
# populator: -> (fragment:, collection:, index:, binding:)
|
5
5
|
#
|
6
6
|
# For collections, the entire collection and the currently deserialised index is passed in.
|
7
7
|
class Reform::Form::Populator
|
@@ -12,49 +12,66 @@ class Reform::Form::Populator
|
|
12
12
|
@value = Uber::Options::Value.new(user_proc) # we can now process Callable, procs, :symbol.
|
13
13
|
end
|
14
14
|
|
15
|
-
def call(
|
16
|
-
|
15
|
+
def call(input, options)
|
16
|
+
model = get(options)
|
17
|
+
twin = call!(options.merge(model: model, collection: model))
|
17
18
|
|
18
|
-
|
19
|
-
twin = call!(form, fragment, options.binding.get, *args)
|
19
|
+
return twin if twin == Representable::Pipeline::Stop
|
20
20
|
|
21
21
|
# this kinda sucks. the proc may call self.composer = Artist.new, but there's no way we can
|
22
22
|
# return the twin instead of the model from the #composer= setter.
|
23
|
-
twin = options
|
23
|
+
twin = get(options) unless options[:binding].array?
|
24
24
|
|
25
|
-
#
|
25
|
+
# we always need to return a twin/form here so we can call nested.deserialize().
|
26
26
|
handle_fail(twin, options)
|
27
27
|
|
28
28
|
twin
|
29
29
|
end
|
30
30
|
|
31
31
|
private
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
32
|
+
def call!(options)
|
33
|
+
form = options[:represented]
|
34
|
+
|
35
|
+
deprecate_positional_args(form, @user_proc, options) do
|
36
|
+
@value.(form, options)
|
37
|
+
end
|
37
38
|
end
|
38
39
|
|
39
40
|
def handle_fail(twin, options)
|
40
|
-
raise "[Reform] Your :populator did not return a Reform::Form instance for `#{options
|
41
|
+
raise "[Reform] Your :populator did not return a Reform::Form instance for `#{options[:binding].name}`." if options[:binding][:nested] && !twin.is_a?(Reform::Form)
|
42
|
+
end
|
43
|
+
|
44
|
+
def get(options)
|
45
|
+
Representable::GetValue.(nil, options)
|
46
|
+
end
|
47
|
+
|
48
|
+
def deprecate_positional_args(form, proc, options) # TODO: remove in 2.2.
|
49
|
+
arity = proc.is_a?(Symbol) ? form.method(proc).arity : proc.arity
|
50
|
+
return yield if arity == 1
|
51
|
+
warn "[Reform] Positional arguments for :populator and friends are deprecated. Please use ->(options) and enjoy the rest of your day. Learn more at http://trailblazerb.org/gems/reform/upgrading-guide.html#to-21"
|
52
|
+
args = []
|
53
|
+
args << options[:index] if options[:index]
|
54
|
+
args << options[:representable_options]
|
55
|
+
form.instance_exec(options[:fragment], options[:model], *args, &proc)
|
41
56
|
end
|
42
57
|
|
43
58
|
|
44
59
|
class IfEmpty < self # Populator
|
45
|
-
|
46
|
-
|
47
|
-
|
60
|
+
def call!(options)
|
61
|
+
binding, twin, index, fragment = options[:binding], options[:model], options[:index], options[:fragment] # TODO: remove once we drop 2.0.
|
62
|
+
form = options[:represented]
|
48
63
|
|
49
|
-
if
|
50
|
-
index = args.first
|
64
|
+
if binding.array?
|
51
65
|
item = twin.original[index] and return item
|
52
66
|
|
53
|
-
|
67
|
+
new_index = [index, twin.count].min # prevents nil items with initially empty/smaller collections and :skip_if's.
|
68
|
+
# this means the fragment index and populated nested form index might be different.
|
69
|
+
|
70
|
+
twin.insert(new_index, run!(form, fragment, options)) # form.songs.insert(Song.new)
|
54
71
|
else
|
55
72
|
return if twin
|
56
73
|
|
57
|
-
form.send(
|
74
|
+
form.send(binding.setter, run!(form, fragment, options)) # form.artist=(Artist.new)
|
58
75
|
end
|
59
76
|
end
|
60
77
|
|
@@ -62,22 +79,47 @@ private
|
|
62
79
|
def run!(form, fragment, options)
|
63
80
|
return @user_proc.new if @user_proc.is_a?(Class) # handle populate_if_empty: Class. this excludes using Callables, though.
|
64
81
|
|
65
|
-
|
82
|
+
deprecate_positional_args(form, @user_proc, options) do
|
83
|
+
@value.(form, options)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def deprecate_positional_args(form, proc, options) # TODO: remove in 2.2.
|
88
|
+
arity = proc.is_a?(Symbol) ? form.method(proc).arity : proc.arity
|
89
|
+
return yield if arity == 1
|
90
|
+
warn "[Reform] Positional arguments for :prepopulate and friends are deprecated. Please use ->(options) and enjoy the rest of your day. Learn more at http://trailblazerb.org/gems/reform/upgrading-guide.html#to-21"
|
91
|
+
|
92
|
+
@value.(form, options[:fragment], options[:user_options])
|
66
93
|
end
|
94
|
+
|
67
95
|
end
|
68
96
|
|
69
97
|
# Sync (default) blindly grabs the corresponding form twin and returns it. This might imply that nil is returned,
|
70
98
|
# and in turn #validate! is called on nil.
|
71
99
|
class Sync < self
|
72
|
-
def call!(
|
73
|
-
options
|
74
|
-
|
75
|
-
if options.binding.array?
|
76
|
-
index = args.first
|
77
|
-
return model[index]
|
100
|
+
def call!(options)
|
101
|
+
if options[:binding].array?
|
102
|
+
return options[:model][options[:index]]
|
78
103
|
else
|
79
|
-
model
|
104
|
+
options[:model]
|
80
105
|
end
|
81
106
|
end
|
82
107
|
end
|
83
|
-
|
108
|
+
|
109
|
+
# This function is added to the deserializer's pipeline.
|
110
|
+
#
|
111
|
+
# When deserializing, the representer will call this function and thereby delegate the
|
112
|
+
# entire population process to the form. The form's :internal_populator will run its
|
113
|
+
# :populator option function and return the new/existing form instance.
|
114
|
+
# The deserializing representer will then continue on that returned form.
|
115
|
+
#
|
116
|
+
# Goal of this indirection is to leave all population logic in the form, while the
|
117
|
+
# representer really just traverses an incoming document and dispatches business logic
|
118
|
+
# (which population is) to the form.
|
119
|
+
class External
|
120
|
+
def call(input, options)
|
121
|
+
options[:represented].class.definitions.
|
122
|
+
get(options[:binding][:name])[:internal_populator].(input, options)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
data/lib/reform/form/validate.rb
CHANGED
@@ -4,9 +4,10 @@ module Reform::Form::Validate
|
|
4
4
|
class AllBlank
|
5
5
|
include Uber::Callable
|
6
6
|
|
7
|
-
def call(form,
|
7
|
+
def call(form, options)
|
8
|
+
params = options[:input]
|
8
9
|
# TODO: Schema should provide property names as plain list.
|
9
|
-
properties = options
|
10
|
+
properties = options[:binding][:nested].definitions.collect { |dfn| dfn[:name] }
|
10
11
|
|
11
12
|
properties.each { |name| params[name].present? and return false }
|
12
13
|
true # skip
|
@@ -15,20 +16,19 @@ module Reform::Form::Validate
|
|
15
16
|
end
|
16
17
|
|
17
18
|
|
18
|
-
# 1. Populate the form object graph so that each incoming object has a representative form object.
|
19
|
-
# 2. Deserialize. This is wrong and should be done in 1.
|
20
|
-
# 3. Validate the form object graph.
|
21
19
|
def validate(params)
|
22
|
-
deprecate_update!(params)
|
23
|
-
|
24
20
|
# allow an external deserializer.
|
25
21
|
block_given? ? yield(params) : deserialize(params)
|
26
22
|
|
27
23
|
super() # run the actual validation on self.
|
28
|
-
# rescue Representable::DeserializeError
|
29
|
-
# raise DeserializeError.new("[Reform] Deserialize error: You probably called #validate without setting up your nested models. Check https://github.com/apotonick/reform#populating-forms-for-validation on how to use populators.")
|
30
24
|
end
|
31
25
|
|
26
|
+
def deserialize(params)
|
27
|
+
params = deserialize!(params)
|
28
|
+
deserializer.new(self).from_hash(params)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
32
|
# Meant to return params processable by the representer. This is the hook for munching date fields, etc.
|
33
33
|
def deserialize!(params)
|
34
34
|
# NOTE: it is completely up to the form user how they want to deserialize (e.g. using an external JSON-API representer).
|
@@ -38,42 +38,18 @@ module Reform::Form::Validate
|
|
38
38
|
params
|
39
39
|
end
|
40
40
|
|
41
|
-
|
42
|
-
private
|
43
|
-
# Some users use this method to pre-populate a form. Not saying this is right, but we'll keep
|
44
|
-
# this method here.
|
45
|
-
# DISCUSS: this is only called once, on the top-level form.
|
46
|
-
def deprecate_update!(params)
|
47
|
-
return unless self.class.instance_methods(false).include?(:update!)
|
48
|
-
warn "[Reform] Form#update! is deprecated and will be removed in Reform 2.1. Please use #prepopulate!"
|
49
|
-
update!(params)
|
50
|
-
end
|
51
|
-
|
52
|
-
def deserialize(params)
|
53
|
-
params = deserialize!(params)
|
54
|
-
|
55
|
-
deserializer.new(self).from_hash(params)
|
56
|
-
end
|
57
|
-
|
58
41
|
# Default deserializer for hash.
|
59
42
|
# This is input-specific, e.g. Hash, JSON, or XML.
|
60
|
-
def deserializer # called on top-level, only, for now.
|
61
|
-
deserializer = Disposable::
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
deserialize: lambda { |decorator, params, options|
|
71
|
-
params = decorator.represented.deserialize!(params) # let them set up params. # FIXME: we could also get a new deserializer here.
|
72
|
-
|
73
|
-
decorator.from_hash(params) # options.binding.deserialize_method.inspect
|
74
|
-
}
|
75
|
-
) if dfn[:twin]
|
76
|
-
end
|
43
|
+
def deserializer(source=self.class, options={}) # called on top-level, only, for now.
|
44
|
+
deserializer = Disposable::Rescheme.from(source,
|
45
|
+
{
|
46
|
+
include: [Representable::Hash::AllowSymbols, Representable::Hash],
|
47
|
+
superclass: Representable::Decorator,
|
48
|
+
definitions_from: lambda { |inline| inline.definitions },
|
49
|
+
options_from: :deserializer,
|
50
|
+
exclude_options: [:default, :populator] # Reform must not copy Disposable/Reform-only options that might confuse representable.
|
51
|
+
}.merge(options)
|
52
|
+
)
|
77
53
|
|
78
54
|
deserializer
|
79
55
|
end
|