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,85 +1,23 @@
1
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}"
2
+ module Registry
3
+ module ClassMethods
4
+ def schemas
5
+ @schemas ||= NxtSchema::Registry::Proxy.new(self)
35
6
  end
36
7
 
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
8
+ def inherited(subclass)
9
+ schemas.each do |key, schema|
10
+ subclass.schemas.register(key, schema)
61
11
  end
62
- end
63
- end
64
-
65
- def namespaced_key_parts(key)
66
- key.downcase.split(namespace_separator)
67
- end
68
12
 
69
- def flat_key(key)
70
- namespaced_key_parts(key).last
13
+ super
14
+ end
71
15
  end
72
16
 
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
17
+ def self.included(base)
18
+ base.extend(ClassMethods)
78
19
 
79
- def to_s
80
- identifier = 'NxtSchema::Registry'
81
- identifier << "#{namespace_separator}#{namespace}" unless namespace.blank?
82
- identifier
20
+ delegate :schemas, to: :class
83
21
  end
84
22
  end
85
23
  end
@@ -0,0 +1,21 @@
1
+ module NxtSchema
2
+ module Registry
3
+ class Proxy
4
+ def initialize(namespace)
5
+ @registry = ::NxtRegistry::Registry.new(namespace, call: false)
6
+ end
7
+
8
+ attr_reader :registry
9
+
10
+ delegate_missing_to :registry
11
+
12
+ def apply(key, input)
13
+ resolve!(key).apply(input: input)
14
+ end
15
+
16
+ def apply!(key, input)
17
+ resolve!(key).apply!(input: input)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,50 @@
1
+ module NxtSchema
2
+ module Template
3
+ class AnyOf < Base
4
+ include HasSubNodes
5
+
6
+ def initialize(name:, type: nil, parent_node:, **options, &block)
7
+ super
8
+ ensure_sub_nodes_present
9
+ end
10
+
11
+ def collection(name = sub_nodes.count, type = NxtSchema::Template::Collection::DEFAULT_TYPE, **options, &block)
12
+ super
13
+ end
14
+
15
+ def schema(name = sub_nodes.count, type = NxtSchema::Template::Schema::DEFAULT_TYPE, **options, &block)
16
+ super
17
+ end
18
+
19
+ def node(name = sub_nodes.count, node_or_type_of_node = nil, **options, &block)
20
+ super
21
+ end
22
+
23
+ def on(*args)
24
+ raise NotImplementedError
25
+ end
26
+
27
+ def maybe(*args)
28
+ raise NotImplementedError
29
+ end
30
+
31
+ private
32
+
33
+ def resolve_type(name_or_type)
34
+ nil
35
+ end
36
+
37
+ def resolve_optional_option
38
+ return unless options.key?(:optional)
39
+
40
+ raise InvalidOptions, "The optional option is not available for nodes of type #{self.class.name}"
41
+ end
42
+
43
+ def resolve_omnipresent_option
44
+ return unless options.key?(:omnipresent)
45
+
46
+ raise InvalidOptions, "The omnipresent option is not available for nodes of type #{self.class.name}"
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,220 @@
1
+ module NxtSchema
2
+ module Template
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
+ node_class # memoize
25
+ configure(&block) if block_given?
26
+ end
27
+
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: Undefined.new, context: self.context, parent: nil, error_key: nil)
46
+ build_node(input: input, context: context, parent: parent, error_key: error_key).call
47
+ end
48
+
49
+ def apply!(input: Undefined.new, context: self.context, parent: nil, error_key: nil)
50
+ result = build_node(input: input, context: context, parent: parent, error_key: error_key).call
51
+ return result if parent
52
+
53
+ raise NxtSchema::Errors::Invalid.new(result) if result.errors.any?
54
+
55
+ result.output
56
+ end
57
+
58
+ def build_node(input: Undefined.new, context: self.context, parent: nil, error_key: nil)
59
+ node_class.new(
60
+ node: self,
61
+ input: input,
62
+ parent: parent,
63
+ context: context,
64
+ error_key: error_key
65
+ )
66
+ end
67
+
68
+ def root_node?
69
+ @is_root_node
70
+ end
71
+
72
+ def optional?
73
+ @optional
74
+ end
75
+
76
+ def omnipresent?
77
+ @omnipresent
78
+ end
79
+
80
+ def default(value = NxtSchema::Undefined.new, &block)
81
+ value = missing_input?(value) ? block : value
82
+ condition = ->(input) { missing_input?(input) || input.nil? }
83
+ on(condition, value)
84
+
85
+ self
86
+ end
87
+
88
+ def on(condition, value = NxtSchema::Undefined.new, &block)
89
+ value = missing_input?(value) ? block : value
90
+ on_evaluators << OnEvaluator.new(condition: condition, value: value)
91
+
92
+ self
93
+ end
94
+
95
+ def maybe(value = NxtSchema::Undefined.new, &block)
96
+ value = missing_input?(value) ? block : value
97
+ maybe_evaluators << MaybeEvaluator.new(value: value)
98
+
99
+ self
100
+ end
101
+
102
+ def validate(key = NxtSchema::Undefined.new, *args, &block)
103
+ # TODO: This does not really work with all kinds of chaining combinations yet!
104
+
105
+ validator = if key.is_a?(Symbol)
106
+ validator(key, *args)
107
+ elsif key.respond_to?(:call)
108
+ key
109
+ elsif block_given?
110
+ if key.is_a?(NxtSchema::Undefined)
111
+ block
112
+ else
113
+ configure(&block)
114
+ end
115
+ else
116
+ raise ArgumentError, "Don't know how to resolve validator from: #{key} with: #{args} #{block}"
117
+ end
118
+
119
+ register_validator(validator)
120
+
121
+ self
122
+ end
123
+
124
+ def validate_with(&block)
125
+ proxy = ->(node) { NxtSchema::Validator::ValidateWithProxy.new(node).validate(&block) }
126
+ register_validator(proxy)
127
+ end
128
+
129
+ private
130
+
131
+ attr_writer :path, :meta, :context, :on_evaluators, :maybe_evaluators
132
+
133
+ def validator(key, *args)
134
+ Validators::REGISTRY.resolve!(key).new(*args).build
135
+ end
136
+
137
+ def register_validator(validator)
138
+ validations << validator
139
+ end
140
+
141
+ def resolve_type(name_or_type)
142
+ @type = root_node.send(:type_resolver).resolve(type_system, name_or_type)
143
+ end
144
+
145
+ def resolve_type_system
146
+ @type_system = TypeSystemResolver.new(node: self).call
147
+ end
148
+
149
+ def type_resolver
150
+ @type_resolver ||= begin
151
+ root_node? ? TypeResolver.new : (raise NoMethodError, 'type_resolver is only available on root node')
152
+ end
153
+ end
154
+
155
+ def node_class
156
+ @node_class ||= "NxtSchema::Node::#{self.class.name.demodulize}".constantize
157
+ end
158
+
159
+ def configure(&block)
160
+ if block.arity == 1
161
+ block.call(self)
162
+ else
163
+ instance_exec(&block)
164
+ end
165
+ end
166
+
167
+ def resolve_additional_keys_strategy
168
+ @additional_keys_strategy = options.fetch(:additional_keys) do
169
+ parent_node&.send(:additional_keys_strategy) || :allow
170
+ end
171
+ end
172
+
173
+ def resolve_optional_option
174
+ optional = options.fetch(:optional, false)
175
+ raise Errors::InvalidOptions, 'Optional nodes are only available within schemas' if optional && !parent_node.is_a?(Schema)
176
+ raise Errors::InvalidOptions, "Can't make omnipresent node optional" if optional && omnipresent?
177
+
178
+ if optional.respond_to?(:call)
179
+ # When a node is conditionally optional we make it optional and add a validator to the parent to check
180
+ # that it's there when the option does not apply.
181
+ optional_node_validator = validator(:optional_node, optional, name)
182
+ parent_node.send(:register_validator, optional_node_validator)
183
+ @optional = true
184
+ else
185
+ @optional = optional
186
+ end
187
+ end
188
+
189
+ def resolve_omnipresent_option
190
+ omnipresent = options.fetch(:omnipresent, false)
191
+ raise Errors::InvalidOptions, 'Omnipresent nodes are only available within schemas' if omnipresent && !parent_node.is_a?(Schema)
192
+ raise Errors::InvalidOptions, "Can't make omnipresent node optional" if optional? && omnipresent
193
+
194
+ @omnipresent = omnipresent
195
+ end
196
+
197
+ def resolve_path
198
+ self.path = root_node? ? name : "#{parent_node.path}.#{name}"
199
+ end
200
+
201
+ def resolve_context
202
+ self.context = options.fetch(:context) { parent_node&.send(:context) }
203
+ end
204
+
205
+ def missing_input?(value)
206
+ value.is_a? Undefined
207
+ end
208
+
209
+ def resolve_key_transformer
210
+ @key_transformer = options.fetch(:transform_keys) { parent_node&.key_transformer || ->(key) { key.to_sym } }
211
+ end
212
+
213
+ def resolve_name(name)
214
+ raise ArgumentError, 'Name can either be a symbol or an integer' unless name.class.in?([Symbol, Integer])
215
+
216
+ @name = name
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,23 @@
1
+ module NxtSchema
2
+ module Template
3
+ class Collection < Template::Base
4
+ include HasSubNodes
5
+
6
+ DEFAULT_TYPE = NxtSchema::Types::Strict::Array
7
+
8
+ def initialize(name:, type: DEFAULT_TYPE, parent_node:, **options, &block)
9
+ super
10
+ ensure_sub_nodes_present
11
+ end
12
+
13
+ private
14
+
15
+ def add_sub_node(node)
16
+ # TODO: Spec that this raises
17
+ raise ArgumentError, "It's not possible to define multiple nodes within a collection" unless sub_nodes.empty?
18
+
19
+ super
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,87 @@
1
+ module NxtSchema
2
+ module Template
3
+ module HasSubNodes
4
+ def collection(name, type = NxtSchema::Template::Collection::DEFAULT_TYPE, **options, &block)
5
+ node = NxtSchema::Template::Collection.new(
6
+ name: name,
7
+ type: type,
8
+ parent_node: self,
9
+ **options,
10
+ &block
11
+ )
12
+
13
+ add_sub_node(node)
14
+ end
15
+
16
+ alias nodes collection
17
+
18
+ def schema(name, type = NxtSchema::Template::Schema::DEFAULT_TYPE, **options, &block)
19
+ node = NxtSchema::Template::Schema.new(
20
+ name: name,
21
+ type: type,
22
+ parent_node: self,
23
+ **options,
24
+ &block
25
+ )
26
+
27
+ add_sub_node(node)
28
+ end
29
+
30
+ def any_of(name, **options, &block)
31
+ node = NxtSchema::Template::AnyOf.new(
32
+ name: name,
33
+ parent_node: self,
34
+ **options,
35
+ &block
36
+ )
37
+
38
+ add_sub_node(node)
39
+ end
40
+
41
+ def node(name, node_or_type_of_node, **options, &block)
42
+ node = if node_or_type_of_node.is_a?(NxtSchema::Template::Base)
43
+ raise ArgumentError, "Can't provide a block along with a node" if block.present?
44
+
45
+ node_or_type_of_node.class.new(
46
+ name: name,
47
+ type: node_or_type_of_node.type,
48
+ parent_node: self,
49
+ **node_or_type_of_node.options.merge(options),
50
+ &node_or_type_of_node.configuration
51
+ )
52
+ else
53
+ NxtSchema::Template::Leaf.new(
54
+ name: name,
55
+ type: node_or_type_of_node,
56
+ parent_node: self,
57
+ **options,
58
+ &block
59
+ )
60
+ end
61
+
62
+ add_sub_node(node)
63
+ end
64
+
65
+ alias required node
66
+
67
+ def add_sub_node(node)
68
+ sub_nodes.add(node)
69
+ node
70
+ end
71
+
72
+ def sub_nodes
73
+ @sub_nodes ||= Template::SubNodes.new
74
+ end
75
+
76
+ def [](key)
77
+ sub_nodes[key]
78
+ end
79
+
80
+ def ensure_sub_nodes_present
81
+ return if sub_nodes.any?
82
+
83
+ raise NxtSchema::Errors::InvalidOptions, "#{self.class.name} must have sub nodes"
84
+ end
85
+ end
86
+ end
87
+ end