nxt_schema 0.1.0

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 (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +7 -0
  5. data/Gemfile +6 -0
  6. data/Gemfile.lock +86 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +376 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/lib/nxt_schema/callable.rb +74 -0
  13. data/lib/nxt_schema/callable_or_value.rb +72 -0
  14. data/lib/nxt_schema/dsl.rb +38 -0
  15. data/lib/nxt_schema/error_messages/en.yaml +15 -0
  16. data/lib/nxt_schema/error_messages.rb +40 -0
  17. data/lib/nxt_schema/errors/error.rb +7 -0
  18. data/lib/nxt_schema/errors/invalid_options_error.rb +5 -0
  19. data/lib/nxt_schema/errors/schema_not_applied_error.rb +5 -0
  20. data/lib/nxt_schema/errors.rb +4 -0
  21. data/lib/nxt_schema/node/base.rb +318 -0
  22. data/lib/nxt_schema/node/collection.rb +73 -0
  23. data/lib/nxt_schema/node/constructor.rb +9 -0
  24. data/lib/nxt_schema/node/default_value_evaluator.rb +20 -0
  25. data/lib/nxt_schema/node/error.rb +13 -0
  26. data/lib/nxt_schema/node/has_subnodes.rb +97 -0
  27. data/lib/nxt_schema/node/leaf.rb +43 -0
  28. data/lib/nxt_schema/node/maybe_evaluator.rb +23 -0
  29. data/lib/nxt_schema/node/schema.rb +147 -0
  30. data/lib/nxt_schema/node/template_store.rb +15 -0
  31. data/lib/nxt_schema/node/type_resolver.rb +24 -0
  32. data/lib/nxt_schema/node/validate_with_proxy.rb +41 -0
  33. data/lib/nxt_schema/node.rb +5 -0
  34. data/lib/nxt_schema/registry.rb +85 -0
  35. data/lib/nxt_schema/types.rb +10 -0
  36. data/lib/nxt_schema/undefined.rb +7 -0
  37. data/lib/nxt_schema/validators/attribute.rb +34 -0
  38. data/lib/nxt_schema/validators/equality.rb +33 -0
  39. data/lib/nxt_schema/validators/excluded.rb +23 -0
  40. data/lib/nxt_schema/validators/excludes.rb +23 -0
  41. data/lib/nxt_schema/validators/greater_than.rb +23 -0
  42. data/lib/nxt_schema/validators/greater_than_or_equal.rb +23 -0
  43. data/lib/nxt_schema/validators/included.rb +23 -0
  44. data/lib/nxt_schema/validators/includes.rb +23 -0
  45. data/lib/nxt_schema/validators/less_than.rb +23 -0
  46. data/lib/nxt_schema/validators/less_than_or_equal.rb +23 -0
  47. data/lib/nxt_schema/validators/optional_node.rb +26 -0
  48. data/lib/nxt_schema/validators/pattern.rb +24 -0
  49. data/lib/nxt_schema/validators/query.rb +28 -0
  50. data/lib/nxt_schema/validators/registry.rb +11 -0
  51. data/lib/nxt_schema/validators/validator.rb +17 -0
  52. data/lib/nxt_schema/version.rb +3 -0
  53. data/lib/nxt_schema.rb +69 -0
  54. data/nxt_schema.gemspec +46 -0
  55. metadata +211 -0
@@ -0,0 +1,23 @@
1
+ module NxtSchema
2
+ module Node
3
+ class MaybeEvaluator
4
+ def initialize(node, evaluator, value)
5
+ @node = node
6
+ @evaluator = evaluator
7
+ @value = value
8
+ end
9
+
10
+ attr_reader :node, :evaluator, :value
11
+
12
+ def call
13
+ if evaluator.respond_to?(:call)
14
+ Callable.new(evaluator).call(node, value)
15
+ elsif value.is_a?(Symbol) && value.respond_to?(evaluator)
16
+ Callable.new(evaluator).bind(value).call(node, value)
17
+ else
18
+ value == evaluator
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,147 @@
1
+ module NxtSchema
2
+ module Node
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
29
+
30
+
31
+
32
+ sanitized_keys.each do |key|
33
+ node = template_store[key]
34
+
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
49
+ end
50
+ end
51
+
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
58
+ end
59
+
60
+ def optional(name, type, **options, &block)
61
+ raise_invalid_options_presence_options if options[:presence]
62
+
63
+ node(name, type, options.merge(optional: true), &block)
64
+ end
65
+
66
+ def present(name, type, **options, &block)
67
+ raise_invalid_options_presence_options if options[:optional]
68
+
69
+ node(name, type, options.merge(presence: true), &block)
70
+ end
71
+
72
+ private
73
+
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
90
+ end
91
+
92
+ def transform_keys(hash)
93
+ return hash unless key_transformer && hash.respond_to?(:transform_keys!)
94
+
95
+ hash.transform_keys! { |key| Callable.new(key_transformer).bind(key).call(key) }
96
+ end
97
+
98
+ def key_transformer
99
+ @key_transformer ||= root.options.fetch(:transform_keys) { false }
100
+ end
101
+
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?
105
+
106
+ 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}"
119
+ end
120
+ end
121
+
122
+ def allowed_additional_key?(key)
123
+ additional_keys_from_input.include?(key)
124
+ end
125
+
126
+ def additional_keys_from_input
127
+ (input&.keys || []) - template_store.keys
128
+ end
129
+
130
+ def additional_keys_allowed?
131
+ additional_keys_strategy.to_s == 'allow'
132
+ end
133
+
134
+ def ignore_additional_keys?
135
+ additional_keys_strategy.to_s == 'ignore'
136
+ end
137
+
138
+ def restrict_additional_keys?
139
+ additional_keys_strategy.to_s == 'restrict'
140
+ end
141
+
142
+ def raise_invalid_options_presence_options
143
+ raise InvalidOptionsError, 'Options :presence and :optional exclude each other'
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,15 @@
1
+ module NxtSchema
2
+ module Node
3
+ class TemplateStore < ::Hash
4
+ def push(node)
5
+ node_name = node.name
6
+ raise_key_error(node_name) if key?(node_name)
7
+ self[node_name] = node
8
+ end
9
+
10
+ def raise_key_error(key)
11
+ raise KeyError, "Node with name '#{key}' already registered! Node names must be unique!"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,24 @@
1
+ module NxtSchema
2
+ module Node
3
+ class TypeResolver
4
+ def resolve(type_system, type)
5
+ @resolve ||= {}
6
+ @resolve[type] ||= begin
7
+ if type.is_a?(Dry::Types::Type)
8
+ type
9
+ else
10
+ # Try to resolve in type system
11
+ type = type_system.const_get(type.to_s.classify)
12
+
13
+ if type.is_a?(Dry::Types::Type)
14
+ type
15
+ else
16
+ # in case it does not exist fallback to Types::Nominal
17
+ "NxtSchema::Types::Nominal::#{type.to_s.classify}".constantize
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,41 @@
1
+ module NxtSchema
2
+ module Node
3
+ class ValidateWithProxy
4
+ def initialize(node)
5
+ @node = node
6
+ @aggregated_errors = []
7
+ end
8
+
9
+ attr_reader :node
10
+
11
+ delegate_missing_to :node
12
+
13
+ def validate(&block)
14
+ result = instance_exec(&block)
15
+ return if result
16
+
17
+ copy_aggregated_errors_to_node
18
+ end
19
+
20
+ def add_error(error)
21
+ aggregated_errors << error
22
+ false
23
+ end
24
+
25
+ def copy_aggregated_errors_to_node
26
+ aggregated_errors.each do |error|
27
+ node.add_error(error)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :aggregated_errors
34
+
35
+ def validator(key, *args)
36
+ validator = node.validator(key, *args)
37
+ validator.call(self, node.value)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,5 @@
1
+ module NxtSchema
2
+ module Node
3
+
4
+ end
5
+ end
@@ -0,0 +1,85 @@
1
+ module NxtSchema
2
+ class Registry
3
+ def initialize(namespace_separator: '::', namespace: '')
4
+ @store = ActiveSupport::HashWithIndifferentAccess.new
5
+ @namespace_separator = namespace_separator
6
+ @namespace = namespace
7
+ end
8
+
9
+ delegate_missing_to :store
10
+
11
+ # register('strict::string')
12
+ # Registry[:strict].register
13
+
14
+ def register(key, value)
15
+ key = key.to_s
16
+ ensure_key_not_registered_already(key)
17
+ namespaced_store(key)[flat_key(key)] = value
18
+ end
19
+
20
+ def resolve(key, *args)
21
+ value = resolve_value(key)
22
+ return value unless value.respond_to?(:call)
23
+
24
+ value.call(*args)
25
+ end
26
+
27
+ def resolve_value(key)
28
+ key = key.to_s
29
+ parts = namespaced_key_parts(key)[0..-2]
30
+
31
+ namespaced_store = parts.inject(store) do |acc, key|
32
+ acc.fetch(key)
33
+ rescue KeyError
34
+ raise KeyError, "No registry found at #{key} in #{acc}"
35
+ end
36
+
37
+ begin
38
+ namespaced_store.fetch(flat_key(key))
39
+ rescue KeyError
40
+ raise KeyError, "Could not find #{flat_key(key)} in #{namespaced_store}"
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :store, :namespace_separator, :namespace
47
+
48
+ def namespaced_store(key)
49
+ parts = namespaced_key_parts(key)
50
+
51
+ current_parts = []
52
+
53
+ parts[0..-2].inject(store) do |acc, namespace|
54
+ current_parts << namespace
55
+ current_namespace = current_parts.join(namespace_separator)
56
+
57
+ acc.fetch(namespace) do
58
+ acc[namespace] = Registry.new(namespace: current_namespace)
59
+ acc = acc[namespace]
60
+ acc
61
+ end
62
+ end
63
+ end
64
+
65
+ def namespaced_key_parts(key)
66
+ key.downcase.split(namespace_separator)
67
+ end
68
+
69
+ def flat_key(key)
70
+ namespaced_key_parts(key).last
71
+ end
72
+
73
+ def ensure_key_not_registered_already(key)
74
+ return unless namespaced_store(key).key?(flat_key(key))
75
+
76
+ raise KeyError, "Key: #{flat_key(key)} already registered in #{namespaced_store(key)}"
77
+ end
78
+
79
+ def to_s
80
+ identifier = 'NxtSchema::Registry'
81
+ identifier << "#{namespace_separator}#{namespace}" unless namespace.blank?
82
+ identifier
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,10 @@
1
+ module NxtSchema
2
+ module Types
3
+ include Dry.Types()
4
+
5
+ StrippedString = Strict::String.constructor(->(string) { string&.strip })
6
+ StrippedNonBlankString = StrippedString.constrained(min_size: 1)
7
+ Enums = -> (*values) { Strict::String.enum(*values) } # Use as NxtSchema::Types::Enums[*ROLES]
8
+ SymbolizedEnums = -> (*values) { Coercible::Symbol.enum(*values) } # Use as NxtSchema::Types::SymboleEnums[*ROLES]
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ module NxtSchema
2
+ class Undefined
3
+ def inspect
4
+ 'undefined'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,34 @@
1
+ module NxtSchema
2
+ module Validators
3
+ class Attribute < Validator
4
+ def initialize(method, expectation)
5
+ @method = method
6
+ @expectation = expectation
7
+ end
8
+
9
+ register_as :attribute, :attr
10
+ attr_reader :method, :expectation
11
+
12
+ # Query any attribute on a value with validator(:attribute, :size, ->(s) { s < 7 })
13
+
14
+ def build
15
+ lambda do |node, value|
16
+ raise ArgumentError, "#{value} does not respond to query: #{method}" unless value.respond_to?(method)
17
+
18
+ if expectation.call(value.send(method))
19
+ true
20
+ else
21
+ node.add_error(
22
+ translate_error(
23
+ node.locale,
24
+ attribute: value,
25
+ attribute_name: method,
26
+ value: value.send(method)
27
+ )
28
+ )
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,33 @@
1
+ module NxtSchema
2
+ module Validators
3
+ class Equality < Validator
4
+ def initialize(expectation)
5
+ @expectation = expectation
6
+ end
7
+
8
+ register_as :equality, :eql
9
+ attr_reader :expectation
10
+
11
+ # Query for equality validator(:equality, 3)
12
+ # Query for equality validator(:eql, -> { 3 * 3 * 60 })
13
+
14
+ def build
15
+ lambda do |node, value|
16
+ expected_value = expectation.respond_to?(:call) ? Callable.new(expectation).call(node, value) : expectation
17
+
18
+ if value == expected_value
19
+ true
20
+ else
21
+ node.add_error(
22
+ translate_error(
23
+ node.locale,
24
+ actual: value,
25
+ expected: expected_value
26
+ )
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,23 @@
1
+ module NxtSchema
2
+ module Validators
3
+ class Excluded < Validator
4
+ def initialize(target)
5
+ @target = target
6
+ end
7
+
8
+ register_as :excluded
9
+ attr_reader :target
10
+
11
+ def build
12
+ lambda do |node, value|
13
+ if target.exclude?(value)
14
+ true
15
+ else
16
+ message = translate_error(node.locale, target: target, value: value)
17
+ node.add_error(message)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module NxtSchema
2
+ module Validators
3
+ class Excludes < Validator
4
+ def initialize(target)
5
+ @target = target
6
+ end
7
+
8
+ register_as :excludes
9
+ attr_reader :target
10
+
11
+ def build
12
+ lambda do |node, value|
13
+ if value.exclude?(target)
14
+ true
15
+ else
16
+ message = translate_error(node.locale, target: target, value: value)
17
+ node.add_error(message)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module NxtSchema
2
+ module Validators
3
+ class GreaterThan < Validator
4
+ def initialize(threshold)
5
+ @threshold = threshold
6
+ end
7
+
8
+ register_as :greater_than
9
+ attr_reader :threshold
10
+
11
+ def build
12
+ lambda do |node, value|
13
+ if value > threshold
14
+ true
15
+ else
16
+ message = translate_error(node.locale, value: value, threshold: threshold)
17
+ node.add_error(message)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module NxtSchema
2
+ module Validators
3
+ class GreaterThanOrEqual < Validator
4
+ def initialize(threshold)
5
+ @threshold = threshold
6
+ end
7
+
8
+ register_as :greater_than_or_equal, :gt_or_eql
9
+ attr_reader :threshold
10
+
11
+ def build
12
+ lambda do |node, value|
13
+ if value >= threshold
14
+ true
15
+ else
16
+ message = translate_error(node.locale, value: value, threshold: threshold)
17
+ node.add_error(message)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module NxtSchema
2
+ module Validators
3
+ class Included < Validator
4
+ def initialize(target)
5
+ @target = target
6
+ end
7
+
8
+ register_as :included
9
+ attr_reader :target
10
+
11
+ def build
12
+ lambda do |node, value|
13
+ if target.include?(value)
14
+ true
15
+ else
16
+ message = translate_error(node.locale, value: value, target: target)
17
+ node.add_error(message)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module NxtSchema
2
+ module Validators
3
+ class Includes < Validator
4
+ def initialize(target)
5
+ @target = target
6
+ end
7
+
8
+ register_as :includes
9
+ attr_reader :target
10
+
11
+ def build
12
+ lambda do |node, value|
13
+ if value.include?(target)
14
+ true
15
+ else
16
+ message = translate_error(node.locale, value: value, target: target)
17
+ node.add_error(message)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module NxtSchema
2
+ module Validators
3
+ class LessThan < Validator
4
+ def initialize(threshold)
5
+ @threshold = threshold
6
+ end
7
+
8
+ register_as :less_than
9
+ attr_reader :threshold
10
+
11
+ def build
12
+ lambda do |node, value|
13
+ if value < threshold
14
+ true
15
+ else
16
+ message = translate_error(node.locale, value: value, threshold: threshold)
17
+ node.add_error(message)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module NxtSchema
2
+ module Validators
3
+ class LessThanOrEqual < Validator
4
+ def initialize(threshold)
5
+ @threshold = threshold
6
+ end
7
+
8
+ register_as :less_than_or_equal, :lt_or_eql
9
+ attr_reader :threshold
10
+
11
+ def build
12
+ lambda do |node, value|
13
+ if value <= threshold
14
+ true
15
+ else
16
+ message = translate_error(node.locale, value: value, threshold: threshold)
17
+ node.add_error(message)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ module NxtSchema
2
+ module Validators
3
+ class OptionalNode < Validator
4
+ def initialize(conditional, missing_key)
5
+ @conditional = conditional
6
+ @missing_key = missing_key
7
+ end
8
+
9
+ register_as :optional_node
10
+ attr_reader :conditional, :missing_key
11
+
12
+ def build
13
+ lambda do |node, value|
14
+ args = [node, value]
15
+
16
+ if conditional.call(*args.take(conditional.arity))
17
+ true
18
+ else
19
+ message = ErrorMessages.resolve(node.locale, :required_key_missing, key: missing_key, target: node.value)
20
+ node.add_error(message)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,24 @@
1
+ module NxtSchema
2
+ module Validators
3
+ class Pattern < Validator
4
+ def initialize(pattern)
5
+ @pattern = pattern
6
+ end
7
+
8
+ register_as :pattern, :format
9
+ attr_reader :pattern
10
+
11
+ def build
12
+ lambda do |node, value|
13
+ if value.match(pattern)
14
+ true
15
+ else
16
+ message = translate_error(node.locale, value: value, pattern: pattern)
17
+ node.add_error(message)
18
+ false
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end