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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +7 -0
  5. data/Gemfile +6 -0
  6. data/Gemfile.lock +86 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +376 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/lib/nxt_schema/callable.rb +74 -0
  13. data/lib/nxt_schema/callable_or_value.rb +72 -0
  14. data/lib/nxt_schema/dsl.rb +38 -0
  15. data/lib/nxt_schema/error_messages/en.yaml +15 -0
  16. data/lib/nxt_schema/error_messages.rb +40 -0
  17. data/lib/nxt_schema/errors/error.rb +7 -0
  18. data/lib/nxt_schema/errors/invalid_options_error.rb +5 -0
  19. data/lib/nxt_schema/errors/schema_not_applied_error.rb +5 -0
  20. data/lib/nxt_schema/errors.rb +4 -0
  21. data/lib/nxt_schema/node/base.rb +318 -0
  22. data/lib/nxt_schema/node/collection.rb +73 -0
  23. data/lib/nxt_schema/node/constructor.rb +9 -0
  24. data/lib/nxt_schema/node/default_value_evaluator.rb +20 -0
  25. data/lib/nxt_schema/node/error.rb +13 -0
  26. data/lib/nxt_schema/node/has_subnodes.rb +97 -0
  27. data/lib/nxt_schema/node/leaf.rb +43 -0
  28. data/lib/nxt_schema/node/maybe_evaluator.rb +23 -0
  29. data/lib/nxt_schema/node/schema.rb +147 -0
  30. data/lib/nxt_schema/node/template_store.rb +15 -0
  31. data/lib/nxt_schema/node/type_resolver.rb +24 -0
  32. data/lib/nxt_schema/node/validate_with_proxy.rb +41 -0
  33. data/lib/nxt_schema/node.rb +5 -0
  34. data/lib/nxt_schema/registry.rb +85 -0
  35. data/lib/nxt_schema/types.rb +10 -0
  36. data/lib/nxt_schema/undefined.rb +7 -0
  37. data/lib/nxt_schema/validators/attribute.rb +34 -0
  38. data/lib/nxt_schema/validators/equality.rb +33 -0
  39. data/lib/nxt_schema/validators/excluded.rb +23 -0
  40. data/lib/nxt_schema/validators/excludes.rb +23 -0
  41. data/lib/nxt_schema/validators/greater_than.rb +23 -0
  42. data/lib/nxt_schema/validators/greater_than_or_equal.rb +23 -0
  43. data/lib/nxt_schema/validators/included.rb +23 -0
  44. data/lib/nxt_schema/validators/includes.rb +23 -0
  45. data/lib/nxt_schema/validators/less_than.rb +23 -0
  46. data/lib/nxt_schema/validators/less_than_or_equal.rb +23 -0
  47. data/lib/nxt_schema/validators/optional_node.rb +26 -0
  48. data/lib/nxt_schema/validators/pattern.rb +24 -0
  49. data/lib/nxt_schema/validators/query.rb +28 -0
  50. data/lib/nxt_schema/validators/registry.rb +11 -0
  51. data/lib/nxt_schema/validators/validator.rb +17 -0
  52. data/lib/nxt_schema/version.rb +3 -0
  53. data/lib/nxt_schema.rb +69 -0
  54. data/nxt_schema.gemspec +46 -0
  55. 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,7 @@
1
+ module NxtSchema
2
+ module Errors
3
+ class Error < StandardError
4
+
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ module NxtSchema
2
+ module Errors
3
+ InvalidOptionsError = Class.new(Error)
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module NxtSchema
2
+ module Errors
3
+ SchemaNotAppliedError = Class.new(Error)
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ module NxtSchema
2
+ module Errors
3
+ end
4
+ 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,9 @@
1
+ module NxtSchema
2
+ module Node
3
+ class Constructor < Node::Schema
4
+ def initialize(name:, type: NxtSchema::Types::Constructor(::OpenStruct), parent_node:, **options, &block)
5
+ super
6
+ end
7
+ end
8
+ end
9
+ 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,13 @@
1
+ module NxtSchema
2
+ module Node
3
+ class Error
4
+ def initialize(path, value, message)
5
+ @path = path
6
+ @value = value
7
+ @message = message
8
+ end
9
+
10
+ attr_reader :path, :value, :message
11
+ end
12
+ end
13
+ 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