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