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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a4076ec4518cc51176684f4bb04fca5bcff3554835016f57cce322debf3e9b8c
|
4
|
+
data.tar.gz: 336acbce68af2023b714b2b023d732575138968279efebc74b984b51ff33de13
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4947f05400dd492a8bed83ae2b0c42110c75ebd6fd495bdcdcef10871f7715c6d69797ab95291006df759945551c0f92869e29f5b03f0343af49f4bcf6aa3497
|
7
|
+
data.tar.gz: 8ed842b8144aacd64f880e3ff7c74c64a9ae218f598522a60f26fb7a0aa2829fb3deaaf93ea0b8600a6346adb6f921d2e56e2249d1ab18e4b5c6427d25bf8759
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.7.0
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,75 +1,73 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
nxt_schema (
|
4
|
+
nxt_schema (1.0.2)
|
5
5
|
activesupport
|
6
6
|
dry-types
|
7
|
+
nxt_init
|
7
8
|
nxt_registry
|
8
9
|
|
9
10
|
GEM
|
10
11
|
remote: https://rubygems.org/
|
11
12
|
specs:
|
12
|
-
activesupport (6.
|
13
|
+
activesupport (6.1.3)
|
13
14
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
14
|
-
i18n (>=
|
15
|
-
minitest (
|
16
|
-
tzinfo (~>
|
17
|
-
zeitwerk (~> 2.
|
18
|
-
coderay (1.1.
|
19
|
-
concurrent-ruby (1.1.
|
20
|
-
diff-lcs (1.
|
21
|
-
dry-configurable (0.
|
15
|
+
i18n (>= 1.6, < 2)
|
16
|
+
minitest (>= 5.1)
|
17
|
+
tzinfo (~> 2.0)
|
18
|
+
zeitwerk (~> 2.3)
|
19
|
+
coderay (1.1.3)
|
20
|
+
concurrent-ruby (1.1.8)
|
21
|
+
diff-lcs (1.4.4)
|
22
|
+
dry-configurable (0.12.1)
|
22
23
|
concurrent-ruby (~> 1.0)
|
23
|
-
dry-core (~> 0.
|
24
|
-
dry-equalizer (~> 0.2)
|
24
|
+
dry-core (~> 0.5, >= 0.5.0)
|
25
25
|
dry-container (0.7.2)
|
26
26
|
concurrent-ruby (~> 1.0)
|
27
27
|
dry-configurable (~> 0.1, >= 0.1.3)
|
28
|
-
dry-core (0.
|
28
|
+
dry-core (0.5.0)
|
29
29
|
concurrent-ruby (~> 1.0)
|
30
|
-
dry-equalizer (0.3.0)
|
31
30
|
dry-inflector (0.2.0)
|
32
|
-
dry-logic (1.0
|
31
|
+
dry-logic (1.1.0)
|
33
32
|
concurrent-ruby (~> 1.0)
|
34
|
-
dry-core (~> 0.
|
35
|
-
|
36
|
-
dry-types (1.3.1)
|
33
|
+
dry-core (~> 0.5, >= 0.5)
|
34
|
+
dry-types (1.5.1)
|
37
35
|
concurrent-ruby (~> 1.0)
|
38
36
|
dry-container (~> 0.3)
|
39
|
-
dry-core (~> 0.
|
40
|
-
dry-equalizer (~> 0.3)
|
37
|
+
dry-core (~> 0.5, >= 0.5)
|
41
38
|
dry-inflector (~> 0.1, >= 0.1.2)
|
42
39
|
dry-logic (~> 1.0, >= 1.0.2)
|
43
40
|
hirb (0.7.3)
|
44
|
-
i18n (1.8.
|
41
|
+
i18n (1.8.9)
|
45
42
|
concurrent-ruby (~> 1.0)
|
46
43
|
method_profiler (2.0.1)
|
47
44
|
hirb (>= 0.6.0)
|
48
|
-
method_source (0.
|
49
|
-
minitest (5.14.
|
50
|
-
|
45
|
+
method_source (1.0.0)
|
46
|
+
minitest (5.14.4)
|
47
|
+
nxt_init (0.1.5)
|
51
48
|
activesupport
|
52
|
-
|
53
|
-
|
54
|
-
|
49
|
+
nxt_registry (0.3.9)
|
50
|
+
activesupport
|
51
|
+
pry (0.13.1)
|
52
|
+
coderay (~> 1.1)
|
53
|
+
method_source (~> 1.0)
|
55
54
|
rake (12.3.3)
|
56
|
-
rspec (3.
|
57
|
-
rspec-core (~> 3.
|
58
|
-
rspec-expectations (~> 3.
|
59
|
-
rspec-mocks (~> 3.
|
60
|
-
rspec-core (3.
|
61
|
-
rspec-support (~> 3.
|
62
|
-
rspec-expectations (3.
|
55
|
+
rspec (3.10.0)
|
56
|
+
rspec-core (~> 3.10.0)
|
57
|
+
rspec-expectations (~> 3.10.0)
|
58
|
+
rspec-mocks (~> 3.10.0)
|
59
|
+
rspec-core (3.10.0)
|
60
|
+
rspec-support (~> 3.10.0)
|
61
|
+
rspec-expectations (3.10.0)
|
63
62
|
diff-lcs (>= 1.2.0, < 2.0)
|
64
|
-
rspec-support (~> 3.
|
65
|
-
rspec-mocks (3.
|
63
|
+
rspec-support (~> 3.10.0)
|
64
|
+
rspec-mocks (3.10.0)
|
66
65
|
diff-lcs (>= 1.2.0, < 2.0)
|
67
|
-
rspec-support (~> 3.
|
68
|
-
rspec-support (3.
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
zeitwerk (2.2.2)
|
66
|
+
rspec-support (~> 3.10.0)
|
67
|
+
rspec-support (3.10.0)
|
68
|
+
tzinfo (2.0.4)
|
69
|
+
concurrent-ruby (~> 1.0)
|
70
|
+
zeitwerk (2.4.2)
|
73
71
|
|
74
72
|
PLATFORMS
|
75
73
|
ruby
|
data/README.md
CHANGED
@@ -16,138 +16,200 @@ Or install it yourself as:
|
|
16
16
|
|
17
17
|
$ gem install nxt_schema
|
18
18
|
|
19
|
-
##
|
19
|
+
## What is it for?
|
20
|
+
|
21
|
+
NxtSchema is a type coercion and validation framework that allows you to coerce and validate arbitrary nested
|
22
|
+
structures of data. The original idea is taken from https://dry-rb.org/gems/dry-schema and
|
23
|
+
https://dry-rb.org/gems/dry-validation from the amazing dry.rb eco system. In contrast to dry-schema,
|
24
|
+
NxtSchema aims to be a simpler solution that hopefully is easier to understand and debug.
|
25
|
+
It also ships with some handy features that dry-schema does not implement.
|
26
|
+
|
27
|
+
### Usage
|
20
28
|
|
21
29
|
```ruby
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
present(:stock_options, :Bool).default(false)
|
27
|
-
|
28
|
-
schema(:address) do
|
29
|
-
requires(:street, :String)
|
30
|
-
requires(:street_number, :Integer)
|
31
|
-
end
|
32
|
-
|
33
|
-
nodes(:employees) do
|
34
|
-
hash(:employee) do
|
35
|
-
POSITIONS = %w[senior junior intern]
|
36
|
-
|
37
|
-
requires(:first_name, :String)
|
38
|
-
requires(:last_name, :String)
|
39
|
-
optional(:email, :String).validate(:format, /\A.*@.*\z/)
|
40
|
-
requires(:position, NxtSchema::Types::Enums[*POSITIONS])
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
# Schema with array root
|
46
|
-
schema = NxtSchema.roots(:companies) do
|
47
|
-
schema(:company) do
|
48
|
-
requires(:name, :String)
|
49
|
-
requires(:value, :Integer).maybe(nil)
|
50
|
-
end
|
30
|
+
PERSON = NxtSchema.schema(:person) do
|
31
|
+
node(:first_name, :String)
|
32
|
+
node(:last_name, :String)
|
33
|
+
node(:email, :String, optional: true).validate(:includes, '@')
|
51
34
|
end
|
52
35
|
|
53
|
-
|
54
|
-
|
36
|
+
input = {
|
37
|
+
first_name: 'Ändy',
|
38
|
+
last_name: 'Robecke',
|
39
|
+
email: 'andreas@robecke.de'
|
40
|
+
}
|
41
|
+
|
42
|
+
result = PERSON.apply(input: input)
|
43
|
+
|
44
|
+
result.valid? # => true
|
45
|
+
result.output # => input
|
46
|
+
```
|
47
|
+
|
48
|
+
### Nodes
|
49
|
+
|
50
|
+
A schema consists of a number of nodes. Every node has a name and an associated type for casting it's input when the
|
51
|
+
schema is applied. Schemas can consist of 4 different kinds of nodes:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
NxtSchema::Node::Schema # => Hash of values
|
55
|
+
NxtSchema::Node::Collection # => Array of values
|
56
|
+
NxtSchema::Node::AnyOf # => Any of the defined schemas
|
57
|
+
NxtSchema::Node::Leaf # => Node without sub nodes
|
55
58
|
```
|
56
59
|
|
57
|
-
|
60
|
+
The kind of node dictates how the schema is applied to the input. On the root level the following methods are available
|
61
|
+
to create schemas:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
NxtSchema.schema { ... } # => Creates a schema node
|
65
|
+
NxtSchema.collection { ... } # => Creates an array of nodes
|
66
|
+
NxtSchema.any_of { ... } # => Creates a collection of allowed schemas
|
67
|
+
```
|
58
68
|
|
59
|
-
|
60
|
-
use `NxtSchema.roots { ... }`. Within the schema you can create node simply with the `node(name, type_or_node, **options)`
|
61
|
-
method. Each node requires a name and a type and accepts additional options. Node are required per default.
|
62
|
-
But you can make them optional by providing the optional option.
|
69
|
+
#### Node predicate aliases
|
63
70
|
|
64
|
-
|
71
|
+
Of course these nodes can be combined and nested in arbitrary manner. When defining nodes within a schema, nodes are
|
72
|
+
always required per default. You can create nodes with the node method or several useful helper methods.
|
65
73
|
|
66
74
|
```ruby
|
67
|
-
NxtSchema.
|
68
|
-
node(:first_name, :String)
|
69
|
-
|
70
|
-
|
75
|
+
NxtSchema.schema(:person) do
|
76
|
+
required(:first_name, :String) # => same as node(:first_name, :String)
|
77
|
+
optional(:last_name, :String) # => same as node(:first_name, :String, optional: true)
|
78
|
+
omnipresent(:email, :String) # => same as node(:first_name, :String, omnipresent: true)
|
71
79
|
end
|
72
80
|
```
|
73
81
|
|
74
|
-
|
75
|
-
(omni)present nodes.
|
82
|
+
**NOTE: The methods above only apply to the keys of your schema and do not make any assumptions about values!**
|
76
83
|
|
77
|
-
|
84
|
+
In other word this means that making a node optional only makes your node optional. When your input contains the key but
|
85
|
+
the value is nil, you will still get an error in case there is no default or maybe expression that applies. Omnipresent
|
86
|
+
node also only inject the node into the schema but do not inject a default value. In order to inject a key with value
|
87
|
+
into a schema you also have to combine the node predicates with default value method described below. For clarification
|
88
|
+
check out the examples below:
|
78
89
|
|
79
90
|
```ruby
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
91
|
+
# Optional node without default value
|
92
|
+
|
93
|
+
schema = NxtSchema.schema(:person) do
|
94
|
+
optional(:email, :String)
|
84
95
|
end
|
96
|
+
|
97
|
+
result = schema.apply(input: { email: nil })
|
98
|
+
result.errors # => {"person.email"=>["nil violates constraints (type?(String, nil) failed)"]}
|
99
|
+
result.output # => {:email=>nil}
|
100
|
+
|
101
|
+
result = schema.apply(input: {})
|
102
|
+
result.errors # => {}
|
103
|
+
result.output # => {}
|
85
104
|
```
|
86
105
|
|
87
|
-
|
106
|
+
```ruby
|
107
|
+
# Optional node with default value
|
88
108
|
|
89
|
-
|
109
|
+
schema = NxtSchema.schema(:person) do
|
110
|
+
optional(:email, :String).default('andreas@robecke.de')
|
111
|
+
end
|
90
112
|
|
91
|
-
|
113
|
+
result = schema.apply(input: { email: nil })
|
114
|
+
result.errors # => {}
|
115
|
+
result.output # => {:email=>"andreas@robecke.de"}
|
92
116
|
|
93
|
-
|
94
|
-
#
|
95
|
-
|
96
|
-
schema(:test) do ... end
|
97
|
-
hash(:test) do ... end
|
117
|
+
result = schema.apply(input: {})
|
118
|
+
result.errors # => {}
|
119
|
+
result.output # => {}
|
98
120
|
```
|
99
121
|
|
100
|
-
|
122
|
+
```ruby
|
123
|
+
# Omnipresent node without default value
|
124
|
+
|
125
|
+
schema = NxtSchema.schema(:person) do
|
126
|
+
omnipresent(:email, :String)
|
127
|
+
end
|
128
|
+
|
129
|
+
result = schema.apply(input: {})
|
130
|
+
result.errors # => {}
|
131
|
+
result.output # => {:email=>NxtSchema::Undefined}
|
132
|
+
```
|
101
133
|
|
102
134
|
```ruby
|
103
|
-
#
|
104
|
-
required(:test, :Collection) do ... end
|
135
|
+
# Omnipresent node with default value and maybe expression to allow default value to break type contract.
|
105
136
|
|
106
|
-
|
107
|
-
|
108
|
-
# As always you need to give it a name. This would result in an array of string items
|
109
|
-
required(:item, :String)
|
137
|
+
schema = NxtSchema.schema(:person) do
|
138
|
+
omnipresent(:email, :String).default(nil).maybe(:nil?)
|
110
139
|
end
|
111
140
|
|
112
|
-
|
141
|
+
result = schema.apply(input: {})
|
142
|
+
result.errors # => {}
|
143
|
+
result.output # => {:email=>nil}
|
144
|
+
|
145
|
+
result = schema.apply(input: { email: 'andreas@robecke.de' })
|
146
|
+
result.errors # => {}
|
147
|
+
result.output # => {:email=>"andreas@robecke.de"}
|
113
148
|
```
|
114
149
|
|
115
|
-
|
150
|
+
##### Conditionally optional nodes
|
151
|
+
|
152
|
+
You can also pass a proc as the optional option. This is a shortcut for adding a validation to the parent node
|
153
|
+
that will result in a validation error in case the optional condition does not apply and the parent node does not
|
154
|
+
contain a sub node with that name (here contact schema not including an email node).
|
116
155
|
|
117
156
|
```ruby
|
118
|
-
|
119
|
-
required(:
|
120
|
-
|
157
|
+
schema = NxtSchema.schema(:contact) do
|
158
|
+
required(:first_name, :String)
|
159
|
+
required(:last_name, :String)
|
160
|
+
node(:email, :String, optional: ->(node) { node.up[:last_name].input == 'Robecke' })
|
161
|
+
end
|
162
|
+
|
163
|
+
result = schema.apply(input: { first_name: 'Andy', last_name: 'Other' })
|
164
|
+
result.errors # => {"contact"=>["Required key :email is missing"]}
|
165
|
+
|
166
|
+
result = schema.apply(input: { first_name: 'Andy', last_name: 'Robecke' })
|
167
|
+
result.errors # => {}
|
168
|
+
```
|
169
|
+
|
170
|
+
#### Combining Schemas
|
121
171
|
|
122
|
-
|
172
|
+
You can also simply reuse a schema by passing it to the node method as the type of a node. When doing so the schema
|
173
|
+
will be cloned with the same options and configuration as the schema passed in.
|
123
174
|
|
124
175
|
```ruby
|
125
|
-
|
126
|
-
|
176
|
+
ADDRESS = NxtSchema.schema(:address) do
|
177
|
+
required(:street, :String)
|
178
|
+
required(:town, :String)
|
179
|
+
required(:zip_code, :String)
|
180
|
+
end
|
181
|
+
|
182
|
+
PERSON = NxtSchema.schema(:person) do
|
183
|
+
required(:first_name, :String)
|
184
|
+
required(:last_name, :String)
|
185
|
+
optional(:address, ADDRESS)
|
186
|
+
end
|
127
187
|
```
|
128
188
|
|
129
189
|
### Types
|
130
190
|
|
131
|
-
The type system is built with dry-types from the amazing https://dry-rb.org
|
191
|
+
The type system is built with dry-types from the amazing https://dry-rb.org eco system. Even though dry-types also
|
132
192
|
offers features such as default values for types as well as maybe types, these features are built directly into
|
133
|
-
NxtSchema.
|
134
|
-
|
135
|
-
|
136
|
-
|
193
|
+
NxtSchema.
|
194
|
+
|
195
|
+
In NxtSchema every node has a type and you can either provide a symbol that will be resolved
|
196
|
+
through the type system of the schema or you can directly provide an instance of dry type and thus use your
|
197
|
+
custom types. This means you can basically build any kind of objects such as structs and models from your data and
|
198
|
+
you are not limited to just hashes arrays and primitives.
|
137
199
|
|
138
200
|
#### Default type system
|
139
201
|
|
140
202
|
You can tell your schema which default type system it should use. Dry-Types comes with a few built in type systems.
|
141
203
|
Per default NxtSchema will use nominal types if not specified otherwise. If the type cannot be resolved from the default
|
142
|
-
type system that was specified
|
143
|
-
a separate type system per node if that's what you
|
204
|
+
type system that was specified NxtSchema will always fallback to nominal types. In theory you can provide
|
205
|
+
a separate type system per node if that's what you need.
|
144
206
|
|
145
207
|
```ruby
|
146
|
-
NxtSchema.
|
208
|
+
NxtSchema.schema do
|
147
209
|
required(:test, :String) # The :String will resolve to NxtSchema::Types::Nominal::String
|
148
210
|
end
|
149
211
|
|
150
|
-
NxtSchema.
|
212
|
+
NxtSchema.schema(type_system: NxtSchema::Types::JSON) do
|
151
213
|
required(:test, :Date) # The :Date will resolve to NxtSchema::Types::JSON::Date
|
152
214
|
# When the type does not exist in the default type system (there is non JSON::String) we fallback to nominal types
|
153
215
|
required(:test, :String)
|
@@ -163,10 +225,42 @@ This is suitable to validate and coerce your query params.
|
|
163
225
|
NxtSchema.params do
|
164
226
|
required(:effective_at, :DateTime) # would resolve to Types::Params::DateTime
|
165
227
|
required(:test, :String) # The :String will resolve to NxtSchema::Types::Nominal::String
|
166
|
-
required(:advanced, NxtSchema::Types::
|
228
|
+
required(:advanced, NxtSchema::Types::Registry::Bool) # long version of required(:advanced, :Bool)
|
167
229
|
end
|
168
230
|
```
|
169
231
|
|
232
|
+
#### NxtSchema::Registry
|
233
|
+
|
234
|
+
To make use of NxtSchema.params in your controller you can simply include the `NxtSchema::Registry` to easily register
|
235
|
+
and apply schemas:
|
236
|
+
|
237
|
+
```ruby
|
238
|
+
class UsersController < ApplicationController
|
239
|
+
include NxtSchema::Registry
|
240
|
+
|
241
|
+
# register the schema for the :create action
|
242
|
+
schemas.register(
|
243
|
+
:create,
|
244
|
+
NxtSchema.params do
|
245
|
+
required(:first_name, :String)
|
246
|
+
required(:last_name, :String)
|
247
|
+
end
|
248
|
+
)
|
249
|
+
|
250
|
+
def create
|
251
|
+
User.create!(**create_params)
|
252
|
+
end
|
253
|
+
|
254
|
+
private
|
255
|
+
|
256
|
+
def create_params
|
257
|
+
# apply the registered schema
|
258
|
+
schemas.apply!(:create, params.fetch(:user))
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
```
|
263
|
+
|
170
264
|
#### Custom types
|
171
265
|
|
172
266
|
You can also register custom types. In order to check out all the cool things you can do with dry types you should
|
@@ -180,8 +274,8 @@ NxtSchema.register_type(
|
|
180
274
|
|
181
275
|
# once registered you can use the type in your schema
|
182
276
|
|
183
|
-
NxtSchema.
|
184
|
-
required(:name,
|
277
|
+
NxtSchema.schema(:company) do
|
278
|
+
required(:name, :MyCustomStrippedString)
|
185
279
|
end
|
186
280
|
```
|
187
281
|
|
@@ -190,35 +284,44 @@ end
|
|
190
284
|
#### Default values
|
191
285
|
|
192
286
|
```ruby
|
193
|
-
# Define default values
|
194
|
-
required(:test, :
|
195
|
-
required(:test, :
|
287
|
+
# Define default values with the default method
|
288
|
+
required(:test, :DateTime).default(nil)
|
289
|
+
required(:test, :DateTime).default(-> { Time.current })
|
196
290
|
```
|
197
291
|
|
198
292
|
#### Maybe values
|
199
293
|
|
200
|
-
|
294
|
+
With maybe expressions you can halt coercion and allow your values to break the type contract.
|
295
|
+
**Note: This means that your output will simply be set to the input without coercing the value!**
|
201
296
|
|
202
297
|
```ruby
|
203
298
|
# Define maybe values (values that do not match the type)
|
204
|
-
required(:test, :String).maybe(
|
205
|
-
|
299
|
+
required(:test, :String).maybe(:nil?)
|
300
|
+
|
301
|
+
nodes(:tests).maybe(:empty?) do # will allow the collection to be empty and thus not contain strings
|
302
|
+
required(:test, :String)
|
303
|
+
end
|
304
|
+
|
206
305
|
```
|
207
306
|
|
208
307
|
### Validations
|
209
308
|
|
210
309
|
NxtSchema comes with a simple validation system and ships with a small set of useful validators. Every node in a schema
|
211
310
|
implements the `:validate` method. Similar to ActiveModel::Validations it allows you to simply add errors to a node
|
212
|
-
based on some condition.
|
311
|
+
based on some condition. When the node is yielded to your validation proc you have access to the nodes input with
|
312
|
+
`node.input` and `node.index` when the node is within a collection of nodes as well as `node.name`. Furthermore you have
|
313
|
+
access to the context that was passed in when defining the schema or passed to the apply method later.
|
314
|
+
|
315
|
+
**NOTE: Validations only run when no maybe expression applies and the node input could be coerced successfully**
|
213
316
|
|
214
317
|
```ruby
|
215
|
-
# Simple validation
|
216
|
-
required(:test, :String).validate
|
318
|
+
# Simple custom validation
|
319
|
+
required(:test, :String).validate(-> (node) { node.add_error("#{node.input} is not valid") if node.input == 'not allowed' })
|
217
320
|
# Built in validations
|
218
321
|
required(:test, :String).validate(:attribute, :size, ->(s) { s < 7 })
|
219
|
-
required(:test, :String).validate(:
|
220
|
-
required(:test, :String).validate(:
|
221
|
-
required(:test, :String).validate(:
|
322
|
+
required(:test, :String).validate(:equal_to, 'same')
|
323
|
+
required(:test, :String).validate(:excluded_in, %w[not_allowed]) # excluded in the target: %w[not_allowed]
|
324
|
+
required(:test, :String).validate(:included_in, %w[allowed]) # included in the target: %w[allowed]
|
222
325
|
required(:test, :Array).validate(:excludes, 'excluded') # array value itself must exclude 'excluded'
|
223
326
|
required(:test, :Array).validate(:includes, 'included') # array value itself must include 'included'
|
224
327
|
required(:test, :Integer).validate(:greater_than, 1)
|
@@ -259,7 +362,7 @@ end
|
|
259
362
|
NxtSchema.register_validator(MyCustomExclusionValidator, :my_custom_exclusion_validator)
|
260
363
|
|
261
364
|
# and then simply reference it with the key you've registered it
|
262
|
-
schema = NxtSchema.
|
365
|
+
schema = NxtSchema.schema(:company) do
|
263
366
|
requires(:name, :String).validate(:my_custom_exclusion_validator, %w[lemonade])
|
264
367
|
end
|
265
368
|
|
@@ -272,25 +375,24 @@ schema.apply(name: 'lemonade').valid? # => false
|
|
272
375
|
- Add translated errors
|
273
376
|
- Interpolate with actual vs. expected
|
274
377
|
|
275
|
-
#### Combining validators
|
378
|
+
#### Combining validators
|
276
379
|
|
277
380
|
`node(:test, String).validate(...)` basically adds a validator to the node. Of course you can add multiple validators.
|
278
|
-
But that means that they will all be executed
|
381
|
+
But that means that they will all be executed. If you want your validator to only run in case
|
279
382
|
another was false, you can use `:validat_with do ... end` in order to combine validators based on custom logic.
|
280
383
|
|
281
384
|
```ruby
|
282
|
-
NxtSchema.
|
385
|
+
NxtSchema.schema do
|
283
386
|
required(:test, :Integer).validate_with do
|
284
387
|
validator(:greater_than, 5) &&
|
285
|
-
validator(:greater_than, 6)
|
388
|
+
validator(:greater_than, 6) ||
|
286
389
|
validator(:greater_than, 7)
|
287
390
|
end
|
288
391
|
end
|
289
392
|
```
|
290
393
|
|
291
|
-
|
292
|
-
|
293
|
-
means that you might not have the full validation errors when combining validations with `:validate_with`
|
394
|
+
Note that this will not run subsequent validators once one was valuated to false and thus might not contain all error
|
395
|
+
messages of all validators that would have failed.
|
294
396
|
|
295
397
|
|
296
398
|
### Schema options
|
@@ -302,21 +404,21 @@ You can change this behaviour by providing a strategy for the `:additional_keys`
|
|
302
404
|
|
303
405
|
```ruby
|
304
406
|
# This will simply ignore any other key except test
|
305
|
-
NxtSchema.
|
407
|
+
NxtSchema.schema(additional_keys: :ignore) do
|
306
408
|
required(:test, :String)
|
307
409
|
end
|
308
410
|
|
309
411
|
# This would give you an error in case you apply anything other than { test: '...' }
|
310
|
-
NxtSchema.
|
412
|
+
NxtSchema.schema(additional_keys: :restrict) do
|
311
413
|
required(:test, :String)
|
312
414
|
end
|
313
415
|
|
314
416
|
# This will merge other keys into your output
|
315
|
-
schema = NxtSchema.
|
417
|
+
schema = NxtSchema.schema(additional_keys: :allow) do
|
316
418
|
required(:test, :String)
|
317
419
|
end
|
318
420
|
|
319
|
-
schema.apply(test: 'getsafe', other: 'Heidelberg')
|
421
|
+
schema.apply(input: {test: 'getsafe', other: 'Heidelberg'})
|
320
422
|
schema.valid? # => true
|
321
423
|
schema.value # => { test: 'getsafe', other: 'Heidelberg' }
|
322
424
|
```
|
@@ -327,12 +429,12 @@ You may want to transform the keys from your input. Therefore specify the transf
|
|
327
429
|
when you want your schema to return only symbolized keys for example.
|
328
430
|
|
329
431
|
```ruby
|
330
|
-
schema = NxtSchema.
|
432
|
+
schema = NxtSchema.schema(transform_keys: ->(key) { key.to_sym}) do
|
331
433
|
required(:test, :String)
|
332
434
|
end
|
333
435
|
|
334
|
-
schema.apply('test' => 'getsafe') # => {:test=>"getsafe"}
|
335
|
-
schema.apply(test: 'getsafe') # => {:test=>"getsafe"}
|
436
|
+
schema.apply(input: { 'test' => 'getsafe' }) # => {:test=>"getsafe"}
|
437
|
+
schema.apply(input: { test: 'getsafe' }) # => {:test=>"getsafe"}
|
336
438
|
```
|
337
439
|
|
338
440
|
#### Adding meta data to nodes
|
@@ -341,7 +443,7 @@ You want to give nodes an ID or some other meta data? You can use the meta metho
|
|
341
443
|
information onto any node.
|
342
444
|
|
343
445
|
```ruby
|
344
|
-
schema = NxtSchema.
|
446
|
+
schema = NxtSchema.schema do
|
345
447
|
ERROR_MESSAGES = {
|
346
448
|
test: 'This is always broken'
|
347
449
|
}
|
@@ -349,10 +451,49 @@ schema = NxtSchema.root do
|
|
349
451
|
required(:test, :String).meta(ERROR_MESSAGES).validate ->(node) { node.add_error(node.meta.fetch(node.name)) }
|
350
452
|
end
|
351
453
|
|
352
|
-
schema.apply(test: 'getsafe')
|
454
|
+
schema.apply(input: { test: 'getsafe' })
|
353
455
|
schema.error # {"root.test"=>["This is always broken"]}
|
354
456
|
```
|
355
457
|
|
458
|
+
#### Contexts
|
459
|
+
|
460
|
+
When defining a schema it is possible to pass in a context option. This can be anything that you would like to access
|
461
|
+
during building your schema. A context could provide custom validators or default values depending of the name of your
|
462
|
+
nodes for instance.
|
463
|
+
|
464
|
+
##### Build time
|
465
|
+
|
466
|
+
```ruby
|
467
|
+
context = OpenStruct.new(email_validator: ->(node) { node.input && node.input.includes?('@') })
|
468
|
+
|
469
|
+
NxtSchema.schema(:developers, context: context) do
|
470
|
+
required(:first_name, :String)
|
471
|
+
required(:last_name, :String)
|
472
|
+
required(:email, :String).validate(context.email_validator)
|
473
|
+
end
|
474
|
+
```
|
475
|
+
|
476
|
+
##### Apply time
|
477
|
+
|
478
|
+
You can also pass in a context at apply time. If you do not pass in a specific
|
479
|
+
context at apply time you can still access the context passed in at build time.
|
480
|
+
Basically passing in a context at apply time will overwrite the context from before. You can access it simply through
|
481
|
+
the node.
|
482
|
+
|
483
|
+
```ruby
|
484
|
+
build_context = OpenStruct.new(email_validator: ->(node) { node.input.includes?('@') })
|
485
|
+
apply_context = OpenStruct.new(default_role: 'BOSS')
|
486
|
+
|
487
|
+
schema = NxtSchema.schema(:developers, context: build_context) do
|
488
|
+
# context at build time
|
489
|
+
required(:email, :String).validate(context.email_validator) #
|
490
|
+
# access the context at apply time through the node
|
491
|
+
required(:role, :String).default { |_, node| node.context.default_role }
|
492
|
+
end
|
493
|
+
|
494
|
+
schema.apply(input: input, context: apply_context)
|
495
|
+
```
|
496
|
+
|
356
497
|
## Development
|
357
498
|
|
358
499
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -361,16 +502,21 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
361
502
|
|
362
503
|
## Contributing
|
363
504
|
|
364
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
505
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/getand.
|
365
506
|
|
366
507
|
## License
|
367
508
|
|
368
509
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
369
510
|
|
370
|
-
|
371
|
-
|
372
|
-
- Explain
|
373
|
-
-
|
374
|
-
-
|
375
|
-
|
376
|
-
-
|
511
|
+
## TODO:
|
512
|
+
|
513
|
+
- Explain node interface
|
514
|
+
- Add apply! method to readme
|
515
|
+
- Allow to disable validation when applying
|
516
|
+
--> Are there attributes that should be moved to apply time?
|
517
|
+
- Should we have a global and a local registry for validators?
|
518
|
+
--> Would be cool to register things for the schema only
|
519
|
+
--> Would be cool if this was extendable
|
520
|
+
- Do we need all off in order to combine multiple schemas?
|
521
|
+
- Allow custom errors
|
522
|
+
- Spec inheritance of params
|