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
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ShoppingCart
|
|
4
|
+
module Item
|
|
5
|
+
module Contract
|
|
6
|
+
cannot_be_nan = ->(val) { val.respond_to?(:nan?) and val.nan? and '%p cannot be nan' }
|
|
7
|
+
cannot_be_inf = ->(val) { val.respond_to?(:infinite?) and val.infinite? and '%p cannot be infinite' }
|
|
8
|
+
must_be_positive = ->(label) { ->(val) { val.positive? or "#{label} (%p) must be positive" } }
|
|
9
|
+
|
|
10
|
+
PricePerUnit = ::BCDD::Contract[::Numeric] & cannot_be_nan & cannot_be_inf & must_be_positive['price per unit']
|
|
11
|
+
Quantity = ::BCDD::Contract[::Integer] & must_be_positive['quantity']
|
|
12
|
+
Name = ::BCDD::Contract[::String] & ->(val) { val.empty? and 'item name must be filled' }
|
|
13
|
+
|
|
14
|
+
NameAndData = ::BCDD::Contract.pairs(Name => { quantity: Quantity, price_per_unit: PricePerUnit })
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module Items
|
|
19
|
+
module Contract
|
|
20
|
+
extend ::BCDD::Contract[::Hash] & ->(items, errors) do
|
|
21
|
+
return if items.empty?
|
|
22
|
+
|
|
23
|
+
Item::Contract::NameAndData[items].then { |it| it.valid? or errors.concat(it.errors) }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(items = {})
|
|
29
|
+
@items = +Items::Contract[items]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def add_item(item_name, quantity, price_per_unit)
|
|
33
|
+
Items::Contract.invariant(@items) do |items|
|
|
34
|
+
item_name = +Item::Contract::Name[item_name]
|
|
35
|
+
|
|
36
|
+
item = items[item_name] ||= { quantity: 0, price_per_unit: 0 }
|
|
37
|
+
|
|
38
|
+
item[:price_per_unit] = +Item::Contract::PricePerUnit[price_per_unit]
|
|
39
|
+
item[:quantity] += +Item::Contract::Quantity[quantity]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def remove_item(item_name, quantity)
|
|
44
|
+
Items::Contract.invariant(@items) do |items|
|
|
45
|
+
item_name = +Item::Contract::Name[item_name]
|
|
46
|
+
quantity = +Item::Contract::Quantity[quantity]
|
|
47
|
+
|
|
48
|
+
item = items[item_name]
|
|
49
|
+
|
|
50
|
+
::BCDD::Contract.assert!(item_name, 'item (%p) not found')
|
|
51
|
+
::BCDD::Contract.refute!(item_name, 'item (%p) not enough quantity to remove') { quantity > item[:quantity] }
|
|
52
|
+
|
|
53
|
+
item[:quantity] -= quantity
|
|
54
|
+
|
|
55
|
+
item[:quantity].tap { |number| items.delete(item_name) if number.zero? }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def total_price
|
|
60
|
+
(+Items::Contract[@items]).sum { |_name, data| data[:quantity] * data[:price_per_unit] }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
- [🧩 Ports and Adapters Example](#-ports-and-adapters-example)
|
|
2
|
+
- [The Port](#the-port)
|
|
3
|
+
- [The Adapters](#the-adapters)
|
|
4
|
+
- [⚖️ What is the benefit of doing this?](#️-what-is-the-benefit-of-doing-this)
|
|
5
|
+
- [How much to do this (create Ports and Adapters)?](#how-much-to-do-this-create-ports-and-adapters)
|
|
6
|
+
- [Is it worth the overhead of contract checking at runtime?](#is-it-worth-the-overhead-of-contract-checking-at-runtime)
|
|
7
|
+
- [🏃♂️ How to run the application?](#️-how-to-run-the-application)
|
|
8
|
+
- [💡 Why is `User::Creation` not validating the name and email?](#-why-is-usercreation-not-validating-the-name-and-email)
|
|
9
|
+
|
|
10
|
+
## 🧩 Ports and Adapters Example
|
|
11
|
+
|
|
12
|
+
Ports and Adapters is an architectural pattern that separates the application's core logic (Ports) from external dependencies (Adapters).
|
|
13
|
+
|
|
14
|
+
This example shows how to implement a simple application using this pattern and the gem `bcdd-contract`.
|
|
15
|
+
|
|
16
|
+
Let's start seeing the code structure:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
├── Rakefile
|
|
20
|
+
├── config.rb
|
|
21
|
+
├── db
|
|
22
|
+
├── app
|
|
23
|
+
│ └── models
|
|
24
|
+
│ └── user
|
|
25
|
+
│ ├── record
|
|
26
|
+
│ │ └── repository.rb
|
|
27
|
+
│ └── record.rb
|
|
28
|
+
├── lib
|
|
29
|
+
│ └── user
|
|
30
|
+
│ ├── creation.rb
|
|
31
|
+
│ ├── data.rb
|
|
32
|
+
│ └── repository.rb
|
|
33
|
+
└── test
|
|
34
|
+
└── user_test
|
|
35
|
+
└── repository.rb
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The files and directories are organized as follows:
|
|
39
|
+
|
|
40
|
+
- `Rakefile` runs the application.
|
|
41
|
+
- `config.rb` file contains the configuration of the application.
|
|
42
|
+
- `db` directory contains the database. It is not part of the application, but it is used by the application.
|
|
43
|
+
- `app` directory contains "Rails" components.
|
|
44
|
+
- `lib` directory contains the core business logic.
|
|
45
|
+
- `test` directory contains the tests.
|
|
46
|
+
|
|
47
|
+
The application is a simple "user management system". It unique core functionality is to create users.
|
|
48
|
+
|
|
49
|
+
Now we understand the code structure, let's see the how the pattern is implemented.
|
|
50
|
+
|
|
51
|
+
### The Port
|
|
52
|
+
|
|
53
|
+
In this application, there is only one business process: `User::Creation` (see `lib/user/creation.rb`), which relies on the `User::Repository` (see `lib/user/repository.rb`) to persist the user.
|
|
54
|
+
|
|
55
|
+
The `User::Repository` is an example of **port**, because it is an interface/contract that defines how the core business logic will persist user records.
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
module User::Repository
|
|
59
|
+
include ::BCDD::Contract::Interface
|
|
60
|
+
|
|
61
|
+
module Methods
|
|
62
|
+
module Input
|
|
63
|
+
is_string = ::BCDD::Contract[String]
|
|
64
|
+
is_filled = ->(val) { val.present? or '%p must be filled' }
|
|
65
|
+
email_format = ->(val) { val.match?(/\A[^@\s]+@[^@\s]+\z/) or '%p must be an email' }
|
|
66
|
+
|
|
67
|
+
Name = is_string & is_filled
|
|
68
|
+
Email = is_string & is_filled & email_format
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def create(name:, email:)
|
|
72
|
+
output = super(name: +Input::Name[name], email: +Input::Email[email])
|
|
73
|
+
|
|
74
|
+
output => ::User::Data[id: Integer, name: Input::Name, email: Input::Email]
|
|
75
|
+
|
|
76
|
+
output
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### The Adapters
|
|
83
|
+
|
|
84
|
+
The `User::Repository` is implemented by two adapters:
|
|
85
|
+
|
|
86
|
+
- `User::Record::Repository` (see `app/models/user/record/repository.rb`) is an adapter that persists user records in the database (through the `User::Record`, that is an `ActiveRecord` model).
|
|
87
|
+
|
|
88
|
+
- `UserTest::Repository` (see `test/user_test/repository.rb`) is an adapter that persists user records in memory (through the `UserTest::Data`, that is a simple in-memory data structure).
|
|
89
|
+
|
|
90
|
+
## ⚖️ What is the benefit of doing this?
|
|
91
|
+
|
|
92
|
+
The benefit of doing this is that the core business logic is decoupled from the external dependencies, which makes it easier to test and promote changes in the code.
|
|
93
|
+
|
|
94
|
+
For example, if we need to change the persistence layer (start to send the data to a REST API or a Redis DB), we just need to implement a new adapter and make the business processes (`User::Creation`) use it.
|
|
95
|
+
|
|
96
|
+
### How much to do this (create Ports and Adapters)?
|
|
97
|
+
|
|
98
|
+
Use this pattern when there is a real need to decouple the core business logic from external dependencies.
|
|
99
|
+
|
|
100
|
+
You can start with a simple implementation (without Ports and Adapters) and refactor it to use this pattern when the need arises.
|
|
101
|
+
|
|
102
|
+
### Is it worth the overhead of contract checking at runtime?
|
|
103
|
+
|
|
104
|
+
You can eliminate the overhead by disabling the `BCDD::Contract::Interface`, which is enabled by default.
|
|
105
|
+
|
|
106
|
+
When it is disabled, the `BCDD::Contract::Interface` won't prepend the interface methods module to the adapter, which means that the adapter won't be checked against the interface.
|
|
107
|
+
|
|
108
|
+
To disable it, set the configuration to false:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
BCDD::Contract.configuration do |config|
|
|
112
|
+
config.interface_enabled = false
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## 🏃♂️ How to run the application?
|
|
117
|
+
|
|
118
|
+
In the same directory as this `README`, run:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
rake # or rake BCDD_CONTRACT_ENABLED=enabled
|
|
122
|
+
|
|
123
|
+
# or
|
|
124
|
+
|
|
125
|
+
rake BCDD_CONTRACT_ENABLED=false
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Proxy enabled**
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
rake # or rake BCDD_CONTRACT_ENABLED=enabled
|
|
132
|
+
|
|
133
|
+
# Output sample:
|
|
134
|
+
#
|
|
135
|
+
# -- Valid input --
|
|
136
|
+
#
|
|
137
|
+
# Created user: #<struct User::Data id=1, name="Jane", email="jane@foo.com">
|
|
138
|
+
# Created user: #<struct User::Data id=1, name="John", email="john@bar.com">
|
|
139
|
+
#
|
|
140
|
+
# -- Invalid input --
|
|
141
|
+
#
|
|
142
|
+
# rake aborted!
|
|
143
|
+
# BCDD::Contract::Error: "jane" must be an email (BCDD::Contract::Error)
|
|
144
|
+
# /.../lib/bcdd/contract/core/checking.rb:26:in `raise_validation_errors!'
|
|
145
|
+
# /.../lib/bcdd/contract/core/checking.rb:30:in `value_or_raise_validation_errors!'
|
|
146
|
+
# /.../examples/ports_and_adapters/lib/user/repository.rb:18:in `create'
|
|
147
|
+
# /.../examples/ports_and_adapters/lib/user/creation.rb:12:in `call'
|
|
148
|
+
# /.../examples/ports_and_adapters/Rakefile:33:in `block in <top (required)>'
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Proxy disabled**
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
rake BCDD_CONTRACT_ENABLED=false
|
|
155
|
+
|
|
156
|
+
# Output sample:
|
|
157
|
+
#
|
|
158
|
+
# -- Valid input --
|
|
159
|
+
#
|
|
160
|
+
# Created user: #<struct User::Data id=1, name="Jane", email="jane@foo.com">
|
|
161
|
+
# Created user: #<struct User::Data id=1, name="John", email="john@bar.com">
|
|
162
|
+
#
|
|
163
|
+
# -- Invalid input --
|
|
164
|
+
#
|
|
165
|
+
# Created user: #<struct User::Data id=2, name="Jane", email="jane">
|
|
166
|
+
# Created user: #<struct User::Data id=3, name="", email=nil>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## 💡 Why is `User::Creation` not validating the name and email?
|
|
170
|
+
|
|
171
|
+
The `User::Creation` process is not validating the name and email because if it did, it wouldn't be possible to see the error messages of the `User::Repository` contract.
|
|
172
|
+
|
|
173
|
+
But in a real-world application, the `User::Creation` process would validate the name and email, as the validation is part of its business logic. The `User::Repository` contract could do the same or simpler checkings (like if the name and email are strings).
|
|
174
|
+
|
|
175
|
+
This is an example of the `User::Creation` performing validations and the `User::Repository` checkings:
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
# lib/user/name.rb
|
|
179
|
+
module User
|
|
180
|
+
module Name
|
|
181
|
+
Contract = ::BCDD::Contract[String] & -> { _1.present? or '%p must be filled' }
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# lib/user/email.rb
|
|
186
|
+
module User
|
|
187
|
+
module Email
|
|
188
|
+
Contract = ::BCDD::Contract[String] & -> { _1.match?(/\A[^@\s]+@[^@\s]+\z/) or '%p must be an email' }
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# lib/user/repository.rb
|
|
193
|
+
module User::Repository
|
|
194
|
+
include ::BCDD::Contract::Interface
|
|
195
|
+
|
|
196
|
+
module Methods
|
|
197
|
+
def create(name:, email:)
|
|
198
|
+
output = super(name: +User::Name::Contract[name], email: +User::Email::Contract[email])
|
|
199
|
+
|
|
200
|
+
output => ::User::Data[id: Integer, name: User::Name::Contract, email: User::Email::Contract]
|
|
201
|
+
|
|
202
|
+
output
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# lib/user/creation.rb
|
|
208
|
+
module User
|
|
209
|
+
class Creation
|
|
210
|
+
def initialize(repository:)
|
|
211
|
+
repository => Repository
|
|
212
|
+
|
|
213
|
+
@repository = repository
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def call(name:, email:)
|
|
217
|
+
name = Name::Contract[name]
|
|
218
|
+
email = Email::Contract[email]
|
|
219
|
+
|
|
220
|
+
return [false, name.errors] if name.invalid?
|
|
221
|
+
return [false, email.errors] if email.invalid?
|
|
222
|
+
|
|
223
|
+
user_data = @repository.create(name: name.value, email: email.value)
|
|
224
|
+
|
|
225
|
+
puts "Created user: #{user_data.inspect}"
|
|
226
|
+
|
|
227
|
+
[true, user_data]
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Usage:
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
memory_creation = User::Creation.new(repository: UserTest::Repository.new)
|
|
237
|
+
|
|
238
|
+
memory_creation.call(name: 'Jane', email: 'jane@email.com')
|
|
239
|
+
# => [true, #<struct User::Data id=1, name="Jane", email="jane@email.com">
|
|
240
|
+
|
|
241
|
+
memory_creation.call(name: '', email: 'jane')
|
|
242
|
+
# => [false, ["\"\" must be filled"]]
|
|
243
|
+
|
|
244
|
+
memory_creation.call(name: 'Jane', email: 'jane')
|
|
245
|
+
# => [false, ["\"jane\" must be an email"]]
|
|
246
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
if RUBY_VERSION <= '3.1'
|
|
4
|
+
puts 'This example requires Ruby 3.1 or higher.'
|
|
5
|
+
exit! 1
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
require_relative 'config'
|
|
9
|
+
|
|
10
|
+
require_relative 'test/user_test/repository'
|
|
11
|
+
|
|
12
|
+
task :default do
|
|
13
|
+
puts
|
|
14
|
+
puts '------------------'
|
|
15
|
+
puts 'Ports and Adapters'
|
|
16
|
+
puts '------------------'
|
|
17
|
+
|
|
18
|
+
# -- User creation instances
|
|
19
|
+
|
|
20
|
+
db_creation = User::Creation.new(repository: User::Record::Repository)
|
|
21
|
+
|
|
22
|
+
memory_creation = User::Creation.new(repository: UserTest::Repository.new)
|
|
23
|
+
|
|
24
|
+
puts
|
|
25
|
+
puts '-- Valid input --'
|
|
26
|
+
puts
|
|
27
|
+
|
|
28
|
+
db_creation.call(name: 'Jane', email: 'jane@foo.com')
|
|
29
|
+
|
|
30
|
+
memory_creation.call(name: 'John', email: 'john@bar.com')
|
|
31
|
+
|
|
32
|
+
puts
|
|
33
|
+
puts '-- Invalid input --'
|
|
34
|
+
puts
|
|
35
|
+
|
|
36
|
+
db_creation.call(name: 'Jane', email: 'jane')
|
|
37
|
+
|
|
38
|
+
memory_creation.call(name: '', email: nil)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Output sample: rake BCDD_CONTRACT_ENABLED=true
|
|
42
|
+
#
|
|
43
|
+
# -- Valid input --
|
|
44
|
+
#
|
|
45
|
+
# Created user: #<struct User::Data id=1, name="Jane", email="jane@foo.com">
|
|
46
|
+
# Created user: #<struct User::Data id=1, name="John", email="john@bar.com">
|
|
47
|
+
#
|
|
48
|
+
# -- Invalid input --
|
|
49
|
+
#
|
|
50
|
+
# rake aborted!
|
|
51
|
+
# BCDD::Contract::Error: "jane" must be an email (BCDD::Contract::Error)
|
|
52
|
+
# lib/bcdd/contract/unit.rb:52:in `+@'
|
|
53
|
+
# examples/ports_and_adapters/lib/user/repository.rb:20:in `create'
|
|
54
|
+
# examples/ports_and_adapters/lib/user/creation.rb:10:in `call'
|
|
55
|
+
# examples/ports_and_adapters/Rakefile:35:in `block in <top (required)>'
|
|
56
|
+
|
|
57
|
+
# Output sample: rake BCDD_CONTRACT_ENABLED=false
|
|
58
|
+
#
|
|
59
|
+
#
|
|
60
|
+
# -- Valid input --
|
|
61
|
+
#
|
|
62
|
+
# Created user: #<struct User::Data id=1, name="Jane", email="jane@foo.com">
|
|
63
|
+
# Created user: #<struct User::Data id=1, name="John", email="john@bar.com">
|
|
64
|
+
#
|
|
65
|
+
# -- Invalid input --
|
|
66
|
+
#
|
|
67
|
+
# Created user: #<struct User::Data id=2, name="Jane", email="jane">
|
|
68
|
+
# Created user: #<struct User::Data id=3, name="", email=nil>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module User
|
|
4
|
+
module Record::Repository
|
|
5
|
+
extend ::User::Repository
|
|
6
|
+
|
|
7
|
+
def self.create(name:, email:)
|
|
8
|
+
record = Record.create!(name:, email:)
|
|
9
|
+
|
|
10
|
+
::User::Data.new(id: record.id, name: record.name, email: record.email)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/inline'
|
|
4
|
+
|
|
5
|
+
$LOAD_PATH.unshift(__dir__)
|
|
6
|
+
|
|
7
|
+
gemfile do
|
|
8
|
+
source 'https://rubygems.org'
|
|
9
|
+
|
|
10
|
+
gem 'sqlite3', '~> 1.7'
|
|
11
|
+
gem 'activerecord', '~> 7.1', '>= 7.1.2', require: 'active_record'
|
|
12
|
+
gem 'bcdd-contract', path: '../../'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
require 'active_support/all'
|
|
16
|
+
|
|
17
|
+
require 'db/setup'
|
|
18
|
+
|
|
19
|
+
::BCDD::Contract.config.interface_enabled = ENV.fetch('BCDD_CONTRACT_ENABLED', 'true') != 'false'
|
|
20
|
+
|
|
21
|
+
module User
|
|
22
|
+
require 'lib/user/data'
|
|
23
|
+
require 'lib/user/repository'
|
|
24
|
+
require 'lib/user/creation'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
require 'app/models/user/record'
|
|
28
|
+
require 'app/models/user/record/repository'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/all'
|
|
4
|
+
|
|
5
|
+
ActiveRecord::Base.establish_connection(
|
|
6
|
+
host: 'localhost',
|
|
7
|
+
adapter: 'sqlite3',
|
|
8
|
+
database: ':memory:'
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
ActiveRecord::Schema.define do
|
|
12
|
+
create_table :users do |t|
|
|
13
|
+
t.column :name, :string
|
|
14
|
+
t.column :email, :string
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module User
|
|
4
|
+
class Creation
|
|
5
|
+
def initialize(repository:)
|
|
6
|
+
repository => Repository
|
|
7
|
+
|
|
8
|
+
@repository = repository
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call(name:, email:)
|
|
12
|
+
user_data = @repository.create(name:, email:)
|
|
13
|
+
|
|
14
|
+
puts "Created user: #{user_data.inspect}"
|
|
15
|
+
|
|
16
|
+
user_data
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module User::Repository
|
|
4
|
+
include ::BCDD::Contract::Interface
|
|
5
|
+
|
|
6
|
+
module Methods
|
|
7
|
+
module Input
|
|
8
|
+
is_string = ::BCDD::Contract[String]
|
|
9
|
+
is_filled = ->(val) { val.present? or '%p must be filled' }
|
|
10
|
+
email_format = ->(val) { val.match?(/\A[^@\s]+@[^@\s]+\z/) or '%p must be an email' }
|
|
11
|
+
|
|
12
|
+
Name = is_string & is_filled
|
|
13
|
+
Email = is_string & is_filled & email_format
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create(name:, email:)
|
|
17
|
+
output = super(name: +Input::Name[name], email: +Input::Email[email])
|
|
18
|
+
|
|
19
|
+
output => ::User::Data[id: Integer, name: Input::Name, email: Input::Email]
|
|
20
|
+
|
|
21
|
+
output
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UserTest
|
|
4
|
+
class Repository
|
|
5
|
+
include ::User::Repository
|
|
6
|
+
|
|
7
|
+
attr_reader :records
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@records = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def create(name:, email:)
|
|
14
|
+
id = @records.size + 1
|
|
15
|
+
|
|
16
|
+
@records[id] = { id:, name:, email: }
|
|
17
|
+
|
|
18
|
+
::User::Data.new(id:, name:, email:)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BCDD::Contract
|
|
4
|
+
module Assertions
|
|
5
|
+
def assert!(...)
|
|
6
|
+
::BCDD::Contract.assert!(...)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def refute!(...)
|
|
10
|
+
::BCDD::Contract.refute!(...)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def assert(...)
|
|
14
|
+
::BCDD::Contract.assert(...)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def refute(...)
|
|
18
|
+
::BCDD::Contract.refute(...)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BCDD::Contract
|
|
4
|
+
# A singleton class to store the configuration of the gem.
|
|
5
|
+
#
|
|
6
|
+
class Config
|
|
7
|
+
include ::Singleton
|
|
8
|
+
|
|
9
|
+
attr_accessor :proxy_enabled, :interface_enabled, :assertions_enabled
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
self.proxy_enabled = true
|
|
13
|
+
self.interface_enabled = true
|
|
14
|
+
self.assertions_enabled = true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def options
|
|
18
|
+
{
|
|
19
|
+
proxy_enabled: proxy_enabled,
|
|
20
|
+
interface_enabled: interface_enabled,
|
|
21
|
+
assertions_enabled: assertions_enabled
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BCDD::Contract
|
|
4
|
+
module Core::Checker
|
|
5
|
+
def [](value)
|
|
6
|
+
checking.new(strategy, value)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def ===(value)
|
|
10
|
+
self[value].valid?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_proc
|
|
14
|
+
->(value) { self[value] }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def invariant(value)
|
|
18
|
+
self[value].raise_validation_errors!
|
|
19
|
+
|
|
20
|
+
output = yield(value)
|
|
21
|
+
|
|
22
|
+
self[value].raise_validation_errors!
|
|
23
|
+
|
|
24
|
+
output
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
protected
|
|
28
|
+
|
|
29
|
+
def checking
|
|
30
|
+
const_get(:CHECKING, false)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def strategy
|
|
34
|
+
const_get(:STRATEGY, false)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BCDD::Contract
|
|
4
|
+
module Core::Checking
|
|
5
|
+
attr_reader :value, :errors
|
|
6
|
+
|
|
7
|
+
def initialize(_checker, _value)
|
|
8
|
+
raise Error, 'not implemented'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def valid?
|
|
12
|
+
errors.empty?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def invalid?
|
|
16
|
+
!valid?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
alias errors? invalid?
|
|
20
|
+
|
|
21
|
+
def errors_message
|
|
22
|
+
raise Error, 'not implemented'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def raise_validation_errors!
|
|
26
|
+
raise Error, errors_message if invalid?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def value_or_raise_validation_errors!
|
|
30
|
+
raise_validation_errors! || value
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
alias !@ value_or_raise_validation_errors!
|
|
34
|
+
alias +@ value_or_raise_validation_errors!
|
|
35
|
+
alias value! value_or_raise_validation_errors!
|
|
36
|
+
alias assert! value_or_raise_validation_errors!
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BCDD::Contract
|
|
4
|
+
module Core::Factory
|
|
5
|
+
module Callbacks
|
|
6
|
+
def included(_base)
|
|
7
|
+
raise Error, 'A contract checker cannot be included'
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def extended(base)
|
|
11
|
+
if !base.is_a?(::Module) || base.is_a?(::Class)
|
|
12
|
+
raise Error, 'A contract checker can only be extended by a module'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
mod = Module.new
|
|
16
|
+
mod.send(:include, Core::Checker)
|
|
17
|
+
|
|
18
|
+
base.const_set(:CHECKING, self::CHECKING)
|
|
19
|
+
base.const_set(:STRATEGY, self::STRATEGY)
|
|
20
|
+
base.extend(mod)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.new(checker, checking, strategy)
|
|
25
|
+
mod = ::Module.new
|
|
26
|
+
mod.const_set(:CHECKING, checking)
|
|
27
|
+
mod.const_set(:STRATEGY, strategy)
|
|
28
|
+
mod.extend(Callbacks)
|
|
29
|
+
mod.extend(checker)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|