nxt_schema 0.1.0 → 1.0.2

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/Gemfile +0 -1
  4. data/Gemfile.lock +40 -42
  5. data/README.md +267 -121
  6. data/lib/nxt_schema.rb +60 -51
  7. data/lib/nxt_schema/callable.rb +21 -55
  8. data/lib/nxt_schema/dsl.rb +41 -31
  9. data/lib/nxt_schema/error.rb +4 -0
  10. data/lib/nxt_schema/errors/{error.rb → coercion_error.rb} +1 -2
  11. data/lib/nxt_schema/errors/invalid.rb +16 -0
  12. data/lib/nxt_schema/errors/invalid_options.rb +6 -0
  13. data/lib/nxt_schema/node/any_of.rb +39 -0
  14. data/lib/nxt_schema/node/base.rb +66 -267
  15. data/lib/nxt_schema/node/collection.rb +40 -56
  16. data/lib/nxt_schema/node/error_store.rb +41 -0
  17. data/lib/nxt_schema/node/errors/schema_error.rb +15 -0
  18. data/lib/nxt_schema/node/errors/validation_error.rb +15 -0
  19. data/lib/nxt_schema/node/leaf.rb +8 -36
  20. data/lib/nxt_schema/node/schema.rb +70 -103
  21. data/lib/nxt_schema/registry.rb +12 -74
  22. data/lib/nxt_schema/registry/proxy.rb +21 -0
  23. data/lib/nxt_schema/template/any_of.rb +50 -0
  24. data/lib/nxt_schema/template/base.rb +220 -0
  25. data/lib/nxt_schema/template/collection.rb +23 -0
  26. data/lib/nxt_schema/template/has_sub_nodes.rb +87 -0
  27. data/lib/nxt_schema/template/leaf.rb +13 -0
  28. data/lib/nxt_schema/template/maybe_evaluator.rb +28 -0
  29. data/lib/nxt_schema/template/on_evaluator.rb +25 -0
  30. data/lib/nxt_schema/template/schema.rb +22 -0
  31. data/lib/nxt_schema/template/sub_nodes.rb +22 -0
  32. data/lib/nxt_schema/template/type_resolver.rb +39 -0
  33. data/lib/nxt_schema/template/type_system_resolver.rb +22 -0
  34. data/lib/nxt_schema/types.rb +7 -4
  35. data/lib/nxt_schema/undefined.rb +4 -2
  36. data/lib/nxt_schema/validators/{equality.rb → equal_to.rb} +2 -2
  37. data/lib/nxt_schema/validators/error_messages.rb +42 -0
  38. data/lib/nxt_schema/{error_messages → validators/error_messages}/en.yaml +6 -5
  39. data/lib/nxt_schema/validators/{excluded.rb → excluded_in.rb} +1 -1
  40. data/lib/nxt_schema/validators/{included.rb → included_in.rb} +1 -1
  41. data/lib/nxt_schema/validators/includes.rb +1 -1
  42. data/lib/nxt_schema/validators/optional_node.rb +11 -6
  43. data/lib/nxt_schema/validators/registry.rb +1 -7
  44. data/lib/nxt_schema/{node → validators}/validate_with_proxy.rb +3 -3
  45. data/lib/nxt_schema/validators/validator.rb +2 -2
  46. data/lib/nxt_schema/version.rb +1 -1
  47. data/nxt_schema.gemspec +1 -0
  48. metadata +44 -21
  49. data/lib/nxt_schema/callable_or_value.rb +0 -72
  50. data/lib/nxt_schema/error_messages.rb +0 -40
  51. data/lib/nxt_schema/errors.rb +0 -4
  52. data/lib/nxt_schema/errors/invalid_options_error.rb +0 -5
  53. data/lib/nxt_schema/errors/schema_not_applied_error.rb +0 -5
  54. data/lib/nxt_schema/node/constructor.rb +0 -9
  55. data/lib/nxt_schema/node/default_value_evaluator.rb +0 -20
  56. data/lib/nxt_schema/node/error.rb +0 -13
  57. data/lib/nxt_schema/node/has_subnodes.rb +0 -97
  58. data/lib/nxt_schema/node/maybe_evaluator.rb +0 -23
  59. data/lib/nxt_schema/node/template_store.rb +0 -15
  60. data/lib/nxt_schema/node/type_resolver.rb +0 -24
@@ -1,72 +1,56 @@
1
1
  module NxtSchema
2
2
  module Node
3
3
  class Collection < Node::Base
4
- def initialize(name:, type: NxtSchema::Types::Strict::Array, parent_node:, **options, &block)
5
- @template_store = TemplateStore.new
6
- super
7
- end
8
-
9
- def apply(input, parent_node: self.parent_node, context: nil)
10
- self.input = input
11
- register_node(context)
12
-
13
- self.parent_node = parent_node
14
- self.schema_errors = { schema_errors_key => [] }
15
- self.validation_errors = { schema_errors_key => [] }
16
- self.value_store = []
17
- self.value = input
18
-
19
- if maybe_criteria_applies?(value)
20
- self.value_store = value
21
- else
22
- self.value = value_or_default_value(value)
23
-
24
- unless maybe_criteria_applies?(value)
25
- self.value = coerce_value(value)
4
+ def call
5
+ apply_on_evaluators
6
+ child_nodes # build nodes here so we can access them even when invalid
7
+ return self if maybe_evaluator_applies?
8
+
9
+ coerce_input
10
+ validate_filled
11
+ return self unless valid?
12
+
13
+ child_nodes.each_with_index do |item, index|
14
+ child_node = item.call
15
+
16
+ if !child_node.valid?
17
+ merge_errors(child_node)
18
+ else
19
+ output[index] = child_node.output
20
+ end
21
+ end
26
22
 
27
- current_node_store = {}
23
+ register_as_coerced_when_no_errors
24
+ run_validations
28
25
 
29
- value.each_with_index do |item, index|
30
- item_schema_errors = schema_errors[index] ||= { schema_errors_key => [] }
31
- validation_errors[index] ||= { schema_errors_key => [] }
26
+ self
27
+ end
32
28
 
33
- template_store.each do |node_name, node|
34
- current_node = node.dup
35
- current_node_store[node_name] = current_node
36
- current_node.apply(item, parent_node: self, context: context)
37
- value_store[index] = current_node.value
29
+ delegate :[], to: :child_nodes
38
30
 
39
- # TODO: Extract method here
40
- unless current_node.schema_errors?
41
- current_node_store.each do |node_name, node|
42
- node.schema_errors = { }
43
- node.validation_errors = { }
44
- item_schema_errors = schema_errors[index][node_name] = node.schema_errors
45
- validation_errors[index][node_name] = node.validation_errors
46
- end
31
+ private
47
32
 
48
- break
49
- else
50
- schema_errors[index][node_name] = current_node.schema_errors
51
- validation_errors[index][node_name] = current_node.validation_errors
52
- end
53
- end
33
+ def validate_filled
34
+ add_schema_error('is not allowed to be empty') if input.blank? && !maybe_evaluator_applies?
35
+ end
54
36
 
55
- item_schema_errors.reject! { |_, v| v.empty? }
56
- end
37
+ def child_nodes
38
+ @child_nodes ||= begin
39
+ return [] unless input.respond_to?(:each_with_index)
57
40
 
58
- # Once we collected all values ensure type by casting again
59
- self.value_store = coerce_value(value_store)
60
- self.value = value_store
41
+ input.each_with_index.map do |item, index|
42
+ build_child_node(item, index)
61
43
  end
62
44
  end
63
45
 
64
- self_without_empty_schema_errors
65
- rescue Dry::Types::ConstraintError, Dry::Types::CoercionError => error
66
- add_schema_error(error.message)
67
- self_without_empty_schema_errors
68
- ensure
69
- mark_as_applied
46
+ end
47
+
48
+ def build_child_node(item, error_key)
49
+ sub_node.build_node(input: item, context: context, parent: self, error_key: error_key)
50
+ end
51
+
52
+ def sub_node
53
+ @sub_node ||= node.sub_nodes.values.first
70
54
  end
71
55
  end
72
56
  end
@@ -0,0 +1,41 @@
1
+ module NxtSchema
2
+ module Node
3
+ class ErrorStore < ::Hash
4
+ def initialize(node)
5
+ super()
6
+ @node = node
7
+ end
8
+
9
+ attr_reader :node
10
+
11
+ def add_schema_error(message:)
12
+ add_error(
13
+ node,
14
+ NxtSchema::Node::Errors::SchemaError.new(
15
+ node: node,
16
+ message: message
17
+ )
18
+ )
19
+ end
20
+
21
+ def add_validation_error(message:)
22
+ add_error(
23
+ node,
24
+ NxtSchema::Node::Errors::ValidationError.new(
25
+ node: node,
26
+ message: message
27
+ )
28
+ )
29
+ end
30
+
31
+ def merge_errors(node)
32
+ merge!(node.errors)
33
+ end
34
+
35
+ def add_error(node, error)
36
+ self[node.error_key] ||= []
37
+ self[node.error_key] << error
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,15 @@
1
+ module NxtSchema
2
+ module Node
3
+ module Errors
4
+ class SchemaError < ::String
5
+ def initialize(node:, message:)
6
+ super(message)
7
+ @node = node
8
+ end
9
+
10
+ attr_reader :node
11
+ end
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,15 @@
1
+ module NxtSchema
2
+ module Node
3
+ module Errors
4
+ class ValidationError < ::String
5
+ def initialize(node:, message:)
6
+ super(message)
7
+ @node = node
8
+ end
9
+
10
+ attr_reader :node
11
+ end
12
+ end
13
+ end
14
+ end
15
+
@@ -1,42 +1,14 @@
1
1
  module NxtSchema
2
2
  module Node
3
3
  class Leaf < Node::Base
4
- def initialize(name:, type:, parent_node:, **options, &block)
5
- super
6
- @type = resolve_type(type)
7
- end
8
-
9
- def leaf?
10
- true
11
- end
12
-
13
- def apply(input, parent_node: self.parent_node, context: nil)
14
- self.input = input
15
- register_node(context)
16
-
17
- self.parent_node = parent_node
18
- self.schema_errors = { schema_errors_key => [] }
19
- self.validation_errors = { schema_errors_key => [] }
20
-
21
- if maybe_criteria_applies?(input)
22
- self.value = input
23
- else
24
- self.value = value_or_default_value(input)
25
- self.value = coerce_value(value) unless maybe_criteria_applies?(value)
26
- end
27
-
28
- self_without_empty_schema_errors
29
- rescue Dry::Types::ConstraintError, Dry::Types::CoercionError => error
30
- add_schema_error(error.message)
31
- self_without_empty_schema_errors
32
- ensure
33
- mark_as_applied
34
- end
35
-
36
- private
37
-
38
- def resolve_type(name_or_type)
39
- root.send(:type_resolver).resolve(type_system, name_or_type)
4
+ def call
5
+ apply_on_evaluators
6
+ return self if maybe_evaluator_applies?
7
+
8
+ coerce_input
9
+ register_as_coerced_when_no_errors
10
+ run_validations
11
+ self
40
12
  end
41
13
  end
42
14
  end
@@ -1,146 +1,113 @@
1
1
  module NxtSchema
2
2
  module Node
3
3
  class Schema < Node::Base
4
- def initialize(name:, type: NxtSchema::Types::Strict::Hash, parent_node:, **options, &block)
5
- @template_store = TemplateStore.new
6
- super
7
- end
8
-
9
- def apply(input, parent_node: self.parent_node, context: nil)
10
- self.input = input
11
- register_node(context)
12
-
13
- self.parent_node = parent_node
14
- self.schema_errors = { schema_errors_key => [] }
15
- self.validation_errors = { schema_errors_key => [] }
16
- self.value_store = {}
17
- self.value = transform_keys(input)
18
-
19
- if maybe_criteria_applies?(value)
20
- self.value_store = value
21
- else
22
- self.value = value_or_default_value(value)
23
-
24
- unless maybe_criteria_applies?(value)
25
- self.value = coerce_value(value)
26
-
27
- # TODO: We should not allow additional keys to be present per default?!
28
- # TODO: Handle this here
4
+ def call
5
+ apply_on_evaluators
6
+ child_nodes # build nodes here so we can access them even when invalid
7
+ return self if maybe_evaluator_applies?
29
8
 
9
+ coerce_input
10
+ return self unless valid?
30
11
 
12
+ flag_missing_keys
13
+ apply_additional_keys_strategy
31
14
 
32
- sanitized_keys.each do |key|
33
- node = template_store[key]
15
+ child_nodes.each do |key, child|
16
+ current_node = child.call
34
17
 
35
- if allowed_additional_key?(key)
36
- value_store[key] = input[key]
37
- elsif node.presence? || input.key?(key)
38
- node.apply(input[key], parent_node: self, context: context).schema_errors?
39
- value_store[key] = node.value
40
- schema_errors[key] = node.schema_errors
41
- validation_errors[key] = node.validation_errors
42
- else
43
- evaluate_optional_option(node, input, key)
44
- end
45
- end
46
-
47
- self.value_store = coerce_value(value_store)
48
- self.value = value_store
18
+ if !current_node.valid?
19
+ merge_errors(current_node)
20
+ else
21
+ output[key] = current_node.output
49
22
  end
50
23
  end
51
24
 
52
- self_without_empty_schema_errors
53
- rescue Dry::Types::ConstraintError, Dry::Types::CoercionError => error
54
- add_schema_error(error.message)
55
- self_without_empty_schema_errors
56
- ensure
57
- mark_as_applied
25
+ transform_keys
26
+ register_as_coerced_when_no_errors
27
+ run_validations
28
+ self
58
29
  end
59
30
 
60
- def optional(name, type, **options, &block)
61
- raise_invalid_options_presence_options if options[:presence]
31
+ delegate :[], to: :child_nodes
62
32
 
63
- node(name, type, options.merge(optional: true), &block)
64
- end
33
+ private
65
34
 
66
- def present(name, type, **options, &block)
67
- raise_invalid_options_presence_options if options[:optional]
35
+ def transform_keys
36
+ transformer = node.key_transformer
37
+ return unless transformer && output.respond_to?(:transform_keys!)
68
38
 
69
- node(name, type, options.merge(presence: true), &block)
39
+ output.transform_keys!(&transformer)
70
40
  end
71
41
 
72
- private
42
+ def keys
43
+ @keys ||= node.sub_nodes.reject { |key, _| optional_and_not_given_key?(key) }.keys
44
+ end
73
45
 
74
- def evaluate_optional_option(node, hash, key)
75
- optional_option = node.options[:optional]
76
-
77
- if optional_option.respond_to?(:call)
78
- # Validator is added to the schema node!
79
- add_validators(validator(:optional_node, optional_option, key))
80
- elsif !optional_option
81
- error_message = ErrorMessages.resolve(
82
- locale,
83
- :required_key_missing,
84
- key: key,
85
- target: hash
86
- )
87
-
88
- add_schema_error(error_message)
89
- end
46
+ def additional_keys
47
+ @additional_keys ||= input.keys - keys
90
48
  end
91
49
 
92
- def transform_keys(hash)
93
- return hash unless key_transformer && hash.respond_to?(:transform_keys!)
50
+ def optional_and_not_given_key?(key)
51
+ node.sub_nodes[key].optional? && !input.key?(key)
52
+ end
94
53
 
95
- hash.transform_keys! { |key| Callable.new(key_transformer).bind(key).call(key) }
54
+ def additional_keys?
55
+ additional_keys.any?
96
56
  end
97
57
 
98
- def key_transformer
99
- @key_transformer ||= root.options.fetch(:transform_keys) { false }
58
+ def missing_keys
59
+ @missing_keys ||= node.sub_nodes.reject { |_, node| node.omnipresent? || node.optional? }.keys - input.keys
100
60
  end
101
61
 
102
- def sanitized_keys
103
- return template_store.keys if additional_keys_from_input.empty? || ignore_additional_keys?
104
- return template_store.keys + additional_keys_from_input if additional_keys_allowed?
62
+ def apply_additional_keys_strategy
63
+ return if allow_additional_keys?
64
+ return unless additional_keys?
105
65
 
106
66
  if restrict_additional_keys?
107
- error_message = ErrorMessages.resolve(
108
- locale,
109
- :additional_keys_detected,
110
- keys: additional_keys_from_input,
111
- target: input
112
- )
113
-
114
- add_schema_error(error_message)
115
-
116
- template_store.keys
117
- else
118
- raise Errors::InvalidOptionsError, "Invalid option for additional keys: #{additional_keys_strategy}"
67
+ add_schema_error("Additional keys are not allowed: #{additional_keys}")
68
+ elsif reject_additional_keys?
69
+ self.output = output.except(*additional_keys)
119
70
  end
120
71
  end
121
72
 
122
- def allowed_additional_key?(key)
123
- additional_keys_from_input.include?(key)
124
- end
73
+ def flag_missing_keys
74
+ return if missing_keys.empty?
125
75
 
126
- def additional_keys_from_input
127
- (input&.keys || []) - template_store.keys
76
+ add_schema_error("The following keys are missing: #{missing_keys}")
128
77
  end
129
78
 
130
- def additional_keys_allowed?
131
- additional_keys_strategy.to_s == 'allow'
79
+ def allow_additional_keys?
80
+ node.additional_keys_strategy == :allow
132
81
  end
133
82
 
134
- def ignore_additional_keys?
135
- additional_keys_strategy.to_s == 'ignore'
83
+ def reject_additional_keys?
84
+ node.additional_keys_strategy == :reject
136
85
  end
137
86
 
138
87
  def restrict_additional_keys?
139
- additional_keys_strategy.to_s == 'restrict'
88
+ node.additional_keys_strategy == :restrict
89
+ end
90
+
91
+ def child_nodes
92
+ @child_nodes ||= begin
93
+ keys.inject({}) do |acc, key|
94
+ child_node = build_child_node(key)
95
+ acc[key] = child_node if child_node.present?
96
+ acc
97
+ end
98
+ end
99
+ end
100
+
101
+ def build_child_node(key)
102
+ sub_node = node.sub_nodes[key]
103
+ return unless sub_node.present?
104
+
105
+ value = input_has_key?(input, key) ? input[key] : Undefined.new
106
+ sub_node.build_node(input: value, context: context, parent: self)
140
107
  end
141
108
 
142
- def raise_invalid_options_presence_options
143
- raise InvalidOptionsError, 'Options :presence and :optional exclude each other'
109
+ def input_has_key?(input, key)
110
+ input.respond_to?(:key?) && input.key?(key)
144
111
  end
145
112
  end
146
113
  end