nxt_schema 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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