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