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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '09685f892e03e96e13235c9f5d98c78ddbacd8118a47062c3a4417530cd2a3b0'
4
- data.tar.gz: ec1b647208395d39dc5f61639dcfdfa1ea65caadfa9cd9011414ed9a5a5d9430
3
+ metadata.gz: a4076ec4518cc51176684f4bb04fca5bcff3554835016f57cce322debf3e9b8c
4
+ data.tar.gz: 336acbce68af2023b714b2b023d732575138968279efebc74b984b51ff33de13
5
5
  SHA512:
6
- metadata.gz: 56489e75d59dc6f21f8237fcebc14236113c2e14cb47adb986989f819d0937353b1dd37763b522737923e3a87966eda2bf339f42055383fc8d174d536f1d32e5
7
- data.tar.gz: 1f595737e27ff2c874c807b2339eee3781799a64cac27bd3724642898db6200ec60279edd72b50f100d3a52ff4f8ef155e97392b8738186258d8fd93dbdd65b0
6
+ metadata.gz: 4947f05400dd492a8bed83ae2b0c42110c75ebd6fd495bdcdcef10871f7715c6d69797ab95291006df759945551c0f92869e29f5b03f0343af49f4bcf6aa3497
7
+ data.tar.gz: 8ed842b8144aacd64f880e3ff7c74c64a9ae218f598522a60f26fb7a0aa2829fb3deaaf93ea0b8600a6346adb6f921d2e56e2249d1ab18e4b5c6427d25bf8759
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.7.0
data/Gemfile CHANGED
@@ -1,6 +1,5 @@
1
1
  source "https://rubygems.org"
2
2
 
3
3
  git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
-
5
4
  # Specify your gem's dependencies in nxt_schema.gemspec
6
5
  gemspec
data/Gemfile.lock CHANGED
@@ -1,75 +1,73 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- nxt_schema (0.1.0)
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.0.2.1)
13
+ activesupport (6.1.3)
13
14
  concurrent-ruby (~> 1.0, >= 1.0.2)
14
- i18n (>= 0.7, < 2)
15
- minitest (~> 5.1)
16
- tzinfo (~> 1.1)
17
- zeitwerk (~> 2.2)
18
- coderay (1.1.2)
19
- concurrent-ruby (1.1.6)
20
- diff-lcs (1.3)
21
- dry-configurable (0.11.2)
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.4, >= 0.4.7)
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.4.9)
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.6)
31
+ dry-logic (1.1.0)
33
32
  concurrent-ruby (~> 1.0)
34
- dry-core (~> 0.2)
35
- dry-equalizer (~> 0.2)
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.4, >= 0.4.4)
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.2)
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.9.2)
49
- minitest (5.14.0)
50
- nxt_registry (0.1.4)
45
+ method_source (1.0.0)
46
+ minitest (5.14.4)
47
+ nxt_init (0.1.5)
51
48
  activesupport
52
- pry (0.12.2)
53
- coderay (~> 1.1.0)
54
- method_source (~> 0.9.0)
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.8.0)
57
- rspec-core (~> 3.8.0)
58
- rspec-expectations (~> 3.8.0)
59
- rspec-mocks (~> 3.8.0)
60
- rspec-core (3.8.2)
61
- rspec-support (~> 3.8.0)
62
- rspec-expectations (3.8.4)
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.8.0)
65
- rspec-mocks (3.8.1)
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.8.0)
68
- rspec-support (3.8.2)
69
- thread_safe (0.3.6)
70
- tzinfo (1.2.6)
71
- thread_safe (~> 0.1)
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
- ## Usage
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
- # Schema with hash root
23
- schema = NxtSchema.root(:company) do
24
- requires(:name, :String)
25
- requires(:value, :Integer).maybe(nil)
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
- schema.apply(your: 'values here')
54
- schema.errors # { 'name.spaced.key': ['all the errors'] }
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
- ### DSL
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
- Create a new schema with `NxtSchema.root { ... }` or in case you have an array node as root,
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
- #### Nodes
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.root do
68
- node(:first_name, :String)
69
- node(:last_name, :String, optional: true)
70
- node(:email, :String, presence: true)
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
- In order to make the schema more readable you can make use of several predicate aliases to create required, optional or
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
- #### Predicate aliases
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
- NxtSchema.root do
81
- required(:first_name, :String)
82
- optional(:last_name, :String)
83
- present(:email, :String)
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
- ### Nodes
106
+ ```ruby
107
+ # Optional node with default value
88
108
 
89
- The following types of nodes exist
109
+ schema = NxtSchema.schema(:person) do
110
+ optional(:email, :String).default('andreas@robecke.de')
111
+ end
90
112
 
91
- #### Schema Nodes
113
+ result = schema.apply(input: { email: nil })
114
+ result.errors # => {}
115
+ result.output # => {:email=>"andreas@robecke.de"}
92
116
 
93
- ```ruby
94
- # Create schema nodes with:
95
- required(:test, :Schema) do ... end
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
- #### Collection Nodes
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
- # Create collection (array) nodes with:
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
- nodes(:test) do
107
- # For type checking of array items you can simply add a node with the expected type.
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
- array(:test) do ... end
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
- #### Leaf Nodes
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
- # Create leaf nodes with a basic type
119
- required(:test, :String) do ... end
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
- #### Struct Nodes
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
- # Create structs from hash inputs
126
- struct(:test) do ... end
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/ eco system. Even though dry-types also
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. Dry.rb also has a gem for schemas and another one dedicated to validations. You should probably
134
- check those out! However, in NxtSchema every node has a type and you can either provide a symbol that will be resolved
135
- through the type system of the schema. But you can also directly provide an instance of dry type and thus use your
136
- custom types.
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, NxtSchema will again try to fallback to nominal types. In theory you can provide
143
- a separate type system per node if that's what you want :-D
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.root do
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.root(type_system: NxtSchema::Types::JSON) do
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::Params::Bool) # long version of required(:advanced, :Bool)
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.root(:company) do
184
- required(:name, NxtSchema::Types::MyCustomStrippedString)
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 as options or with the default method
194
- required(:test, :String).default(value_or_proc)
195
- required(:test, :String, default: value_or_proc) do ... end
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
- Allow specific values that are not being coerced
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(value_or_proc)
205
- required(:test, :String, maybe: value_or_proc) do ... end
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 -> (node, value) { node.add_error("#{value} is not valid") if value == 'not allowed' }
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(:equality, 'same')
220
- required(:test, :String).validate(:excluded, %w[not_allowed]) # excluded in the target: %w[not_allowed]
221
- required(:test, :String).validate(:included, %w[allowed]) # included in the target: %w[allowed]
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.root(:company) do
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 with custom logic
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 and errors aggregated. If you want your validator to only run in case
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.root do
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
- This has one drawback however. Let's say your test value is 4. This would only run your first validator and then exit
292
- from the logic since validators are combined with &&. In this example it might not make much sense, but it basically
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.root(additional_keys: :ignore) do
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.root(additional_keys: :restrict) do
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.root(additional_keys: :allow) do
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.root(transform_keys: :to_sym) do
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.root do
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/[USERNAME]/nxt_schema.
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
- # TODO:
371
-
372
- - Explain the difference between array nodes and typed array nodes
373
- - Should we translate coercion errors as well?
374
- - Test the different scenarios of merging schemas array, hash, ...
375
- - Structure Errors
376
- - NxtSchema::Json => Use json types, maybe even parse Json with Oj
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