stannum 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -1
- data/README.md +129 -1263
- data/config/locales/en.rb +4 -0
- data/lib/stannum/association.rb +293 -0
- data/lib/stannum/associations/many.rb +250 -0
- data/lib/stannum/associations/one.rb +106 -0
- data/lib/stannum/associations.rb +11 -0
- data/lib/stannum/attribute.rb +86 -8
- data/lib/stannum/constraints/base.rb +3 -5
- data/lib/stannum/constraints/enum.rb +1 -1
- data/lib/stannum/constraints/equality.rb +1 -1
- data/lib/stannum/constraints/format.rb +72 -0
- data/lib/stannum/constraints/hashes/extra_keys.rb +7 -12
- data/lib/stannum/constraints/identity.rb +1 -1
- data/lib/stannum/constraints/properties/base.rb +1 -1
- data/lib/stannum/constraints/properties/do_not_match_property.rb +11 -11
- data/lib/stannum/constraints/properties/match_property.rb +11 -11
- data/lib/stannum/constraints/properties/matching.rb +7 -7
- data/lib/stannum/constraints/signature.rb +2 -2
- data/lib/stannum/constraints/tuples/extra_items.rb +6 -6
- data/lib/stannum/constraints/type.rb +3 -3
- data/lib/stannum/constraints/types/array_type.rb +2 -2
- data/lib/stannum/constraints/types/hash_type.rb +4 -4
- data/lib/stannum/constraints/union.rb +1 -1
- data/lib/stannum/constraints/uuid.rb +30 -0
- data/lib/stannum/constraints.rb +2 -0
- data/lib/stannum/contract.rb +7 -7
- data/lib/stannum/contracts/array_contract.rb +2 -7
- data/lib/stannum/contracts/base.rb +15 -15
- data/lib/stannum/contracts/builder.rb +2 -2
- data/lib/stannum/contracts/hash_contract.rb +3 -9
- data/lib/stannum/contracts/indifferent_hash_contract.rb +2 -2
- data/lib/stannum/contracts/map_contract.rb +6 -10
- data/lib/stannum/contracts/parameters/arguments_contract.rb +1 -1
- data/lib/stannum/contracts/parameters/keywords_contract.rb +1 -1
- data/lib/stannum/contracts/parameters/signature_contract.rb +1 -1
- data/lib/stannum/contracts/parameters_contract.rb +4 -4
- data/lib/stannum/contracts/tuple_contract.rb +5 -5
- data/lib/stannum/entities/associations.rb +451 -0
- data/lib/stannum/entities/attributes.rb +116 -18
- data/lib/stannum/entities/constraints.rb +3 -2
- data/lib/stannum/entities/primary_key.rb +148 -0
- data/lib/stannum/entities/properties.rb +30 -8
- data/lib/stannum/entities.rb +5 -2
- data/lib/stannum/entity.rb +4 -0
- data/lib/stannum/errors.rb +9 -13
- data/lib/stannum/messages/default_strategy.rb +2 -2
- data/lib/stannum/parameter_validation.rb +10 -10
- data/lib/stannum/rspec/match_errors_matcher.rb +1 -1
- data/lib/stannum/rspec/validate_parameter.rb +2 -2
- data/lib/stannum/rspec/validate_parameter_matcher.rb +15 -13
- data/lib/stannum/schema.rb +62 -62
- data/lib/stannum/support/optional.rb +1 -1
- data/lib/stannum/version.rb +4 -4
- data/lib/stannum.rb +3 -0
- metadata +14 -79
data/README.md
CHANGED
@@ -2,24 +2,23 @@
|
|
2
2
|
|
3
3
|
A library for defining and validating data structures.
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
## About
|
5
|
+
<blockquote>
|
6
|
+
Read The
|
7
|
+
<a href="https://www.sleepingkingstudios.com/stannum" target="_blank">
|
8
|
+
Documentation
|
9
|
+
</a>
|
10
|
+
</blockquote>
|
13
11
|
|
14
12
|
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
13
|
|
16
|
-
|
14
|
+
It defines the following objects:
|
17
15
|
|
18
|
-
|
16
|
+
- [Constraints](http://sleepingkingstudios.github.io/stannum/constraints): A validator object that responds to `#match`, `#matches?` and `#errors_for` for a given object.
|
17
|
+
- [Contracts](http://sleepingkingstudios.github.io/stannum/contracts): A collection of constraints about an object or its properties. Obeys the `Constraint` interface.
|
18
|
+
- [Errors](http://sleepingkingstudios.github.io/stannum/errors): Data object for storing validation errors. Supports arbitrary nesting of errors.
|
19
|
+
- [Entities](http://sleepingkingstudios.github.io/stannum/entities): Defines a mutable data object with a specified set of typed attributes.
|
19
20
|
|
20
|
-
|
21
|
-
|
22
|
-
### Why Stannum?
|
21
|
+
## Why Stannum?
|
23
22
|
|
24
23
|
Stannum is not tied to any framework. You can create constraints and contracts to validate Ruby objects and Entities, data structures such as Arrays, Hashes, and Sets, and even framework objects such as `ActiveRecord::Model`s and `Mongoid::Document`s.
|
25
24
|
|
@@ -33,15 +32,19 @@ Still, most projects and applications use one framework to handle their data. Wh
|
|
33
32
|
|
34
33
|
### Compatibility
|
35
34
|
|
36
|
-
Stannum is tested against Ruby (MRI)
|
35
|
+
Stannum is tested against Ruby (MRI) 3.1 through 3.4.
|
37
36
|
|
38
37
|
### Documentation
|
39
38
|
|
40
|
-
|
39
|
+
Code documentation is generated using [YARD](https://yardoc.org/), and can be generated locally using the `yard` gem.
|
40
|
+
|
41
|
+
The full documentation is available via [GitHub Pages](http://sleepingkingstudios.github.io/stannum), and includes the code documentation as well as a deeper explanation of Stannum's features and design philosophy. It also includes documentation for prior versions of the gem.
|
42
|
+
|
43
|
+
To generate documentation locally, see the [SleepingKingStudios::Docs](https://github.com/sleepingkingstudios/sleeping_king_studios-docs) gem.
|
41
44
|
|
42
45
|
### License
|
43
46
|
|
44
|
-
Copyright (c) 2019-
|
47
|
+
Copyright (c) 2019-2025 Rob Smith
|
45
48
|
|
46
49
|
Stannum is released under the [MIT License](https://opensource.org/licenses/MIT).
|
47
50
|
|
@@ -57,1335 +60,198 @@ To contribute code, please fork the repository, make the desired updates, and th
|
|
57
60
|
|
58
61
|
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
62
|
|
60
|
-
|
63
|
+
## Getting Started
|
64
|
+
Let's take a look at using Stannum to model a problem domain. Consider the following case study: we are implementing an ecommerce application. We want to ensure that our Order object is valid through each stage of our ordering process. For the sake of simplicity, let's say our process has three steps:
|
61
65
|
|
62
|
-
|
66
|
+
- A customer creates an order.
|
67
|
+
- The customer is billed for the order.
|
68
|
+
- The order is shipped to the customer.
|
63
69
|
|
64
|
-
|
70
|
+
### Defining Entities
|
65
71
|
|
66
|
-
|
72
|
+
Our first step is to define some entities to represent our data.
|
67
73
|
|
68
74
|
```ruby
|
69
|
-
|
70
|
-
|
75
|
+
class Customer
|
76
|
+
include Stannum::Entity
|
71
77
|
|
72
|
-
|
78
|
+
define_primary_key :id, Integer
|
73
79
|
|
74
|
-
|
80
|
+
define_attribute :email, String
|
81
|
+
define_attribute :address, String
|
75
82
|
|
76
|
-
|
77
|
-
constraint = Stannum::Constraint.new do |object|
|
78
|
-
object.is_a?(String) && !object.empty?
|
83
|
+
define_association :many, :orders
|
79
84
|
end
|
80
85
|
```
|
81
86
|
|
82
|
-
|
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.
|
87
|
+
Our `Customer` class represents a user who can create an order in our system. We define an `#id` primary key, attributes `#email` and `#address`, and a plural association to our `Order` entity.
|
184
88
|
|
185
89
|
```ruby
|
186
|
-
class
|
187
|
-
|
188
|
-
TYPE = 'examples.constraints.even'
|
90
|
+
class Payment
|
91
|
+
include Stannum::Entity
|
189
92
|
|
190
|
-
|
191
|
-
return super if actual.is_a?(Integer)
|
93
|
+
define_primary_key :id, Integer
|
192
94
|
|
193
|
-
|
194
|
-
.add('examples.constraints.type', type: Integer)
|
195
|
-
end
|
95
|
+
define_attribute :amount, BigDecimal
|
196
96
|
|
197
|
-
|
198
|
-
actual.is_a?(Integer) && actual.even?
|
199
|
-
end
|
97
|
+
define_association :one, :order, foreign_key: true
|
200
98
|
end
|
201
99
|
```
|
202
100
|
|
203
|
-
|
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
|
101
|
+
Our `Payment` class represents a payment submitted by our customer for an order. We again define an `#id` primary key, as well as an `#amount` attribute and a singular association to an `Order`. Note that we are specifying a foreign key for the `#order` association, which automatically creates an `#order_id` attribute.
|
232
102
|
|
233
103
|
```ruby
|
234
|
-
|
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.
|
104
|
+
class Order
|
105
|
+
include Stannum::Entity
|
238
106
|
|
239
|
-
|
107
|
+
define_primary_key :id, Integer
|
240
108
|
|
241
|
-
|
242
|
-
contract = Stannum::Contract.new do
|
243
|
-
constraint(type: 'examples.constraints.numeric') do |actual|
|
244
|
-
actual.is_a?(Numeric)
|
245
|
-
end
|
109
|
+
define_attribute :amount, BigDecimal
|
246
110
|
|
247
|
-
|
248
|
-
|
249
|
-
end
|
111
|
+
define_association :one, :customer, foreign_key: true
|
112
|
+
define_association :one, :payment
|
250
113
|
|
251
|
-
constraint
|
252
|
-
actual >= 0 && actual <= 10 rescue false
|
253
|
-
end
|
114
|
+
constraint :customer, Stannum::Constraints::Presence.new
|
254
115
|
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
116
|
```
|
274
117
|
|
275
|
-
|
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
|
118
|
+
Our core class for this workflow is the `Order` entity. We define our `#id` and `#amount`, and associations to the `Customer` and `Payment` entities - again, by passing `foreign_key: true` to our `#customer` association, we also define a `#customer_id` attribute. Finally, we are defining an additional constraint. When validating the `Order` using the default contract, we will require the `#customer` association to be populated - an `Order` must always have an associated `Customer`. However, we are *not* requiring the presence of a `Payment`, since that requirement is not applicable to the entire `Order` lifecycle.
|
283
119
|
|
284
|
-
|
285
|
-
|
286
|
-
contract.matches?(5)
|
287
|
-
#=> false
|
288
|
-
contract.errors_for(99).map(&:type)
|
289
|
-
#=> ['examples.constraints.even']
|
120
|
+
### Defining Validators
|
290
121
|
|
291
|
-
|
292
|
-
#=> true
|
293
|
-
```
|
122
|
+
Actually implementing the business logic for orders is outside the scope of Stannum - for a structured approach to defining your business logic, take a look at the [Cuprum](https://www.sleepingkingstudios.com/cuprum/) gem.
|
294
123
|
|
295
|
-
|
124
|
+
However, that doesn't mean we're finished. One of the challenges in implementing a multi-step process like our ordering flow is validation. Specifically, this kind of workflow requires *contextual* validation - an `Order` object that is valid for one part of the flow may not be valid for others. Rather than defining conditional logic in our `Order` class, let's instead apply the concept of a Validator: an object that is responsible for validating an entity or data structure *in a particular context*.
|
296
125
|
|
297
|
-
|
126
|
+
Let's start with our first step, order creation. Creating an order requires a valid `#id`, a valid `#amount` (can be zero at this point in the workflow), and an associated `#customer`. Fortunately, we already have a contract defined for these requirements: the existing `Order::Contract`, which validates the entity's attributes and any additional `constraint`s defined on the entity.
|
298
127
|
|
299
|
-
|
128
|
+
Here's how we could use that in our business logic:
|
300
129
|
|
301
130
|
```ruby
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
hsh[:color] == 'circle'
|
309
|
-
end
|
310
|
-
end
|
131
|
+
customer = Customer.new(
|
132
|
+
id: 0,
|
133
|
+
email: 'user@example.com',
|
134
|
+
address: '123 Example St'
|
135
|
+
)
|
136
|
+
order = Order.new(id: 1)
|
311
137
|
|
312
|
-
|
313
|
-
#=> true
|
314
|
-
contract.does_not_match?({ color: 'red', shape: 'circle' })
|
138
|
+
Order::Contract.matches?(order)
|
315
139
|
#=> false
|
316
|
-
|
317
|
-
#=>
|
140
|
+
errors = Order::Contract.errors_for(order)
|
141
|
+
#=> an instance of Stannum::Errors
|
142
|
+
errors.summary
|
143
|
+
#=> "amount: is not a BigDecimal, customer: is nil or empty"
|
318
144
|
|
319
|
-
|
320
|
-
|
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']
|
145
|
+
order.amount = BigDecimal('0.0')
|
146
|
+
order.customer = customer
|
325
147
|
|
326
|
-
|
327
|
-
#=> false
|
328
|
-
contract.does_not_match?({ color: 'blue', shape: 'square'})
|
148
|
+
Order::Contract.matches?(order)
|
329
149
|
#=> true
|
330
150
|
```
|
331
151
|
|
332
|
-
|
333
|
-
|
334
|
-
#### Constraining Properties
|
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.
|
152
|
+
If our contract `#matches?` the order, we proceed with the creation logic. Otherwise, we return an error message, possibly using the errors `#summary`.
|
376
153
|
|
377
|
-
|
154
|
+
Now we move on to validating that an order is ready for billing. Our validation logic gets more complicated here: in addition to requiring a valid order (the same validations as above), we need to make sure that the billable amount is greater than zero.
|
378
155
|
|
379
|
-
|
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.
|
156
|
+
One common approach is to add conditional validation to the `Order` class itself. For example, defining a `#status` attribute asserting that the `#amount` is greater than zero if the status matches a value. However, as new cases and conditions are added, this approach quickly becomes difficult to read and reason about. Instead, we're going to define a validator object.
|
431
157
|
|
432
158
|
```ruby
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
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.
|
159
|
+
module Orders
|
160
|
+
module Contracts
|
161
|
+
IS_BILLABLE = Stannum::Contract.new do
|
162
|
+
concat(Order::Contract)
|
441
163
|
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
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" }
|
164
|
+
property :amount,
|
165
|
+
message: 'must be greater than zero',
|
166
|
+
type: 'orders.constraints.greater_than_zero' \
|
167
|
+
do |value|
|
168
|
+
value.is_a?(Numeric) && value > 0
|
169
|
+
end
|
170
|
+
property :payment, Stannum::Constraints::Types::NilType.new
|
457
171
|
end
|
458
172
|
end
|
459
173
|
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
174
|
```
|
495
175
|
|
496
|
-
|
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']]
|
176
|
+
Our validator is an instance of `Stannum::Contract`, and our first step is to `concat` the existing `Order::Contract`. This means that all of the constraints in the `Order::Contract` will also be applied when matching an order with the `IS_BILLABLE` contract. This means we don't need to duplicate our existing constraints.
|
525
177
|
|
526
|
-
|
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
|
178
|
+
Second, we are adding a custom `constraint` on the `#amount` attribute. Notice that the first check inside the block checks that the value is `Numeric`; otherwise, comparing a `nil` value would raise an exception, rather than failing the validation. We are also defining a custom `message` and `type` for the constraint. The `message` is intended to be a human-readable representation of the error, while the `type` is intended for machines.
|
538
179
|
|
539
|
-
|
540
|
-
#=> false
|
541
|
-
contract.errors_for(response).map { |err| [err.path, err.type] }
|
542
|
-
#=> [[%i[role], 'stannum.constraints.hashes.extra_keys']]
|
543
|
-
```
|
180
|
+
Finally, we validate that the `#payment` association is nil, since we don't want to accidentally bill the same order twice. Here we can see why a validator object is so powerful - we obviously can't add this kind of constraint directly to `Order`, since orders later in the workflow will clearly not match. However, since our `IS_BILLABLE` contract applies only to this specific context, we can make the validation logic as specific as we want.
|
544
181
|
|
545
|
-
|
182
|
+
Our final step is to ship the order to the customer. Again, to determine if the order is ready to be shipped, we define a validator object:
|
546
183
|
|
547
184
|
```ruby
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
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`.
|
185
|
+
module Orders
|
186
|
+
module Contracts
|
187
|
+
IS_SHIPPABLE = Stannum::Contract.new do
|
188
|
+
concat(Order::Contract)
|
555
189
|
|
556
|
-
|
190
|
+
property :payment, Stannum::Constraints::Presence.new
|
557
191
|
|
558
|
-
|
559
|
-
|
560
|
-
|
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
|
192
|
+
property :customer, Stannum::Contract.new {
|
193
|
+
property :address, Stannum::Constraints::Presence.new
|
194
|
+
}
|
567
195
|
end
|
568
196
|
end
|
569
197
|
end
|
570
198
|
```
|
571
199
|
|
572
|
-
|
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
|
-
```
|
200
|
+
Again, we define a custom `Stannum::Contract` and `concat` the existing `Order::Contract`, and we add a constraint that the `#payment` association needs to be populated - we don't want to ship an order that hasn't been paid for yet. Next, we define a nested contract to assert that the `#customer` association has a present `#address` attribute. You can define complex validation logic easily by composing together multiple constraints and contracts.
|
595
201
|
|
596
|
-
|
202
|
+
Here is how we would use the `IS_BILLABLE` and `IS_SHIPPABLE` contracts in our business logic:
|
597
203
|
|
598
204
|
```ruby
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
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.
|
205
|
+
customer = Customer.new(
|
206
|
+
id: 0,
|
207
|
+
email: 'user@example.com',
|
208
|
+
address: '123 Example St'
|
209
|
+
)
|
210
|
+
order = Order.new(id: 1, customer:)
|
622
211
|
|
623
|
-
|
624
|
-
errors.count
|
625
|
-
#=> 3
|
626
|
-
errors.empty?
|
212
|
+
Orders::Contracts::IS_BILLABLE.matches?(order)
|
627
213
|
#=> false
|
628
|
-
errors.
|
629
|
-
|
630
|
-
|
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`.
|
214
|
+
errors = Orders::Contracts::IS_BILLABLE.errors_for(order)
|
215
|
+
errors.summary
|
216
|
+
#=> "amount: is not a BigDecimal, amount: must be greater than zero"
|
644
217
|
|
645
|
-
|
646
|
-
|
647
|
-
errors.count
|
648
|
-
#=> 0
|
649
|
-
errors.empty?
|
218
|
+
order.amount = BigDecimal('100.0')
|
219
|
+
Orders::Contracts::IS_BILLABLE.matches?(order)
|
650
220
|
#=> true
|
651
221
|
|
652
|
-
|
653
|
-
#=> the errors object
|
654
|
-
errors.count
|
655
|
-
#=> 1
|
656
|
-
errors.empty?
|
222
|
+
Orders::Contracts::IS_SHIPPABLE.matches?(order)
|
657
223
|
#=> false
|
658
|
-
errors.
|
659
|
-
|
660
|
-
|
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.
|
224
|
+
errors = Orders::Contracts::IS_SHIPPABLE.errors_for(order)
|
225
|
+
errors.summary
|
226
|
+
#=> "payment: is nil or empty"
|
716
227
|
|
717
|
-
|
718
|
-
|
719
|
-
#=>
|
720
|
-
|
721
|
-
errors = errors.with_messages.first.message
|
722
|
-
errors.first.message
|
723
|
-
#=> 'is invalid'
|
228
|
+
order.payment = Payment.new(id: 2, amount: order.amount)
|
229
|
+
Orders::Contracts::IS_SHIPPABLE.matches?(order)
|
230
|
+
#=> true
|
724
231
|
```
|
725
232
|
|
726
|
-
|
727
|
-
|
728
|
-
<a id="entities"></a>
|
233
|
+
As we define our `BillOrder` and `ShipOrder` classes, we will be able to use our `IS_BILLABLE` and `IS_SHIPPABLE` contracts to quickly identify orders that are invalid for that context.
|
729
234
|
|
730
|
-
###
|
235
|
+
### Validating Other Data
|
731
236
|
|
732
|
-
|
733
|
-
|
734
|
-
Entities are defined by creating a new class and including `Stannum::Entity`:
|
237
|
+
In addition to using them with entities, `Stannum` constraints and contracts can be used to validate almost any sort of data. For example, consider our ordering workflow. Perhaps we make a API call to a company that actually ships the order to the customer. We want to ensure that the API response contains the expected data. We can define a contract to validate the returned JSON body:
|
735
238
|
|
736
239
|
```ruby
|
737
|
-
|
240
|
+
SUCCESS_RESPONSE_CONTRACT = Stannum::Contract.new do
|
241
|
+
property :status, Stannum::Constraints::Identity.new(200)
|
738
242
|
|
739
|
-
|
740
|
-
|
243
|
+
property :body, Stannum::Contracts::HashContract.new {
|
244
|
+
key 'ok', Stannum::Constraints::Identity.new(true)
|
741
245
|
|
742
|
-
|
743
|
-
|
744
|
-
|
246
|
+
key 'shipping_confirmation',
|
247
|
+
Stannum::Constraint.new(
|
248
|
+
message: 'be a string with length 24',
|
249
|
+
type: 'orders.constraints.valid_shipping_confirmation'
|
250
|
+
) { |value|
|
251
|
+
value.is_a?(String) && value.size == 24
|
252
|
+
}
|
253
|
+
}
|
745
254
|
end
|
746
|
-
|
747
|
-
gadget = Gadget.new(name: 'Self-Sealing Stem Bolt')
|
748
|
-
gadget.name
|
749
|
-
#=> 'Self-Sealing Stem Bolt'
|
750
|
-
gadget.description
|
751
|
-
#=> nil
|
752
|
-
gadget.attributes
|
753
|
-
#=> {
|
754
|
-
# name: 'Self-Sealing Stem Bolt',
|
755
|
-
# description: nil,
|
756
|
-
# quantity: 0
|
757
|
-
# }
|
758
|
-
|
759
|
-
gadget.quantity = 10
|
760
|
-
gadget.quantity
|
761
|
-
#=> 10
|
762
|
-
|
763
|
-
gadget[:description] = 'No one is sure what a self-sealing stem bolt is.'
|
764
|
-
gadget[:description]
|
765
|
-
#=> 'No one is sure what a self-sealing stem bolt is.'
|
766
255
|
```
|
767
256
|
|
768
|
-
|
769
|
-
|
770
|
-
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 entity's attributes and values using the `#attributes` method.
|
771
|
-
|
772
|
-
`Stannum::Entity` defines a number of helper methods for interacting with a entity's attributes:
|
773
|
-
|
774
|
-
- `#[](attribute)`: Returns the value of the given attribute.
|
775
|
-
- `#[]=(attribute, value)`: Writes the given value to the given attribute.
|
776
|
-
- `#assign_attributes(values)`: Updates the entity's attributes using the given values. If an attribute is not given, that value is unchanged.
|
777
|
-
- `#attributes`: Returns a hash containing the attribute keys and values.
|
778
|
-
- `#attributes=(values)`: Sets the entity's attributes to the given values. If an attribute is not given, that attribute is set to `nil`.
|
779
|
-
|
780
|
-
For all of the above methods, if a given attribute is invalid or the attribute is not defined on the entity, an `ArgumentError` will be raised.
|
781
|
-
|
782
|
-
#### Attributes
|
783
|
-
|
784
|
-
A entity's attributes are defined using the `.attribute` class method, and can be accessed and enumerated using the `.attributes` class method on the entity 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`.
|
785
|
-
|
786
|
-
```ruby
|
787
|
-
Gadget::Attributes
|
788
|
-
#=> an instance of Stannum::Schema
|
789
|
-
Gadget.attributes
|
790
|
-
#=> an instance of Stannum::Schema
|
791
|
-
Gadget.attributes.count
|
792
|
-
#=> 3
|
793
|
-
Gadget.attributes.keys
|
794
|
-
#=> [:name, :description, :quantity]
|
795
|
-
Gadget.attributes[:name]
|
796
|
-
#=> an instance of Stannum::Attribute
|
797
|
-
Gadget.attributes[:quantity].options
|
798
|
-
#=> { default: 0, required: true }
|
799
|
-
```
|
800
|
-
|
801
|
-
##### Default Values
|
802
|
-
|
803
|
-
Entities can define default values for attributes by passing a `:default` value to the `.attribute` call.
|
804
|
-
|
805
|
-
```ruby
|
806
|
-
class LightsCounter
|
807
|
-
include Stannum::Entity
|
808
|
-
|
809
|
-
attribute :count, Integer, default: 4
|
810
|
-
end
|
811
|
-
|
812
|
-
LightsCounter.new.count
|
813
|
-
#=> 4
|
814
|
-
```
|
815
|
-
|
816
|
-
##### Optional Attributes
|
817
|
-
|
818
|
-
Entity classes can also mark attributes as `optional`. When an entity is validated (see [Validation](#entities-validation), below), optional attributes will pass with a value of `nil`.
|
819
|
-
|
820
|
-
```ruby
|
821
|
-
class WhereWeAreGoing
|
822
|
-
include Stannum::Entity
|
823
|
-
|
824
|
-
attribute :roads, Object, optional: true
|
825
|
-
end
|
826
|
-
```
|
827
|
-
|
828
|
-
`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.
|
829
|
-
|
830
|
-
<a id="entities-validation"></a>
|
831
|
-
|
832
|
-
#### Validation
|
833
|
-
|
834
|
-
Each `Stannum::Entity` automatically generates a contract that can be used to validate instances of the entity class. The contract can be accessed using the `.contract` class method or via the `::Contract` constant.
|
835
|
-
|
836
|
-
```ruby
|
837
|
-
class Gadget
|
838
|
-
attribute :name, String
|
839
|
-
attribute :description, String, optional: true
|
840
|
-
attribute :quantity, Integer, default: 0
|
841
|
-
end
|
842
|
-
|
843
|
-
Gadget::Contract
|
844
|
-
#=> an instance of Stannum::Contract
|
845
|
-
Gadget.contract
|
846
|
-
#=> an instance of Stannum::Contract
|
847
|
-
|
848
|
-
gadget = Gadget.new
|
849
|
-
Gadget.contract.matches?(gadget)
|
850
|
-
#=> false
|
851
|
-
Gadget.contract.errors_for(gadget)
|
852
|
-
#=> [
|
853
|
-
# {
|
854
|
-
# data: { type: String },
|
855
|
-
# message: nil,
|
856
|
-
# path: [:name],
|
857
|
-
# type: 'stannum.constraints.is_not_type'
|
858
|
-
# }
|
859
|
-
# ]
|
860
|
-
|
861
|
-
gadget = Gadget.new(name: 'Self-Sealing Stem Bolt')
|
862
|
-
Gadget.contract.matches?(gadget)
|
863
|
-
#=> true
|
864
|
-
```
|
865
|
-
|
866
|
-
You can also define additional constraints using the `.constraint` class method.
|
867
|
-
|
868
|
-
```ruby
|
869
|
-
class Gadget
|
870
|
-
constraint :name, Stannum::Constraints::Presence.new
|
871
|
-
|
872
|
-
constraint :quantity do |qty|
|
873
|
-
qty >= 0
|
874
|
-
end
|
875
|
-
end
|
876
|
-
|
877
|
-
gadget = Gadget.new(name: '')
|
878
|
-
Gadget.contract.matches?(gadget)
|
879
|
-
#=> false
|
880
|
-
Gadget.contract.errors_for(gadget)
|
881
|
-
#=> [
|
882
|
-
# {
|
883
|
-
# data: {},
|
884
|
-
# message: nil,
|
885
|
-
# path: [:name],
|
886
|
-
# type: 'stannum.constraints.absent'
|
887
|
-
# }
|
888
|
-
# ]
|
889
|
-
```
|
890
|
-
|
891
|
-
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.
|
892
|
-
|
893
|
-
<a id="builtin-constraints"></a>
|
894
|
-
|
895
|
-
### Built-In Constraints
|
896
|
-
|
897
|
-
Stannum defines a set of built-in constraints that can be used in any project.
|
898
|
-
|
899
|
-
**Absence Constraint**
|
900
|
-
|
901
|
-
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.
|
902
|
-
|
903
|
-
```ruby
|
904
|
-
constraint = Stannum::Constraints::Absence.new
|
905
|
-
|
906
|
-
constraint.matches?(nil)
|
907
|
-
#=> true
|
908
|
-
constraint.matches?('')
|
909
|
-
#=> true
|
910
|
-
constraint.matches?('Greetings, programs!')
|
911
|
-
#=> false
|
912
|
-
constraint.matches?(Object.new)
|
913
|
-
#=> false
|
914
|
-
```
|
915
|
-
|
916
|
-
**Anything Constraint**
|
917
|
-
|
918
|
-
Matches any object, even `nil`.
|
919
|
-
|
920
|
-
```ruby
|
921
|
-
constraint = Stannum::Constraints::Anything.new
|
922
|
-
|
923
|
-
constraint.matches?(nil)
|
924
|
-
#=> true
|
925
|
-
constraint.matches?(Object.new)
|
926
|
-
#=> true
|
927
|
-
constraint.matches?('Hello, world')
|
928
|
-
#=> true
|
929
|
-
```
|
930
|
-
|
931
|
-
**Boolean Constraint**
|
932
|
-
|
933
|
-
Matches `true` and `false`.
|
934
|
-
|
935
|
-
```ruby
|
936
|
-
constraint = Stannum::Constraints::Boolean.new
|
937
|
-
|
938
|
-
constraint.matches?(nil)
|
939
|
-
#=> false
|
940
|
-
constraint.matches?(Object.new)
|
941
|
-
#=> false
|
942
|
-
constraint.matches?(false)
|
943
|
-
#=> true
|
944
|
-
constraint.matches?(true)
|
945
|
-
#=> true
|
946
|
-
```
|
947
|
-
|
948
|
-
**Enum Constraint**
|
949
|
-
|
950
|
-
Matches any the specified values.
|
951
|
-
|
952
|
-
```ruby
|
953
|
-
constraint = Stannum::Constraints::Enum.new('red', 'blue', 'green')
|
954
|
-
|
955
|
-
constraint.matches?(nil)
|
956
|
-
#=> false
|
957
|
-
constraint.matches?('purple')
|
958
|
-
#=> false
|
959
|
-
constraint.matches?('red')
|
960
|
-
#=> true
|
961
|
-
constraint.matches?('green')
|
962
|
-
#=> true
|
963
|
-
```
|
964
|
-
|
965
|
-
**Equality Constraint**
|
966
|
-
|
967
|
-
Matches any object equal to the given object.
|
968
|
-
|
969
|
-
```ruby
|
970
|
-
value = 'Greetings, programs!'
|
971
|
-
constraint = Stannum::Constraints::Equality.new(value)
|
972
|
-
|
973
|
-
constraint.matches?(nil)
|
974
|
-
#=> false
|
975
|
-
constraint.matches?(value.dup)
|
976
|
-
#=> true
|
977
|
-
constraint.matches?(value)
|
978
|
-
#=> true
|
979
|
-
```
|
980
|
-
|
981
|
-
**Identity Constraint**
|
982
|
-
|
983
|
-
Matches the given object.
|
984
|
-
|
985
|
-
```ruby
|
986
|
-
value = 'Greetings, starfighter!'
|
987
|
-
constraint = Stannum::Constraints::Identity.new(value)
|
988
|
-
|
989
|
-
constraint.matches?(nil)
|
990
|
-
#=> false
|
991
|
-
constraint.matches?(value.dup)
|
992
|
-
#=> false
|
993
|
-
constraint.matches?(value)
|
994
|
-
#=> true
|
995
|
-
```
|
996
|
-
|
997
|
-
**Nothing Constraint**
|
998
|
-
|
999
|
-
Does not match any objects.
|
1000
|
-
|
1001
|
-
```ruby
|
1002
|
-
constraint = Stannum::Constraints::Nothing.new
|
1003
|
-
|
1004
|
-
constraint.matches?(nil)
|
1005
|
-
#=> false
|
1006
|
-
constraint.matches?(Object.new)
|
1007
|
-
#=> false
|
1008
|
-
constraint.matches?('Hello, world')
|
1009
|
-
#=> false
|
1010
|
-
```
|
1011
|
-
|
1012
|
-
<a id="builtin-constraints-presence"></a>
|
1013
|
-
|
1014
|
-
**Presence Constraint**
|
1015
|
-
|
1016
|
-
Matches objects that are not `nil`, and that either do not respond to `#empty?` or for whom `#empty?` returns false.
|
1017
|
-
|
1018
|
-
```ruby
|
1019
|
-
constraint = Stannum::Constraints::Presence.new
|
1020
|
-
|
1021
|
-
constraint.matches?(nil)
|
1022
|
-
#=> false
|
1023
|
-
constraint.matches?('')
|
1024
|
-
#=> false
|
1025
|
-
constraint.matches?('Greetings, programs!')
|
1026
|
-
#=> true
|
1027
|
-
constraint.matches?(Object.new)
|
1028
|
-
#=> true
|
1029
|
-
```
|
1030
|
-
|
1031
|
-
**Signature Constraint**
|
1032
|
-
|
1033
|
-
Matches if the object responds to all of the specified methods.
|
1034
|
-
|
1035
|
-
```ruby
|
1036
|
-
constraint = Stannum::Constraints::Signature.new(:[], :keys)
|
1037
|
-
|
1038
|
-
constraint.matches?(nil)
|
1039
|
-
#=> false
|
1040
|
-
constraint.matches?([])
|
1041
|
-
#=> false
|
1042
|
-
constraint.matches?({})
|
1043
|
-
#=> true
|
1044
|
-
```
|
1045
|
-
|
1046
|
-
<a id="builtin-constraints-type"></a>
|
1047
|
-
|
1048
|
-
**Type Constraint**
|
1049
|
-
|
1050
|
-
Matches if the specified type is an ancestor of the object.
|
1051
|
-
|
1052
|
-
```ruby
|
1053
|
-
constraint = Stannum::Constraints::Type.new(StandardError)
|
1054
|
-
|
1055
|
-
constraint.matches?(nil)
|
1056
|
-
#=> false
|
1057
|
-
constraint.matches?(Object.new)
|
1058
|
-
#=> false
|
1059
|
-
constraint.matches?(StandardError.new)
|
1060
|
-
#=> true
|
1061
|
-
constraint.matches?(ArgumentError.new)
|
1062
|
-
#=> true
|
1063
|
-
```
|
1064
|
-
|
1065
|
-
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.
|
1066
|
-
|
1067
|
-
```ruby
|
1068
|
-
constraint = Stannum::Constraints::Type.new(String, optional: true)
|
1069
|
-
|
1070
|
-
constraint.matches?(nil)
|
1071
|
-
#=> true
|
1072
|
-
constraint.matches?(Object.new)
|
1073
|
-
#=> false
|
1074
|
-
constraint.matches?('a String')
|
1075
|
-
#=> true
|
1076
|
-
```
|
1077
|
-
|
1078
|
-
**Union Constraint**
|
1079
|
-
|
1080
|
-
Matches if the object matches any of the given constraints.
|
1081
|
-
|
1082
|
-
```ruby
|
1083
|
-
constraint = Stannum::Constraints::Union.new(
|
1084
|
-
Stannum::Constraints::Type.new(String),
|
1085
|
-
Stannum::Constraints::Type.new(Symbol)
|
1086
|
-
)
|
1087
|
-
|
1088
|
-
constraint.matches?(nil)
|
1089
|
-
#=> false
|
1090
|
-
constraint.matches?(Object.new)
|
1091
|
-
#=> false
|
1092
|
-
constraint.matches?('a String')
|
1093
|
-
#=> true
|
1094
|
-
constraint.matches?(:a_symbol)
|
1095
|
-
#=> true
|
1096
|
-
```
|
1097
|
-
|
1098
|
-
#### Property Constraints
|
1099
|
-
|
1100
|
-
Property constraints match against the properties of the object.
|
1101
|
-
|
1102
|
-
**Do Not Match Property Constraint**
|
1103
|
-
|
1104
|
-
Matches if none of the values of the given properties are equal to the value of the expected property.
|
1105
|
-
|
1106
|
-
```ruby
|
1107
|
-
UpdatePassword = Struct.new(:old_password, :new_password)
|
1108
|
-
constraint = Stannum::Constraints::Properties::DoNotMatchProperty.new(
|
1109
|
-
:old_password,
|
1110
|
-
:new_password
|
1111
|
-
)
|
1112
|
-
|
1113
|
-
params = UpdatePassword.new('tronlives', 'ifightfortheusers')
|
1114
|
-
constraint.matches?(params)
|
1115
|
-
#=> true
|
1116
|
-
|
1117
|
-
params = UpdatePassword.new('tronlives', 'tronlives')
|
1118
|
-
constraint.matches?(params)
|
1119
|
-
#=> false
|
1120
|
-
constraint.errors_for(params)
|
1121
|
-
#=> [
|
1122
|
-
{
|
1123
|
-
path: [:confirmation],
|
1124
|
-
type: 'stannum.constraints.is_equal_to',
|
1125
|
-
data: { expected: '[FILTERED]', actual: '[FILTERED]' }
|
1126
|
-
}
|
1127
|
-
]
|
1128
|
-
```
|
1129
|
-
|
1130
|
-
**Match Property Constraint**
|
1131
|
-
|
1132
|
-
Matches if all the values of the given properties are equal to the value of the expected property.
|
1133
|
-
|
1134
|
-
```ruby
|
1135
|
-
ConfirmPassword = Struct.new(:password, :confirmation)
|
1136
|
-
constraint = Stannum::Constraints::Properties::MatchProperty.new(
|
1137
|
-
:password,
|
1138
|
-
:confirmation
|
1139
|
-
)
|
1140
|
-
|
1141
|
-
params = ConfirmPassword.new('tronlives', 'ifightfortheusers')
|
1142
|
-
constraint.matches?(params)
|
1143
|
-
#=> false
|
1144
|
-
constraint.errors_for(params)
|
1145
|
-
#=> [
|
1146
|
-
{
|
1147
|
-
path: [:confirmation],
|
1148
|
-
type: 'stannum.constraints.is_not_equal_to',
|
1149
|
-
data: { expected: '[FILTERED]', actual: '[FILTERED]' }
|
1150
|
-
}
|
1151
|
-
]
|
1152
|
-
|
1153
|
-
params = ConfirmPassword.new('tronlives', 'tronlives')
|
1154
|
-
constraint.matches?(params)
|
1155
|
-
#=> true
|
1156
|
-
```
|
1157
|
-
|
1158
|
-
#### Type Constraints
|
1159
|
-
|
1160
|
-
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.
|
1161
|
-
|
1162
|
-
```ruby
|
1163
|
-
constraint = Stannum::Constraints::Types::StringType.new
|
1164
|
-
|
1165
|
-
constraint.matches?(nil)
|
1166
|
-
#=> false
|
1167
|
-
constraint.matches?(Object.new)
|
1168
|
-
#=> false
|
1169
|
-
constraint.matches?('a String')
|
1170
|
-
#=> true
|
1171
|
-
```
|
1172
|
-
|
1173
|
-
The following type constraints are defined:
|
1174
|
-
|
1175
|
-
- **ArrayType**
|
1176
|
-
- **BigDecimalType**
|
1177
|
-
- **DateTimeType**
|
1178
|
-
- **DateType**
|
1179
|
-
- **FloatType**
|
1180
|
-
- **HashType**
|
1181
|
-
- **IntegerType**
|
1182
|
-
- **NilType**
|
1183
|
-
- **ProcType**
|
1184
|
-
- **StringType**
|
1185
|
-
- **SymbolType**
|
1186
|
-
- **TimeType**
|
1187
|
-
|
1188
|
-
In addition, the following type constraints have additional options or behavior.
|
1189
|
-
|
1190
|
-
**ArrayType Constraint**
|
1191
|
-
|
1192
|
-
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.
|
1193
|
-
|
1194
|
-
```ruby
|
1195
|
-
constraint = Stannum::Constraints::Types::ArrayType.new(item_type: String)
|
1196
|
-
|
1197
|
-
constraint.matches?(nil)
|
1198
|
-
#=> false
|
1199
|
-
constraint.matches?([])
|
1200
|
-
#=> true
|
1201
|
-
constraint.matches?([1, 2, 3])
|
1202
|
-
#=> false
|
1203
|
-
constraint.matches?(['uno', 'dos', 'tres'])
|
1204
|
-
#=> true
|
1205
|
-
```
|
1206
|
-
|
1207
|
-
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`.
|
1208
|
-
|
1209
|
-
```ruby
|
1210
|
-
constraint = Stannum::Constraints::Types::ArrayType.new(allow_empty: false)
|
1211
|
-
|
1212
|
-
constraint.matches?(nil)
|
1213
|
-
#=> false
|
1214
|
-
constraint.matches?([])
|
1215
|
-
#=> false
|
1216
|
-
constraint.matches?([1, 2, 3])
|
1217
|
-
#=> true
|
1218
|
-
```
|
1219
|
-
|
1220
|
-
**HashType Constraint**
|
1221
|
-
|
1222
|
-
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.
|
1223
|
-
|
1224
|
-
```ruby
|
1225
|
-
constraint = Stannum::Constraints::Types::HashType.new(key_type: String, value_type: Integer)
|
1226
|
-
|
1227
|
-
constraint.matches?(nil)
|
1228
|
-
#=> false
|
1229
|
-
constraint.matches?({})
|
1230
|
-
#=> true
|
1231
|
-
constraint.matches?({ ichi: 1 })
|
1232
|
-
#=> false
|
1233
|
-
constraint.matches?({ 'ichi' => 'one' })
|
1234
|
-
#=> false
|
1235
|
-
constraint.matches?({ 'ichi' => 1 })
|
1236
|
-
```
|
1237
|
-
|
1238
|
-
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`.
|
1239
|
-
|
1240
|
-
```ruby
|
1241
|
-
constraint = Stannum::Constraints::Types::HashType.new(allow_empty: false
|
1242
|
-
|
1243
|
-
constraint.matches?(nil)
|
1244
|
-
#=> false
|
1245
|
-
constraint.matches?({})
|
1246
|
-
#=> false
|
1247
|
-
constraint.matches?({ ichi: 1 })
|
1248
|
-
#=> true
|
1249
|
-
```
|
1250
|
-
|
1251
|
-
There are predefined constraints for matching `Hash`es with common key types:
|
1252
|
-
|
1253
|
-
- **HashWithIndifferentKeys:** Matches keys that are either `String`s or `Symbol`s and not empty.
|
1254
|
-
- **HashWithStringKeys:** Matches keys that are `String`s.
|
1255
|
-
- **HashWithSymbolKeys:** Matches keys that are `Symbol`s.
|
1256
|
-
|
1257
|
-
<a id="signature-constraints"></a>
|
1258
|
-
|
1259
|
-
#### Signature Constraints
|
1260
|
-
|
1261
|
-
Stannum provides a small number of built-in signature constraints.
|
1262
|
-
|
1263
|
-
```ruby
|
1264
|
-
constraint = Stannum::Constraints::Signatures::Map.new
|
1265
|
-
|
1266
|
-
constraint.matches?(nil)
|
1267
|
-
#=> false
|
1268
|
-
constraint.matches?([])
|
1269
|
-
#=> false
|
1270
|
-
constraint.matches?({})
|
1271
|
-
#=> true
|
1272
|
-
```
|
1273
|
-
|
1274
|
-
- **Map:** Matches objects that behave like a `Hash`. Specifically, objects responding to `#[]`, `#each`, and `#keys`.
|
1275
|
-
- **Tuple:** Matches objects that behave like an `Array`. Specifically, objects responding to `#[]`, `#each`, and `#size`.
|
1276
|
-
|
1277
|
-
<a id="builtin-contracts"></a>
|
1278
|
-
|
1279
|
-
### Built-In Contracts
|
1280
|
-
|
1281
|
-
Stannum defines some pre-defined contracts.
|
1282
|
-
|
1283
|
-
**Array Contract**
|
1284
|
-
|
1285
|
-
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.
|
1286
|
-
|
1287
|
-
```ruby
|
1288
|
-
class BaseballContract < Stannum::Contracts::ArrayContract
|
1289
|
-
def initialize
|
1290
|
-
super do
|
1291
|
-
item { |actual| actual == 'Who' }
|
1292
|
-
item { |actual| actual == 'What' }
|
1293
|
-
item { |actual| actual == "I Don't Know" }
|
1294
|
-
end
|
1295
|
-
end
|
1296
|
-
end
|
1297
|
-
```
|
1298
|
-
|
1299
|
-
**Hash Contract**
|
1300
|
-
|
1301
|
-
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.
|
1302
|
-
|
1303
|
-
```ruby
|
1304
|
-
class ResponseContract < Stannum::Contracts::HashContract
|
1305
|
-
def initialize
|
1306
|
-
super do
|
1307
|
-
key :status, Stannum::Constraints::Types::IntegerType.new
|
1308
|
-
|
1309
|
-
key :json,
|
1310
|
-
Stannum::Contracts::HashContract.new(allow_extra_keys: true) do
|
1311
|
-
key :ok, Stannum::Constraints::Boolean.new
|
1312
|
-
end
|
1313
|
-
|
1314
|
-
key :signature, Stannum::Constraints::Presence.new
|
1315
|
-
end
|
1316
|
-
end
|
1317
|
-
end
|
1318
|
-
```
|
1319
|
-
|
1320
|
-
Stannum also defines an `IndifferentHashContract` class, which will match against both string and symbol keys.
|
1321
|
-
|
1322
|
-
**Map Contract**
|
1323
|
-
|
1324
|
-
As a `HashContract`, but matches against any object which uses the `#[]` operator to access key-value data. See [Map Constraint](signature-constraints), above.
|
1325
|
-
|
1326
|
-
**Parameters Contract**
|
1327
|
-
|
1328
|
-
Matches the parameters of a method call.
|
1329
|
-
|
1330
|
-
```ruby
|
1331
|
-
class AuthorizationParameters < Stannum::Contracts::ParametersContract
|
1332
|
-
def initialize
|
1333
|
-
super do
|
1334
|
-
argument :action, Symbol
|
1335
|
-
|
1336
|
-
argument :record_class, Class, default: true
|
1337
|
-
|
1338
|
-
keyword :role, String, default: true
|
1339
|
-
|
1340
|
-
keyword :user, Stannum::Constraints::Type.new(User)
|
1341
|
-
end
|
1342
|
-
end
|
1343
|
-
end
|
1344
|
-
|
1345
|
-
contract = AuthorizationParameters.new
|
1346
|
-
parameters = {
|
1347
|
-
arguments: [:create, Article],
|
1348
|
-
keywords: {},
|
1349
|
-
block: nil
|
1350
|
-
}
|
1351
|
-
contract.matches?(parameters)
|
1352
|
-
#=> false
|
1353
|
-
errors = contract.errors_for(parameters)
|
1354
|
-
errors[:arguments].empty?
|
1355
|
-
#=> true
|
1356
|
-
errors[:keywords].empty?
|
1357
|
-
#=> false
|
1358
|
-
```
|
1359
|
-
|
1360
|
-
Each `ParametersContract` defines `.argument`, `.keyword`, and `.block` class methods to define the expected method parameters.
|
1361
|
-
|
1362
|
-
- 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.
|
1363
|
-
- The `.keyword` class method defines an expected keyword.
|
1364
|
-
- 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.
|
1365
|
-
|
1366
|
-
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.
|
1367
|
-
|
1368
|
-
`ParametersContract` also has support for variadic arguments and keywords.
|
1369
|
-
|
1370
|
-
```ruby
|
1371
|
-
class RecipeParameters < Stannum::Contracts::ParametersContract
|
1372
|
-
def initialize
|
1373
|
-
super do
|
1374
|
-
arguments :tools, String
|
1375
|
-
keywords :ingredients, Stannum::Contracts::TupleContract.new do
|
1376
|
-
item Stannum::Constraints::Type.new(String),
|
1377
|
-
property_name: :amount
|
1378
|
-
item Stannum::Constraints::Type.new(String, optional: true),
|
1379
|
-
property_name: :unit
|
1380
|
-
end
|
1381
|
-
block true
|
1382
|
-
end
|
1383
|
-
end
|
1384
|
-
end
|
1385
|
-
```
|
1386
|
-
|
1387
|
-
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).
|
1388
|
-
|
1389
|
-
**Tuple Contract**
|
1390
|
-
|
1391
|
-
As an `ArrayContract`, but matches against any object which uses the `#[]` operator to access indexed data. See [Tuple Constraint](signature-constraints), above.
|
257
|
+
We can then validate the API response by calling `SUCCESS_RESPONSE_CONTRACT.matches?(response)`.
|