nxt_schema 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +3 -3
- data/README.md +80 -38
- data/lib/nxt_schema.rb +17 -17
- data/lib/nxt_schema/dsl.rb +7 -7
- data/lib/nxt_schema/{application.rb → node.rb} +1 -1
- data/lib/nxt_schema/node/any_of.rb +21 -33
- data/lib/nxt_schema/node/base.rb +70 -171
- data/lib/nxt_schema/node/collection.rb +43 -8
- data/lib/nxt_schema/{application → node}/error_store.rb +5 -5
- data/lib/nxt_schema/{application → node}/errors/schema_error.rb +1 -1
- data/lib/nxt_schema/{application → node}/errors/validation_error.rb +1 -1
- data/lib/nxt_schema/node/leaf.rb +7 -5
- data/lib/nxt_schema/node/schema.rb +101 -8
- data/lib/nxt_schema/template/any_of.rb +50 -0
- data/lib/nxt_schema/template/base.rb +218 -0
- data/lib/nxt_schema/template/collection.rb +23 -0
- data/lib/nxt_schema/{node → template}/has_sub_nodes.rb +16 -10
- data/lib/nxt_schema/template/leaf.rb +13 -0
- data/lib/nxt_schema/{node → template}/maybe_evaluator.rb +1 -1
- data/lib/nxt_schema/{node → template}/on_evaluator.rb +1 -1
- data/lib/nxt_schema/template/schema.rb +22 -0
- data/lib/nxt_schema/{node → template}/sub_nodes.rb +1 -1
- data/lib/nxt_schema/{node → template}/type_resolver.rb +2 -2
- data/lib/nxt_schema/{node → template}/type_system_resolver.rb +1 -1
- data/lib/nxt_schema/version.rb +1 -1
- metadata +17 -17
- data/lib/nxt_schema/application/any_of.rb +0 -40
- data/lib/nxt_schema/application/base.rb +0 -116
- data/lib/nxt_schema/application/collection.rb +0 -57
- data/lib/nxt_schema/application/leaf.rb +0 -15
- data/lib/nxt_schema/application/schema.rb +0 -114
data/lib/nxt_schema/node/base.rb
CHANGED
@@ -1,217 +1,116 @@
|
|
1
1
|
module NxtSchema
|
2
2
|
module Node
|
3
3
|
class Base
|
4
|
-
def initialize(
|
5
|
-
|
6
|
-
|
7
|
-
@
|
8
|
-
@
|
9
|
-
@
|
10
|
-
@
|
11
|
-
@
|
12
|
-
@
|
13
|
-
@
|
14
|
-
@
|
15
|
-
@
|
16
|
-
|
17
|
-
resolve_key_transformer
|
18
|
-
resolve_context
|
19
|
-
resolve_optional_option
|
20
|
-
resolve_omnipresent_option
|
21
|
-
resolve_type_system
|
22
|
-
resolve_type(type)
|
23
|
-
resolve_additional_keys_strategy
|
24
|
-
application_class # memoize
|
25
|
-
configure(&block) if block_given?
|
26
|
-
end
|
4
|
+
def initialize(node:, input: MissingInput.new, parent:, context:, error_key:)
|
5
|
+
@node = node
|
6
|
+
@input = input
|
7
|
+
@parent = parent
|
8
|
+
@output = nil
|
9
|
+
@error_key = error_key
|
10
|
+
@context = context || parent&.context
|
11
|
+
@coerced = false
|
12
|
+
@coerced_nodes = parent&.coerced_nodes || []
|
13
|
+
@is_root = parent.nil?
|
14
|
+
@root = parent.nil? ? self : parent.root
|
15
|
+
@errors = ErrorStore.new(self)
|
16
|
+
@locale = node.options.fetch(:locale) { parent&.locale || 'en' }.to_s
|
27
17
|
|
28
|
-
|
29
|
-
|
30
|
-
:options,
|
31
|
-
:type,
|
32
|
-
:root_node,
|
33
|
-
:additional_keys_strategy
|
34
|
-
|
35
|
-
attr_reader :type_system,
|
36
|
-
:path,
|
37
|
-
:context,
|
38
|
-
:meta,
|
39
|
-
:on_evaluators,
|
40
|
-
:maybe_evaluators,
|
41
|
-
:validations,
|
42
|
-
:configuration,
|
43
|
-
:key_transformer
|
44
|
-
|
45
|
-
def apply(input: MissingInput.new, context: self.context, parent: nil, error_key: nil)
|
46
|
-
build_application(input: input, context: context, parent: parent, error_key: error_key).call
|
18
|
+
@index = error_key
|
19
|
+
resolve_error_key(error_key)
|
47
20
|
end
|
48
21
|
|
49
|
-
|
50
|
-
|
51
|
-
return result if parent || result.errors.empty?
|
22
|
+
attr_accessor :output, :node, :input
|
23
|
+
attr_reader :parent, :context, :error_key, :coerced, :coerced_nodes, :root, :errors, :locale, :index
|
52
24
|
|
53
|
-
|
25
|
+
def call
|
26
|
+
raise NotImplementedError, 'Implement this in our sub class'
|
54
27
|
end
|
55
28
|
|
56
|
-
|
57
|
-
application_class.new(
|
58
|
-
node: self,
|
59
|
-
input: input,
|
60
|
-
parent: parent,
|
61
|
-
context: context,
|
62
|
-
error_key: error_key
|
63
|
-
)
|
64
|
-
end
|
29
|
+
delegate :name, :options, to: :node
|
65
30
|
|
66
|
-
def
|
67
|
-
@
|
31
|
+
def root?
|
32
|
+
@is_root
|
68
33
|
end
|
69
34
|
|
70
|
-
def
|
71
|
-
|
35
|
+
def valid?
|
36
|
+
errors.empty?
|
72
37
|
end
|
73
38
|
|
74
|
-
def
|
75
|
-
|
39
|
+
def add_error(error)
|
40
|
+
errors.add_validation_error(message: error)
|
76
41
|
end
|
77
42
|
|
78
|
-
def
|
79
|
-
|
80
|
-
condition = ->(input) { missing_input?(input) || input.nil? }
|
81
|
-
on(condition, value)
|
82
|
-
|
83
|
-
self
|
43
|
+
def add_schema_error(error)
|
44
|
+
errors.add_schema_error(message: error)
|
84
45
|
end
|
85
46
|
|
86
|
-
def
|
87
|
-
|
88
|
-
on_evaluators << OnEvaluator.new(condition: condition, value: value)
|
89
|
-
|
90
|
-
self
|
47
|
+
def merge_errors(application)
|
48
|
+
errors.merge_errors(application)
|
91
49
|
end
|
92
50
|
|
93
|
-
def
|
94
|
-
|
95
|
-
maybe_evaluators << MaybeEvaluator.new(value: value)
|
51
|
+
def run_validations
|
52
|
+
return false unless coerced?
|
96
53
|
|
97
|
-
|
54
|
+
node.validations.each do |validation|
|
55
|
+
args = [self, input]
|
56
|
+
validation.call(*args.take(validation.arity))
|
57
|
+
end
|
98
58
|
end
|
99
59
|
|
100
|
-
def
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
validator(key, *args)
|
105
|
-
elsif key.respond_to?(:call)
|
106
|
-
key
|
107
|
-
elsif block_given?
|
108
|
-
if key.is_a?(NxtSchema::MissingInput)
|
109
|
-
block
|
110
|
-
else
|
111
|
-
configure(&block)
|
112
|
-
end
|
113
|
-
else
|
114
|
-
raise ArgumentError, "Don't know how to resolve validator from: #{key} with: #{args} #{block}"
|
115
|
-
end
|
116
|
-
|
117
|
-
register_validator(validator)
|
118
|
-
|
119
|
-
self
|
120
|
-
end
|
60
|
+
def up(levels = 1)
|
61
|
+
0.upto(levels - 1).inject(self) do |acc, _|
|
62
|
+
parent = acc.send(:parent)
|
63
|
+
break acc unless parent
|
121
64
|
|
122
|
-
|
123
|
-
|
124
|
-
register_validator(proxy)
|
65
|
+
parent
|
66
|
+
end
|
125
67
|
end
|
126
68
|
|
127
69
|
private
|
128
70
|
|
129
|
-
attr_writer :
|
71
|
+
attr_writer :coerced, :root
|
130
72
|
|
131
|
-
def
|
132
|
-
|
133
|
-
|
73
|
+
def coerce_input
|
74
|
+
output = input.is_a?(MissingInput) && node.omnipresent? ? input : node.type[input]
|
75
|
+
self.output = output
|
134
76
|
|
135
|
-
|
136
|
-
|
77
|
+
rescue Dry::Types::CoercionError => error
|
78
|
+
add_schema_error(error.message)
|
137
79
|
end
|
138
80
|
|
139
|
-
def
|
140
|
-
|
81
|
+
def apply_on_evaluators
|
82
|
+
node.on_evaluators.each { |evaluator| evaluator.call(input, self, context) { |result| self.input = result } }
|
141
83
|
end
|
142
84
|
|
143
|
-
def
|
144
|
-
@
|
145
|
-
|
85
|
+
def maybe_evaluator_applies?
|
86
|
+
@maybe_evaluator_applies ||= node.maybe_evaluators.inject(false) do |acc, evaluator|
|
87
|
+
result = (acc || evaluator.call(input, self, context))
|
146
88
|
|
147
|
-
|
148
|
-
|
149
|
-
|
89
|
+
if result
|
90
|
+
self.output = input
|
91
|
+
break true
|
92
|
+
else
|
93
|
+
false
|
94
|
+
end
|
150
95
|
end
|
151
96
|
end
|
152
97
|
|
153
|
-
def
|
154
|
-
|
155
|
-
end
|
98
|
+
def register_as_coerced_when_no_errors
|
99
|
+
return unless valid?
|
156
100
|
|
157
|
-
|
158
|
-
|
159
|
-
block.call(self)
|
160
|
-
else
|
161
|
-
instance_exec(&block)
|
162
|
-
end
|
101
|
+
self.coerced = true
|
102
|
+
coerced_nodes << self
|
163
103
|
end
|
164
104
|
|
165
|
-
def
|
166
|
-
|
167
|
-
|
168
|
-
|
105
|
+
def resolve_error_key(key)
|
106
|
+
parts = [parent&.error_key].compact
|
107
|
+
parts << (key.present? ? "#{node.name}[#{key}]" : node.name)
|
108
|
+
@error_key = parts.join('.')
|
169
109
|
end
|
170
110
|
|
171
|
-
def
|
172
|
-
|
173
|
-
|
174
|
-
raise Errors::InvalidOptions, "Can't make omnipresent node optional" if optional && omnipresent?
|
175
|
-
|
176
|
-
if optional.respond_to?(:call)
|
177
|
-
# When a node is conditionally optional we make it optional and add a validator to the parent to check
|
178
|
-
# that it's there when the option does not apply.
|
179
|
-
optional_node_validator = validator(:optional_node, optional, name)
|
180
|
-
parent_node.send(:register_validator, optional_node_validator)
|
181
|
-
@optional = true
|
182
|
-
else
|
183
|
-
@optional = optional
|
184
|
-
end
|
185
|
-
end
|
186
|
-
|
187
|
-
def resolve_omnipresent_option
|
188
|
-
omnipresent = options.fetch(:omnipresent, false)
|
189
|
-
raise Errors::InvalidOptions, 'Omnipresent nodes are only available within schemas' if omnipresent && !parent_node.is_a?(Schema)
|
190
|
-
raise Errors::InvalidOptions, "Can't make omnipresent node optional" if optional? && omnipresent
|
191
|
-
|
192
|
-
@omnipresent = omnipresent
|
193
|
-
end
|
194
|
-
|
195
|
-
def resolve_path
|
196
|
-
self.path = root_node? ? name : "#{parent_node.path}.#{name}"
|
197
|
-
end
|
198
|
-
|
199
|
-
def resolve_context
|
200
|
-
self.context = options.fetch(:context) { parent_node&.send(:context) }
|
201
|
-
end
|
202
|
-
|
203
|
-
def missing_input?(value)
|
204
|
-
value.is_a? MissingInput
|
205
|
-
end
|
206
|
-
|
207
|
-
def resolve_key_transformer
|
208
|
-
@key_transformer = options.fetch(:transform_keys) { parent_node&.key_transformer || ->(key) { key.to_sym } }
|
209
|
-
end
|
210
|
-
|
211
|
-
def resolve_name(name)
|
212
|
-
raise ArgumentError, 'Name can either be a symbol or an integer' unless name.class.in?([Symbol, Integer])
|
213
|
-
|
214
|
-
@name = name
|
111
|
+
def coerced?(&block)
|
112
|
+
block.call(self) if @coerced && block_given?
|
113
|
+
@coerced
|
215
114
|
end
|
216
115
|
end
|
217
116
|
end
|
@@ -1,21 +1,56 @@
|
|
1
1
|
module NxtSchema
|
2
2
|
module Node
|
3
3
|
class Collection < Node::Base
|
4
|
-
|
4
|
+
def call
|
5
|
+
apply_on_evaluators
|
6
|
+
child_applications # build applications here so we can access them even when invalid
|
7
|
+
return self if maybe_evaluator_applies?
|
5
8
|
|
6
|
-
|
9
|
+
coerce_input
|
10
|
+
validate_filled
|
11
|
+
return self unless valid?
|
7
12
|
|
8
|
-
|
9
|
-
|
13
|
+
child_applications.each_with_index do |item, index|
|
14
|
+
current_application = item.call
|
15
|
+
|
16
|
+
if !current_application.valid?
|
17
|
+
merge_errors(current_application)
|
18
|
+
else
|
19
|
+
output[index] = current_application.output
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
register_as_coerced_when_no_errors
|
24
|
+
run_validations
|
25
|
+
|
26
|
+
self
|
10
27
|
end
|
11
28
|
|
29
|
+
delegate :[], to: :child_applications
|
30
|
+
|
12
31
|
private
|
13
32
|
|
14
|
-
def
|
15
|
-
|
16
|
-
|
33
|
+
def validate_filled
|
34
|
+
add_schema_error('is not allowed to be empty') if input.blank? && !maybe_evaluator_applies?
|
35
|
+
end
|
36
|
+
|
37
|
+
def child_applications
|
38
|
+
@child_applications ||= begin
|
39
|
+
return [] unless input.respond_to?(:each_with_index)
|
40
|
+
|
41
|
+
input.each_with_index.map do |item, index|
|
42
|
+
build_child_application(item, index)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
def build_child_application(item, error_key)
|
49
|
+
sub_node.build_application(input: item, context: context, parent: self, error_key: error_key)
|
50
|
+
end
|
17
51
|
|
18
|
-
|
52
|
+
def sub_node
|
53
|
+
@sub_node ||= node.sub_nodes.values.first
|
19
54
|
end
|
20
55
|
end
|
21
56
|
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module NxtSchema
|
2
|
-
module
|
2
|
+
module Node
|
3
3
|
class ErrorStore < ::Hash
|
4
4
|
def initialize(application)
|
5
5
|
super()
|
@@ -11,7 +11,7 @@ module NxtSchema
|
|
11
11
|
def add_schema_error(message:)
|
12
12
|
add_error(
|
13
13
|
application,
|
14
|
-
NxtSchema::
|
14
|
+
NxtSchema::Node::Errors::SchemaError.new(
|
15
15
|
application: application,
|
16
16
|
message: message
|
17
17
|
)
|
@@ -21,7 +21,7 @@ module NxtSchema
|
|
21
21
|
def add_validation_error(message:)
|
22
22
|
add_error(
|
23
23
|
application,
|
24
|
-
NxtSchema::
|
24
|
+
NxtSchema::Node::Errors::ValidationError.new(
|
25
25
|
application: application,
|
26
26
|
message: message
|
27
27
|
)
|
@@ -39,7 +39,7 @@ module NxtSchema
|
|
39
39
|
|
40
40
|
# def schema_errors
|
41
41
|
# inject({}) do |acc, (k, v)|
|
42
|
-
# errors = v.select { |e| e.is_a?(NxtSchema::
|
42
|
+
# errors = v.select { |e| e.is_a?(NxtSchema::Node::Errors::SchemaError) }
|
43
43
|
# acc[k] = errors if errors.any?
|
44
44
|
# acc
|
45
45
|
# end
|
@@ -47,7 +47,7 @@ module NxtSchema
|
|
47
47
|
#
|
48
48
|
# def validation_errors
|
49
49
|
# inject({}) do |acc, (k, v)|
|
50
|
-
# errors = v.select { |e| e.is_a?(NxtSchema::
|
50
|
+
# errors = v.select { |e| e.is_a?(NxtSchema::Node::Errors::ValidationError) }
|
51
51
|
# acc[k] = errors if errors.any?
|
52
52
|
# acc
|
53
53
|
# end
|
data/lib/nxt_schema/node/leaf.rb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
module NxtSchema
|
2
2
|
module Node
|
3
3
|
class Leaf < Node::Base
|
4
|
-
def
|
5
|
-
|
6
|
-
|
4
|
+
def call
|
5
|
+
apply_on_evaluators
|
6
|
+
return self if maybe_evaluator_applies?
|
7
7
|
|
8
|
-
|
9
|
-
|
8
|
+
coerce_input
|
9
|
+
register_as_coerced_when_no_errors
|
10
|
+
run_validations
|
11
|
+
self
|
10
12
|
end
|
11
13
|
end
|
12
14
|
end
|
@@ -1,20 +1,113 @@
|
|
1
1
|
module NxtSchema
|
2
2
|
module Node
|
3
3
|
class Schema < Node::Base
|
4
|
-
|
4
|
+
def call
|
5
|
+
apply_on_evaluators
|
6
|
+
child_applications # build applications here so we can access them even when invalid
|
7
|
+
return self if maybe_evaluator_applies?
|
5
8
|
|
6
|
-
|
9
|
+
coerce_input
|
10
|
+
return self unless valid?
|
7
11
|
|
8
|
-
|
9
|
-
|
12
|
+
flag_missing_keys
|
13
|
+
apply_additional_keys_strategy
|
14
|
+
|
15
|
+
child_applications.each do |key, child|
|
16
|
+
current_application = child.call
|
17
|
+
|
18
|
+
if !current_application.valid?
|
19
|
+
merge_errors(current_application)
|
20
|
+
else
|
21
|
+
output[key] = current_application.output
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
transform_keys
|
26
|
+
register_as_coerced_when_no_errors
|
27
|
+
run_validations
|
28
|
+
self
|
10
29
|
end
|
11
30
|
|
12
|
-
|
13
|
-
|
31
|
+
delegate :[], to: :child_applications
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def transform_keys
|
36
|
+
transformer = node.key_transformer
|
37
|
+
return unless transformer && output.respond_to?(:transform_keys!)
|
38
|
+
|
39
|
+
output.transform_keys!(&transformer)
|
40
|
+
end
|
41
|
+
|
42
|
+
def keys
|
43
|
+
@keys ||= node.sub_nodes.reject { |key, _| optional_and_not_given_key?(key) }.keys
|
44
|
+
end
|
45
|
+
|
46
|
+
def additional_keys
|
47
|
+
@additional_keys ||= input.keys - keys
|
48
|
+
end
|
49
|
+
|
50
|
+
def optional_and_not_given_key?(key)
|
51
|
+
node.sub_nodes[key].optional? && !input.key?(key)
|
52
|
+
end
|
53
|
+
|
54
|
+
def additional_keys?
|
55
|
+
additional_keys.any?
|
56
|
+
end
|
57
|
+
|
58
|
+
def missing_keys
|
59
|
+
@missing_keys ||= node.sub_nodes.reject { |_, node| node.omnipresent? || node.optional? }.keys - input.keys
|
60
|
+
end
|
61
|
+
|
62
|
+
def apply_additional_keys_strategy
|
63
|
+
return if allow_additional_keys?
|
64
|
+
return unless additional_keys?
|
65
|
+
|
66
|
+
if restrict_additional_keys?
|
67
|
+
add_schema_error("Additional keys are not allowed: #{additional_keys}")
|
68
|
+
elsif reject_additional_keys?
|
69
|
+
self.output = output.except(*additional_keys)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def flag_missing_keys
|
74
|
+
return if missing_keys.empty?
|
75
|
+
|
76
|
+
add_schema_error("The following keys are missing: #{missing_keys}")
|
77
|
+
end
|
78
|
+
|
79
|
+
def allow_additional_keys?
|
80
|
+
node.additional_keys_strategy == :allow
|
81
|
+
end
|
82
|
+
|
83
|
+
def reject_additional_keys?
|
84
|
+
node.additional_keys_strategy == :reject
|
85
|
+
end
|
86
|
+
|
87
|
+
def restrict_additional_keys?
|
88
|
+
node.additional_keys_strategy == :restrict
|
89
|
+
end
|
90
|
+
|
91
|
+
def child_applications
|
92
|
+
@child_applications ||= begin
|
93
|
+
keys.inject({}) do |acc, key|
|
94
|
+
child_application = build_child_application(key)
|
95
|
+
acc[key] = child_application if child_application.present?
|
96
|
+
acc
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def build_child_application(key)
|
102
|
+
sub_node = node.sub_nodes[key]
|
103
|
+
return unless sub_node.present?
|
104
|
+
|
105
|
+
value = input_has_key?(input, key) ? input[key] : MissingInput.new
|
106
|
+
sub_node.build_application(input: value, context: context, parent: self)
|
14
107
|
end
|
15
108
|
|
16
|
-
def
|
17
|
-
|
109
|
+
def input_has_key?(input, key)
|
110
|
+
input.respond_to?(:key?) && input.key?(key)
|
18
111
|
end
|
19
112
|
end
|
20
113
|
end
|