stannum 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +21 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/DEVELOPMENT.md +105 -0
- data/LICENSE +22 -0
- data/README.md +1327 -0
- data/config/locales/en.rb +47 -0
- data/lib/stannum/attribute.rb +115 -0
- data/lib/stannum/constraint.rb +65 -0
- data/lib/stannum/constraints/absence.rb +42 -0
- data/lib/stannum/constraints/anything.rb +28 -0
- data/lib/stannum/constraints/base.rb +285 -0
- data/lib/stannum/constraints/boolean.rb +33 -0
- data/lib/stannum/constraints/delegator.rb +71 -0
- data/lib/stannum/constraints/enum.rb +64 -0
- data/lib/stannum/constraints/equality.rb +47 -0
- data/lib/stannum/constraints/hashes/extra_keys.rb +126 -0
- data/lib/stannum/constraints/hashes/indifferent_key.rb +74 -0
- data/lib/stannum/constraints/hashes.rb +11 -0
- data/lib/stannum/constraints/identity.rb +46 -0
- data/lib/stannum/constraints/nothing.rb +28 -0
- data/lib/stannum/constraints/presence.rb +42 -0
- data/lib/stannum/constraints/signature.rb +92 -0
- data/lib/stannum/constraints/signatures/map.rb +17 -0
- data/lib/stannum/constraints/signatures/tuple.rb +17 -0
- data/lib/stannum/constraints/signatures.rb +11 -0
- data/lib/stannum/constraints/tuples/extra_items.rb +84 -0
- data/lib/stannum/constraints/tuples.rb +10 -0
- data/lib/stannum/constraints/type.rb +113 -0
- data/lib/stannum/constraints/types/array_type.rb +148 -0
- data/lib/stannum/constraints/types/big_decimal_type.rb +16 -0
- data/lib/stannum/constraints/types/date_time_type.rb +16 -0
- data/lib/stannum/constraints/types/date_type.rb +16 -0
- data/lib/stannum/constraints/types/float_type.rb +14 -0
- data/lib/stannum/constraints/types/hash_type.rb +205 -0
- data/lib/stannum/constraints/types/hash_with_indifferent_keys.rb +21 -0
- data/lib/stannum/constraints/types/hash_with_string_keys.rb +21 -0
- data/lib/stannum/constraints/types/hash_with_symbol_keys.rb +21 -0
- data/lib/stannum/constraints/types/integer_type.rb +14 -0
- data/lib/stannum/constraints/types/nil_type.rb +20 -0
- data/lib/stannum/constraints/types/proc_type.rb +14 -0
- data/lib/stannum/constraints/types/string_type.rb +14 -0
- data/lib/stannum/constraints/types/symbol_type.rb +14 -0
- data/lib/stannum/constraints/types/time_type.rb +14 -0
- data/lib/stannum/constraints/types.rb +25 -0
- data/lib/stannum/constraints/union.rb +85 -0
- data/lib/stannum/constraints.rb +26 -0
- data/lib/stannum/contract.rb +243 -0
- data/lib/stannum/contracts/array_contract.rb +108 -0
- data/lib/stannum/contracts/base.rb +597 -0
- data/lib/stannum/contracts/builder.rb +72 -0
- data/lib/stannum/contracts/definition.rb +74 -0
- data/lib/stannum/contracts/hash_contract.rb +136 -0
- data/lib/stannum/contracts/indifferent_hash_contract.rb +78 -0
- data/lib/stannum/contracts/map_contract.rb +199 -0
- data/lib/stannum/contracts/parameters/arguments_contract.rb +185 -0
- data/lib/stannum/contracts/parameters/keywords_contract.rb +174 -0
- data/lib/stannum/contracts/parameters/signature_contract.rb +29 -0
- data/lib/stannum/contracts/parameters.rb +15 -0
- data/lib/stannum/contracts/parameters_contract.rb +530 -0
- data/lib/stannum/contracts/tuple_contract.rb +213 -0
- data/lib/stannum/contracts.rb +19 -0
- data/lib/stannum/errors.rb +730 -0
- data/lib/stannum/messages/default_strategy.rb +124 -0
- data/lib/stannum/messages.rb +25 -0
- data/lib/stannum/parameter_validation.rb +216 -0
- data/lib/stannum/rspec/match_errors.rb +17 -0
- data/lib/stannum/rspec/match_errors_matcher.rb +93 -0
- data/lib/stannum/rspec/validate_parameter.rb +23 -0
- data/lib/stannum/rspec/validate_parameter_matcher.rb +506 -0
- data/lib/stannum/rspec.rb +8 -0
- data/lib/stannum/schema.rb +131 -0
- data/lib/stannum/struct.rb +444 -0
- data/lib/stannum/support/coercion.rb +114 -0
- data/lib/stannum/support/optional.rb +69 -0
- data/lib/stannum/support.rb +8 -0
- data/lib/stannum/version.rb +57 -0
- data/lib/stannum.rb +27 -0
- metadata +216 -0
data/README.md
ADDED
@@ -0,0 +1,1327 @@
|
|
1
|
+
# Stannum
|
2
|
+
|
3
|
+
A library for defining and validating data structures.
|
4
|
+
|
5
|
+
Stannum defines the following objects:
|
6
|
+
|
7
|
+
- [Constraints](#constraints): A validator object that responds to `#match`, `#matches?` and `#errors_for` for a given object.
|
8
|
+
- [Contracts](#contracts): A collection of constraints about an object or its properties. Obeys the `Constraint` interface.
|
9
|
+
- [Errors](#errors): Data object for storing validation errors. Supports arbitrary nesting of errors.
|
10
|
+
- [Structs](#structs): Defines a mutable data object with a specified set of typed attributes.
|
11
|
+
|
12
|
+
## About
|
13
|
+
|
14
|
+
Stannum provides a framework-independent toolkit for defining structured data entities and validations. It provides a middle ground between unstructured data (raw `Hash`es, `Structs`, or libraries like `Hashie`) and full frameworks like `ActiveModel`.
|
15
|
+
|
16
|
+
First and foremost, Stannum provides you with the tools to validate your data. Using a `Stannum::Constraint`, you can apply your validation logic to literally any object, whether pure Ruby or from any framework or toolkit. Stannum provides a range of pre-defined constraints, including constraints for validating object types, defined methods, and more. You can also define custom constraints for any check that can output either `true` or `false`.
|
17
|
+
|
18
|
+
Finally, you can combine your constraints into a `Stannum::Contract` to combine multiple validations of your object and its properties. Stannum provides pre-defined contracts for asserting on objects, `Array`s, `Hash`es, and even method parameters.
|
19
|
+
|
20
|
+
Stannum also defines the `Stannum::Struct` module for defining structured data entities that are not tied to any framework or datastore. Stannum structs have more functionality and a friendlier interface than a core library `Struct`, provide more structure than a `Hash` or hash-like object (such as an `OpenStruct` or `Hashie::Mash`), and are completely independent from the source of the data. Need to load seed data from a YAML configuration file, perform operations in a SQL database, cross-reference with a MongoDB data store, and use an in-memory data array for lightning-fast tests? A `Stannum::Struct` won't fight you every step of the way.
|
21
|
+
|
22
|
+
### Why Stannum?
|
23
|
+
|
24
|
+
Stannum is not tied to any framework. You can create constraints and contracts to validate Ruby objects and Structs, data structures such as Arrays, Hashes, and Sets, and even framework objects such as `ActiveRecord::Model`s and `Mongoid::Document`s.
|
25
|
+
|
26
|
+
Still, most projects and applications use one framework to handle their data. Why use Stannum constraints?
|
27
|
+
|
28
|
+
- **Composability:** Because Stannum contracts are their own objects, they can be combined together. Reuse validation logic without duplicating code or defining abstract ancestor classes .
|
29
|
+
- **Polymorphism:** Your data validation is separate from your model definitions. This gives you two major advantages over the traditional approach:
|
30
|
+
- You can use the same contract to validate different objects. Do you have a shared concern that cuts across multiple domain objects, such as attaching images, having comments, or creating an audit trail? You can write one contract for the concern and apply that same contract to each applicable model or object.
|
31
|
+
- You can use different contracts to validate the same object in different contexts. Need different validations for a regular user versus an admin? Need to handle published articles more strictly than drafts? Need to provide custom validations for each step in your state machine? Stannum has you covered, and because contracts are composable, you can pull in the constraints you need without duplicating your logic.
|
32
|
+
- **Separation of Concerns:** Your data validation is independent from your entities. This means that you can use the same tools to validate anything from controller parameters to models to configuration files.
|
33
|
+
|
34
|
+
### Compatibility
|
35
|
+
|
36
|
+
Stannum is tested against Ruby (MRI) 2.6 through 3.0.
|
37
|
+
|
38
|
+
### Documentation
|
39
|
+
|
40
|
+
Documentation is generated using [YARD](https://yardoc.org/), and can be generated locally using the `yard` gem.
|
41
|
+
|
42
|
+
### License
|
43
|
+
|
44
|
+
Copyright (c) 2019-2021 Rob Smith
|
45
|
+
|
46
|
+
Stannum is released under the [MIT License](https://opensource.org/licenses/MIT).
|
47
|
+
|
48
|
+
### Contribute
|
49
|
+
|
50
|
+
The canonical repository for this gem is located at https://github.com/sleepingkingstudios/stannum.
|
51
|
+
|
52
|
+
To report a bug or submit a feature request, please use the [Issue Tracker](https://github.com/sleepingkingstudios/stannum/issues).
|
53
|
+
|
54
|
+
To contribute code, please fork the repository, make the desired updates, and then provide a [Pull Request](https://github.com/sleepingkingstudios/stannum/pulls). Pull requests must include appropriate tests for consideration, and all code must be properly formatted.
|
55
|
+
|
56
|
+
### Code of Conduct
|
57
|
+
|
58
|
+
Please note that the `Stannum` project is released with a [Contributor Code of Conduct](https://github.com/sleepingkingstudios/stannum/blob/master/CODE_OF_CONDUCT.md). By contributing to this project, you agree to abide by its terms.
|
59
|
+
|
60
|
+
<!-- ## Getting Started -->
|
61
|
+
|
62
|
+
## Reference
|
63
|
+
|
64
|
+
<a id="constraints"></a>
|
65
|
+
|
66
|
+
### Constraints
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
require 'stannum/constraint'
|
70
|
+
```
|
71
|
+
|
72
|
+
Constraints provide the foundation for data validation in Stannum. Fundamentally, each `Stannum::Constraint`encapsulates a predicate - a statement that can be either true or false - that can be applied to other objects. If the statement is true about the object, that object "matches" the constraint. If the statement is false, the object does not match the constraint.
|
73
|
+
|
74
|
+
The easiest way to define a constraint is by passing a block to `Stannum::Constraint.new`:
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
constraint = Stannum::Constraint.new do |object|
|
78
|
+
object.is_a?(String) && !object.empty?
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
Here, we've created a very simple constraint, which will match any non-empty string and will not match empty strings or other objects. When you define a constraint using `Stannum::Constraint.new`, the constraint will pass for an object if and only if the block returns `true` for that object. We can also pass in additional metadata about the constraint such as a type or message to display - we will revisit this in [Errors, Types and Messages](#constraints-errors-types-messages), below.
|
83
|
+
|
84
|
+
#### Matching Objects
|
85
|
+
|
86
|
+
Defining a constraint is only half the battle - next, we need to use the constraint. Each `Stannum::Constraint` defines a standard set of methods to match objects.
|
87
|
+
|
88
|
+
First, the `#matches?` method will return true if the object matches the constraint, and will return false if the object does not match.
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
constraint.matches?(nil)
|
92
|
+
#=> false
|
93
|
+
|
94
|
+
constraint.matches?('')
|
95
|
+
#=> false
|
96
|
+
|
97
|
+
constraint.matches?('Greetings, programs!')
|
98
|
+
#=> true
|
99
|
+
```
|
100
|
+
|
101
|
+
Knowing that an object does not match isn't always enough information - we need to know why. Stannum defines the `Stannum::Errors` object for this purpose (see [Errors](#errors), below). We can use the `#match` method to both check whether an object matches the constraint and return any errors with one method call.
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
status, errors = constraint.matches?(nil)
|
105
|
+
status
|
106
|
+
#=> false
|
107
|
+
errors
|
108
|
+
#=> an instance of Stannum::Errors
|
109
|
+
errors.empty?
|
110
|
+
#=> false
|
111
|
+
|
112
|
+
status, errors = constraint.matches?('Greetings, programs!')
|
113
|
+
status
|
114
|
+
#=> true
|
115
|
+
errors
|
116
|
+
#=> an instance of Stannum::Errors
|
117
|
+
errors.empty?
|
118
|
+
#=> true
|
119
|
+
```
|
120
|
+
|
121
|
+
Finally, if we already know that an object does not match the constraint, we can check its errors using the `#errors_for` method.
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
errors = constraint.errors_for(nil)
|
125
|
+
#=> an instance of Stannum::Errors
|
126
|
+
errors.empty?
|
127
|
+
#=> false
|
128
|
+
```
|
129
|
+
|
130
|
+
*Important Note:* Stannum **does not** guarantee that `#errors_for` will return an empty `Errors` object for an object that matches the constraint. Always check whether the object matches the constraint before checking the errors.
|
131
|
+
|
132
|
+
#### Negated Matching
|
133
|
+
|
134
|
+
A constraint can also be used to check if an object does not match the constraint. Each `Stannum::Constraint` defines helpers for the negated use case.
|
135
|
+
|
136
|
+
The `#does_not_match?` method is the inverse of `#matches?`. It will return false if the object matches the constraint, and will return true if the object does not match.
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
constraint.does_not_match?(nil)
|
140
|
+
#=> true
|
141
|
+
|
142
|
+
constraint.does_not_match?('')
|
143
|
+
#=> true
|
144
|
+
|
145
|
+
constraint.does_not_match?('Greetings, programs!')
|
146
|
+
#=> false
|
147
|
+
```
|
148
|
+
|
149
|
+
Negated matches can also generate errors objects. Whereas the errors from a standard match will list how the object fails to match the constraint, the errors from a negated match will list how the object does match the constraint. The `#negated_match` method will both check that the object does not match the constraint and return the relevant errors, while the `#negated_errors_for` method will return the negated errors for a matching object.
|
150
|
+
|
151
|
+
<a id="constraints-errors-types-messages"></a>
|
152
|
+
|
153
|
+
#### Errors, Types and Messages
|
154
|
+
|
155
|
+
We can customize the error returned by the constraint for a non-matching object by setting the constraint type and/or message.
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
constraint = Stannum::Constraint.new(
|
159
|
+
message: 'must be even',
|
160
|
+
type: 'example.constraints.even'
|
161
|
+
) { |i| i.even? }
|
162
|
+
```
|
163
|
+
|
164
|
+
The constraint `#type` identifies the kind of constraint. For example, a `case` or conditional statement that checks for an error of a particular variety would look at the error's type. The constraint `#message`, on the other hand, is a human-readable description of the error. A flash message or rendered might use the error's message to display the status to the user. An API response might provide both the type and the message.
|
165
|
+
|
166
|
+
The constraint type and message are used to generate the corresponding error:
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
errors = constraint.errors_for(nil)
|
170
|
+
errors.count
|
171
|
+
#=> 1
|
172
|
+
errors.first.message
|
173
|
+
#=> 'must be even'
|
174
|
+
errors.first.type
|
175
|
+
#=> 'example.constraints.even'
|
176
|
+
|
177
|
+
```
|
178
|
+
|
179
|
+
The error message can also be generated automatically from the type (see [Generating Messages](#errors-generating-messages), below).
|
180
|
+
|
181
|
+
#### Constraint Subclasses
|
182
|
+
|
183
|
+
Defining a subclass of `Stannum::Constraint` allows for greater control over the predicate logic and the generated errors.
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
class EvenIntegerConstraint < Stannum::Constraint
|
187
|
+
NEGATED_TYPE = 'examples.constraints.odd'
|
188
|
+
TYPE = 'examples.constraints.even'
|
189
|
+
|
190
|
+
def errors_for(actual, errors: nil)
|
191
|
+
return super if actual.is_a?(Integer)
|
192
|
+
|
193
|
+
(errors || Stannum::Errors.new)
|
194
|
+
.add('examples.constraints.type', type: Integer)
|
195
|
+
end
|
196
|
+
|
197
|
+
def matches?(actual)
|
198
|
+
actual.is_a?(Integer) && actual.even?
|
199
|
+
end
|
200
|
+
end
|
201
|
+
```
|
202
|
+
|
203
|
+
Let's take it from the top. We start by defining `::NEGATED_TYPE` and `::TYPE` constraints. These serve two purposes: first, the constraint will use these values as the default `#type` and `#negated_type` properties, without having to pass in values to the constructor. Second, we are declaring the type of error this constraints will return to the rest of the code. This allows us to reference these values elsewhere, such as a `case` or conditional statement checking for the presense of this error.
|
204
|
+
|
205
|
+
Second, we define our `#matches?` method. This method takes one parameter (the object being matched) and returns either `true` or `false`. Our other matching methods - `#does_not_match?`, `#match`, and `#negated_match` - will delegate to this implementation unless we specifically override them.
|
206
|
+
|
207
|
+
Finally, we are defining the errors to be returned from our constraint using the `#errors_for` method. This method takes one required argument `actual`, which is the object being matched. If the object is an integer, then we fall back to the default behavior: `super` will add an error with a `#type` equal to the constraint's `#type` (or the `:type` passed into the constructor, if any). If the object is not an integer, then we instead display a custom error. In addition to the error `#type`, we are defining some error `#data`. In addition, `#errors_for` can take an optional keyword `:errors`, which is either an instance of `Stannum::Errors` or `nil`. This allows the user to pass an existing errors object to `#errors_for`, which will add its own errors to the given errors object instead of creating a new one.
|
208
|
+
|
209
|
+
```ruby
|
210
|
+
errors = constraint.errors_for(nil)
|
211
|
+
errors.count
|
212
|
+
#=> 1
|
213
|
+
errors.first.type
|
214
|
+
#=> 'examples.constraints.type'
|
215
|
+
errors.first.data
|
216
|
+
#=> { type: Integer }
|
217
|
+
|
218
|
+
errors = constraint.errors_for('')
|
219
|
+
errors.count
|
220
|
+
#=> 1
|
221
|
+
errors.first.type
|
222
|
+
#=> 'examples.constraints.even'
|
223
|
+
errors.first.data
|
224
|
+
#=> {}
|
225
|
+
```
|
226
|
+
|
227
|
+
We can likewise define the behavior of the constraint when negated. We've already set the `::NEGATED_TYPE` constant, but we can go further and override the `#does_not_match?` and/or `#negated_errors_for` methods as well for full control over the behavior when performing a negated match.
|
228
|
+
|
229
|
+
<a id="contracts"></a>
|
230
|
+
|
231
|
+
### Contracts
|
232
|
+
|
233
|
+
```ruby
|
234
|
+
require 'stannum/contract'
|
235
|
+
```
|
236
|
+
|
237
|
+
A contract is a collection of constraints that validate an object and its properties. Each `Stannum::Contract` holds a set of `Stannum::Constraints`, each of which must match an object or the referenced property for that object to match the contract as a whole. Contracts also obey the Constraint interface, and can be used inside other contracts to compose complex or nested validations.
|
238
|
+
|
239
|
+
Like constraints, contracts can be created by passing a block to `Stannum::Contract.new`:
|
240
|
+
|
241
|
+
```ruby
|
242
|
+
contract = Stannum::Contract.new do
|
243
|
+
constraint(type: 'examples.constraints.numeric') do |actual|
|
244
|
+
actual.is_a?(Numeric)
|
245
|
+
end
|
246
|
+
|
247
|
+
constraint(type: 'examples.constraints.integer') do |actual|
|
248
|
+
actual.is_a?(Integer)
|
249
|
+
end
|
250
|
+
|
251
|
+
constraint(type: 'examples.constraints.in_range') do |actual|
|
252
|
+
actual >= 0 && actual <= 10 rescue false
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
contract.matches?(nil)
|
257
|
+
#=> false
|
258
|
+
contract.errors_for(nil).map(&:type)
|
259
|
+
#=> ['examples.constraints.numeric', 'examples.constraints.integer', 'examples.constraints.in_range']
|
260
|
+
|
261
|
+
contract.matches?(99.0)
|
262
|
+
#=> false
|
263
|
+
contract.errors_for(99.0).map(&:type)
|
264
|
+
#=> ['examples.constraints.integer', 'examples.constraints.in_range']
|
265
|
+
|
266
|
+
contract.matches?(99)
|
267
|
+
#=> false
|
268
|
+
contract.errors_for(99).map(&:type)
|
269
|
+
#=> ['examples.constraints.in_range']
|
270
|
+
|
271
|
+
contract.matches?(5)
|
272
|
+
#=> true
|
273
|
+
```
|
274
|
+
|
275
|
+
As you can see, the contract matches the object against each of its constraints. If any of the constraints fail to match the object, then the contract also does not match. Finally, the errors from each failing constraint are aggregated together.
|
276
|
+
|
277
|
+
You can also add constraints to an existing contract using the `#add_constraint` method.
|
278
|
+
|
279
|
+
```ruby
|
280
|
+
constraint = Stannum::Constraint.new(type: 'examples.constraints.even') do |actual|
|
281
|
+
actual.respond_to?(:even) && actual.even?
|
282
|
+
end
|
283
|
+
|
284
|
+
contract.add_constraint(constraint)
|
285
|
+
|
286
|
+
contract.matches?(5)
|
287
|
+
#=> false
|
288
|
+
contract.errors_for(99).map(&:type)
|
289
|
+
#=> ['examples.constraints.even']
|
290
|
+
|
291
|
+
contract.matches?(6)
|
292
|
+
#=> true
|
293
|
+
```
|
294
|
+
|
295
|
+
The `#add_constraint` method returns the contract, so you can chain multiple `#add_constraint` calls together.
|
296
|
+
|
297
|
+
#### Negated Matching
|
298
|
+
|
299
|
+
Like a constraint, a contract can perform a negated match. Whereas an object matches the contract if **all** of the constraints match the object, the object will pass a negated match if **none** of the constraints match the object.
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
contract = Stannum::Contract.new do
|
303
|
+
constraint(type: 'examples.constraints.color') do |hsh|
|
304
|
+
hsh[:color] == 'red'
|
305
|
+
end
|
306
|
+
|
307
|
+
constraint(type: 'examples.constraints.shape') do |hsh|
|
308
|
+
hsh[:color] == 'circle'
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
contract.matches?({ color: 'red', shape: 'circle' })
|
313
|
+
#=> true
|
314
|
+
contract.does_not_match?({ color: 'red', shape: 'circle' })
|
315
|
+
#=> false
|
316
|
+
contract.errors_for({ color: 'red', shape: 'square' }).map(&:type)
|
317
|
+
#=> ['examples.constraints.color', 'examples.constraints.shape']
|
318
|
+
|
319
|
+
contract.matches?({ color: 'red', shape: 'square' })
|
320
|
+
#=> false
|
321
|
+
contract.does_not_match?({ color: 'red', shape: 'square' })
|
322
|
+
#=> false
|
323
|
+
contract.errors_for({ color: 'red', shape: 'square' }).map(&:type)
|
324
|
+
#=> ['examples.constraints.color']
|
325
|
+
|
326
|
+
contract.matches?({ color: 'blue', shape: 'square'})
|
327
|
+
#=> false
|
328
|
+
contract.does_not_match?({ color: 'blue', shape: 'square'})
|
329
|
+
#=> true
|
330
|
+
```
|
331
|
+
|
332
|
+
Note that for an object that partially matches the contract, both `#matches?` and `#does_not_match?` methods will return false. If you want to check whether **any** of the constraints do not match the object, use the `#matches?` method and apply the `!` boolean negation operator (or switch from an `if` to an `unless`).
|
333
|
+
|
334
|
+
#### Property Constraints
|
335
|
+
|
336
|
+
Constraints can also define constraints on the *properties* of the matched object. This is a powerful feature for defining validations on objects and nested data structures. To define a property constraint, use the `property` macro in a contract constructor block, or use the `#add_property_constraint` method on an existing contract.
|
337
|
+
|
338
|
+
```ruby
|
339
|
+
gadget_contract = Stannum::Contract.new do
|
340
|
+
property :name, Stannum::Constraints::Presence.new
|
341
|
+
|
342
|
+
property :name, Stannum::Constraints::Types::StringType.new
|
343
|
+
|
344
|
+
property(:size, type: 'examples.constraints.size') do |size|
|
345
|
+
%w[small medium large].include?(size)
|
346
|
+
end
|
347
|
+
|
348
|
+
property :manufacturer, Stannum::Contract.new do
|
349
|
+
constraint Stannum::Constraints::Presence.new
|
350
|
+
|
351
|
+
property :address, Stannum::Constraints::Presence.new
|
352
|
+
end
|
353
|
+
end
|
354
|
+
```
|
355
|
+
|
356
|
+
There's a lot going on here, so let's break it down. First, we're defining constraints on the *properties* of the object, rather than on the object as a whole. In particular, note that we're setting multiple constraints on the `:name` property - an object will only match the contract if it's `#name` matches both of those constraints.
|
357
|
+
|
358
|
+
We're also using some pre-defined constraints, rather than having to start from scratch. The `Presence` constraint validates that an object is not `nil` and not `#empty?`, while the `Types::StringType` constraint validates that the object is an instance of `String`. For a full list of pre-defined constraints, see [Built-In Constraints](#builtin-constraints) and [Contracts](#builtin-contracts), below. You can also define your own constraint classes and reference them in your contracts.
|
359
|
+
|
360
|
+
Finally, note that the constraint for the `:manufacturer` property is itself a contract. We are asserting that the actual object has a non-`nil` `#manufacturer` property and that the manufacturer's `#address` is also non-`nil` (and not `#empty?`).
|
361
|
+
|
362
|
+
```ruby
|
363
|
+
gadget = Gadget.new(manufacturer: Manufacturer.new)
|
364
|
+
gadget_contract.matches?(gadget)
|
365
|
+
#=> false
|
366
|
+
gadget_contract.errors_for(gadget).map { |err| [err.path, err.type] }
|
367
|
+
#=> [
|
368
|
+
# [%i[name], 'stannum.constraints.absent'],
|
369
|
+
# [%i[name], 'stannum.constraints.is_not_type'],
|
370
|
+
# [%i[size], 'examples.constraints.size'],
|
371
|
+
# [%i[manufacturer address], 'stannum.constraints.absent']
|
372
|
+
# ]
|
373
|
+
```
|
374
|
+
|
375
|
+
We've established that each error has a `#type`, which identifies which type of constraint failed to match the object. Here, we can see that each error also has a `#path` property, which represents the relative path of the property from the original matched object. For example, errors on the `gadget.name` property will have a path of `%i[name]`, while the error on the `gadget.manufacturer.address` will have a path of `%i[manufacturer address]`. A constraint without a property, i.e. on the matched object itself, will have a path of `[]`, an empty string.
|
376
|
+
|
377
|
+
The errors for a property or nested contract can also be accessed using the `#[]` operator or the `#dig` method.
|
378
|
+
|
379
|
+
```ruby
|
380
|
+
gadget_contract.errors_for(gadget)[:manufacturer].map { |err| [err.path, err.type] }
|
381
|
+
#=> [[%i[address], 'stannum.constraints.absent']]
|
382
|
+
|
383
|
+
gadget_contract.errors_for(gadget).dig(:manufacturer, :address).map { |err| [err.path, err.type] }
|
384
|
+
#=> [[[], 'stannum.constraints.absent']]
|
385
|
+
```
|
386
|
+
|
387
|
+
Be careful when defining property constraints on a contract that might be matched against `nil` or an unknown object type - Ruby will raise a `NoMethodError` when trying to access the property. To avoid this, you can add a sanity constraint (see below) to ensure that the contract only validates the expected type of object.
|
388
|
+
|
389
|
+
#### Sanity Constraints
|
390
|
+
|
391
|
+
In some cases, before running through the full set of constraints in a contract, we want to run a quick sanity check to make sure the contract is even applicable to the object. By adding `sanity: true` when defining the constraint, you can mark a constraint as a sanity check.
|
392
|
+
|
393
|
+
```ruby
|
394
|
+
gadget_contract.add_constraint(Stannum::Constraints::Type.new(Gadget), sanity: true)
|
395
|
+
```
|
396
|
+
|
397
|
+
When matching an object, all of a contract's sanity constraints will be evaluated first. The remaining constraints will be matched against the object *only* if all of the sanity constraints match the object. This can be especially important if some of the constraints return nonsensical results or even raise exceptions when given an invalid object.
|
398
|
+
|
399
|
+
```ruby
|
400
|
+
gadget_contract.matches?(nil)
|
401
|
+
#=> false
|
402
|
+
gadget_contract.errors_for(nil).map { |err| [err.path, err.type] }
|
403
|
+
#=> [[[], 'stannum.constraints.is_not_type']]
|
404
|
+
```
|
405
|
+
|
406
|
+
Likewise, when performing a negated match, the sanity constraints will be evaluated first, and the remaining constraints will be evaluated only if all of the sanity constraints match.
|
407
|
+
|
408
|
+
#### Combining Contracts
|
409
|
+
|
410
|
+
Stannum provides two mechanisms for composing contracts together. Each contract is a constraint, and so can be added to another contract (with or without a property or scope). This allows you to create and reuse validation logic simply by adding a contract as a constraint:
|
411
|
+
|
412
|
+
```ruby
|
413
|
+
named_contract = Stannum::Contract.new do
|
414
|
+
property :name, Stannum::Constraints::Presence.new
|
415
|
+
end
|
416
|
+
|
417
|
+
widget_contract = Stannum::Contract.new do
|
418
|
+
constraint(Stannum::Constraints::Type.new(Widget))
|
419
|
+
|
420
|
+
constraint(named_contract)
|
421
|
+
end
|
422
|
+
|
423
|
+
widget = Widget.new
|
424
|
+
widget_contract.matches?(Widget.new)
|
425
|
+
#=> false
|
426
|
+
widget_contract.matches?(Widget.new(name: 'Whirlygig'))
|
427
|
+
#=> true
|
428
|
+
```
|
429
|
+
|
430
|
+
The second mechanism is contract *concatenation*. Under the hood, concatenation directly pulls in the constraints from a concatenated contract, rather than evaluating that contract on its own. This can be likened to inheriting methods from a superclass or an included Module.
|
431
|
+
|
432
|
+
```ruby
|
433
|
+
gadget_contract = Stannum::Contract.new do
|
434
|
+
constraint(Stannum::Constraints::Type.new(Gadget))
|
435
|
+
|
436
|
+
concat(named_contract)
|
437
|
+
end
|
438
|
+
```
|
439
|
+
|
440
|
+
Using concatenation, you have finer control over the constraints that are added to the contract. Specifically, when defining a contract you can mark certain constraints as excluded from concatenation by adding the `concatenatable: false` keyword to `#add_constraint`. As an example, this can be useful if you want to inherit constraints about the properties of an object, but not potentially conflicting constraints about the object's type.
|
441
|
+
|
442
|
+
<a id="array-contracts"></a>
|
443
|
+
|
444
|
+
#### Array Contracts
|
445
|
+
|
446
|
+
By default, a `Stannum::Contract` accesses an object's properties as method calls, using the `.` dot notation. When validating `Array`s and `Hash`es, this approach is less useful. Therefore, Stannum provides special contracts for operating on data structures.
|
447
|
+
|
448
|
+
A `Stannum::Contracts::ArrayContract` is used for validating sequential data, using the `#[]` method to access indexed values.
|
449
|
+
|
450
|
+
```ruby
|
451
|
+
class BaseballContract < Stannum::Contracts::ArrayContract
|
452
|
+
def initialize
|
453
|
+
super do
|
454
|
+
item { |actual| actual == 'Who' }
|
455
|
+
item { |actual| actual == 'What' }
|
456
|
+
item { |actual| actual == "I Don't Know" }
|
457
|
+
end
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
contract = BaseballContract.new
|
462
|
+
contract.matches?(nil)
|
463
|
+
#=> false
|
464
|
+
contract.errors_for(nil).map { |err| [err.path, err.type] }
|
465
|
+
#=> [[[], 'stannum.constraints.is_not_type']]
|
466
|
+
|
467
|
+
array = %w[Who What]
|
468
|
+
contract.matches?(array)
|
469
|
+
#=> false
|
470
|
+
contract.errors_for(array).map { |err| [err.path, err.type] }
|
471
|
+
#=> [[[2], 'stannum.constraints.invalid']]
|
472
|
+
|
473
|
+
array = ['Who', 'What', "I Don't Know"]
|
474
|
+
contract.matches?(array)
|
475
|
+
#=> true
|
476
|
+
|
477
|
+
array = ['Who', 'What', "I Don't Know", 'Tomorrow']
|
478
|
+
contract.matches?(array)
|
479
|
+
#=> false
|
480
|
+
contract.errors_for(array).map { |err| [err.path, err.type] }
|
481
|
+
#=> [[[3], 'stannum.constraints.tuples.extra_items']]
|
482
|
+
```
|
483
|
+
|
484
|
+
Here, we are defining an ArrayContract using the `#item` macro, which defines an item constraint for each successive item in the array. We can also define a property constraint using the `#property` macro, using an Integer as the property to validate. This would allow us to add multiple constraints for the value at a given index, although the recommended approach is to use a nested contract.
|
485
|
+
|
486
|
+
When matching an object, the contract first validates that the object is an instance of `Array`. If not, it will immedidately fail matching and the remaining constraints will not be matched against the object. If the object is an an array, then the contract checks each of the defined constraints against the value of the array at that index.
|
487
|
+
|
488
|
+
Finally, the constraint checks for the highest index expected by an item constraint. If the array contains additional items after this index, those items will fail with a type of `"extra_items"`. To allow additional items instead, pass `allow_extra_items: true` to the `ArrayContract` constructor.
|
489
|
+
|
490
|
+
```ruby
|
491
|
+
contract = BaseballContract.new(allow_extra_items: true)
|
492
|
+
contract.matches?(['Who', 'What', "I Don't Know", 'Tomorrow'])
|
493
|
+
#=> true
|
494
|
+
```
|
495
|
+
|
496
|
+
An `ArrayContract` will first validate that the object is an instance of `Array`. For validating Array-like objects that access indexed data using the `#[]` method, you can instead use a `Stannum::Contracts::TupleContract`.
|
497
|
+
|
498
|
+
<a id="hash-contracts"></a>
|
499
|
+
|
500
|
+
#### Hash Contracts
|
501
|
+
|
502
|
+
A `Stannum::Contracts::HashContract` is used for validating key-value data, using the `#[]` method to access values by key.
|
503
|
+
|
504
|
+
```ruby
|
505
|
+
class ResponseContract < Stannum::Contracts::HashContract
|
506
|
+
def initialize
|
507
|
+
super do
|
508
|
+
key :status, Stannum::Constraints::Types::IntegerType.new
|
509
|
+
|
510
|
+
key :json,
|
511
|
+
Stannum::Contracts::HashContract.new(allow_extra_keys: true) do
|
512
|
+
key :ok, Stannum::Constraints::Boolean.new
|
513
|
+
end
|
514
|
+
|
515
|
+
key :signature, Stannum::Constraints::Presence.new
|
516
|
+
end
|
517
|
+
end
|
518
|
+
end
|
519
|
+
|
520
|
+
contract = ResponseContract.new
|
521
|
+
contract.matches?(nil)
|
522
|
+
#=> false
|
523
|
+
contract.errors_for(nil).map { |err| [err.path, err.type] }
|
524
|
+
#=> [[[], 'stannum.constraints.is_not_type']]
|
525
|
+
|
526
|
+
response = { status: 500, json: {} }
|
527
|
+
contract.matches?(response)
|
528
|
+
#=> false
|
529
|
+
contract.errors_for(response).map { |err| [err.path, err.type] }
|
530
|
+
#=> [
|
531
|
+
# %i[json ok], 'stannum.constraints.is_not_boolean'],
|
532
|
+
# %i[signature], 'stannum.constraints.absent'
|
533
|
+
# ]
|
534
|
+
|
535
|
+
response = { status: 200, json: { ok: true }, signature: '12345' }
|
536
|
+
contract.matches?(response)
|
537
|
+
#=> true
|
538
|
+
|
539
|
+
response = { status: 200, json: { ok: true }, signature: '12345', role: 'admin' }
|
540
|
+
#=> false
|
541
|
+
contract.errors_for(response).map { |err| [err.path, err.type] }
|
542
|
+
#=> [[%i[role], 'stannum.constraints.hashes.extra_keys']]
|
543
|
+
```
|
544
|
+
|
545
|
+
We define a HashContract using the `#key` macro, which defines a key-value constraint for the specified value in the hash. When validating a Hash, the value at each key must match the given constraint. The contract will also fail if there are additional keys without a corresponding constraint. To allow additional keys instead, pass `allow_extra_keys: true` to the `HashContract` constructor.
|
546
|
+
|
547
|
+
```ruby
|
548
|
+
contract = ResponseContract.new(allow_extra_keys: true)
|
549
|
+
response = { status: 200, json: { ok: true }, signature: '12345', role: 'admin' }
|
550
|
+
contract.matches?(response)
|
551
|
+
#=> true
|
552
|
+
```
|
553
|
+
|
554
|
+
A `HashContract` will first validate that the object is an instance of `Hash`. For validating Hash-like objects that access key-value data using the `#[]` method, you can instead use a `Stannum::Contracts::MapContract`.
|
555
|
+
|
556
|
+
#### Contract Subclasses
|
557
|
+
|
558
|
+
For most use cases, defining a custom contract subclass will involve adding default constraints for the contact. Stannum provides two easy methods for doing so. First, you can leverage the default behavior by passing a block to `super` in the contract constructor. This will allow you to take advantage of the `constraint`, `property`, and other macros.
|
559
|
+
|
560
|
+
```ruby
|
561
|
+
class GizmoContract
|
562
|
+
def initialize(**_options)
|
563
|
+
super do
|
564
|
+
constraint Stannum::Constraints::Type.new(Gizmo), sanity: true
|
565
|
+
|
566
|
+
property :complexity, Stannum::Constraints::Presence.new
|
567
|
+
end
|
568
|
+
end
|
569
|
+
end
|
570
|
+
```
|
571
|
+
|
572
|
+
As an alternative, `Stannum::Contract` defines a private `#define_constraints` method that is used to initialize any constraints.
|
573
|
+
|
574
|
+
```ruby
|
575
|
+
class WhirlygigContract
|
576
|
+
private
|
577
|
+
|
578
|
+
def define_constraints
|
579
|
+
super
|
580
|
+
|
581
|
+
constraint Stannum::Constraints::Type.new(Whirlygig), sanity: true
|
582
|
+
|
583
|
+
property :rotation_speed, Stannum::Constraints::Types::Float.new
|
584
|
+
end
|
585
|
+
end
|
586
|
+
```
|
587
|
+
|
588
|
+
<a id="errors"></a>
|
589
|
+
|
590
|
+
### Errors
|
591
|
+
|
592
|
+
```ruby
|
593
|
+
require 'stannum/errors'
|
594
|
+
```
|
595
|
+
|
596
|
+
When a constraint or contract fails to match an object, Stannum can return the reasons for that failure in the form of an errors object. Specifically, a `Stannum::Errors` object is returned by calling `#errors_for` or `#negated_errors-for` with a failing object, or as part of the result of calling `#match` or `#negated_match`.
|
597
|
+
|
598
|
+
```ruby
|
599
|
+
contract.matches?(nil)
|
600
|
+
#=> false
|
601
|
+
contract.errors_for(nil)
|
602
|
+
#=> an instance of Stannum::Errors
|
603
|
+
status, errors = contract.match(nil)
|
604
|
+
status
|
605
|
+
#=> false
|
606
|
+
errors
|
607
|
+
#=> an instance of Stannum::Errors
|
608
|
+
```
|
609
|
+
|
610
|
+
A `Stannum::Errors` object is an `Enumerable` collection, while each error is a `Hash` with the following properties:
|
611
|
+
|
612
|
+
- `#type`: A unique value that defines what kind of error was encountered. Each error's type should be a namespaced `String`, e.g. `"stannum.constraints.invalid"`.
|
613
|
+
- `#data`: A `Hash` of additional data about the error. For example, a failed type validation will include the expected type for the value; a failed range validation might include the minimum and maximum allowable values.
|
614
|
+
- `#path`: The path of the error relative to the top-level object that was validated. The path is always an `Array`, and each item in the array is either an `Integer` or a non-empty `Symbol`. Some examples:
|
615
|
+
- An error on the object itself will have an empty path, `[]`.
|
616
|
+
- An error on the item at index `3`of an array would have a path of `[3]`.
|
617
|
+
- An error on the `#name` property of an object would have a path of `[:name]`.
|
618
|
+
- An error on the address of the first manufacturer would have a path of `[:manufacturers, 3, :address]`.
|
619
|
+
- `#message`: A human-readable description of the error. Error messages are not generated by default; either specify a message when defining the constraint, or call `#with_messages` to generate the error messages based on the error types and data. See [Generating Messages](errors-generating-messages), below.
|
620
|
+
|
621
|
+
The simplest way to access the errors in a `Stannum::Errors` object is via the `#each` method, which will yield each error in the collection to the given block. Because each `Stannum::Errors` is enumerable, you can use the standard `Enumerable` methods such as `#map`, `#reduce`, `#select`, and so on. You can also use `#count` to return the number of errors, or `#empty?` to check if there are any errors in the collection.
|
622
|
+
|
623
|
+
```ruby
|
624
|
+
errors.count
|
625
|
+
#=> 3
|
626
|
+
errors.empty?
|
627
|
+
#=> false
|
628
|
+
errors.first
|
629
|
+
#=> {
|
630
|
+
# data: {},
|
631
|
+
# message: nil,
|
632
|
+
# path: [:name],
|
633
|
+
# type: 'stannum.constraints.invalid'
|
634
|
+
# }
|
635
|
+
errors.map(&:type)
|
636
|
+
#=> [
|
637
|
+
# 'stannum.constraints.invalid',
|
638
|
+
# 'stannum.constraints.absent',
|
639
|
+
# 'stannum.constraints.is_not_type'
|
640
|
+
# ]
|
641
|
+
```
|
642
|
+
|
643
|
+
Usually, an errors object is generated automatically by a constraint or contract with its errors already defined. If you want to add custom errors to an errors object, use the `#add` method, which takes the error `type` as one required argument. You can also specify the `message` keyword, which sets the message of the error. Finally, any additional keywords are added to the error `data`.
|
644
|
+
|
645
|
+
```ruby
|
646
|
+
errors = Stannum::Errors.new
|
647
|
+
errors.count
|
648
|
+
#=> 0
|
649
|
+
errors.empty?
|
650
|
+
#=> true
|
651
|
+
|
652
|
+
errors.add('example.constraints.out_of_range', message: 'out of range', min: 0, max: 10)
|
653
|
+
#=> the errors object
|
654
|
+
errors.count
|
655
|
+
#=> 1
|
656
|
+
errors.empty?
|
657
|
+
#=> false
|
658
|
+
errors.first
|
659
|
+
#=> {
|
660
|
+
# data: { min: 0, max: 10 },
|
661
|
+
# message: 'out of range',
|
662
|
+
# path: [],
|
663
|
+
# type: 'example.constraints.out_of_range'
|
664
|
+
# }
|
665
|
+
```
|
666
|
+
|
667
|
+
Conveniently, `#add` returns the errors object itself, so you can chain together multiple `#add` calls.
|
668
|
+
|
669
|
+
#### Nested Errors
|
670
|
+
|
671
|
+
To represent the properties of an object or the values in a data structure, `Stannum::Errors` can be nested together. Nested error objects are accessed using the `#[]` operator.
|
672
|
+
|
673
|
+
```ruby
|
674
|
+
errors = Stannum::Errors.new
|
675
|
+
errors[:manufacturers][0][:address].add('stannum.constraints.invalid')
|
676
|
+
errors[:manufacturers][0][:address]
|
677
|
+
#=> an instance of Stannum::Errors
|
678
|
+
errors[:manufacturers][0][:address].count
|
679
|
+
#=> 1
|
680
|
+
errors[:manufacturers][0][:address].first
|
681
|
+
#=> {
|
682
|
+
# data: {},
|
683
|
+
# message: nil,
|
684
|
+
# path: [],
|
685
|
+
# type: 'stannum.constraints.invalid'
|
686
|
+
# }
|
687
|
+
|
688
|
+
errors.count
|
689
|
+
#=> 1
|
690
|
+
errors.first
|
691
|
+
#=> {
|
692
|
+
# data: {},
|
693
|
+
# message: nil,
|
694
|
+
# path: [:manufacturers, 0, :address],
|
695
|
+
# type: 'stannum.constraints.invalid'
|
696
|
+
# }
|
697
|
+
```
|
698
|
+
|
699
|
+
You can also use the `#dig` method to access nested errors:
|
700
|
+
|
701
|
+
```ruby
|
702
|
+
errors.dig(:manufacturers, 0, :address).first
|
703
|
+
#=> {
|
704
|
+
# data: {},
|
705
|
+
# message: nil,
|
706
|
+
# path: [],
|
707
|
+
# type: 'stannum.constraints.invalid'
|
708
|
+
# }
|
709
|
+
```
|
710
|
+
|
711
|
+
<a id="errors-generating-messages"></a>
|
712
|
+
|
713
|
+
#### Generating Messages
|
714
|
+
|
715
|
+
By default, errors objects do not generate messages. `Stannum::Errors` defines the `#with_messages` method to generate messages for a given errors object. If the `:force` keyword is set to true, then `#with_messages` will overwrite any messages that are already set on an error, whether from a constraint or generated by a different strategy.
|
716
|
+
|
717
|
+
```ruby
|
718
|
+
errors.first.message
|
719
|
+
#=> nil
|
720
|
+
|
721
|
+
errors = errors.with_messages.first.message
|
722
|
+
errors.first.message
|
723
|
+
#=> 'is invalid'
|
724
|
+
```
|
725
|
+
|
726
|
+
Stannum uses the strategy pattern to determine how error messages are generated. You can pass the `strategy:` keyword to `#with_messages` to force Stannum to use the specified strategy, or set the `Stannum::Messages.strategy` property to define the default for your application. The default strategy for Stannum uses an I18n-like configuration file to define messages based on the type and optionally the data for each error.
|
727
|
+
|
728
|
+
<a id="structs"></a>
|
729
|
+
|
730
|
+
### Structs
|
731
|
+
|
732
|
+
While constraints and contracts are used to validate data, structs are used to define and structure that data. Each `Stannum::Struct` contains a specific set of attributes, and each attribute has a type definition that is a `Class` or `Module` or the name of a Class or Module.
|
733
|
+
|
734
|
+
Structs are defined by creating a new class and including `Stannum::Struct`:
|
735
|
+
|
736
|
+
```ruby
|
737
|
+
class Gadget
|
738
|
+
attribute :name, String
|
739
|
+
attribute :description, String, optional: true
|
740
|
+
attribute :quantity, Integer, default: 0
|
741
|
+
end
|
742
|
+
|
743
|
+
gadget = Gadget.new(name: 'Self-Sealing Stem Bolt')
|
744
|
+
gadget.name
|
745
|
+
#=> 'Self-Sealing Stem Bolt'
|
746
|
+
gadget.description
|
747
|
+
#=> nil
|
748
|
+
gadget.attributes
|
749
|
+
#=> {
|
750
|
+
# name: 'Self-Sealing Stem Bolt',
|
751
|
+
# description: nil,
|
752
|
+
# quantity: 0
|
753
|
+
# }
|
754
|
+
|
755
|
+
gadget.quantity = 10
|
756
|
+
gadget.quantity
|
757
|
+
#=> 10
|
758
|
+
|
759
|
+
gadget[:description] = 'No one is sure what a self-sealing stem bolt is.'
|
760
|
+
gadget[:description]
|
761
|
+
#=> 'No one is sure what a self-sealing stem bolt is.'
|
762
|
+
```
|
763
|
+
|
764
|
+
Our `Gadget` class has three attributes: `#name`, `#description`, and `#quantity`, which we are defining using the `.attribute` class method.
|
765
|
+
|
766
|
+
We can initialize a gadget with values by passing the desired attributes to `.new`. We can read or write the attributes using either dot `.` notation or `#[]` notation. Finally, we can access all of a struct's attributes and values using the `#attributes` method.
|
767
|
+
|
768
|
+
`Stannum::Struct` defines a number of helper methods for interacting with a struct's attributes:
|
769
|
+
|
770
|
+
- `#[](attribute)`: Returns the value of the given attribute.
|
771
|
+
- `#[]=(attribute, value)`: Writes the given value to the given attribute.
|
772
|
+
- `#assign_attributes(values)`: Updates the struct's attributes using the given values. If an attribute is not given, that value is unchanged.
|
773
|
+
- `#attributes`: Returns a hash containing the attribute keys and values.
|
774
|
+
- `#attributes=(values)`: Sets the struct's attributes to the given values. If an attribute is not given, that attribute is set to `nil`.
|
775
|
+
|
776
|
+
For all of the above methods, if a given attribute is invalid or the attribute is not defined on the struct, an `ArgumentError` will be raised.
|
777
|
+
|
778
|
+
#### Attributes
|
779
|
+
|
780
|
+
A struct's attributes are defined using the `.attribute` class method, and can be accessed and enumerated using the `.attributes` class method on the struct class or via the `::Attributes` constant. Internally, each attribute is represented by a `Stannum::Attribute` instance, which stores the attribute's `:name`, `:type`, and `:attributes`.
|
781
|
+
|
782
|
+
```ruby
|
783
|
+
Gadget::Attributes
|
784
|
+
#=> an instance of Stannum::Schema
|
785
|
+
Gadget.attributes
|
786
|
+
#=> an instance of Stannum::Schema
|
787
|
+
Gadget.attributes.count
|
788
|
+
#=> 3
|
789
|
+
Gadget.attributes.keys
|
790
|
+
#=> [:name, :description, :quantity]
|
791
|
+
Gadget.attributes[:name]
|
792
|
+
#=> an instance of Stannum::Attribute
|
793
|
+
Gadget.attributes[:quantity].options
|
794
|
+
#=> { default: 0, required: true }
|
795
|
+
```
|
796
|
+
|
797
|
+
##### Default Values
|
798
|
+
|
799
|
+
Structs can define default values for attributes by passing a `:default` value to the `.attribute` call.
|
800
|
+
|
801
|
+
```ruby
|
802
|
+
class LightsCounter
|
803
|
+
include Stannum::Struct
|
804
|
+
|
805
|
+
attribute :count, Integer, default: 4
|
806
|
+
end
|
807
|
+
|
808
|
+
LightsCounter.new.count
|
809
|
+
#=> 4
|
810
|
+
```
|
811
|
+
|
812
|
+
##### Optional Attributes
|
813
|
+
|
814
|
+
Struct classes can also mark attributes as `optional`. When a struct is validated (see [Validation](#structs-validation), below), optional attributes will pass with a value of `nil`.
|
815
|
+
|
816
|
+
```ruby
|
817
|
+
class WhereWeAreGoing
|
818
|
+
include Stannum::Struct
|
819
|
+
|
820
|
+
attribute :roads, Object, optional: true
|
821
|
+
end
|
822
|
+
```
|
823
|
+
|
824
|
+
`Stannum` supports both `:optional` and `:required` as keys. Passing either `optional: true` or `required: false` will mark the attribute as optional. Attributes are required by default.
|
825
|
+
|
826
|
+
<a id="structs-validation"></a>
|
827
|
+
|
828
|
+
#### Validation
|
829
|
+
|
830
|
+
Each `Stannum::Struct` automatically generates a contract that can be used to validate instances of the struct class. The contract can be accessed using the `.contract` class method or via the `::Contract` constant.
|
831
|
+
|
832
|
+
```ruby
|
833
|
+
class Gadget
|
834
|
+
attribute :name, String
|
835
|
+
attribute :description, String, optional: true
|
836
|
+
attribute :quantity, Integer, default: 0
|
837
|
+
end
|
838
|
+
|
839
|
+
Gadget::Contract
|
840
|
+
#=> an instance of Stannum::Contract
|
841
|
+
Gadget.contract
|
842
|
+
#=> an instance of Stannum::Contract
|
843
|
+
|
844
|
+
gadget = Gadget.new
|
845
|
+
Gadget.contract.matches?(gadget)
|
846
|
+
#=> false
|
847
|
+
Gadget.contract.errors_for(gadget)
|
848
|
+
#=> [
|
849
|
+
# {
|
850
|
+
# data: { type: String },
|
851
|
+
# message: nil,
|
852
|
+
# path: [:name],
|
853
|
+
# type: 'stannum.constraints.is_not_type'
|
854
|
+
# }
|
855
|
+
# ]
|
856
|
+
|
857
|
+
gadget = Gadget.new(name: 'Self-Sealing Stem Bolt')
|
858
|
+
Gadget.contract.matches?(gadget)
|
859
|
+
#=> true
|
860
|
+
```
|
861
|
+
|
862
|
+
You can also define additional constraints using the `.constraint` class method.
|
863
|
+
|
864
|
+
```ruby
|
865
|
+
class Gadget
|
866
|
+
constraint :name, Stannum::Constraints::Presence.new
|
867
|
+
|
868
|
+
constraint :quantity do |qty|
|
869
|
+
qty >= 0
|
870
|
+
end
|
871
|
+
end
|
872
|
+
|
873
|
+
gadget = Gadget.new(name: '')
|
874
|
+
Gadget.contract.matches?(gadget)
|
875
|
+
#=> false
|
876
|
+
Gadget.contract.errors_for(gadget)
|
877
|
+
#=> [
|
878
|
+
# {
|
879
|
+
# data: {},
|
880
|
+
# message: nil,
|
881
|
+
# path: [:name],
|
882
|
+
# type: 'stannum.constraints.absent'
|
883
|
+
# }
|
884
|
+
# ]
|
885
|
+
```
|
886
|
+
|
887
|
+
The `.constraint` class method takes either an instance of `Stannum::Constraint` or a block. If given an attribute name, the constraint will be matched against the value of that attribute; otherwise, the constraint will be matched against the object itself.
|
888
|
+
|
889
|
+
<a id="builtin-constraints"></a>
|
890
|
+
|
891
|
+
### Built-In Constraints
|
892
|
+
|
893
|
+
Stannum defines a set of built-in constraints that can be used in any project.
|
894
|
+
|
895
|
+
**Absence Constraint**
|
896
|
+
|
897
|
+
The inverse of a [Presence constraint](#builtin-constraints-presence). Matches `nil`, and objects that both respond to `#empty?` and for whom `#empty?` returns true, such as empty `String`s, `Array`s and `Hash`es.
|
898
|
+
|
899
|
+
```ruby
|
900
|
+
constraint = Stannum::Constraints::Absence.new
|
901
|
+
|
902
|
+
constraint.matches?(nil)
|
903
|
+
#=> true
|
904
|
+
constraint.matches?('')
|
905
|
+
#=> true
|
906
|
+
constraint.matches?('Greetings, programs!')
|
907
|
+
#=> false
|
908
|
+
constraint.matches?(Object.new)
|
909
|
+
#=> false
|
910
|
+
```
|
911
|
+
|
912
|
+
**Anything Constraint**
|
913
|
+
|
914
|
+
Matches any object, even `nil`.
|
915
|
+
|
916
|
+
```ruby
|
917
|
+
constraint = Stannum::Constraints::Anything.new
|
918
|
+
|
919
|
+
constraint.matches?(nil)
|
920
|
+
#=> true
|
921
|
+
constraint.matches?(Object.new)
|
922
|
+
#=> true
|
923
|
+
constraint.matches?('Hello, world')
|
924
|
+
#=> true
|
925
|
+
```
|
926
|
+
|
927
|
+
**Boolean Constraint**
|
928
|
+
|
929
|
+
Matches `true` and `false`.
|
930
|
+
|
931
|
+
```ruby
|
932
|
+
constraint = Stannum::Constraints::Boolean.new
|
933
|
+
|
934
|
+
constraint.matches?(nil)
|
935
|
+
#=> false
|
936
|
+
constraint.matches?(Object.new)
|
937
|
+
#=> false
|
938
|
+
constraint.matches?(false)
|
939
|
+
#=> true
|
940
|
+
constraint.matches?(true)
|
941
|
+
#=> true
|
942
|
+
```
|
943
|
+
|
944
|
+
**Enum Constraint**
|
945
|
+
|
946
|
+
Matches any the specified values.
|
947
|
+
|
948
|
+
```ruby
|
949
|
+
constraint = Stannum::Constraints::Enum.new('red', 'blue', 'green')
|
950
|
+
|
951
|
+
constraint.matches?(nil)
|
952
|
+
#=> false
|
953
|
+
constraint.matches?('purple')
|
954
|
+
#=> false
|
955
|
+
constraint.matches?('red')
|
956
|
+
#=> true
|
957
|
+
constraint.matches?('green')
|
958
|
+
#=> true
|
959
|
+
```
|
960
|
+
|
961
|
+
**Equality Constraint**
|
962
|
+
|
963
|
+
Matches any object equal to the given object.
|
964
|
+
|
965
|
+
```ruby
|
966
|
+
value = 'Greetings, programs!'
|
967
|
+
constraint = Stannum::Constraints::Equality.new(value)
|
968
|
+
|
969
|
+
constraint.matches?(nil)
|
970
|
+
#=> false
|
971
|
+
constraint.matches?(value.dup)
|
972
|
+
#=> true
|
973
|
+
constraint.matches?(value)
|
974
|
+
#=> true
|
975
|
+
```
|
976
|
+
|
977
|
+
**Identity Constraint**
|
978
|
+
|
979
|
+
Matches the given object.
|
980
|
+
|
981
|
+
```ruby
|
982
|
+
value = 'Greetings, starfighter!'
|
983
|
+
constraint = Stannum::Constraints::Identity.new(value)
|
984
|
+
|
985
|
+
constraint.matches?(nil)
|
986
|
+
#=> false
|
987
|
+
constraint.matches?(value.dup)
|
988
|
+
#=> false
|
989
|
+
constraint.matches?(value)
|
990
|
+
#=> true
|
991
|
+
```
|
992
|
+
|
993
|
+
**Nothing Constraint**
|
994
|
+
|
995
|
+
Does not match any objects.
|
996
|
+
|
997
|
+
```ruby
|
998
|
+
constraint = Stannum::Constraints::Nothing.new
|
999
|
+
|
1000
|
+
constraint.matches?(nil)
|
1001
|
+
#=> false
|
1002
|
+
constraint.matches?(Object.new)
|
1003
|
+
#=> false
|
1004
|
+
constraint.matches?('Hello, world')
|
1005
|
+
#=> false
|
1006
|
+
```
|
1007
|
+
|
1008
|
+
<a id="builtin-constraints-presence"></a>
|
1009
|
+
|
1010
|
+
**Presence Constraint**
|
1011
|
+
|
1012
|
+
Matches objects that are not `nil`, and that either do not respond to `#empty?` or for whom `#empty?` returns false.
|
1013
|
+
|
1014
|
+
```ruby
|
1015
|
+
constraint = Stannum::Constraints::Presence.new
|
1016
|
+
|
1017
|
+
constraint.matches?(nil)
|
1018
|
+
#=> false
|
1019
|
+
constraint.matches?('')
|
1020
|
+
#=> false
|
1021
|
+
constraint.matches?('Greetings, programs!')
|
1022
|
+
#=> true
|
1023
|
+
constraint.matches?(Object.new)
|
1024
|
+
#=> true
|
1025
|
+
```
|
1026
|
+
|
1027
|
+
**Signature Constraint**
|
1028
|
+
|
1029
|
+
Matches if the object responds to all of the specified methods.
|
1030
|
+
|
1031
|
+
```ruby
|
1032
|
+
constraint = Stannum::Constraints::Signature.new(:[], :keys)
|
1033
|
+
|
1034
|
+
constraint.matches?(nil)
|
1035
|
+
#=> false
|
1036
|
+
constraint.matches?([])
|
1037
|
+
#=> false
|
1038
|
+
constraint.matches?({})
|
1039
|
+
#=> true
|
1040
|
+
```
|
1041
|
+
|
1042
|
+
<a id="builtin-constraints-type"></a>
|
1043
|
+
|
1044
|
+
**Type Constraint**
|
1045
|
+
|
1046
|
+
Matches if the specified type is an ancestor of the object.
|
1047
|
+
|
1048
|
+
```ruby
|
1049
|
+
constraint = Stannum::Constraints::Type.new(StandardError)
|
1050
|
+
|
1051
|
+
constraint.matches?(nil)
|
1052
|
+
#=> false
|
1053
|
+
constraint.matches?(Object.new)
|
1054
|
+
#=> false
|
1055
|
+
constraint.matches?(StandardError.new)
|
1056
|
+
#=> true
|
1057
|
+
constraint.matches?(ArgumentError.new)
|
1058
|
+
#=> true
|
1059
|
+
```
|
1060
|
+
|
1061
|
+
Type constraints can be `optional` by passing either `optional: true` or `required: false` to the constructor. An optional type constraint will also accept `nil` as a value.
|
1062
|
+
|
1063
|
+
```ruby
|
1064
|
+
constraint = Stannum::Constraints::Type.new(String, optional: true)
|
1065
|
+
|
1066
|
+
constraint.matches?(nil)
|
1067
|
+
#=> true
|
1068
|
+
constraint.matches?(Object.new)
|
1069
|
+
#=> false
|
1070
|
+
constraint.matches?('a String')
|
1071
|
+
#=> true
|
1072
|
+
```
|
1073
|
+
|
1074
|
+
**Union Constraint**
|
1075
|
+
|
1076
|
+
Matches if the object matches any of the given constraints.
|
1077
|
+
|
1078
|
+
```ruby
|
1079
|
+
constraint = Stannum::Constraints::Union.new(
|
1080
|
+
Stannum::Constraints::Type.new(String),
|
1081
|
+
Stannum::Constraints::Type.new(Symbol)
|
1082
|
+
)
|
1083
|
+
|
1084
|
+
constraint.matches?(nil)
|
1085
|
+
#=> false
|
1086
|
+
constraint.matches?(Object.new)
|
1087
|
+
#=> false
|
1088
|
+
constraint.matches?('a String')
|
1089
|
+
#=> true
|
1090
|
+
constraint.matches?(:a_symbol)
|
1091
|
+
#=> true
|
1092
|
+
```
|
1093
|
+
|
1094
|
+
#### Type Constraints
|
1095
|
+
|
1096
|
+
Stannum also defines a set of built-in type constraints. Unless otherwise noted, these are identical to a [Type Constraint](#builtin-constraints-type) with the given Class.
|
1097
|
+
|
1098
|
+
```ruby
|
1099
|
+
constraint = Stannum::Constraints::Types::StringType.new
|
1100
|
+
|
1101
|
+
constraint.matches?(nil)
|
1102
|
+
#=> false
|
1103
|
+
constraint.matches?(Object.new)
|
1104
|
+
#=> false
|
1105
|
+
constraint.matches?('a String')
|
1106
|
+
#=> true
|
1107
|
+
```
|
1108
|
+
|
1109
|
+
The following type constraints are defined:
|
1110
|
+
|
1111
|
+
- **ArrayType**
|
1112
|
+
- **BigDecimalType**
|
1113
|
+
- **DateTimeType**
|
1114
|
+
- **DateType**
|
1115
|
+
- **FloatType**
|
1116
|
+
- **HashType**
|
1117
|
+
- **IntegerType**
|
1118
|
+
- **NilType**
|
1119
|
+
- **ProcType**
|
1120
|
+
- **StringType**
|
1121
|
+
- **SymbolType**
|
1122
|
+
- **TimeType**
|
1123
|
+
|
1124
|
+
In addition, the following type constraints have additional options or behavior.
|
1125
|
+
|
1126
|
+
**ArrayType Constraint**
|
1127
|
+
|
1128
|
+
You can specify an `item_type` for an `ArrayType` constraint. An object will only match if the object is an `Array` and all of the array's items are of the specified type or match the given constraint.
|
1129
|
+
|
1130
|
+
```ruby
|
1131
|
+
constraint = Stannum::Constraints::Types::ArrayType.new(item_type: String)
|
1132
|
+
|
1133
|
+
constraint.matches?(nil)
|
1134
|
+
#=> false
|
1135
|
+
constraint.matches?([])
|
1136
|
+
#=> true
|
1137
|
+
constraint.matches?([1, 2, 3])
|
1138
|
+
#=> false
|
1139
|
+
constraint.matches?(['uno', 'dos', 'tres'])
|
1140
|
+
#=> true
|
1141
|
+
```
|
1142
|
+
|
1143
|
+
You can also specify whether the constraint allows an empty Array by setting the `allow_empty: false` keyword in the constructor. If `false`, the constraint will only match arrays with one or more items. The default value is `true`.
|
1144
|
+
|
1145
|
+
```ruby
|
1146
|
+
constraint = Stannum::Constraints::Types::ArrayType.new(allow_empty: false)
|
1147
|
+
|
1148
|
+
constraint.matches?(nil)
|
1149
|
+
#=> false
|
1150
|
+
constraint.matches?([])
|
1151
|
+
#=> false
|
1152
|
+
constraint.matches?([1, 2, 3])
|
1153
|
+
#=> true
|
1154
|
+
```
|
1155
|
+
|
1156
|
+
**HashType Constraint**
|
1157
|
+
|
1158
|
+
You can specify a `key_type` and/or a `value_type` for a `HashType` constraint. An object will only match if the object is a `Hash`, all of the hash's keys and/or values are of the specified type or match the given constraint.
|
1159
|
+
|
1160
|
+
```ruby
|
1161
|
+
constraint = Stannum::Constraints::Types::HashType.new(key_type: String, value_type: Integer)
|
1162
|
+
|
1163
|
+
constraint.matches?(nil)
|
1164
|
+
#=> false
|
1165
|
+
constraint.matches?({})
|
1166
|
+
#=> true
|
1167
|
+
constraint.matches?({ ichi: 1 })
|
1168
|
+
#=> false
|
1169
|
+
constraint.matches?({ 'ichi' => 'one' })
|
1170
|
+
#=> false
|
1171
|
+
constraint.matches?({ 'ichi' => 1 })
|
1172
|
+
```
|
1173
|
+
|
1174
|
+
You can also specify whether the constraint allows an empty Hash by setting the `allow_empty: false` keyword in the constructor. If `false`, the constraint will only match hashes with one or more keys. The default value is `true`.
|
1175
|
+
|
1176
|
+
```ruby
|
1177
|
+
constraint = Stannum::Constraints::Types::HashType.new(allow_empty: false
|
1178
|
+
|
1179
|
+
constraint.matches?(nil)
|
1180
|
+
#=> false
|
1181
|
+
constraint.matches?({})
|
1182
|
+
#=> false
|
1183
|
+
constraint.matches?({ ichi: 1 })
|
1184
|
+
#=> true
|
1185
|
+
```
|
1186
|
+
|
1187
|
+
There are predefined constraints for matching `Hash`es with common key types:
|
1188
|
+
|
1189
|
+
- **HashWithIndifferentKeys:** Matches keys that are either `String`s or `Symbol`s and not empty.
|
1190
|
+
- **HashWithStringKeys:** Matches keys that are `String`s.
|
1191
|
+
- **HashWithSymbolKeys:** Matches keys that are `Symbol`s.
|
1192
|
+
|
1193
|
+
<a id="signature-constraints"></a>
|
1194
|
+
|
1195
|
+
#### Signature Constraints
|
1196
|
+
|
1197
|
+
Stannum provides a small number of built-in signature constraints.
|
1198
|
+
|
1199
|
+
```ruby
|
1200
|
+
constraint = Stannum::Constraints::Signatures::Map.new
|
1201
|
+
|
1202
|
+
constraint.matches?(nil)
|
1203
|
+
#=> false
|
1204
|
+
constraint.matches?([])
|
1205
|
+
#=> false
|
1206
|
+
constraint.matches?({})
|
1207
|
+
#=> true
|
1208
|
+
```
|
1209
|
+
|
1210
|
+
- **Map:** Matches objects that behave like a `Hash`. Specifically, objects responding to `#[]`, `#each`, and `#keys`.
|
1211
|
+
- **Tuple:** Matches objects that behave like an `Array`. Specifically, objects responding to `#[]`, `#each`, and `#size`.
|
1212
|
+
|
1213
|
+
<a id="builtin-contracts"></a>
|
1214
|
+
|
1215
|
+
### Built-In Contracts
|
1216
|
+
|
1217
|
+
Stannum defines some pre-defined contracts.
|
1218
|
+
|
1219
|
+
**Array Contract**
|
1220
|
+
|
1221
|
+
Matches an instance of `Array` and defines the `.item` class method to add constraints on the array items. See also [Array Contracts](#array-contracts), above.
|
1222
|
+
|
1223
|
+
```ruby
|
1224
|
+
class BaseballContract < Stannum::Contracts::ArrayContract
|
1225
|
+
def initialize
|
1226
|
+
super do
|
1227
|
+
item { |actual| actual == 'Who' }
|
1228
|
+
item { |actual| actual == 'What' }
|
1229
|
+
item { |actual| actual == "I Don't Know" }
|
1230
|
+
end
|
1231
|
+
end
|
1232
|
+
end
|
1233
|
+
```
|
1234
|
+
|
1235
|
+
**Hash Contract**
|
1236
|
+
|
1237
|
+
Matches an instance of `Hash` and defines the `.key` class method to add constraints on the hash keys and values. See also [Hash Contracts](#hash-contracts), above.
|
1238
|
+
|
1239
|
+
```ruby
|
1240
|
+
class ResponseContract < Stannum::Contracts::HashContract
|
1241
|
+
def initialize
|
1242
|
+
super do
|
1243
|
+
key :status, Stannum::Constraints::Types::IntegerType.new
|
1244
|
+
|
1245
|
+
key :json,
|
1246
|
+
Stannum::Contracts::HashContract.new(allow_extra_keys: true) do
|
1247
|
+
key :ok, Stannum::Constraints::Boolean.new
|
1248
|
+
end
|
1249
|
+
|
1250
|
+
key :signature, Stannum::Constraints::Presence.new
|
1251
|
+
end
|
1252
|
+
end
|
1253
|
+
end
|
1254
|
+
```
|
1255
|
+
|
1256
|
+
Stannum also defines an `IndifferentHashContract` class, which will match against both string and symbol keys.
|
1257
|
+
|
1258
|
+
**Map Contract**
|
1259
|
+
|
1260
|
+
As a `HashContract`, but matches against any object which uses the `#[]` operator to access key-value data. See [Map Constraint](signature-constraints), above.
|
1261
|
+
|
1262
|
+
**Parameters Contract**
|
1263
|
+
|
1264
|
+
Matches the parameters of a method call.
|
1265
|
+
|
1266
|
+
```ruby
|
1267
|
+
class AuthorizationParameters < Stannum::Contracts::ParametersContract
|
1268
|
+
def initialize
|
1269
|
+
super do
|
1270
|
+
argument :action, Symbol
|
1271
|
+
|
1272
|
+
argument :record_class, Class, default: true
|
1273
|
+
|
1274
|
+
keyword :role, String, default: true
|
1275
|
+
|
1276
|
+
keyword :user, Stannum::Constraints::Type.new(User)
|
1277
|
+
end
|
1278
|
+
end
|
1279
|
+
end
|
1280
|
+
|
1281
|
+
contract = AuthorizationParameters.new
|
1282
|
+
parameters = {
|
1283
|
+
arguments: [:create, Article],
|
1284
|
+
keywords: {},
|
1285
|
+
block: nil
|
1286
|
+
}
|
1287
|
+
contract.matches?(parameters)
|
1288
|
+
#=> false
|
1289
|
+
errors = contract.errors_for(parameters)
|
1290
|
+
errors[:arguments].empty?
|
1291
|
+
#=> true
|
1292
|
+
errors[:keywords].empty?
|
1293
|
+
#=> false
|
1294
|
+
```
|
1295
|
+
|
1296
|
+
Each `ParametersContract` defines `.argument`, `.keyword`, and `.block` class methods to define the expected method parameters.
|
1297
|
+
|
1298
|
+
- The `.argument` class method defines an expected argument. Like the `.item` class method in an `ArrayContract` (see [Array Contracts](#array-contracts), above), each call to `.argument` will reference the next positional argument.
|
1299
|
+
- The `.keyword` class method defines an expected keyword.
|
1300
|
+
- The `.block` class method can accept either a constraint, or `true` or `false`. If given a constraint, the block passed to the method will be matched against the constraint. If given `true`, then the contract will match against any block and will fail if the method is not called with a block; likewise, if given `false`, the contract will match if no block is given and fail if the method is called with a block.
|
1301
|
+
|
1302
|
+
Because of Ruby's semantics around arguments and keywords with default values, the `:default` keyword has a special meaning for parameters contracts. If `.argument` or `.keyword` is called with the `:default` keyword, it indicates that that parameter has a default value in the method definition. If that argument or keyword is *omitted*, the parameters will still match the contract. However, an explicit value of `nil` will still fail unless `nil` is a valid value for the relevant constraint.
|
1303
|
+
|
1304
|
+
`ParametersContract` also has support for variadic arguments and keywords.
|
1305
|
+
|
1306
|
+
```ruby
|
1307
|
+
class RecipeParameters < Stannum::Contracts::ParametersContract
|
1308
|
+
def initialize
|
1309
|
+
super do
|
1310
|
+
arguments :tools, String
|
1311
|
+
keywords :ingredients, Stannum::Contracts::TupleContract.new do
|
1312
|
+
item Stannum::Constraints::Type.new(String),
|
1313
|
+
property_name: :amount
|
1314
|
+
item Stannum::Constraints::Type.new(String, optional: true),
|
1315
|
+
property_name: :unit
|
1316
|
+
end
|
1317
|
+
block true
|
1318
|
+
end
|
1319
|
+
end
|
1320
|
+
end
|
1321
|
+
```
|
1322
|
+
|
1323
|
+
The `.arguments` class method creates a constraint that matches against any arguments without an explicit `.argument` expectation. Likewise, the `.keywords` class method creates a constraint that matches against any keywords without an explicit `.keyword` expectation. The contract will automatically convert a Class into the corresponding Type constraint (see [Type Constraint](#builtin-constraints-type), above).
|
1324
|
+
|
1325
|
+
**Tuple Contract**
|
1326
|
+
|
1327
|
+
As an `ArrayContract`, but matches against any object which uses the `#[]` operator to access indexed data. See [Tuple Constraint](signature-constraints), above.
|