stannum 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +21 -0
  3. data/CODE_OF_CONDUCT.md +132 -0
  4. data/DEVELOPMENT.md +105 -0
  5. data/LICENSE +22 -0
  6. data/README.md +1327 -0
  7. data/config/locales/en.rb +47 -0
  8. data/lib/stannum/attribute.rb +115 -0
  9. data/lib/stannum/constraint.rb +65 -0
  10. data/lib/stannum/constraints/absence.rb +42 -0
  11. data/lib/stannum/constraints/anything.rb +28 -0
  12. data/lib/stannum/constraints/base.rb +285 -0
  13. data/lib/stannum/constraints/boolean.rb +33 -0
  14. data/lib/stannum/constraints/delegator.rb +71 -0
  15. data/lib/stannum/constraints/enum.rb +64 -0
  16. data/lib/stannum/constraints/equality.rb +47 -0
  17. data/lib/stannum/constraints/hashes/extra_keys.rb +126 -0
  18. data/lib/stannum/constraints/hashes/indifferent_key.rb +74 -0
  19. data/lib/stannum/constraints/hashes.rb +11 -0
  20. data/lib/stannum/constraints/identity.rb +46 -0
  21. data/lib/stannum/constraints/nothing.rb +28 -0
  22. data/lib/stannum/constraints/presence.rb +42 -0
  23. data/lib/stannum/constraints/signature.rb +92 -0
  24. data/lib/stannum/constraints/signatures/map.rb +17 -0
  25. data/lib/stannum/constraints/signatures/tuple.rb +17 -0
  26. data/lib/stannum/constraints/signatures.rb +11 -0
  27. data/lib/stannum/constraints/tuples/extra_items.rb +84 -0
  28. data/lib/stannum/constraints/tuples.rb +10 -0
  29. data/lib/stannum/constraints/type.rb +113 -0
  30. data/lib/stannum/constraints/types/array_type.rb +148 -0
  31. data/lib/stannum/constraints/types/big_decimal_type.rb +16 -0
  32. data/lib/stannum/constraints/types/date_time_type.rb +16 -0
  33. data/lib/stannum/constraints/types/date_type.rb +16 -0
  34. data/lib/stannum/constraints/types/float_type.rb +14 -0
  35. data/lib/stannum/constraints/types/hash_type.rb +205 -0
  36. data/lib/stannum/constraints/types/hash_with_indifferent_keys.rb +21 -0
  37. data/lib/stannum/constraints/types/hash_with_string_keys.rb +21 -0
  38. data/lib/stannum/constraints/types/hash_with_symbol_keys.rb +21 -0
  39. data/lib/stannum/constraints/types/integer_type.rb +14 -0
  40. data/lib/stannum/constraints/types/nil_type.rb +20 -0
  41. data/lib/stannum/constraints/types/proc_type.rb +14 -0
  42. data/lib/stannum/constraints/types/string_type.rb +14 -0
  43. data/lib/stannum/constraints/types/symbol_type.rb +14 -0
  44. data/lib/stannum/constraints/types/time_type.rb +14 -0
  45. data/lib/stannum/constraints/types.rb +25 -0
  46. data/lib/stannum/constraints/union.rb +85 -0
  47. data/lib/stannum/constraints.rb +26 -0
  48. data/lib/stannum/contract.rb +243 -0
  49. data/lib/stannum/contracts/array_contract.rb +108 -0
  50. data/lib/stannum/contracts/base.rb +597 -0
  51. data/lib/stannum/contracts/builder.rb +72 -0
  52. data/lib/stannum/contracts/definition.rb +74 -0
  53. data/lib/stannum/contracts/hash_contract.rb +136 -0
  54. data/lib/stannum/contracts/indifferent_hash_contract.rb +78 -0
  55. data/lib/stannum/contracts/map_contract.rb +199 -0
  56. data/lib/stannum/contracts/parameters/arguments_contract.rb +185 -0
  57. data/lib/stannum/contracts/parameters/keywords_contract.rb +174 -0
  58. data/lib/stannum/contracts/parameters/signature_contract.rb +29 -0
  59. data/lib/stannum/contracts/parameters.rb +15 -0
  60. data/lib/stannum/contracts/parameters_contract.rb +530 -0
  61. data/lib/stannum/contracts/tuple_contract.rb +213 -0
  62. data/lib/stannum/contracts.rb +19 -0
  63. data/lib/stannum/errors.rb +730 -0
  64. data/lib/stannum/messages/default_strategy.rb +124 -0
  65. data/lib/stannum/messages.rb +25 -0
  66. data/lib/stannum/parameter_validation.rb +216 -0
  67. data/lib/stannum/rspec/match_errors.rb +17 -0
  68. data/lib/stannum/rspec/match_errors_matcher.rb +93 -0
  69. data/lib/stannum/rspec/validate_parameter.rb +23 -0
  70. data/lib/stannum/rspec/validate_parameter_matcher.rb +506 -0
  71. data/lib/stannum/rspec.rb +8 -0
  72. data/lib/stannum/schema.rb +131 -0
  73. data/lib/stannum/struct.rb +444 -0
  74. data/lib/stannum/support/coercion.rb +114 -0
  75. data/lib/stannum/support/optional.rb +69 -0
  76. data/lib/stannum/support.rb +8 -0
  77. data/lib/stannum/version.rb +57 -0
  78. data/lib/stannum.rb +27 -0
  79. 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.