nxt_schema 0.1.0 → 1.0.2

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