nxt_schema 1.0.0 → 1.0.1

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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +3 -3
  3. data/README.md +80 -38
  4. data/lib/nxt_schema.rb +17 -17
  5. data/lib/nxt_schema/dsl.rb +7 -7
  6. data/lib/nxt_schema/{application.rb → node.rb} +1 -1
  7. data/lib/nxt_schema/node/any_of.rb +21 -33
  8. data/lib/nxt_schema/node/base.rb +70 -171
  9. data/lib/nxt_schema/node/collection.rb +43 -8
  10. data/lib/nxt_schema/{application → node}/error_store.rb +5 -5
  11. data/lib/nxt_schema/{application → node}/errors/schema_error.rb +1 -1
  12. data/lib/nxt_schema/{application → node}/errors/validation_error.rb +1 -1
  13. data/lib/nxt_schema/node/leaf.rb +7 -5
  14. data/lib/nxt_schema/node/schema.rb +101 -8
  15. data/lib/nxt_schema/template/any_of.rb +50 -0
  16. data/lib/nxt_schema/template/base.rb +218 -0
  17. data/lib/nxt_schema/template/collection.rb +23 -0
  18. data/lib/nxt_schema/{node → template}/has_sub_nodes.rb +16 -10
  19. data/lib/nxt_schema/template/leaf.rb +13 -0
  20. data/lib/nxt_schema/{node → template}/maybe_evaluator.rb +1 -1
  21. data/lib/nxt_schema/{node → template}/on_evaluator.rb +1 -1
  22. data/lib/nxt_schema/template/schema.rb +22 -0
  23. data/lib/nxt_schema/{node → template}/sub_nodes.rb +1 -1
  24. data/lib/nxt_schema/{node → template}/type_resolver.rb +2 -2
  25. data/lib/nxt_schema/{node → template}/type_system_resolver.rb +1 -1
  26. data/lib/nxt_schema/version.rb +1 -1
  27. metadata +17 -17
  28. data/lib/nxt_schema/application/any_of.rb +0 -40
  29. data/lib/nxt_schema/application/base.rb +0 -116
  30. data/lib/nxt_schema/application/collection.rb +0 -57
  31. data/lib/nxt_schema/application/leaf.rb +0 -15
  32. data/lib/nxt_schema/application/schema.rb +0 -114
@@ -1,217 +1,116 @@
1
1
  module NxtSchema
2
2
  module Node
3
3
  class Base
4
- def initialize(name:, type:, parent_node:, **options, &block)
5
- resolve_name(name)
6
-
7
- @parent_node = parent_node
8
- @options = options
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 = []
14
- @validations = []
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
- end
4
+ def initialize(node:, input: MissingInput.new, parent:, context:, error_key:)
5
+ @node = node
6
+ @input = input
7
+ @parent = parent
8
+ @output = nil
9
+ @error_key = error_key
10
+ @context = context || parent&.context
11
+ @coerced = false
12
+ @coerced_nodes = parent&.coerced_nodes || []
13
+ @is_root = parent.nil?
14
+ @root = parent.nil? ? self : parent.root
15
+ @errors = ErrorStore.new(self)
16
+ @locale = node.options.fetch(:locale) { parent&.locale || 'en' }.to_s
27
17
 
28
- attr_accessor :name,
29
- :parent_node,
30
- :options,
31
- :type,
32
- :root_node,
33
- :additional_keys_strategy
34
-
35
- attr_reader :type_system,
36
- :path,
37
- :context,
38
- :meta,
39
- :on_evaluators,
40
- :maybe_evaluators,
41
- :validations,
42
- :configuration,
43
- :key_transformer
44
-
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
18
+ @index = error_key
19
+ resolve_error_key(error_key)
47
20
  end
48
21
 
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?
22
+ attr_accessor :output, :node, :input
23
+ attr_reader :parent, :context, :error_key, :coerced, :coerced_nodes, :root, :errors, :locale, :index
52
24
 
53
- raise NxtSchema::Errors::Invalid.new(result)
25
+ def call
26
+ raise NotImplementedError, 'Implement this in our sub class'
54
27
  end
55
28
 
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
- )
64
- end
29
+ delegate :name, :options, to: :node
65
30
 
66
- def root_node?
67
- @is_root_node
31
+ def root?
32
+ @is_root
68
33
  end
69
34
 
70
- def optional?
71
- @optional
35
+ def valid?
36
+ errors.empty?
72
37
  end
73
38
 
74
- def omnipresent?
75
- @omnipresent
39
+ def add_error(error)
40
+ errors.add_validation_error(message: error)
76
41
  end
77
42
 
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)
82
-
83
- self
43
+ def add_schema_error(error)
44
+ errors.add_schema_error(message: error)
84
45
  end
85
46
 
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)
89
-
90
- self
47
+ def merge_errors(application)
48
+ errors.merge_errors(application)
91
49
  end
92
50
 
93
- def maybe(value = NxtSchema::MissingInput.new, &block)
94
- value = missing_input?(value) ? block : value
95
- maybe_evaluators << MaybeEvaluator.new(value: value)
51
+ def run_validations
52
+ return false unless coerced?
96
53
 
97
- self
54
+ node.validations.each do |validation|
55
+ args = [self, input]
56
+ validation.call(*args.take(validation.arity))
57
+ end
98
58
  end
99
59
 
100
- def validate(key = NxtSchema::MissingInput.new, *args, &block)
101
- # TODO: This does not really work with all kinds of chaining combinations yet!
102
-
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
116
-
117
- register_validator(validator)
118
-
119
- self
120
- end
60
+ def up(levels = 1)
61
+ 0.upto(levels - 1).inject(self) do |acc, _|
62
+ parent = acc.send(:parent)
63
+ break acc unless parent
121
64
 
122
- def validate_with(&block)
123
- proxy = ->(node) { NxtSchema::Validator::ValidateWithProxy.new(node).validate(&block) }
124
- register_validator(proxy)
65
+ parent
66
+ end
125
67
  end
126
68
 
127
69
  private
128
70
 
129
- attr_writer :path, :meta, :context, :on_evaluators, :maybe_evaluators
71
+ attr_writer :coerced, :root
130
72
 
131
- def validator(key, *args)
132
- Validators::REGISTRY.resolve!(key).new(*args).build
133
- end
73
+ def coerce_input
74
+ output = input.is_a?(MissingInput) && node.omnipresent? ? input : node.type[input]
75
+ self.output = output
134
76
 
135
- def register_validator(validator)
136
- validations << validator
77
+ rescue Dry::Types::CoercionError => error
78
+ add_schema_error(error.message)
137
79
  end
138
80
 
139
- def resolve_type(name_or_type)
140
- @type = root_node.send(:type_resolver).resolve(type_system, name_or_type)
81
+ def apply_on_evaluators
82
+ node.on_evaluators.each { |evaluator| evaluator.call(input, self, context) { |result| self.input = result } }
141
83
  end
142
84
 
143
- def resolve_type_system
144
- @type_system = TypeSystemResolver.new(node: self).call
145
- end
85
+ def maybe_evaluator_applies?
86
+ @maybe_evaluator_applies ||= node.maybe_evaluators.inject(false) do |acc, evaluator|
87
+ result = (acc || evaluator.call(input, self, context))
146
88
 
147
- def type_resolver
148
- @type_resolver ||= begin
149
- root_node? ? TypeResolver.new : (raise NoMethodError, 'type_resolver is only available on root node')
89
+ if result
90
+ self.output = input
91
+ break true
92
+ else
93
+ false
94
+ end
150
95
  end
151
96
  end
152
97
 
153
- def application_class
154
- @application_class ||= "NxtSchema::Application::#{self.class.name.demodulize}".constantize
155
- end
98
+ def register_as_coerced_when_no_errors
99
+ return unless valid?
156
100
 
157
- def configure(&block)
158
- if block.arity == 1
159
- block.call(self)
160
- else
161
- instance_exec(&block)
162
- end
101
+ self.coerced = true
102
+ coerced_nodes << self
163
103
  end
164
104
 
165
- def resolve_additional_keys_strategy
166
- @additional_keys_strategy = options.fetch(:additional_keys) do
167
- parent_node&.send(:additional_keys_strategy) || :allow
168
- end
105
+ def resolve_error_key(key)
106
+ parts = [parent&.error_key].compact
107
+ parts << (key.present? ? "#{node.name}[#{key}]" : node.name)
108
+ @error_key = parts.join('.')
169
109
  end
170
110
 
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
182
- else
183
- @optional = optional
184
- end
185
- end
186
-
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
191
-
192
- @omnipresent = omnipresent
193
- end
194
-
195
- def resolve_path
196
- self.path = root_node? ? name : "#{parent_node.path}.#{name}"
197
- end
198
-
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
205
- end
206
-
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
111
+ def coerced?(&block)
112
+ block.call(self) if @coerced && block_given?
113
+ @coerced
215
114
  end
216
115
  end
217
116
  end
@@ -1,21 +1,56 @@
1
1
  module NxtSchema
2
2
  module Node
3
3
  class Collection < Node::Base
4
- include HasSubNodes
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?
5
8
 
6
- DEFAULT_TYPE = NxtSchema::Types::Strict::Array
9
+ coerce_input
10
+ validate_filled
11
+ return self unless valid?
7
12
 
8
- def initialize(name:, type: DEFAULT_TYPE, parent_node:, **options, &block)
9
- super
13
+ child_applications.each_with_index do |item, index|
14
+ current_application = item.call
15
+
16
+ if !current_application.valid?
17
+ merge_errors(current_application)
18
+ else
19
+ output[index] = current_application.output
20
+ end
21
+ end
22
+
23
+ register_as_coerced_when_no_errors
24
+ run_validations
25
+
26
+ self
10
27
  end
11
28
 
29
+ delegate :[], to: :child_applications
30
+
12
31
  private
13
32
 
14
- def add_sub_node(node)
15
- # TODO: Spec that this raises
16
- raise ArgumentError, "It's not possible to define multiple nodes within a collection" unless sub_nodes.empty?
33
+ def validate_filled
34
+ add_schema_error('is not allowed to be empty') if input.blank? && !maybe_evaluator_applies?
35
+ end
36
+
37
+ def child_applications
38
+ @child_applications ||= begin
39
+ return [] unless input.respond_to?(:each_with_index)
40
+
41
+ input.each_with_index.map do |item, index|
42
+ build_child_application(item, index)
43
+ end
44
+ end
45
+
46
+ end
47
+
48
+ def build_child_application(item, error_key)
49
+ sub_node.build_application(input: item, context: context, parent: self, error_key: error_key)
50
+ end
17
51
 
18
- super
52
+ def sub_node
53
+ @sub_node ||= node.sub_nodes.values.first
19
54
  end
20
55
  end
21
56
  end
@@ -1,5 +1,5 @@
1
1
  module NxtSchema
2
- module Application
2
+ module Node
3
3
  class ErrorStore < ::Hash
4
4
  def initialize(application)
5
5
  super()
@@ -11,7 +11,7 @@ module NxtSchema
11
11
  def add_schema_error(message:)
12
12
  add_error(
13
13
  application,
14
- NxtSchema::Application::Errors::SchemaError.new(
14
+ NxtSchema::Node::Errors::SchemaError.new(
15
15
  application: application,
16
16
  message: message
17
17
  )
@@ -21,7 +21,7 @@ module NxtSchema
21
21
  def add_validation_error(message:)
22
22
  add_error(
23
23
  application,
24
- NxtSchema::Application::Errors::ValidationError.new(
24
+ NxtSchema::Node::Errors::ValidationError.new(
25
25
  application: application,
26
26
  message: message
27
27
  )
@@ -39,7 +39,7 @@ module NxtSchema
39
39
 
40
40
  # def schema_errors
41
41
  # inject({}) do |acc, (k, v)|
42
- # errors = v.select { |e| e.is_a?(NxtSchema::Application::Errors::SchemaError) }
42
+ # errors = v.select { |e| e.is_a?(NxtSchema::Node::Errors::SchemaError) }
43
43
  # acc[k] = errors if errors.any?
44
44
  # acc
45
45
  # end
@@ -47,7 +47,7 @@ module NxtSchema
47
47
  #
48
48
  # def validation_errors
49
49
  # inject({}) do |acc, (k, v)|
50
- # errors = v.select { |e| e.is_a?(NxtSchema::Application::Errors::ValidationError) }
50
+ # errors = v.select { |e| e.is_a?(NxtSchema::Node::Errors::ValidationError) }
51
51
  # acc[k] = errors if errors.any?
52
52
  # acc
53
53
  # end
@@ -1,5 +1,5 @@
1
1
  module NxtSchema
2
- module Application
2
+ module Node
3
3
  module Errors
4
4
  class SchemaError < ::String
5
5
  def initialize(application:, message:)
@@ -1,5 +1,5 @@
1
1
  module NxtSchema
2
- module Application
2
+ module Node
3
3
  module Errors
4
4
  class ValidationError < ::String
5
5
  def initialize(application:, message:)
@@ -1,12 +1,14 @@
1
1
  module NxtSchema
2
2
  module Node
3
3
  class Leaf < Node::Base
4
- def initialize(name:, type: :String, parent_node:, **options, &block)
5
- super
6
- end
4
+ def call
5
+ apply_on_evaluators
6
+ return self if maybe_evaluator_applies?
7
7
 
8
- def leaf?
9
- true
8
+ coerce_input
9
+ register_as_coerced_when_no_errors
10
+ run_validations
11
+ self
10
12
  end
11
13
  end
12
14
  end
@@ -1,20 +1,113 @@
1
1
  module NxtSchema
2
2
  module Node
3
3
  class Schema < Node::Base
4
- include HasSubNodes
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?
5
8
 
6
- DEFAULT_TYPE = NxtSchema::Types::Strict::Hash
9
+ coerce_input
10
+ return self unless valid?
7
11
 
8
- def initialize(name:, type: DEFAULT_TYPE, parent_node:, **options, &block)
9
- super
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_coerced_when_no_errors
27
+ run_validations
28
+ self
10
29
  end
11
30
 
12
- def optional(name, node_or_type_of_node, **options, &block)
13
- node(name, node_or_type_of_node, **options.merge(optional: true), &block)
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)
14
107
  end
15
108
 
16
- def omnipresent(name, node_or_type_of_node, **options, &block)
17
- node(name, node_or_type_of_node, **options.merge(omnipresent: true), &block)
109
+ def input_has_key?(input, key)
110
+ input.respond_to?(:key?) && input.key?(key)
18
111
  end
19
112
  end
20
113
  end