nxt_schema 0.1.2 → 1.0.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/Gemfile +0 -1
  4. data/Gemfile.lock +32 -29
  5. data/README.md +186 -116
  6. data/lib/nxt_schema.rb +56 -49
  7. data/lib/nxt_schema/{node.rb → application.rb} +1 -1
  8. data/lib/nxt_schema/application/any_of.rb +40 -0
  9. data/lib/nxt_schema/application/base.rb +116 -0
  10. data/lib/nxt_schema/application/collection.rb +57 -0
  11. data/lib/nxt_schema/application/error_store.rb +57 -0
  12. data/lib/nxt_schema/application/errors/schema_error.rb +15 -0
  13. data/lib/nxt_schema/application/errors/validation_error.rb +15 -0
  14. data/lib/nxt_schema/application/leaf.rb +15 -0
  15. data/lib/nxt_schema/application/schema.rb +114 -0
  16. data/lib/nxt_schema/callable.rb +21 -55
  17. data/lib/nxt_schema/dsl.rb +41 -31
  18. data/lib/nxt_schema/error.rb +4 -0
  19. data/lib/nxt_schema/errors/invalid.rb +16 -0
  20. data/lib/nxt_schema/errors/{error.rb → invalid_options.rb} +1 -2
  21. data/lib/nxt_schema/missing_input.rb +9 -0
  22. data/lib/nxt_schema/node/any_of.rb +51 -0
  23. data/lib/nxt_schema/node/base.rb +135 -233
  24. data/lib/nxt_schema/node/collection.rb +10 -65
  25. data/lib/nxt_schema/node/has_sub_nodes.rb +81 -0
  26. data/lib/nxt_schema/node/leaf.rb +1 -31
  27. data/lib/nxt_schema/node/maybe_evaluator.rb +15 -10
  28. data/lib/nxt_schema/node/on_evaluator.rb +25 -0
  29. data/lib/nxt_schema/node/schema.rb +8 -134
  30. data/lib/nxt_schema/node/sub_nodes.rb +22 -0
  31. data/lib/nxt_schema/node/type_system_resolver.rb +22 -0
  32. data/lib/nxt_schema/types.rb +1 -1
  33. data/lib/nxt_schema/validators/attribute.rb +3 -3
  34. data/lib/nxt_schema/validators/{equality.rb → equal_to.rb} +5 -5
  35. data/lib/nxt_schema/validators/error_messages.rb +42 -0
  36. data/lib/nxt_schema/{error_messages → validators/error_messages}/en.yaml +3 -3
  37. data/lib/nxt_schema/validators/{excluded.rb → excluded_in.rb} +4 -4
  38. data/lib/nxt_schema/validators/excludes.rb +3 -3
  39. data/lib/nxt_schema/validators/greater_than.rb +3 -3
  40. data/lib/nxt_schema/validators/greater_than_or_equal.rb +3 -3
  41. data/lib/nxt_schema/validators/{included.rb → included_in.rb} +4 -4
  42. data/lib/nxt_schema/validators/includes.rb +3 -3
  43. data/lib/nxt_schema/validators/less_than.rb +3 -3
  44. data/lib/nxt_schema/validators/less_than_or_equal.rb +3 -3
  45. data/lib/nxt_schema/validators/optional_node.rb +13 -8
  46. data/lib/nxt_schema/validators/pattern.rb +3 -3
  47. data/lib/nxt_schema/validators/query.rb +4 -4
  48. data/lib/nxt_schema/validators/registry.rb +1 -7
  49. data/lib/nxt_schema/{node → validators}/validate_with_proxy.rb +8 -8
  50. data/lib/nxt_schema/validators/validator.rb +2 -2
  51. data/lib/nxt_schema/version.rb +1 -1
  52. data/nxt_schema.gemspec +1 -0
  53. metadata +42 -22
  54. data/lib/nxt_schema/callable_or_value.rb +0 -72
  55. data/lib/nxt_schema/error_messages.rb +0 -40
  56. data/lib/nxt_schema/errors.rb +0 -4
  57. data/lib/nxt_schema/errors/invalid_options_error.rb +0 -5
  58. data/lib/nxt_schema/errors/schema_not_applied_error.rb +0 -5
  59. data/lib/nxt_schema/node/constructor.rb +0 -9
  60. data/lib/nxt_schema/node/default_value_evaluator.rb +0 -20
  61. data/lib/nxt_schema/node/error.rb +0 -13
  62. data/lib/nxt_schema/node/has_subnodes.rb +0 -97
  63. data/lib/nxt_schema/node/template_store.rb +0 -15
  64. data/lib/nxt_schema/registry.rb +0 -85
  65. data/lib/nxt_schema/undefined.rb +0 -7
@@ -0,0 +1,114 @@
1
+ module NxtSchema
2
+ module Application
3
+ class Schema < Application::Base
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?
8
+
9
+ coerce_input
10
+ return self unless valid?
11
+
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_applied_when_valid
27
+ run_validations
28
+ self
29
+ end
30
+
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)
107
+ end
108
+
109
+ def input_has_key?(input, key)
110
+ input.respond_to?(:key?) && input.key?(key)
111
+ end
112
+ end
113
+ end
114
+ end
@@ -1,74 +1,40 @@
1
1
  module NxtSchema
2
2
  class Callable
3
- def initialize(callee)
4
- @callee = callee
5
-
6
- if callee.is_a?(Symbol)
7
- self.type = :method
8
- elsif callee.respond_to?(:call)
9
- self.type = :proc
10
- self.context = callee.binding
11
- else
12
- raise ArgumentError, "Callee is nor symbol nor a proc: #{callee}"
13
- end
14
- end
15
-
16
- def bind!(execution_context)
17
- self.context = execution_context
18
- ensure_context_not_missing
19
- self
3
+ def initialize(callable, target = nil, *args)
4
+ @callable = callable
5
+ @target = target
6
+ @args = args
20
7
  end
21
8
 
22
- def bind(execution_context = nil)
23
- return self if context
9
+ def call
10
+ return callable if value?
11
+ return callable.call(*args_from_arity) if proc?
24
12
 
25
- self.context = execution_context
26
- ensure_context_not_missing
27
- self
13
+ target.send(callable, *args_from_arity)
28
14
  end
29
15
 
30
- # NOTE: Currently we only allow arguments! Not keyword args or **options
31
- # If we would allow **options and we would pass a hash as the only argument it would
32
- # automatically be parsed as the options!
33
- def call(*args)
34
- ensure_context_not_missing
35
-
36
- args = args.take(arity)
16
+ def method?
17
+ @method ||= callable.class.in?([Symbol, String]) && target.respond_to?(callable)
18
+ end
37
19
 
38
- if method?
39
- context.send(callee, *args)
40
- else
41
- context.instance_exec(*args, &callee)
42
- end
20
+ def proc?
21
+ @proc ||= callable.respond_to?(:call)
43
22
  end
44
23
 
45
- def arity
46
- if proc?
47
- callee.arity
48
- elsif method?
49
- method = context.send(:method, callee)
50
- method.arity
51
- else
52
- raise ArgumentError, "Can't resolve arity from #{callee}"
53
- end
24
+ def value?
25
+ !method? && !proc?
54
26
  end
55
27
 
56
28
  private
57
29
 
58
- def proc?
59
- type == :proc
60
- end
30
+ attr_reader :callable, :target, :args
61
31
 
62
- def method?
63
- type == :method
32
+ def arity
33
+ proc? ? callable.arity : 0
64
34
  end
65
35
 
66
- def ensure_context_not_missing
67
- return if context
68
-
69
- raise ArgumentError, "Missing context: #{context}"
36
+ def args_from_arity
37
+ @args_from_arity ||= ([target] + args).take(arity)
70
38
  end
71
-
72
- attr_accessor :context, :callee, :type
73
39
  end
74
- end
40
+ end
@@ -1,38 +1,48 @@
1
1
  module NxtSchema
2
- def schema(name = :root, **options, &block)
3
- Node::Schema.new(name: name, parent_node: nil, **options, &block)
4
- end
2
+ module Dsl
3
+ DEFAULT_OPTIONS = { type_system: NxtSchema::Types }.freeze
5
4
 
6
- def collection(name = :roots, **options, &block)
7
- Node::Collection.new(name: name, parent_node: nil, **options, &block)
8
- end
5
+ def collection(name = :root, type: NxtSchema::Node::Collection::DEFAULT_TYPE, **options, &block)
6
+ NxtSchema::Node::Collection.new(
7
+ name: name,
8
+ type: type,
9
+ parent_node: nil,
10
+ **DEFAULT_OPTIONS.merge(options),
11
+ &block
12
+ )
13
+ end
9
14
 
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
15
+ alias nodes collection
20
16
 
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
17
+ def schema(name = :roots, type: NxtSchema::Node::Schema::DEFAULT_TYPE, **options, &block)
18
+ NxtSchema::Node::Schema.new(
19
+ name: name,
20
+ type: type,
21
+ parent_node: nil,
22
+ **DEFAULT_OPTIONS.merge(options),
23
+ &block
24
+ )
25
+ end
26
+
27
+ def any_of(name = :roots, **options, &block)
28
+ NxtSchema::Node::AnyOf.new(
29
+ name: name,
30
+ parent_node: nil,
31
+ **DEFAULT_OPTIONS.merge(options),
32
+ &block
33
+ )
34
+ end
31
35
 
32
- alias_method :node, :schema
33
- alias_method :root, :schema
34
- alias_method :nodes, :collection
35
- alias_method :roots, :collection
36
+ # schema root with NxtSchema::Types::Params type system
36
37
 
37
- module_function :root, :roots, :node, :nodes, :collection, :schema, :params
38
+ def params(name = :params, type: NxtSchema::Node::Schema::DEFAULT_TYPE, **options, &block)
39
+ NxtSchema::Node::Schema.new(
40
+ name: name,
41
+ type: type,
42
+ parent_node: nil,
43
+ **options.merge(type_system: NxtSchema::Types::Params),
44
+ &block
45
+ )
46
+ end
47
+ end
38
48
  end
@@ -0,0 +1,4 @@
1
+ module NxtSchema
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -0,0 +1,16 @@
1
+ module NxtSchema
2
+ module Errors
3
+ class Invalid < NxtSchema::Error
4
+ def initialize(node)
5
+ @node = node
6
+ super(build_message)
7
+ end
8
+
9
+ attr_reader :node
10
+
11
+ def build_message
12
+ node.errors
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,7 +1,6 @@
1
1
  module NxtSchema
2
2
  module Errors
3
- class Error < StandardError
4
-
3
+ class InvalidOptions < NxtSchema::Error
5
4
  end
6
5
  end
7
6
  end
@@ -0,0 +1,9 @@
1
+ module NxtSchema
2
+ class MissingInput
3
+ def inspect
4
+ self.class.name
5
+ end
6
+
7
+ alias to_s inspect
8
+ end
9
+ end
@@ -0,0 +1,51 @@
1
+ module NxtSchema
2
+ module Node
3
+ class AnyOf < Base
4
+ include HasSubNodes
5
+
6
+ def initialize(name:, type: nil, parent_node:, **options, &block)
7
+ super
8
+ end
9
+
10
+ def collection(name = sub_nodes.count, type = NxtSchema::Node::Collection::DEFAULT_TYPE, **options, &block)
11
+ super
12
+ end
13
+
14
+ def schema(name = sub_nodes.count, type = NxtSchema::Node::Schema::DEFAULT_TYPE, **options, &block)
15
+ super
16
+ end
17
+
18
+ def node(name = sub_nodes.count, node_or_type_of_node = nil, **options, &block)
19
+ super
20
+ end
21
+
22
+ # TODO: Maybe overwrite sub node methods to not have to provide a name here and use node count instead
23
+
24
+ def on(*args)
25
+ raise NotImplementedError
26
+ end
27
+
28
+ def maybe(*args)
29
+ raise NotImplementedError
30
+ end
31
+
32
+ private
33
+
34
+ def resolve_type(name_or_type)
35
+ nil
36
+ end
37
+
38
+ def resolve_optional_option
39
+ return unless options.key?(:optional)
40
+
41
+ raise InvalidOptions, "The optional option is not available for nodes of type #{self.class.name}"
42
+ end
43
+
44
+ def resolve_omnipresent_option
45
+ return unless options.key?(:omnipresent)
46
+
47
+ raise InvalidOptions, "The omnipresent option is not available for nodes of type #{self.class.name}"
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,315 +1,217 @@
1
1
  module NxtSchema
2
2
  module Node
3
3
  class Base
4
- def initialize(name: name_from_index, type:, parent_node:, **options, &block)
5
- @name = name
4
+ def initialize(name:, type:, parent_node:, **options, &block)
5
+ resolve_name(name)
6
+
6
7
  @parent_node = parent_node
7
8
  @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)
9
+ @is_root_node = parent_node.nil?
10
+ @root_node = parent_node.nil? ? self : parent_node.root_node
11
+ @path = resolve_path
12
+ @on_evaluators = []
13
+ @maybe_evaluators = []
12
14
  @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 = options.fetch(: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?
15
+ @configuration = block
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
26
  end
27
27
 
28
28
  attr_accessor :name,
29
29
  :parent_node,
30
30
  :options,
31
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
32
+ :root_node,
33
+ :additional_keys_strategy
55
34
 
56
- alias_method :up, :parent
35
+ attr_reader :type_system,
36
+ :path,
37
+ :context,
38
+ :meta,
39
+ :on_evaluators,
40
+ :maybe_evaluators,
41
+ :validations,
42
+ :configuration,
43
+ :key_transformer
57
44
 
58
- def default(default_value, &block)
59
- options.merge!(default: default_value)
60
- evaluate_block(block) if block_given?
61
- self
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
62
47
  end
63
48
 
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
49
+ def apply!(input: MissingInput.new, context: self.context, parent: nil, error_key: nil)
50
+ result = build_application(input: input, context: context, parent: parent, error_key: error_key).call
51
+ return result if parent || result.errors.empty?
72
52
 
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
53
+ raise NxtSchema::Errors::Invalid.new(result)
79
54
  end
80
55
 
81
- def maybe(maybe_value, &block)
82
- options.merge!(maybe: maybe_value)
83
- evaluate_block(block) if block_given?
84
- self
56
+ def build_application(input: MissingInput.new, context: self.context, parent: nil, error_key: nil)
57
+ application_class.new(
58
+ node: self,
59
+ input: input,
60
+ parent: parent,
61
+ context: context,
62
+ error_key: error_key
63
+ )
85
64
  end
86
65
 
87
- def optional(optional_value = nil, &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 || block)
91
- self
66
+ def root_node?
67
+ @is_root_node
92
68
  end
93
69
 
94
- def presence(presence_value = nil, &block)
95
- raise ArgumentError, 'Present nodes can only exist within schemas' unless parent.is_a?(NxtSchema::Node::Schema)
96
-
97
- options.merge!(presence: presence_value || block)
98
- self
70
+ def optional?
71
+ @optional
99
72
  end
100
73
 
101
- def presence?
102
- @presence ||= begin
103
- presence_option = options[:presence]
104
-
105
- options[:presence] = if presence_option.respond_to?(:call)
106
- Callable.new(presence_option).call(self, value)
107
- else
108
- presence_option
109
- end
110
- end
74
+ def omnipresent?
75
+ @omnipresent
111
76
  end
112
77
 
113
- def validate(key, *args, &block)
114
- if key.is_a?(Symbol)
115
- validator = validator(key, *args)
116
- elsif key.respond_to?(:call)
117
- validator = key
118
- else
119
- raise ArgumentError, "Don't know how to resolve validator from: #{key}"
120
- end
78
+ def default(value = NxtSchema::MissingInput.new, &block)
79
+ value = missing_input?(value) ? block : value
80
+ condition = ->(input) { missing_input?(input) || input.nil? }
81
+ on(condition, value)
121
82
 
122
- add_validators(validator)
123
- evaluate_block(block) if block_given?
124
83
  self
125
84
  end
126
85
 
127
- def add_error(error, index = schema_errors_key)
128
- validation_errors[index] ||= []
129
- validation_errors[index] << error
130
- false
131
- end
132
-
133
- def validate_all_nodes
134
- sorted_nodes = all_nodes.values.sort do |node, other_node|
135
- [node.level, (!node.leaf?).to_s] <=> [other_node.level, (!other_node.leaf?).to_s]
136
- end
137
-
138
- # we have to start from the bottom, leafs before others on the same level
139
- sorted_nodes.reverse_each(&:apply_validations)
140
- end
141
-
142
- def apply_validations
143
- # We don't run validations in case there are schema errors
144
- # to avoid weird errors
145
- # First reject empty schema_errors
146
- schema_errors.reject! { |_, v| v.empty? }
147
-
148
- # TODO: Is this correct? - Do not apply validations when maybe criteria applies?
149
- unless schema_errors[schema_errors_key]&.any? && !maybe_criteria_applies?(value)
150
- build_validations
151
-
152
- validations.each do |validation|
153
- args = [self, value]
154
- validation.call(*args.take(validation.arity))
155
- end
156
- end
157
-
158
- if self.is_a?(NxtSchema::Node::Collection) && value.respond_to?(:each)
159
- value.each_with_index do |item, index|
160
- validation_errors[index]&.reject! { |_, v| v.empty? }
161
- end
162
- end
163
-
164
- validation_errors.reject! { |_, v| v.empty? }
86
+ def on(condition, value = NxtSchema::MissingInput.new, &block)
87
+ value = missing_input?(value) ? block : value
88
+ on_evaluators << OnEvaluator.new(condition: condition, value: value)
165
89
 
166
90
  self
167
91
  end
168
92
 
169
- def build_validations
170
- validations_from_options = Array(options.fetch(:validate, []))
171
- self.validations = validations_from_options
172
- end
173
-
174
- def schema_errors?
175
- schema_errors.reject! { |_, v| v.empty? }
176
- schema_errors.any?
177
- end
93
+ def maybe(value = NxtSchema::MissingInput.new, &block)
94
+ value = missing_input?(value) ? block : value
95
+ maybe_evaluators << MaybeEvaluator.new(value: value)
178
96
 
179
- def validation_errors?
180
- validation_errors.reject! { |_, v| v.empty? }
181
- validation_errors.any?
182
- end
183
-
184
- def root?
185
- @is_root
97
+ self
186
98
  end
187
99
 
188
- def leaf?
189
- false
190
- end
100
+ def validate(key = NxtSchema::MissingInput.new, *args, &block)
101
+ # TODO: This does not really work with all kinds of chaining combinations yet!
191
102
 
192
- def valid?
193
- raise SchemaNotAppliedError, 'Schema was not applied yet' unless applied?
103
+ validator = if key.is_a?(Symbol)
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
194
116
 
195
- validation_errors.empty?
196
- end
117
+ register_validator(validator)
197
118
 
198
- def add_validators(validator)
199
- options[:validate] ||= []
200
- options[:validate] = Array(options.fetch(:validate, []))
201
- options[:validate] << validator
202
- end
203
-
204
- def validator(key, *args)
205
- Validators::Registry::VALIDATORS.resolve(key).new(*args).build
119
+ self
206
120
  end
207
121
 
208
122
  def validate_with(&block)
209
- add_validators(
210
- ->(node) { NxtSchema::Node::ValidateWithProxy.new(node).validate(&block) }
211
- )
123
+ proxy = ->(node) { NxtSchema::Validator::ValidateWithProxy.new(node).validate(&block) }
124
+ register_validator(proxy)
212
125
  end
213
126
 
214
127
  private
215
128
 
216
- def register_node(current_context = context)
217
- return if all_nodes.key?(object_id)
129
+ attr_writer :path, :meta, :context, :on_evaluators, :maybe_evaluators
218
130
 
219
- self.context = current_context
220
- all_nodes[object_id] = self
131
+ def validator(key, *args)
132
+ Validators::REGISTRY.resolve!(key).new(*args).build
221
133
  end
222
134
 
223
- def applied?
224
- @applied
135
+ def register_validator(validator)
136
+ validations << validator
225
137
  end
226
138
 
227
- def mark_as_applied
228
- self.applied = true
139
+ def resolve_type(name_or_type)
140
+ @type = root_node.send(:type_resolver).resolve(type_system, name_or_type)
229
141
  end
230
142
 
231
- def add_schema_error(error, index = schema_errors_key)
232
- schema_errors[index] ||= []
233
- schema_errors[index] << error
234
-
235
- add_error(error, index)
143
+ def resolve_type_system
144
+ @type_system = TypeSystemResolver.new(node: self).call
236
145
  end
237
146
 
238
- def maybe_criteria_applies?(value)
239
- @maybe_criteria_applies ||= begin
240
- options.key?(:maybe) && MaybeEvaluator.new(self, options.fetch(:maybe), value).call
147
+ def type_resolver
148
+ @type_resolver ||= begin
149
+ root_node? ? TypeResolver.new : (raise NoMethodError, 'type_resolver is only available on root node')
241
150
  end
242
151
  end
243
152
 
244
- def self_without_empty_schema_errors
245
- schema_errors.reject! { |_, v| v.empty? }
246
- validate_all_nodes if root?
247
- self.errors = flat_validation_errors(validation_errors, name)
248
- self
153
+ def application_class
154
+ @application_class ||= "NxtSchema::Application::#{self.class.name.demodulize}".constantize
249
155
  end
250
156
 
251
- def flat_validation_errors(errors, namespace, acc = {})
252
- errors.each_with_object(acc) do |(key, val), acc|
253
- current_namespace = [namespace, key].reject { |namespace| namespace == schema_errors_key }.compact.join('.')
254
-
255
- if val.is_a?(::Hash)
256
- flat_validation_errors(val, current_namespace, acc)
257
- else
258
- acc[current_namespace] ||= []
259
- acc[current_namespace] += Array(val)
260
- end
157
+ def configure(&block)
158
+ if block.arity == 1
159
+ block.call(self)
160
+ else
161
+ instance_exec(&block)
261
162
  end
262
163
  end
263
164
 
264
- def name_from_index
265
- if parent_node
266
- if parent_node.is_a?(NxtSchema::Node::Collection)
267
- size + 1
268
- else
269
- raise ArgumentError, "Nodes with parent_node: #{parent_node} cannot be anonymous"
270
- end
271
- else
272
- :root
165
+ def resolve_additional_keys_strategy
166
+ @additional_keys_strategy = options.fetch(:additional_keys) do
167
+ parent_node&.send(:additional_keys_strategy) || :allow
273
168
  end
274
169
  end
275
170
 
276
- def evaluate_block(block)
277
- if block.arity.zero?
278
- instance_exec(&block)
171
+ def resolve_optional_option
172
+ optional = options.fetch(:optional, false)
173
+ raise Errors::InvalidOptions, 'Optional nodes are only available within schemas' if optional && !parent_node.is_a?(Schema)
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
279
182
  else
280
- evaluator_args = [self, value]
281
- block.call(*evaluator_args.take(block.arity))
183
+ @optional = optional
282
184
  end
283
185
  end
284
186
 
285
- def resolve_type_system
286
- type_system = options.fetch(:type_system) { parent_node&.type_system }
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
287
191
 
288
- self.type_system = if type_system.is_a?(Module)
289
- type_system
290
- elsif type_system.is_a?(Symbol) || type_system.is_a?(String)
291
- "NxtSchema::Types::#{type_system.to_s.classify}".constantize
292
- else
293
- NxtSchema::Types
294
- end
192
+ @omnipresent = omnipresent
295
193
  end
296
194
 
297
- def resolve_additional_keys_strategy
298
- options.fetch(:additional_keys) { parent_node&.send(:resolve_additional_keys_strategy) || :ignore }
195
+ def resolve_path
196
+ self.path = root_node? ? name : "#{parent_node.path}.#{name}"
299
197
  end
300
198
 
301
- def type_resolver
302
- @type_resolver ||= begin
303
- if root?
304
- TypeResolver.new
305
- else
306
- raise NoMethodError, 'type_resolver is only available on root node'
307
- end
308
- end
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
309
205
  end
310
206
 
311
- def coerce_value(value)
312
- type[value]
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
313
215
  end
314
216
  end
315
217
  end