nxt_schema 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/.gitignore +13 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +86 -0
- data/LICENSE.txt +21 -0
- data/README.md +376 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/nxt_schema/callable.rb +74 -0
- data/lib/nxt_schema/callable_or_value.rb +72 -0
- data/lib/nxt_schema/dsl.rb +38 -0
- data/lib/nxt_schema/error_messages/en.yaml +15 -0
- data/lib/nxt_schema/error_messages.rb +40 -0
- data/lib/nxt_schema/errors/error.rb +7 -0
- data/lib/nxt_schema/errors/invalid_options_error.rb +5 -0
- data/lib/nxt_schema/errors/schema_not_applied_error.rb +5 -0
- data/lib/nxt_schema/errors.rb +4 -0
- data/lib/nxt_schema/node/base.rb +318 -0
- data/lib/nxt_schema/node/collection.rb +73 -0
- data/lib/nxt_schema/node/constructor.rb +9 -0
- data/lib/nxt_schema/node/default_value_evaluator.rb +20 -0
- data/lib/nxt_schema/node/error.rb +13 -0
- data/lib/nxt_schema/node/has_subnodes.rb +97 -0
- data/lib/nxt_schema/node/leaf.rb +43 -0
- data/lib/nxt_schema/node/maybe_evaluator.rb +23 -0
- data/lib/nxt_schema/node/schema.rb +147 -0
- data/lib/nxt_schema/node/template_store.rb +15 -0
- data/lib/nxt_schema/node/type_resolver.rb +24 -0
- data/lib/nxt_schema/node/validate_with_proxy.rb +41 -0
- data/lib/nxt_schema/node.rb +5 -0
- data/lib/nxt_schema/registry.rb +85 -0
- data/lib/nxt_schema/types.rb +10 -0
- data/lib/nxt_schema/undefined.rb +7 -0
- data/lib/nxt_schema/validators/attribute.rb +34 -0
- data/lib/nxt_schema/validators/equality.rb +33 -0
- data/lib/nxt_schema/validators/excluded.rb +23 -0
- data/lib/nxt_schema/validators/excludes.rb +23 -0
- data/lib/nxt_schema/validators/greater_than.rb +23 -0
- data/lib/nxt_schema/validators/greater_than_or_equal.rb +23 -0
- data/lib/nxt_schema/validators/included.rb +23 -0
- data/lib/nxt_schema/validators/includes.rb +23 -0
- data/lib/nxt_schema/validators/less_than.rb +23 -0
- data/lib/nxt_schema/validators/less_than_or_equal.rb +23 -0
- data/lib/nxt_schema/validators/optional_node.rb +26 -0
- data/lib/nxt_schema/validators/pattern.rb +24 -0
- data/lib/nxt_schema/validators/query.rb +28 -0
- data/lib/nxt_schema/validators/registry.rb +11 -0
- data/lib/nxt_schema/validators/validator.rb +17 -0
- data/lib/nxt_schema/version.rb +3 -0
- data/lib/nxt_schema.rb +69 -0
- data/nxt_schema.gemspec +46 -0
- metadata +211 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
module NxtSchema
|
2
|
+
def schema(name = :root, **options, &block)
|
3
|
+
Node::Schema.new(name: name, parent_node: nil, **options, &block)
|
4
|
+
end
|
5
|
+
|
6
|
+
def collection(name = :roots, **options, &block)
|
7
|
+
Node::Collection.new(name: name, parent_node: nil, **options, &block)
|
8
|
+
end
|
9
|
+
|
10
|
+
def params(name = :root, **options, &block)
|
11
|
+
Node::Schema.new(
|
12
|
+
name: name,
|
13
|
+
parent_node: nil,
|
14
|
+
**options.merge(
|
15
|
+
type_system: NxtSchema::Types::Params,
|
16
|
+
).reverse_merge(transform_keys: :to_sym),
|
17
|
+
&block
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def json(name = :root, **options, &block)
|
22
|
+
Node::Schema.new(
|
23
|
+
name: name,
|
24
|
+
parent_node: nil,
|
25
|
+
**options.merge(
|
26
|
+
type_system: NxtSchema::Types::JSON,
|
27
|
+
).reverse_merge(transform_keys: :to_sym),
|
28
|
+
&block
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
alias_method :node, :schema
|
33
|
+
alias_method :root, :schema
|
34
|
+
alias_method :nodes, :collection
|
35
|
+
alias_method :roots, :collection
|
36
|
+
|
37
|
+
module_function :root, :roots, :node, :nodes, :collection, :schema, :params
|
38
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
en:
|
2
|
+
required_key_missing: "Required key :%{key} is missing in %{target}"
|
3
|
+
additional_keys_detected: "Additional keys %{keys} not allowed in %{target}"
|
4
|
+
attribute: "%{attribute} has invalid %{attribute_name} attribute of %{value}"
|
5
|
+
equality: "%{actual} does not equal %{expected}"
|
6
|
+
excludes: "%{value} cannot contain %{target}"
|
7
|
+
includes: "%{value} must include %{target}"
|
8
|
+
excluded: "%{value} must be excluded in %{target}"
|
9
|
+
included: "%{value} must be included in %{target}"
|
10
|
+
greater_than: "%{value} must be greater than %{threshold}"
|
11
|
+
greater_than_or_equal: "%{value} must be greater than or equal to %{threshold}"
|
12
|
+
less_than: "%{value} must be less than %{threshold}"
|
13
|
+
less_than_or_equal: "%{value} must be less than or equal to %{threshold}"
|
14
|
+
pattern: "%{value} must match pattern %{pattern}"
|
15
|
+
query: "%{value}.%{query} was %{actual} and must be true"
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module NxtSchema
|
2
|
+
class ErrorMessages
|
3
|
+
class << self
|
4
|
+
def values
|
5
|
+
@values ||= {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def values=(value)
|
9
|
+
@values = value
|
10
|
+
end
|
11
|
+
|
12
|
+
def load(paths = files)
|
13
|
+
Array(paths).each do |path|
|
14
|
+
new_values = YAML.load(ERB.new(File.read(path)).result).with_indifferent_access
|
15
|
+
self.values = values.deep_merge!(new_values)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def resolve(locale, key, **options)
|
20
|
+
message = begin
|
21
|
+
values.fetch(locale).fetch(key)
|
22
|
+
rescue KeyError
|
23
|
+
raise "Could not resolve error message for #{locale}->#{key}"
|
24
|
+
end
|
25
|
+
|
26
|
+
message % options
|
27
|
+
end
|
28
|
+
|
29
|
+
def files
|
30
|
+
@files ||= begin
|
31
|
+
files = Dir.entries(File.expand_path('../error_messages/', __FILE__)).map do |filename|
|
32
|
+
File.expand_path("../error_messages/#{filename}", __FILE__)
|
33
|
+
end
|
34
|
+
|
35
|
+
files.select { |f| !File.directory? f }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,318 @@
|
|
1
|
+
module NxtSchema
|
2
|
+
module Node
|
3
|
+
class Base
|
4
|
+
def initialize(name: name_from_index, type:, parent_node:, **options, &block)
|
5
|
+
@name = name
|
6
|
+
@parent_node = parent_node
|
7
|
+
@options = options
|
8
|
+
@type_system = resolve_type_system
|
9
|
+
@additional_keys_strategy = resolve_additional_keys_strategy
|
10
|
+
@type = type
|
11
|
+
@schema_errors_key = options.fetch(:schema_errors_key, :itself)
|
12
|
+
@validations = []
|
13
|
+
@level = parent_node ? parent_node.level + 1 : 0
|
14
|
+
@all_nodes = parent_node ? (parent_node.all_nodes || {}) : {}
|
15
|
+
@is_root = parent_node.nil?
|
16
|
+
@root = parent_node.nil? ? self : parent_node.root
|
17
|
+
@errors = {}
|
18
|
+
@context = nil
|
19
|
+
@applied = false
|
20
|
+
@input = nil
|
21
|
+
@value = NxtSchema::Undefined.new
|
22
|
+
@locale = options.fetch(:locale) { parent_node&.locale || 'en' }
|
23
|
+
|
24
|
+
# Note that it is not possible to use present? on an instance of NxtSchema::Schema since it inherits from Hash
|
25
|
+
evaluate_block(block) if block_given?
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_accessor :name,
|
29
|
+
:parent_node,
|
30
|
+
:options,
|
31
|
+
:type,
|
32
|
+
:schema_errors,
|
33
|
+
:namespace,
|
34
|
+
:errors,
|
35
|
+
:validations,
|
36
|
+
:schema_errors_key,
|
37
|
+
:level,
|
38
|
+
:validation_errors,
|
39
|
+
:all_nodes,
|
40
|
+
:value,
|
41
|
+
:type_system,
|
42
|
+
:root,
|
43
|
+
:context,
|
44
|
+
:applied,
|
45
|
+
:input,
|
46
|
+
:additional_keys_strategy,
|
47
|
+
:locale
|
48
|
+
|
49
|
+
|
50
|
+
alias_method :types, :type_system
|
51
|
+
|
52
|
+
def parent(level = 1)
|
53
|
+
level.times.inject(self) { |acc| acc.parent_node }
|
54
|
+
end
|
55
|
+
|
56
|
+
alias_method :up, :parent
|
57
|
+
|
58
|
+
def default(default_value, &block)
|
59
|
+
options.merge!(default: default_value)
|
60
|
+
evaluate_block(block) if block_given?
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
def meta(value = NxtSchema::Undefined.new)
|
65
|
+
if value.is_a?(NxtSchema::Undefined)
|
66
|
+
@meta
|
67
|
+
else
|
68
|
+
@meta = value
|
69
|
+
self
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def value_or_default_value(value)
|
74
|
+
if !value && options.key?(:default)
|
75
|
+
DefaultValueEvaluator.new(self, options.fetch(:default)).call
|
76
|
+
else
|
77
|
+
value
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def maybe(maybe_value, &block)
|
82
|
+
options.merge!(maybe: maybe_value)
|
83
|
+
evaluate_block(block) if block_given?
|
84
|
+
self
|
85
|
+
end
|
86
|
+
|
87
|
+
def optional(optional_value, &block)
|
88
|
+
raise ArgumentError, 'Optional nodes can only exist within schemas' unless parent.is_a?(NxtSchema::Node::Schema)
|
89
|
+
|
90
|
+
options.merge!(optional: optional_value)
|
91
|
+
evaluate_block(block) if block_given?
|
92
|
+
self
|
93
|
+
end
|
94
|
+
|
95
|
+
def presence(presence_value, &block)
|
96
|
+
raise ArgumentError, 'Present nodes can only exist within schemas' unless parent.is_a?(NxtSchema::Node::Schema)
|
97
|
+
|
98
|
+
options.merge!(presence: presence_value)
|
99
|
+
evaluate_block(block) if block_given?
|
100
|
+
self
|
101
|
+
end
|
102
|
+
|
103
|
+
def presence?
|
104
|
+
@presence ||= begin
|
105
|
+
presence_option = options[:presence]
|
106
|
+
|
107
|
+
options[:presence] = if presence_option.respond_to?(:call)
|
108
|
+
Callable.new(presence_option).call(self, value)
|
109
|
+
else
|
110
|
+
presence_option
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def validate(key, *args, &block)
|
116
|
+
if key.is_a?(Symbol)
|
117
|
+
validator = validator(key, *args)
|
118
|
+
elsif key.respond_to?(:call)
|
119
|
+
validator = key
|
120
|
+
else
|
121
|
+
raise ArgumentError, "Don't know how to resolve validator from: #{key}"
|
122
|
+
end
|
123
|
+
|
124
|
+
add_validators(validator)
|
125
|
+
evaluate_block(block) if block_given?
|
126
|
+
self
|
127
|
+
end
|
128
|
+
|
129
|
+
def add_error(error, index = schema_errors_key)
|
130
|
+
validation_errors[index] ||= []
|
131
|
+
validation_errors[index] << error
|
132
|
+
false
|
133
|
+
end
|
134
|
+
|
135
|
+
def validate_all_nodes
|
136
|
+
sorted_nodes = all_nodes.values.sort do |node, other_node|
|
137
|
+
[node.level, (!node.leaf?).to_s] <=> [other_node.level, (!other_node.leaf?).to_s]
|
138
|
+
end
|
139
|
+
|
140
|
+
# we have to start from the bottom, leafs before others on the same level
|
141
|
+
sorted_nodes.reverse_each(&:apply_validations)
|
142
|
+
end
|
143
|
+
|
144
|
+
def apply_validations
|
145
|
+
# We don't run validations in case there are schema errors
|
146
|
+
# to avoid weird errors
|
147
|
+
# First reject empty schema_errors
|
148
|
+
schema_errors.reject! { |_, v| v.empty? }
|
149
|
+
|
150
|
+
# TODO: Is this correct? - Do not apply validations when maybe criteria applies?
|
151
|
+
unless schema_errors[schema_errors_key]&.any? && !maybe_criteria_applies?(value)
|
152
|
+
build_validations
|
153
|
+
|
154
|
+
validations.each do |validation|
|
155
|
+
args = [self, value]
|
156
|
+
validation.call(*args.take(validation.arity))
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
if self.is_a?(NxtSchema::Node::Collection) && value.respond_to?(:each)
|
161
|
+
value.each_with_index do |item, index|
|
162
|
+
validation_errors[index]&.reject! { |_, v| v.empty? }
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
validation_errors.reject! { |_, v| v.empty? }
|
167
|
+
|
168
|
+
self
|
169
|
+
end
|
170
|
+
|
171
|
+
def build_validations
|
172
|
+
validations_from_options = Array(options.fetch(:validate, []))
|
173
|
+
self.validations = validations_from_options
|
174
|
+
end
|
175
|
+
|
176
|
+
def schema_errors?
|
177
|
+
schema_errors.reject! { |_, v| v.empty? }
|
178
|
+
schema_errors.any?
|
179
|
+
end
|
180
|
+
|
181
|
+
def validation_errors?
|
182
|
+
validation_errors.reject! { |_, v| v.empty? }
|
183
|
+
validation_errors.any?
|
184
|
+
end
|
185
|
+
|
186
|
+
def root?
|
187
|
+
@is_root
|
188
|
+
end
|
189
|
+
|
190
|
+
def leaf?
|
191
|
+
false
|
192
|
+
end
|
193
|
+
|
194
|
+
def valid?
|
195
|
+
raise SchemaNotAppliedError, 'Schema was not applied yet' unless applied?
|
196
|
+
|
197
|
+
validation_errors.empty?
|
198
|
+
end
|
199
|
+
|
200
|
+
def add_validators(validator)
|
201
|
+
options[:validate] ||= []
|
202
|
+
options[:validate] = Array(options.fetch(:validate, []))
|
203
|
+
options[:validate] << validator
|
204
|
+
end
|
205
|
+
|
206
|
+
def validator(key, *args)
|
207
|
+
Validators::Registry::VALIDATORS.resolve(key).new(*args).build
|
208
|
+
end
|
209
|
+
|
210
|
+
def validate_with(&block)
|
211
|
+
add_validators(
|
212
|
+
->(node) { NxtSchema::Node::ValidateWithProxy.new(node).validate(&block) }
|
213
|
+
)
|
214
|
+
end
|
215
|
+
|
216
|
+
private
|
217
|
+
|
218
|
+
def register_node(context)
|
219
|
+
return if all_nodes.key?(object_id)
|
220
|
+
|
221
|
+
self.context = context
|
222
|
+
all_nodes[object_id] = self
|
223
|
+
end
|
224
|
+
|
225
|
+
def applied?
|
226
|
+
@applied
|
227
|
+
end
|
228
|
+
|
229
|
+
def mark_as_applied
|
230
|
+
self.applied = true
|
231
|
+
end
|
232
|
+
|
233
|
+
def add_schema_error(error, index = schema_errors_key)
|
234
|
+
schema_errors[index] ||= []
|
235
|
+
schema_errors[index] << error
|
236
|
+
|
237
|
+
add_error(error, index)
|
238
|
+
end
|
239
|
+
|
240
|
+
def maybe_criteria_applies?(value)
|
241
|
+
@maybe_criteria_applies ||= begin
|
242
|
+
options.key?(:maybe) && MaybeEvaluator.new(self, options.fetch(:maybe), value).call
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def self_without_empty_schema_errors
|
247
|
+
schema_errors.reject! { |_, v| v.empty? }
|
248
|
+
validate_all_nodes if root?
|
249
|
+
self.errors = flat_validation_errors(validation_errors, name)
|
250
|
+
self
|
251
|
+
end
|
252
|
+
|
253
|
+
def flat_validation_errors(errors, namespace, acc = {})
|
254
|
+
errors.each_with_object(acc) do |(key, val), acc|
|
255
|
+
current_namespace = [namespace, key].reject { |namespace| namespace == schema_errors_key }.compact.join('.')
|
256
|
+
|
257
|
+
if val.is_a?(::Hash)
|
258
|
+
flat_validation_errors(val, current_namespace, acc)
|
259
|
+
else
|
260
|
+
acc[current_namespace] ||= []
|
261
|
+
acc[current_namespace] += Array(val)
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def name_from_index
|
267
|
+
if parent_node
|
268
|
+
if parent_node.is_a?(NxtSchema::Node::Collection)
|
269
|
+
size + 1
|
270
|
+
else
|
271
|
+
raise ArgumentError, "Nodes with parent_node: #{parent_node} cannot be anonymous"
|
272
|
+
end
|
273
|
+
else
|
274
|
+
:root
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def evaluate_block(block)
|
279
|
+
if block.arity.zero?
|
280
|
+
instance_exec(&block)
|
281
|
+
else
|
282
|
+
evaluator_args = [self, value]
|
283
|
+
block.call(*evaluator_args.take(block.arity))
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
def resolve_type_system
|
288
|
+
type_system = options.fetch(:type_system) { parent_node&.type_system }
|
289
|
+
|
290
|
+
self.type_system = if type_system.is_a?(Module)
|
291
|
+
type_system
|
292
|
+
elsif type_system.is_a?(Symbol) || type_system.is_a?(String)
|
293
|
+
"NxtSchema::Types::#{type_system.to_s.classify}".constantize
|
294
|
+
else
|
295
|
+
NxtSchema::Types
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
def resolve_additional_keys_strategy
|
300
|
+
options.fetch(:additional_keys) { parent_node&.send(:resolve_additional_keys_strategy) || :ignore }
|
301
|
+
end
|
302
|
+
|
303
|
+
def type_resolver
|
304
|
+
@type_resolver ||= begin
|
305
|
+
if root?
|
306
|
+
TypeResolver.new
|
307
|
+
else
|
308
|
+
raise NoMethodError, 'type_resolver is only available on root node'
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
def coerce_value(value)
|
314
|
+
type[value]
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module NxtSchema
|
2
|
+
module Node
|
3
|
+
class Collection < Node::Base
|
4
|
+
def initialize(name:, type: NxtSchema::Types::Strict::Array, parent_node:, **options, &block)
|
5
|
+
@template_store = TemplateStore.new
|
6
|
+
super
|
7
|
+
end
|
8
|
+
|
9
|
+
def apply(input, parent_node: self.parent_node, context: nil)
|
10
|
+
self.input = input
|
11
|
+
register_node(context)
|
12
|
+
|
13
|
+
self.parent_node = parent_node
|
14
|
+
self.schema_errors = { schema_errors_key => [] }
|
15
|
+
self.validation_errors = { schema_errors_key => [] }
|
16
|
+
self.value_store = []
|
17
|
+
self.value = input
|
18
|
+
|
19
|
+
if maybe_criteria_applies?(value)
|
20
|
+
self.value_store = value
|
21
|
+
else
|
22
|
+
self.value = value_or_default_value(value)
|
23
|
+
|
24
|
+
unless maybe_criteria_applies?(value)
|
25
|
+
self.value = coerce_value(value)
|
26
|
+
|
27
|
+
current_node_store = {}
|
28
|
+
|
29
|
+
value.each_with_index do |item, index|
|
30
|
+
item_schema_errors = schema_errors[index] ||= { schema_errors_key => [] }
|
31
|
+
validation_errors[index] ||= { schema_errors_key => [] }
|
32
|
+
|
33
|
+
template_store.each do |node_name, node|
|
34
|
+
current_node = node.dup
|
35
|
+
current_node_store[node_name] = current_node
|
36
|
+
current_node.apply(item, parent_node: self, context: context)
|
37
|
+
value_store[index] = current_node.value
|
38
|
+
|
39
|
+
# TODO: Extract method here
|
40
|
+
unless current_node.schema_errors?
|
41
|
+
current_node_store.each do |node_name, node|
|
42
|
+
node.schema_errors = { }
|
43
|
+
node.validation_errors = { }
|
44
|
+
item_schema_errors = schema_errors[index][node_name] = node.schema_errors
|
45
|
+
validation_errors[index][node_name] = node.validation_errors
|
46
|
+
end
|
47
|
+
|
48
|
+
break
|
49
|
+
else
|
50
|
+
schema_errors[index][node_name] = current_node.schema_errors
|
51
|
+
validation_errors[index][node_name] = current_node.validation_errors
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
item_schema_errors.reject! { |_, v| v.empty? }
|
56
|
+
end
|
57
|
+
|
58
|
+
# Once we collected all values ensure type by casting again
|
59
|
+
self.value_store = coerce_value(value_store)
|
60
|
+
self.value = value_store
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
self_without_empty_schema_errors
|
65
|
+
rescue Dry::Types::ConstraintError, Dry::Types::CoercionError => error
|
66
|
+
add_schema_error(error.message)
|
67
|
+
self_without_empty_schema_errors
|
68
|
+
ensure
|
69
|
+
mark_as_applied
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module NxtSchema
|
2
|
+
module Node
|
3
|
+
class DefaultValueEvaluator
|
4
|
+
def initialize(node, evaluator_or_value)
|
5
|
+
@node = node
|
6
|
+
@evaluator_or_value = evaluator_or_value
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :node, :evaluator_or_value
|
10
|
+
|
11
|
+
def call
|
12
|
+
if evaluator_or_value.respond_to?(:call)
|
13
|
+
Callable.new(evaluator_or_value).call(node)
|
14
|
+
else
|
15
|
+
evaluator_or_value
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require_relative 'schema'
|
2
|
+
require_relative 'collection'
|
3
|
+
require_relative 'constructor'
|
4
|
+
require_relative 'leaf'
|
5
|
+
|
6
|
+
module NxtSchema
|
7
|
+
module Node
|
8
|
+
module HasSubNodes
|
9
|
+
attr_accessor :template_store, :value_store
|
10
|
+
|
11
|
+
# TODO: Would be cool if we could register custom node types!
|
12
|
+
def node(name, type_or_node, **options, &block)
|
13
|
+
child_node = case type_or_node.to_s.to_sym
|
14
|
+
when :Schema
|
15
|
+
NxtSchema::Node::Schema.new(name: name, type: NxtSchema::Types::Strict::Hash, parent_node: self, **options, &block)
|
16
|
+
when :Collection
|
17
|
+
NxtSchema::Node::Collection.new(name: name, type: NxtSchema::Types::Strict::Array, parent_node: self, **options, &block)
|
18
|
+
when :Struct
|
19
|
+
NxtSchema::Node::Constructor.new(
|
20
|
+
name: name,
|
21
|
+
type: NxtSchema::Types::Constructor(::Struct) { |hash| ::Struct.new(*hash.keys).new(*hash.values) },
|
22
|
+
parent_node: self,
|
23
|
+
**options,
|
24
|
+
&block
|
25
|
+
)
|
26
|
+
when :OpenStruct
|
27
|
+
NxtSchema::Node::Constructor.new(
|
28
|
+
name: name,
|
29
|
+
type: NxtSchema::Types::Constructor(::OpenStruct),
|
30
|
+
parent_node: self,
|
31
|
+
**options,
|
32
|
+
&block
|
33
|
+
)
|
34
|
+
else
|
35
|
+
if type_or_node.is_a?(NxtSchema::Node::Base)
|
36
|
+
node = type_or_node.clone
|
37
|
+
node.options.merge!(options)
|
38
|
+
node.name = name
|
39
|
+
node.parent_node = self
|
40
|
+
node
|
41
|
+
else
|
42
|
+
NxtSchema::Node::Leaf.new(name: name, type: type_or_node, parent_node: self, **options)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# TODO: Should we check if there is a
|
47
|
+
raise KeyError, "Duplicate registration for key: #{name}" if template_store.key?(name)
|
48
|
+
template_store.push(child_node)
|
49
|
+
|
50
|
+
child_node
|
51
|
+
end
|
52
|
+
|
53
|
+
def required(name, type, **options, &block)
|
54
|
+
node(name, type, options, &block)
|
55
|
+
end
|
56
|
+
|
57
|
+
alias_method :requires, :required
|
58
|
+
|
59
|
+
def nodes(name, **options, &block)
|
60
|
+
node(name, :Collection, options, &block)
|
61
|
+
end
|
62
|
+
|
63
|
+
alias_method :array, :nodes
|
64
|
+
|
65
|
+
def schema(name, **options, &block)
|
66
|
+
node(name, :Schema, options, &block)
|
67
|
+
end
|
68
|
+
|
69
|
+
alias_method :hash, :schema
|
70
|
+
|
71
|
+
def struct(name, **options, &block)
|
72
|
+
node(name, NxtSchema::Types::Constructor(::OpenStruct), options, &block)
|
73
|
+
end
|
74
|
+
|
75
|
+
def dup
|
76
|
+
result = super
|
77
|
+
result.template_store = template_store.deep_dup
|
78
|
+
result.options = options.deep_dup
|
79
|
+
result
|
80
|
+
end
|
81
|
+
|
82
|
+
delegate_missing_to :value_store
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def value_violates_emptiness?(value)
|
87
|
+
return true unless value.respond_to?(:empty?)
|
88
|
+
|
89
|
+
value.empty?
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
NxtSchema::Node::Schema.include(::NxtSchema::Node::HasSubNodes)
|
96
|
+
NxtSchema::Node::Collection.include(::NxtSchema::Node::HasSubNodes)
|
97
|
+
NxtSchema::Node::Constructor.include(::NxtSchema::Node::HasSubNodes)
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module NxtSchema
|
2
|
+
module Node
|
3
|
+
class Leaf < Node::Base
|
4
|
+
def initialize(name:, type:, parent_node:, **options, &block)
|
5
|
+
super
|
6
|
+
@type = resolve_type(type)
|
7
|
+
end
|
8
|
+
|
9
|
+
def leaf?
|
10
|
+
true
|
11
|
+
end
|
12
|
+
|
13
|
+
def apply(input, parent_node: self.parent_node, context: nil)
|
14
|
+
self.input = input
|
15
|
+
register_node(context)
|
16
|
+
|
17
|
+
self.parent_node = parent_node
|
18
|
+
self.schema_errors = { schema_errors_key => [] }
|
19
|
+
self.validation_errors = { schema_errors_key => [] }
|
20
|
+
|
21
|
+
if maybe_criteria_applies?(input)
|
22
|
+
self.value = input
|
23
|
+
else
|
24
|
+
self.value = value_or_default_value(input)
|
25
|
+
self.value = coerce_value(value) unless maybe_criteria_applies?(value)
|
26
|
+
end
|
27
|
+
|
28
|
+
self_without_empty_schema_errors
|
29
|
+
rescue Dry::Types::ConstraintError, Dry::Types::CoercionError => error
|
30
|
+
add_schema_error(error.message)
|
31
|
+
self_without_empty_schema_errors
|
32
|
+
ensure
|
33
|
+
mark_as_applied
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def resolve_type(name_or_type)
|
39
|
+
root.send(:type_resolver).resolve(type_system, name_or_type)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|