nxt_schema 1.0.0 → 1.0.1

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