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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/Gemfile +0 -1
- data/Gemfile.lock +40 -42
- data/README.md +267 -121
- data/lib/nxt_schema.rb +60 -51
- data/lib/nxt_schema/callable.rb +21 -55
- data/lib/nxt_schema/dsl.rb +41 -31
- data/lib/nxt_schema/error.rb +4 -0
- data/lib/nxt_schema/errors/{error.rb → coercion_error.rb} +1 -2
- data/lib/nxt_schema/errors/invalid.rb +16 -0
- data/lib/nxt_schema/errors/invalid_options.rb +6 -0
- data/lib/nxt_schema/node/any_of.rb +39 -0
- data/lib/nxt_schema/node/base.rb +66 -267
- data/lib/nxt_schema/node/collection.rb +40 -56
- data/lib/nxt_schema/node/error_store.rb +41 -0
- data/lib/nxt_schema/node/errors/schema_error.rb +15 -0
- data/lib/nxt_schema/node/errors/validation_error.rb +15 -0
- data/lib/nxt_schema/node/leaf.rb +8 -36
- data/lib/nxt_schema/node/schema.rb +70 -103
- data/lib/nxt_schema/registry.rb +12 -74
- data/lib/nxt_schema/registry/proxy.rb +21 -0
- data/lib/nxt_schema/template/any_of.rb +50 -0
- data/lib/nxt_schema/template/base.rb +220 -0
- data/lib/nxt_schema/template/collection.rb +23 -0
- data/lib/nxt_schema/template/has_sub_nodes.rb +87 -0
- data/lib/nxt_schema/template/leaf.rb +13 -0
- data/lib/nxt_schema/template/maybe_evaluator.rb +28 -0
- data/lib/nxt_schema/template/on_evaluator.rb +25 -0
- data/lib/nxt_schema/template/schema.rb +22 -0
- data/lib/nxt_schema/template/sub_nodes.rb +22 -0
- data/lib/nxt_schema/template/type_resolver.rb +39 -0
- data/lib/nxt_schema/template/type_system_resolver.rb +22 -0
- data/lib/nxt_schema/types.rb +7 -4
- data/lib/nxt_schema/undefined.rb +4 -2
- data/lib/nxt_schema/validators/{equality.rb → equal_to.rb} +2 -2
- data/lib/nxt_schema/validators/error_messages.rb +42 -0
- data/lib/nxt_schema/{error_messages → validators/error_messages}/en.yaml +6 -5
- data/lib/nxt_schema/validators/{excluded.rb → excluded_in.rb} +1 -1
- data/lib/nxt_schema/validators/{included.rb → included_in.rb} +1 -1
- data/lib/nxt_schema/validators/includes.rb +1 -1
- data/lib/nxt_schema/validators/optional_node.rb +11 -6
- data/lib/nxt_schema/validators/registry.rb +1 -7
- data/lib/nxt_schema/{node → validators}/validate_with_proxy.rb +3 -3
- data/lib/nxt_schema/validators/validator.rb +2 -2
- data/lib/nxt_schema/version.rb +1 -1
- data/nxt_schema.gemspec +1 -0
- metadata +44 -21
- data/lib/nxt_schema/callable_or_value.rb +0 -72
- data/lib/nxt_schema/error_messages.rb +0 -40
- data/lib/nxt_schema/errors.rb +0 -4
- data/lib/nxt_schema/errors/invalid_options_error.rb +0 -5
- data/lib/nxt_schema/errors/schema_not_applied_error.rb +0 -5
- data/lib/nxt_schema/node/constructor.rb +0 -9
- data/lib/nxt_schema/node/default_value_evaluator.rb +0 -20
- data/lib/nxt_schema/node/error.rb +0 -13
- data/lib/nxt_schema/node/has_subnodes.rb +0 -97
- data/lib/nxt_schema/node/maybe_evaluator.rb +0 -23
- data/lib/nxt_schema/node/template_store.rb +0 -15
- data/lib/nxt_schema/node/type_resolver.rb +0 -24
data/lib/nxt_schema/registry.rb
CHANGED
@@ -1,85 +1,23 @@
|
|
1
1
|
module NxtSchema
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
70
|
-
|
13
|
+
super
|
14
|
+
end
|
71
15
|
end
|
72
16
|
|
73
|
-
def
|
74
|
-
|
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
|
-
|
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
|