bcdd-contract 0.1.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 +7 -0
- data/.rubocop.yml +128 -0
- data/CHANGELOG.md +45 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +964 -0
- data/Rakefile +18 -0
- data/Steepfile +32 -0
- data/examples/README.md +11 -0
- data/examples/anti_corruption_layer/README.md +212 -0
- data/examples/anti_corruption_layer/Rakefile +30 -0
- data/examples/anti_corruption_layer/app/models/payment/charge_credit_card.rb +36 -0
- data/examples/anti_corruption_layer/config.rb +20 -0
- data/examples/anti_corruption_layer/lib/payment_gateways/adapters/circle_up.rb +19 -0
- data/examples/anti_corruption_layer/lib/payment_gateways/adapters/pay_friend.rb +19 -0
- data/examples/anti_corruption_layer/lib/payment_gateways/contract.rb +15 -0
- data/examples/anti_corruption_layer/lib/payment_gateways/response.rb +5 -0
- data/examples/anti_corruption_layer/lib/payment_gateways.rb +11 -0
- data/examples/anti_corruption_layer/vendor/circle_up/client.rb +11 -0
- data/examples/anti_corruption_layer/vendor/pay_friend/client.rb +11 -0
- data/examples/business_processes/README.md +245 -0
- data/examples/business_processes/Rakefile +50 -0
- data/examples/business_processes/config.rb +14 -0
- data/examples/business_processes/lib/division.rb +58 -0
- data/examples/design_by_contract/README.md +227 -0
- data/examples/design_by_contract/Rakefile +60 -0
- data/examples/design_by_contract/config.rb +13 -0
- data/examples/design_by_contract/lib/shopping_cart.rb +62 -0
- data/examples/ports_and_adapters/README.md +246 -0
- data/examples/ports_and_adapters/Rakefile +68 -0
- data/examples/ports_and_adapters/app/models/user/record/repository.rb +13 -0
- data/examples/ports_and_adapters/app/models/user/record.rb +7 -0
- data/examples/ports_and_adapters/config.rb +28 -0
- data/examples/ports_and_adapters/db/setup.rb +16 -0
- data/examples/ports_and_adapters/lib/user/creation.rb +19 -0
- data/examples/ports_and_adapters/lib/user/data.rb +5 -0
- data/examples/ports_and_adapters/lib/user/repository.rb +24 -0
- data/examples/ports_and_adapters/test/user_test/repository.rb +21 -0
- data/lib/bcdd/contract/assertions.rb +21 -0
- data/lib/bcdd/contract/config.rb +25 -0
- data/lib/bcdd/contract/core/checker.rb +37 -0
- data/lib/bcdd/contract/core/checking.rb +38 -0
- data/lib/bcdd/contract/core/factory.rb +32 -0
- data/lib/bcdd/contract/core/proxy.rb +19 -0
- data/lib/bcdd/contract/core.rb +12 -0
- data/lib/bcdd/contract/interface.rb +25 -0
- data/lib/bcdd/contract/list.rb +45 -0
- data/lib/bcdd/contract/map/pairs.rb +47 -0
- data/lib/bcdd/contract/map/schema.rb +50 -0
- data/lib/bcdd/contract/map.rb +10 -0
- data/lib/bcdd/contract/proxy.rb +40 -0
- data/lib/bcdd/contract/registry.rb +67 -0
- data/lib/bcdd/contract/unit/checker.rb +51 -0
- data/lib/bcdd/contract/unit/factory.rb +53 -0
- data/lib/bcdd/contract/unit.rb +40 -0
- data/lib/bcdd/contract/version.rb +7 -0
- data/lib/bcdd/contract.rb +118 -0
- data/lib/bcdd-contract.rb +3 -0
- data/sig/bcdd/contract/assertions.rbs +7 -0
- data/sig/bcdd/contract/config.rbs +15 -0
- data/sig/bcdd/contract/core.rbs +60 -0
- data/sig/bcdd/contract/interface.rbs +12 -0
- data/sig/bcdd/contract/list.rbs +21 -0
- data/sig/bcdd/contract/map.rbs +45 -0
- data/sig/bcdd/contract/proxy.rbs +8 -0
- data/sig/bcdd/contract/registry.rbs +25 -0
- data/sig/bcdd/contract/unit.rbs +39 -0
- data/sig/bcdd/contract.rbs +31 -0
- metadata +116 -0
data/README.md
ADDED
|
@@ -0,0 +1,964 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<h1 align="center" id="-bcddcontract">🚦 BCDD::Contract</h1>
|
|
3
|
+
<p align="center"><i>Reliable contract definition, data validation, and type checking for Ruby.</i></p>
|
|
4
|
+
<p align="center">
|
|
5
|
+
<img src="https://img.shields.io/badge/ruby->%3D%202.7.0-ruby.svg?colorA=99004d&colorB=cc0066" alt="Ruby">
|
|
6
|
+
<a href="https://rubygems.org/gems/bcdd-contract"><img src="https://badge.fury.io/rb/bcdd-contract.svg" alt="bcdd-contract gem version" height="18"></a>
|
|
7
|
+
<a href="https://codeclimate.com/github/B-CDD/contract/maintainability"><img src="https://api.codeclimate.com/v1/badges/14e87347cd2b660ae3cf/maintainability" /></a>
|
|
8
|
+
<a href="https://codeclimate.com/github/B-CDD/contract/test_coverage"><img src="https://api.codeclimate.com/v1/badges/14e87347cd2b660ae3cf/test_coverage" /></a>
|
|
9
|
+
</p>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
- [Introduction](#introduction)
|
|
13
|
+
- [Features](#features)
|
|
14
|
+
- [Motivation](#motivation)
|
|
15
|
+
- [Examples](#examples)
|
|
16
|
+
- [Installation](#installation)
|
|
17
|
+
- [Usage](#usage)
|
|
18
|
+
- [Contract Units](#contract-units)
|
|
19
|
+
- [Lambda Based](#lambda-based)
|
|
20
|
+
- [Type Based](#type-based)
|
|
21
|
+
- [Union Based](#union-based)
|
|
22
|
+
- [Using `nil` to define optional checkers](#using-nil-to-define-optional-checkers)
|
|
23
|
+
- [Data Structure Checkers](#data-structure-checkers)
|
|
24
|
+
- [List Schema](#list-schema)
|
|
25
|
+
- [Hash Schema](#hash-schema)
|
|
26
|
+
- [Hash key/value Pairs Schema](#hash-keyvalue-pairs-schema)
|
|
27
|
+
- [Registered Checkers](#registered-checkers)
|
|
28
|
+
- [Defining Interfaces](#defining-interfaces)
|
|
29
|
+
- [`BCDD::Contract::Interface`](#bcddcontractinterface)
|
|
30
|
+
- [`BCDD::Contract::Proxy`](#bcddcontractproxy)
|
|
31
|
+
- [Assertions](#assertions)
|
|
32
|
+
- [Configuration](#configuration)
|
|
33
|
+
- [Switchable features](#switchable-features)
|
|
34
|
+
- [Non-switchable features](#non-switchable-features)
|
|
35
|
+
- [Reference](#reference)
|
|
36
|
+
- [The Contract Checker API](#the-contract-checker-api)
|
|
37
|
+
- [`.===`](#)
|
|
38
|
+
- [`.to_proc`](#to_proc)
|
|
39
|
+
- [`.invariant`](#invariant)
|
|
40
|
+
- [The Contract Checking API](#the-contract-checking-api)
|
|
41
|
+
- [Unary operators](#unary-operators)
|
|
42
|
+
- [`BCDD::Contract` methods](#bcddcontract-methods)
|
|
43
|
+
- [`BCDD::Contract::Assertions`](#bcddcontractassertions)
|
|
44
|
+
- [About](#about)
|
|
45
|
+
- [Development](#development)
|
|
46
|
+
- [Contributing](#contributing)
|
|
47
|
+
- [License](#license)
|
|
48
|
+
- [Code of Conduct](#code-of-conduct)
|
|
49
|
+
|
|
50
|
+
## Introduction
|
|
51
|
+
|
|
52
|
+
`bcdd-contract` is a library for implementing contracts in Ruby. It provides abstractions to validate data structures, perform type checks, and define contracts inlined or through proxies.
|
|
53
|
+
|
|
54
|
+
## Features
|
|
55
|
+
|
|
56
|
+
- Strict **type checking**.
|
|
57
|
+
- Value validation with **error messages**.
|
|
58
|
+
- **Data structure validation**: Hashes, Arrays, Sets.
|
|
59
|
+
- **Interface** mechanisms.
|
|
60
|
+
- **Configurable** features.
|
|
61
|
+
- **Pattern matching** integration.
|
|
62
|
+
- **More Ruby** and less DSL.
|
|
63
|
+
- **Simple** and easy **to use**.
|
|
64
|
+
|
|
65
|
+
## Motivation
|
|
66
|
+
|
|
67
|
+
Due to the addition of pattern matching, Ruby now has an excellent tool for doing type checks.
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
def divide(a, b)
|
|
71
|
+
a => Float | Integer
|
|
72
|
+
b => Float | Integer
|
|
73
|
+
|
|
74
|
+
outcome = a / b => Float | Integer
|
|
75
|
+
outcome
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
divide('4', 2) # Integer === "4" does not return true (NoMatchingPatternError)
|
|
79
|
+
divide(4, '2') # Integer === "2" does not return true (NoMatchingPatternError)
|
|
80
|
+
divide(4, 2r) # Integer === (2/1) does not return true (NoMatchingPatternError)
|
|
81
|
+
|
|
82
|
+
divide(4, 2.0) # 2.0
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
However, more is needed to implement contracts. Often, the object is of the expected type but does not have a valid state.
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
# Examples of floats that are undesirable (invalid state)
|
|
89
|
+
|
|
90
|
+
divide(0.0, 0.0) # NaN
|
|
91
|
+
divide(0.0, 1.0) # Infinity
|
|
92
|
+
|
|
93
|
+
divide(Float::NAN, 2) # NaN
|
|
94
|
+
divide(Float::INFINITY, 2) # Infinity
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Let's see how we can use `bcdd-contract` can be used to implement contracts that will work with and without pattern matching.
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
module FloatOrInt
|
|
101
|
+
is_finite = ->(val) { val.finite? or "%p must be finite" }
|
|
102
|
+
|
|
103
|
+
extend (BCDD::Contract[Float] & is_finite) | Integer
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def divide(a, b)
|
|
107
|
+
a => FloatOrInt
|
|
108
|
+
b => FloatOrInt
|
|
109
|
+
|
|
110
|
+
outcome = a / b => FloatOrInt
|
|
111
|
+
outcome
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
divide('4', 2) # FloatOrInt === "4" does not return true (NoMatchingPatternError)
|
|
115
|
+
divide(4, '2') # FloatOrInt === "2" does not return true (NoMatchingPatternError)
|
|
116
|
+
divide(4, 2r) # FloatOrInt === (2/1) does not return true (NoMatchingPatternError)
|
|
117
|
+
divide(0.0, 0.0) # FloatOrInt === NaN does not return true (NoMatchingPatternError)
|
|
118
|
+
divide(0.0, 1.0) # FloatOrInt === Infinity does not return true (NoMatchingPatternError)
|
|
119
|
+
divide(Float::NAN, 2) # FloatOrInt === NaN does not return true (NoMatchingPatternError)
|
|
120
|
+
divide(Float::INFINITY, 2) # FloatOrInt === Infinity does not return true (NoMatchingPatternError)
|
|
121
|
+
|
|
122
|
+
divide(4, 2.0) # 2.0
|
|
123
|
+
|
|
124
|
+
# The contract can be used to validate values
|
|
125
|
+
|
|
126
|
+
FloatOrInt['1'].valid? # false
|
|
127
|
+
FloatOrInt['2'].invalid? # true
|
|
128
|
+
FloatOrInt['3'].errors # ['"3" must be a Float OR "3" must be a Integer']
|
|
129
|
+
FloatOrInt['4'].value # "4"
|
|
130
|
+
FloatOrInt['5'].value! # "5" must be a Float OR "4" must be a Integer (BCDD::Contract::Error)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Although all of this, the idea of contracts goes far beyond type checking or value validation. They are a way to define an expected behavior (method's inputs and outputs) and ensure pre-conditions, post-conditions, and invariants (Design by Contract concepts).
|
|
134
|
+
|
|
135
|
+
It looks good? So, let's see what more `bcdd-contract` can do.
|
|
136
|
+
|
|
137
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|
|
138
|
+
|
|
139
|
+
## Examples
|
|
140
|
+
|
|
141
|
+
Check the [examples](examples) directory to see different applications of `bcdd-contract`.
|
|
142
|
+
|
|
143
|
+
> **Attention:** Each example has its own **README** with more details.
|
|
144
|
+
|
|
145
|
+
1. [Ports and Adapters](examples/ports_and_adapters) - Implements the Ports and Adapters pattern. It uses [**`BCDD::Contract::Interface`**](#bcddcontractinterface) to provide an interface from the application's core to other layers.
|
|
146
|
+
|
|
147
|
+
2. [Anti-Corruption Layer](examples/anti_corruption_layer) - Implements the Anti-Corruption Layer pattern. It uses the [**`BCDD::Contract::Proxy`**](#bcddcontractproxy) to define an inteface for a set of adapters, which will be used to translate an external interface (`vendors`) to the application's core interface.
|
|
148
|
+
|
|
149
|
+
3. [Business Processes](examples/business_processes) - Implements a business process using the [`bcdd-result`](https://github.com/B-CDD/result) gem and uses the `bcdd-contract` to define its contract.
|
|
150
|
+
|
|
151
|
+
4. [Design by Contract](examples/design_by_contract) - Shows how the `bcdd-contract` can be used to establish pre-conditions, post-conditions, and invariants in a class.
|
|
152
|
+
|
|
153
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|
|
154
|
+
|
|
155
|
+
## Installation
|
|
156
|
+
|
|
157
|
+
Add this line to your application's Gemfile:
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
gem 'bcdd-contract'
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
And then execute:
|
|
164
|
+
|
|
165
|
+
$ bundle install
|
|
166
|
+
|
|
167
|
+
Or install it yourself as:
|
|
168
|
+
|
|
169
|
+
$ gem install bcdd-contract
|
|
170
|
+
|
|
171
|
+
And require it:
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
require 'bcdd/contract' # or require 'bcdd-contract'
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|
|
178
|
+
|
|
179
|
+
## Usage
|
|
180
|
+
|
|
181
|
+
### Contract Units
|
|
182
|
+
|
|
183
|
+
A unit can be used to check any object, use it when you need to check the type of an object or validate its value.
|
|
184
|
+
|
|
185
|
+
#### Lambda Based
|
|
186
|
+
|
|
187
|
+
There are two ways to create a unit checker using a Ruby lambda.
|
|
188
|
+
|
|
189
|
+
The difference between them is the number of arguments that the lambda receive.
|
|
190
|
+
|
|
191
|
+
**One argument**
|
|
192
|
+
|
|
193
|
+
When the lambda receives only one argument, it will be considered an error when it returns a string. Otherwise, it will be valid.
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
# Using and, or keywords
|
|
197
|
+
|
|
198
|
+
BCDD::Contract[->(val) { val.empty? or "%p must be empty" }]
|
|
199
|
+
BCDD::Contract[->(val) { val.empty? and "%p must be filled" }]
|
|
200
|
+
|
|
201
|
+
# The same as above, but using if/unless + return
|
|
202
|
+
|
|
203
|
+
BCDD::Contract[->(val) { "%p must be empty" unless val.empty? }]
|
|
204
|
+
BCDD::Contract[->(val) { "%p must be filled" if val.empty? }]
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
You can also use numbered parameters to make the code more concise.
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
BCDD::Contract[-> { _1.empty? or "%p must be empty" }]
|
|
211
|
+
BCDD::Contract[-> { _1.empty? and "%p must be filled" }]
|
|
212
|
+
|
|
213
|
+
BCDD::Contract[-> { "%p must be empty" unless _1.empty? }]
|
|
214
|
+
BCDD::Contract[-> { "%p must be filled" if _1.empty? }]
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Two arguments**
|
|
218
|
+
|
|
219
|
+
When the lambda receives two arguments, the first will be the value to be checked, and the second will be an array of errors. If the value is invalid, the lambda must add an error message to the array.
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
MustBeFilled = BCDD::Contract[->(val, err) { err << "%p must be filled" if val.empty? }]
|
|
223
|
+
|
|
224
|
+
MustBeFilled[''].valid? # false
|
|
225
|
+
MustBeFilled[[]].valid? # false
|
|
226
|
+
MustBeFilled[{}].valid? # false
|
|
227
|
+
|
|
228
|
+
MustBeFilled['4'].valid? # true
|
|
229
|
+
MustBeFilled[[5]].valid? # true
|
|
230
|
+
MustBeFilled[{six: 6}].valid? # true
|
|
231
|
+
|
|
232
|
+
[] => MustBeFilled # MustBeFilled === [] does not return true (NoMatchingPatternError)
|
|
233
|
+
{} => MustBeFilled # MustBeFilled === {} does not return true (NoMatchingPatternError)
|
|
234
|
+
|
|
235
|
+
checking = MustBeFilled[[]]
|
|
236
|
+
|
|
237
|
+
checking.errors # ["[] must be filled"]
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
> Check out the [Registered Contract Checkers](#registered-contract-checkers) section to see how to avoid duplication of checker definitions.
|
|
241
|
+
|
|
242
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|
|
243
|
+
|
|
244
|
+
#### Type Based
|
|
245
|
+
|
|
246
|
+
Pass a Ruby module or class to `BCDD::Contract[]` to create a type checker.
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
IsEnumerable = BCDD::Contract[Enumerable]
|
|
250
|
+
|
|
251
|
+
IsEnumerable[[]].valid? # true
|
|
252
|
+
IsEnumerable[{}].valid? # true
|
|
253
|
+
IsEnumerable[1].valid? # false
|
|
254
|
+
|
|
255
|
+
{} => IsEnumerable # nil
|
|
256
|
+
[] => IsEnumerable # nil
|
|
257
|
+
1 => IsEnumerable # IsEnumerable === 1 does not return true (NoMatchingPatternError)
|
|
258
|
+
|
|
259
|
+
checking = IsEnumerable[1]
|
|
260
|
+
|
|
261
|
+
checking.errors # ["1 must be a Enumerable"]
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
> Check out the [Registered Contract Checkers](#registered-contract-checkers) section to see how to avoid duplication of checker definitions.
|
|
265
|
+
|
|
266
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|
|
267
|
+
|
|
268
|
+
#### Union Based
|
|
269
|
+
|
|
270
|
+
After creating a unit checker, you can use the methods `|` (OR) and `&` (AND) to create union/intersection checkers.
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
is_filled = -> { _1.empty? and "%p must be filled" }
|
|
274
|
+
|
|
275
|
+
FilledArrayOrHash = (BCDD::Contract[Array] | Hash) & is_filled
|
|
276
|
+
|
|
277
|
+
FilledArrayOrHash[[]].valid? # false
|
|
278
|
+
FilledArrayOrHash[{}].valid? # false
|
|
279
|
+
|
|
280
|
+
FilledArrayOrHash[['1']].valid? # true
|
|
281
|
+
FilledArrayOrHash[{one: '1'}].valid? # true
|
|
282
|
+
|
|
283
|
+
[] => FilledArrayOrHash # FilledArrayOrHash === [] does not return true (NoMatchingPatternError)
|
|
284
|
+
{} => FilledArrayOrHash # FilledArrayOrHash === {} does not return true (NoMatchingPatternError)
|
|
285
|
+
|
|
286
|
+
checking = FilledArrayOrHash[[]]
|
|
287
|
+
|
|
288
|
+
checking.errors # ["[] must be filled"]
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
> Check out the [Registered Contract Checkers](#registered-contract-checkers) section to see how to avoid duplication of checker definitions.
|
|
292
|
+
|
|
293
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|
|
294
|
+
|
|
295
|
+
##### Using `nil` to define optional checkers
|
|
296
|
+
|
|
297
|
+
You can use `nil` to create optional contract checkers.
|
|
298
|
+
|
|
299
|
+
```ruby
|
|
300
|
+
IsStringOrNil = BCDD::Contract[String] | nil
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|
|
304
|
+
|
|
305
|
+
### Data Structure Checkers
|
|
306
|
+
|
|
307
|
+
#### List Schema
|
|
308
|
+
|
|
309
|
+
Use an array to define a schema. Only one element is allowed. Use the union checker to allow multiple types.
|
|
310
|
+
|
|
311
|
+
If the element is not a checker, it will be transformed into one.
|
|
312
|
+
|
|
313
|
+
The checker only accept arrays and sets.
|
|
314
|
+
|
|
315
|
+
```ruby
|
|
316
|
+
ListOfString = ::BCDD::Contract([String])
|
|
317
|
+
|
|
318
|
+
ListOfString[[]].valid? # false
|
|
319
|
+
ListOfString[{}].valid? # false
|
|
320
|
+
|
|
321
|
+
ListOfString[['1', '2', '3']].valid? # true
|
|
322
|
+
ListOfString[Set['1', '2', '3']].valid? # true
|
|
323
|
+
|
|
324
|
+
['1', '2', 3] => ListOfString # ListOfString === ["1", "2", 3] does not return true (NoMatchingPatternError)
|
|
325
|
+
|
|
326
|
+
Set['1', '2', 3] => ListOfString # ListOfString === #<Set: {"1", "2", 3}> does not return true (NoMatchingPatternError)
|
|
327
|
+
|
|
328
|
+
checking = ListOfString[[1, '2', 3]]
|
|
329
|
+
|
|
330
|
+
checking.errors
|
|
331
|
+
# [
|
|
332
|
+
# "0: 1 must be a String",
|
|
333
|
+
# "2: 3 must be a String"
|
|
334
|
+
# ]
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
> Check out the [Registered Contract Checkers](#registered-contract-checkers) section to see how to avoid duplication of checker definitions.
|
|
338
|
+
|
|
339
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|
|
340
|
+
|
|
341
|
+
#### Hash Schema
|
|
342
|
+
|
|
343
|
+
Use a hash to define a schema. The keys will be used to match the keys, and the values will be transformed into checkers (if they are not). You can use any kind of checker, including other hash schemas.
|
|
344
|
+
|
|
345
|
+
```ruby
|
|
346
|
+
PersonParams = ::BCDD::Contract[{
|
|
347
|
+
name: String,
|
|
348
|
+
age: Integer,
|
|
349
|
+
address: {
|
|
350
|
+
street: String,
|
|
351
|
+
number: Integer,
|
|
352
|
+
city: String,
|
|
353
|
+
state: String,
|
|
354
|
+
country: String
|
|
355
|
+
},
|
|
356
|
+
phone_numbers: ::BCDD::Contract([String])
|
|
357
|
+
}]
|
|
358
|
+
|
|
359
|
+
PersonParams[{}].valid? # => false
|
|
360
|
+
|
|
361
|
+
PersonParams[{
|
|
362
|
+
name: 'John',
|
|
363
|
+
age: 30,
|
|
364
|
+
address: {
|
|
365
|
+
street: 'Main Street',
|
|
366
|
+
number: 123,
|
|
367
|
+
city: 'New York',
|
|
368
|
+
state: 'NY',
|
|
369
|
+
country: 'USA'
|
|
370
|
+
},
|
|
371
|
+
phone_numbers: ['+1 555 123 4567']
|
|
372
|
+
}].valid? # => true
|
|
373
|
+
|
|
374
|
+
params_checking = PersonParams[{
|
|
375
|
+
name: 'John',
|
|
376
|
+
age: '30',
|
|
377
|
+
address: {
|
|
378
|
+
street: 'Main Street',
|
|
379
|
+
number: 123,
|
|
380
|
+
city: nil,
|
|
381
|
+
state: :NY,
|
|
382
|
+
country: 'USA'
|
|
383
|
+
},
|
|
384
|
+
phone_numbers: ['+1 555 123 4567']
|
|
385
|
+
}]
|
|
386
|
+
|
|
387
|
+
params_checking.errors
|
|
388
|
+
# {
|
|
389
|
+
# :age => ["\"30\" must be a Integer"],
|
|
390
|
+
# :address => {
|
|
391
|
+
# :city => ["is missing"],
|
|
392
|
+
# :state => [":NY must be a String"]
|
|
393
|
+
# }
|
|
394
|
+
# }
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
> Check out the [Registered Contract Checkers](#registered-contract-checkers) section to see how to avoid duplication of checker definitions.
|
|
398
|
+
|
|
399
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|
|
400
|
+
|
|
401
|
+
#### Hash key/value Pairs Schema
|
|
402
|
+
|
|
403
|
+
Use a hash to define a schema. The key and value will be transformed into checkers (if they are not).
|
|
404
|
+
|
|
405
|
+
```ruby
|
|
406
|
+
is_int_str = -> { _1.is_a?(String) && _1.match?(/\A\d+\z/) or "%p must be a Integer String" }
|
|
407
|
+
|
|
408
|
+
PlayerRankings = ::BCDD::Contract.pairs(is_int_str => { name: String, username: String })
|
|
409
|
+
|
|
410
|
+
PlayerRankings[{}].valid? # => false
|
|
411
|
+
|
|
412
|
+
PlayerRankings[{
|
|
413
|
+
'1' => { name: 'John', username: 'john' },
|
|
414
|
+
'2' => { name: 'Mary', username: 'mary' },
|
|
415
|
+
'3' => { name: 'Paul', username: 'paul' }
|
|
416
|
+
}].valid? # => true
|
|
417
|
+
|
|
418
|
+
checking = PlayerRankings[{
|
|
419
|
+
'1' => { name: :John, username: 'john' },
|
|
420
|
+
'two' => { name: 'Mary', username: 'mary' },
|
|
421
|
+
'3' => { name: 'Paul', username: 3 }
|
|
422
|
+
}]
|
|
423
|
+
|
|
424
|
+
checking.errors
|
|
425
|
+
# [
|
|
426
|
+
# "1: (name: :John must be a String)",
|
|
427
|
+
# "key: \"two\" must be a Integer String",
|
|
428
|
+
# "3: (username: 3 must be a String)"
|
|
429
|
+
# ]
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
> Check out the [Registered Contract Checkers](#registered-contract-checkers) section to see how to avoid duplication of checker definitions.
|
|
433
|
+
|
|
434
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|
|
435
|
+
|
|
436
|
+
### Registered Checkers
|
|
437
|
+
|
|
438
|
+
Sometimes you need to use the same checker in different places. To avoid code duplication, you can register a checker and use it later.
|
|
439
|
+
|
|
440
|
+
Use the `BCDD::Contract.register` method to give a name and register a checker.
|
|
441
|
+
|
|
442
|
+
You can register any kind of checker:
|
|
443
|
+
- **Unit**
|
|
444
|
+
- [Lambda based](#lambda-based)
|
|
445
|
+
- [Type based](#type-based)
|
|
446
|
+
- [Union based](#union-based)
|
|
447
|
+
- **Data Structure**
|
|
448
|
+
- [List schema](#list-schema)
|
|
449
|
+
- [Hash schema](#hash-schema)
|
|
450
|
+
- [Hash key/value pairs schema](#hash-keyvalue-pairs-schema)
|
|
451
|
+
|
|
452
|
+
```ruby
|
|
453
|
+
is_string = ::BCDD::Contract[::String]
|
|
454
|
+
is_filled = -> { _1.empty? and "%p must be filled" }
|
|
455
|
+
|
|
456
|
+
uuid_format = -> { _1.match?(/\A[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}\z/) or "%p must be a valid UUID" }
|
|
457
|
+
email_format = -> { _1.match?(/\A[^@\s]+@[^@\s]+\z/) or "%p must be a valid email" }
|
|
458
|
+
|
|
459
|
+
::BCDD::Contract.register(
|
|
460
|
+
is_uuid: is_string & uuid_format,
|
|
461
|
+
is_email: is_string & email_format,
|
|
462
|
+
is_filled: is_filled
|
|
463
|
+
)
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
To use them, use a symbol with `BCDD::Contract[]` or a method that transforms a value into a checker.
|
|
467
|
+
|
|
468
|
+
```ruby
|
|
469
|
+
str_filled = ::BCDD::Contract[:is_str] & :is_filled
|
|
470
|
+
|
|
471
|
+
PersonParams = ::BCDD::Contract[{
|
|
472
|
+
uuid: :is_uuid,
|
|
473
|
+
name: str_filled,
|
|
474
|
+
email: :is_email,
|
|
475
|
+
tags: [str_filled]
|
|
476
|
+
}]
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
You can use registered checkers with unions and intersections.
|
|
480
|
+
|
|
481
|
+
```ruby
|
|
482
|
+
BCDD::Contract.register(
|
|
483
|
+
is_hash: Hash,
|
|
484
|
+
is_array: Array,
|
|
485
|
+
is_filled: -> { _1.empty? and "%p must be filled" }
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
filled_array_or_hash = (BCDD::Contract[:is_array] | :is_hash) & :is_filled
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|
|
492
|
+
|
|
493
|
+
### Defining Interfaces
|
|
494
|
+
|
|
495
|
+
#### `BCDD::Contract::Interface`
|
|
496
|
+
|
|
497
|
+
This feature allows the creation of a module that will be used as an interface.
|
|
498
|
+
|
|
499
|
+
It will check if the class that includes it or the object that extends it implements all the expected methods.
|
|
500
|
+
|
|
501
|
+
```ruby
|
|
502
|
+
module User::Repository
|
|
503
|
+
include ::BCDD::Contract::Interface
|
|
504
|
+
|
|
505
|
+
module Methods
|
|
506
|
+
IsString = ::BCDD::Contract[String]
|
|
507
|
+
|
|
508
|
+
def create(name:, email:)
|
|
509
|
+
output = super(name: +IsString[name], email: +IsString[email])
|
|
510
|
+
|
|
511
|
+
output => ::User::Data[id: Integer, name: IsString, email: IsString]
|
|
512
|
+
|
|
513
|
+
output
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
Let's break down the example above.
|
|
520
|
+
|
|
521
|
+
1. The `User::Repository` module includes `BCDD::Contract::Interface`.
|
|
522
|
+
2. Defines the `Methods` module. It is mandatory, as these will be the methods to be implemented.
|
|
523
|
+
3. The `create` method is defined inside the `Method`s' module.
|
|
524
|
+
1. This method receives two arguments: `name` and `email`.
|
|
525
|
+
2. The arguments are checked using the `IsString` checker.
|
|
526
|
+
* The `+` operator performs a strict check. An error will be raised if the value is invalid. Otherwise, the value will be returned.
|
|
527
|
+
3. `super` is called to invoke the `create` method of the superclass. Which will be the class/object that includes/extends the `User::Repository` module.
|
|
528
|
+
4. The `output` is checked using pattern matching.
|
|
529
|
+
* The `=>` operator performs strict checks. If the value is invalid, a `NoMatchingPatternError` will be raised.
|
|
530
|
+
5. The `output` is returned.
|
|
531
|
+
|
|
532
|
+
Now, let's see how to use it in a class.
|
|
533
|
+
|
|
534
|
+
```ruby
|
|
535
|
+
class User::Record::Repository
|
|
536
|
+
include User::Repository
|
|
537
|
+
|
|
538
|
+
def create(name:, email:)
|
|
539
|
+
record = Record.create(name:, email:)
|
|
540
|
+
|
|
541
|
+
::User::Data.new(id: record.id, name: record.name, email: record.email)
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
And how to use it in a module with singleton methods.
|
|
547
|
+
|
|
548
|
+
```ruby
|
|
549
|
+
module User::Record::Repository
|
|
550
|
+
extend User::Repository
|
|
551
|
+
|
|
552
|
+
def self.create(name:, email:)
|
|
553
|
+
record = Record.create(name:, email:)
|
|
554
|
+
|
|
555
|
+
::User::Data.new(id: record.id, name: record.name, email: record.email)
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
**What happend when an interface module is included/extended?**
|
|
561
|
+
|
|
562
|
+
1. An instance of the class will be a `User::Repository`.
|
|
563
|
+
2. The module, class, object, that extended the interface will be a `User::Repository`.
|
|
564
|
+
|
|
565
|
+
```ruby
|
|
566
|
+
class User::Record::Repository
|
|
567
|
+
include User::Repository
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
module UserTest::RepositoryInMemory
|
|
571
|
+
extend User::Repository
|
|
572
|
+
# ...
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
User::Record::Repository.new.is_a?(User::Repository) # true
|
|
576
|
+
|
|
577
|
+
UserTest::RepositoryInMemory.is_a?(User::Repository) # true
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
**Why this is useful?**
|
|
581
|
+
|
|
582
|
+
Use `is_a?` to ensure that the class/object implements the expected methods.
|
|
583
|
+
|
|
584
|
+
```ruby
|
|
585
|
+
class User::Creation
|
|
586
|
+
def initialize(repository)
|
|
587
|
+
repository => User::Repository
|
|
588
|
+
|
|
589
|
+
@repository = repository
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
# ...
|
|
593
|
+
end
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
> Access the [**Ports and Adapters example**](examples/ports_and_adapters) to see, test, and run something that uses the `BCDD::Contract::Interface`
|
|
597
|
+
|
|
598
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|
|
599
|
+
|
|
600
|
+
#### `BCDD::Contract::Proxy`
|
|
601
|
+
|
|
602
|
+
This feature allows the creation of a class that will be used as a proxy for another objects.
|
|
603
|
+
|
|
604
|
+
The idea is to define an interface for the object that will be proxied.
|
|
605
|
+
|
|
606
|
+
Let's implement the example from the [previous section](#bcddcontractinterface) using a proxy.
|
|
607
|
+
|
|
608
|
+
```ruby
|
|
609
|
+
class User::Repository < BCDD::Contract::Proxy
|
|
610
|
+
IsString = ::BCDD::Contract[String]
|
|
611
|
+
|
|
612
|
+
def create(name:, email:)
|
|
613
|
+
output = object.create(name: +IsString[name], email: +IsString[email])
|
|
614
|
+
|
|
615
|
+
output => ::User::Data[id: Integer, name: IsString, email: IsString]
|
|
616
|
+
|
|
617
|
+
output
|
|
618
|
+
end
|
|
619
|
+
end
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
**How to use it?**
|
|
623
|
+
|
|
624
|
+
Inside the proxy you will use `object` to access the proxied object. This means the proxy must be initialized with an object. And the object must implement the methods defined in the proxy.
|
|
625
|
+
|
|
626
|
+
```ruby
|
|
627
|
+
class User::Record::Repository
|
|
628
|
+
# ...
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
module UserTest::RepositoryInMemory
|
|
632
|
+
extend self
|
|
633
|
+
# ...
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
# The proxy must be initialized with an object that implements the expected methods
|
|
637
|
+
|
|
638
|
+
memory_repository = User::Repository.new(UserTest::RepositoryInMemory)
|
|
639
|
+
|
|
640
|
+
record_repository = User::Repository.new(User::Record::Repository.new)
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
> Access the [**Anti-Corruption Layer**](examples/anti_corruption_layer) to see, test, and run something that uses the `BCDD::Contract::Proxy`
|
|
644
|
+
|
|
645
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|
|
646
|
+
|
|
647
|
+
### Assertions
|
|
648
|
+
|
|
649
|
+
Use the `BCDD::Contract.assert` method to check if a value is truthy or use the `BCDD::Contract.refute` method to check if a value is falsey.
|
|
650
|
+
|
|
651
|
+
Both methods always expect a value and a message. The third argument is optional and can be used to perform a more complex check.
|
|
652
|
+
|
|
653
|
+
If the value is falsey for an assertion or truthy for a refutation, an error will be raised with the message.
|
|
654
|
+
|
|
655
|
+
**Assertions withouth a block**
|
|
656
|
+
|
|
657
|
+
```ruby
|
|
658
|
+
item_name1 = nil
|
|
659
|
+
item_name2 = 'Item 2'
|
|
660
|
+
|
|
661
|
+
BCDD::Contract.assert!(item_name1, 'item (%p) not found')
|
|
662
|
+
# item (nil) not found (BCDD::Contract::Error)
|
|
663
|
+
|
|
664
|
+
BCDD::Contract.assert!(item_name2, 'item (%p) not found')
|
|
665
|
+
# "Item 2"
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
**Refutations withouth a block**
|
|
669
|
+
|
|
670
|
+
```ruby
|
|
671
|
+
allowed_quantity = 10
|
|
672
|
+
|
|
673
|
+
BCDD::Contract.refute!(20 > allowed_quantity, 'quantity is greater than allowed')
|
|
674
|
+
# quantity is greater than allowed (BCDD::Contract::Error)
|
|
675
|
+
|
|
676
|
+
BCDD::Contract.refute!(5 > allowed_quantity, 'quantity is greater than allowed')
|
|
677
|
+
# false
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
**Assertions/Refutations with a block**
|
|
681
|
+
|
|
682
|
+
You can use a block to perform a more complex check. The value passed to `assert`/`refute` will be yielded to the block.
|
|
683
|
+
|
|
684
|
+
```ruby
|
|
685
|
+
item_name = 'Item 1'
|
|
686
|
+
item_quantity = 10
|
|
687
|
+
|
|
688
|
+
# ---
|
|
689
|
+
|
|
690
|
+
quantity_to_remove = 11
|
|
691
|
+
|
|
692
|
+
BCDD::Contract.assert(item_name, 'item (%p) not enough quantity to remove') { quantity_to_remove <= item_quantity }
|
|
693
|
+
# item ("Item 1") not enough quantity to remove (BCDD::Contract::Error)
|
|
694
|
+
|
|
695
|
+
BCDD::Contract.refute(item_name, 'item (%p) not enough quantity to remove') { quantity_to_remove > item_quantity }
|
|
696
|
+
# item ("Item 1") not enough quantity to remove (BCDD::Contract::Error)
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
quantity_to_remove = 10
|
|
700
|
+
|
|
701
|
+
BCDD::Contract.assert(item_name, 'item (%p) not enough quantity to remove') { quantity_to_remove <= item_quantity }
|
|
702
|
+
# "Item 1"
|
|
703
|
+
|
|
704
|
+
BCDD::Contract.refute(item_name, 'item (%p) not enough quantity to remove') { quantity_to_remove > item_quantity }
|
|
705
|
+
# "Item 1"
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
> Access the [**Design by Contract**](examples/design_by_contract) to see, test, and run something that uses the `BCDD::Contract` assertions.
|
|
709
|
+
|
|
710
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|
|
711
|
+
|
|
712
|
+
## Configuration
|
|
713
|
+
|
|
714
|
+
By default, the `BCDD::Contract` enables all its features. You can disable them by setting the configuration.
|
|
715
|
+
|
|
716
|
+
### Switchable features
|
|
717
|
+
|
|
718
|
+
```ruby
|
|
719
|
+
BCDD::Contract.configuration do |config|
|
|
720
|
+
dev_or_test = ::Rails.env.local?
|
|
721
|
+
|
|
722
|
+
config.proxy_enabled = dev_or_test
|
|
723
|
+
config.interface_enabled = dev_or_test
|
|
724
|
+
config.assertions_enabled = dev_or_test
|
|
725
|
+
end
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
In the example above, the `BCDD::Contract::Proxy`, `BCDD::Contract::Interface`, and `BCDD::Contract.assert`/`BCDD::Contract.refute` will be disabled in production.
|
|
729
|
+
|
|
730
|
+
### Non-switchable features
|
|
731
|
+
|
|
732
|
+
The following variants are always enabled. You cannot disable them through the configuration.
|
|
733
|
+
|
|
734
|
+
- [`BCDD::Contract::Proxy::AlwaysEnabled`](#bcddcontractproxy).
|
|
735
|
+
- [`BCDD::Contract::Interface::AlwaysEnabled`](#bcddcontractinterface).
|
|
736
|
+
- [`BCDD::Contract.assert!`](#assertions).
|
|
737
|
+
- [`BCDD::Contract.refute!`](#assertions).
|
|
738
|
+
|
|
739
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|
|
740
|
+
|
|
741
|
+
## Reference
|
|
742
|
+
|
|
743
|
+
### The Contract Checker API
|
|
744
|
+
|
|
745
|
+
This section describes the common API for all contract checkers:
|
|
746
|
+
- **Unit**
|
|
747
|
+
- [Lambda based](#lambda-based)
|
|
748
|
+
- [Type based](#type-based)
|
|
749
|
+
- [Union based](#union-based)
|
|
750
|
+
- **Data Structure**
|
|
751
|
+
- [List schema](#list-schema)
|
|
752
|
+
- [Hash schema](#hash-schema)
|
|
753
|
+
- [Hash key/value pairs schema](#hash-keyvalue-pairs-schema)
|
|
754
|
+
|
|
755
|
+
Let's the following contract checker to illustrate the API.
|
|
756
|
+
|
|
757
|
+
```ruby
|
|
758
|
+
IsFilled = BCDD::Contract[-> { _1.empty? and "%p must be filled" }]
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
#### `.===`
|
|
762
|
+
|
|
763
|
+
You can use the `===` operator to check if a value is valid. This operator is also used by the `case` statement and `pattern matching` operators.
|
|
764
|
+
|
|
765
|
+
```ruby
|
|
766
|
+
# ===
|
|
767
|
+
|
|
768
|
+
IsFilled === '' # false
|
|
769
|
+
IsFilled === [] # false
|
|
770
|
+
|
|
771
|
+
IsFilled === '1' # true
|
|
772
|
+
|
|
773
|
+
# case statement
|
|
774
|
+
|
|
775
|
+
case {}
|
|
776
|
+
when IsFilled
|
|
777
|
+
# ...
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
# pattern matching
|
|
781
|
+
|
|
782
|
+
case []
|
|
783
|
+
in IsFilled
|
|
784
|
+
# ...
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
'' in IsFilled # false
|
|
788
|
+
|
|
789
|
+
Set.new => IsFilled # is_filled === #<Set: {}> does not return true (NoMatchingPatternError)
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|
|
793
|
+
|
|
794
|
+
#### `.to_proc`
|
|
795
|
+
|
|
796
|
+
You can use the `to_proc` method to transform a value into a checking object.
|
|
797
|
+
|
|
798
|
+
```ruby
|
|
799
|
+
[
|
|
800
|
+
'',
|
|
801
|
+
[],
|
|
802
|
+
{}
|
|
803
|
+
].map(&IsFilled).all?(&:valid?) # false
|
|
804
|
+
|
|
805
|
+
[
|
|
806
|
+
'1',
|
|
807
|
+
'2',
|
|
808
|
+
'3'
|
|
809
|
+
].map(&IsFilled).all?(&:valid?) # true
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|
|
813
|
+
|
|
814
|
+
#### `.invariant`
|
|
815
|
+
|
|
816
|
+
Use the `invariant` to perform an strict check before and after the block execution.
|
|
817
|
+
|
|
818
|
+
```ruby
|
|
819
|
+
IsFilled.invariant([1]) { |numbers| numbers.pop }
|
|
820
|
+
# [] must be filled (BCDD::Contract::Error)
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
> Access the [**Design by Contract**](examples/design_by_contract) a better example of how to use `invariant`.
|
|
824
|
+
|
|
825
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|
|
826
|
+
|
|
827
|
+
### The Contract Checking API
|
|
828
|
+
|
|
829
|
+
This section describes the common API for all contract checking objects. Objects that are created by a contract checker.
|
|
830
|
+
|
|
831
|
+
```ruby
|
|
832
|
+
IsFilled = BCDD::Contract[-> { _1.empty? and "%p must be filled" }]
|
|
833
|
+
|
|
834
|
+
checking = IsFilled['']
|
|
835
|
+
|
|
836
|
+
checking.valid? # false
|
|
837
|
+
checking.invalid? # true
|
|
838
|
+
checking.errors? # true
|
|
839
|
+
checking.errors # ['"" must be filled']
|
|
840
|
+
checking.errors_message # '"" must be filled'
|
|
841
|
+
|
|
842
|
+
checking.value # ""
|
|
843
|
+
|
|
844
|
+
+checking # "" must be filled (BCDD::Contract::Error)
|
|
845
|
+
!checking # "" must be filled (BCDD::Contract::Error)
|
|
846
|
+
checking.value! # "" must be filled (BCDD::Contract::Error)
|
|
847
|
+
checking.assert! # "" must be filled (BCDD::Contract::Error)
|
|
848
|
+
|
|
849
|
+
# ---
|
|
850
|
+
|
|
851
|
+
checking = IsFilled['John']
|
|
852
|
+
|
|
853
|
+
+checking # "John"
|
|
854
|
+
!checking # "John"
|
|
855
|
+
checking.value! # "John"
|
|
856
|
+
checking.assert! # "John"
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|
|
860
|
+
|
|
861
|
+
#### Unary operators
|
|
862
|
+
|
|
863
|
+
You can use the unary operators `+` and `!` to perform a strict check. If the value is invalid, an error will be raised. Otherwise, the value will be returned.
|
|
864
|
+
|
|
865
|
+
```ruby
|
|
866
|
+
+IsFilled[''] # "" must be filled (BCDD::Contract::Error)
|
|
867
|
+
!IsFilled[''] # "" must be filled (BCDD::Contract::Error)
|
|
868
|
+
|
|
869
|
+
+IsFilled['John'] # "John"
|
|
870
|
+
!IsFilled['John'] # "John"
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|
|
874
|
+
|
|
875
|
+
### `BCDD::Contract` methods
|
|
876
|
+
|
|
877
|
+
```ruby
|
|
878
|
+
BCDD::Contract[lambda] # returns a unit checker
|
|
879
|
+
BCDD::Contract[module] # returns a type checker
|
|
880
|
+
BCDD::Contract[<Array>] # returns a list schema checker
|
|
881
|
+
BCDD::Contract[<Hash>] # returns a hash schema checker
|
|
882
|
+
|
|
883
|
+
BCDD::Contract(<Object>) # alias for BCDD::Contract[<Object>]
|
|
884
|
+
BCDD::Contract.new(<Object>) # alias for BCDD::Contract[<Object>]
|
|
885
|
+
|
|
886
|
+
BCDD::Contract.to_proc # returns a proc that transforms a value into a checker
|
|
887
|
+
|
|
888
|
+
BCDD::Contract.pairs(<Hash>) # returns a hash key/value pairs schema checker
|
|
889
|
+
BCDD::Contract.schema(<Hash>) # returns a hash schema checker
|
|
890
|
+
BCDD::Contract.list(<Object>) # returns a list schema checker
|
|
891
|
+
BCDD::Contract.unit(<Object>) # returns a unit checker (lambda/type based)
|
|
892
|
+
|
|
893
|
+
BCDD::Contract.error!(<String>) # raises a BCDD::Contract::Error
|
|
894
|
+
|
|
895
|
+
BCDD::Contract.assert(value, message, &block) # raises a BCDD::Contract::Error if the value/block is falsey
|
|
896
|
+
BCDD::Contract.refute(value, message, &block) # raises a BCDD::Contract::Error if the value/block is truthy
|
|
897
|
+
|
|
898
|
+
BCDD::Contract.assert!(value, message, &block) # same as BCDD::Contract.assert but cannot be disabled
|
|
899
|
+
BCDD::Contract.refute!(value, message, &block) # same as BCDD::Contract.refute but cannot be disabled
|
|
900
|
+
|
|
901
|
+
# Produces a BCDD::Contract::Proxy class
|
|
902
|
+
BCDD::Contract.proxy do
|
|
903
|
+
# ...
|
|
904
|
+
end
|
|
905
|
+
|
|
906
|
+
# Produces a BCDD::Contract::Proxy::AlwaysEnabled class
|
|
907
|
+
BCDD::Contract.proxy(always_enabled: true) do
|
|
908
|
+
# ...
|
|
909
|
+
end
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|
|
913
|
+
|
|
914
|
+
### `BCDD::Contract::Assertions`
|
|
915
|
+
|
|
916
|
+
Use this module to include/extend the `BCDD::Contract` assertions (`#assert`/`#assert!` and `#refute`/`#refute!`).
|
|
917
|
+
|
|
918
|
+
The methods without bang (`#assert` and `#refute`) can be disabled through the assertions configuration.
|
|
919
|
+
|
|
920
|
+
```ruby
|
|
921
|
+
class User::Creation
|
|
922
|
+
include BCDD::Contract::Assertions
|
|
923
|
+
|
|
924
|
+
def initialize(repository)
|
|
925
|
+
assert!(repository, '%p must be a User::Repository') { _1.repository.is_a?(User::Repository) }
|
|
926
|
+
|
|
927
|
+
@repository = repository
|
|
928
|
+
end
|
|
929
|
+
|
|
930
|
+
# ...
|
|
931
|
+
end
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|
|
935
|
+
|
|
936
|
+
## About
|
|
937
|
+
|
|
938
|
+
[Rodrigo Serradura](https://github.com/serradura) created this project. He is the B/CDD process/method creator and has already made similar gems like the [u-case](https://github.com/serradura/u-case) and [kind](https://github.com/serradura/kind/blob/main/lib/kind/result.rb). This gem can be used independently, but it also contains essential features that facilitate the adoption of B/CDD in code.
|
|
939
|
+
|
|
940
|
+
## Development
|
|
941
|
+
|
|
942
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
943
|
+
|
|
944
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
945
|
+
|
|
946
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|
|
947
|
+
|
|
948
|
+
## Contributing
|
|
949
|
+
|
|
950
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/B-CDD/contract. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/B-CDD/contract/blob/main/CODE_OF_CONDUCT.md).
|
|
951
|
+
|
|
952
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|
|
953
|
+
|
|
954
|
+
## License
|
|
955
|
+
|
|
956
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
957
|
+
|
|
958
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|
|
959
|
+
|
|
960
|
+
## Code of Conduct
|
|
961
|
+
|
|
962
|
+
Everyone interacting in the `BCDD::Contract` project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/B-CDD/contract/blob/main/CODE_OF_CONDUCT.md).
|
|
963
|
+
|
|
964
|
+
<p align="right"><a href="#-bcddcontract">⬆️ back to top</a></p>
|