nxt_schema 0.1.2 → 1.0.0

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