nxt_schema 1.0.0 → 1.0.1
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/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
|