nxt_schema 0.1.0

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