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,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