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.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +128 -0
  3. data/CHANGELOG.md +45 -0
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +964 -0
  7. data/Rakefile +18 -0
  8. data/Steepfile +32 -0
  9. data/examples/README.md +11 -0
  10. data/examples/anti_corruption_layer/README.md +212 -0
  11. data/examples/anti_corruption_layer/Rakefile +30 -0
  12. data/examples/anti_corruption_layer/app/models/payment/charge_credit_card.rb +36 -0
  13. data/examples/anti_corruption_layer/config.rb +20 -0
  14. data/examples/anti_corruption_layer/lib/payment_gateways/adapters/circle_up.rb +19 -0
  15. data/examples/anti_corruption_layer/lib/payment_gateways/adapters/pay_friend.rb +19 -0
  16. data/examples/anti_corruption_layer/lib/payment_gateways/contract.rb +15 -0
  17. data/examples/anti_corruption_layer/lib/payment_gateways/response.rb +5 -0
  18. data/examples/anti_corruption_layer/lib/payment_gateways.rb +11 -0
  19. data/examples/anti_corruption_layer/vendor/circle_up/client.rb +11 -0
  20. data/examples/anti_corruption_layer/vendor/pay_friend/client.rb +11 -0
  21. data/examples/business_processes/README.md +245 -0
  22. data/examples/business_processes/Rakefile +50 -0
  23. data/examples/business_processes/config.rb +14 -0
  24. data/examples/business_processes/lib/division.rb +58 -0
  25. data/examples/design_by_contract/README.md +227 -0
  26. data/examples/design_by_contract/Rakefile +60 -0
  27. data/examples/design_by_contract/config.rb +13 -0
  28. data/examples/design_by_contract/lib/shopping_cart.rb +62 -0
  29. data/examples/ports_and_adapters/README.md +246 -0
  30. data/examples/ports_and_adapters/Rakefile +68 -0
  31. data/examples/ports_and_adapters/app/models/user/record/repository.rb +13 -0
  32. data/examples/ports_and_adapters/app/models/user/record.rb +7 -0
  33. data/examples/ports_and_adapters/config.rb +28 -0
  34. data/examples/ports_and_adapters/db/setup.rb +16 -0
  35. data/examples/ports_and_adapters/lib/user/creation.rb +19 -0
  36. data/examples/ports_and_adapters/lib/user/data.rb +5 -0
  37. data/examples/ports_and_adapters/lib/user/repository.rb +24 -0
  38. data/examples/ports_and_adapters/test/user_test/repository.rb +21 -0
  39. data/lib/bcdd/contract/assertions.rb +21 -0
  40. data/lib/bcdd/contract/config.rb +25 -0
  41. data/lib/bcdd/contract/core/checker.rb +37 -0
  42. data/lib/bcdd/contract/core/checking.rb +38 -0
  43. data/lib/bcdd/contract/core/factory.rb +32 -0
  44. data/lib/bcdd/contract/core/proxy.rb +19 -0
  45. data/lib/bcdd/contract/core.rb +12 -0
  46. data/lib/bcdd/contract/interface.rb +25 -0
  47. data/lib/bcdd/contract/list.rb +45 -0
  48. data/lib/bcdd/contract/map/pairs.rb +47 -0
  49. data/lib/bcdd/contract/map/schema.rb +50 -0
  50. data/lib/bcdd/contract/map.rb +10 -0
  51. data/lib/bcdd/contract/proxy.rb +40 -0
  52. data/lib/bcdd/contract/registry.rb +67 -0
  53. data/lib/bcdd/contract/unit/checker.rb +51 -0
  54. data/lib/bcdd/contract/unit/factory.rb +53 -0
  55. data/lib/bcdd/contract/unit.rb +40 -0
  56. data/lib/bcdd/contract/version.rb +7 -0
  57. data/lib/bcdd/contract.rb +118 -0
  58. data/lib/bcdd-contract.rb +3 -0
  59. data/sig/bcdd/contract/assertions.rbs +7 -0
  60. data/sig/bcdd/contract/config.rbs +15 -0
  61. data/sig/bcdd/contract/core.rbs +60 -0
  62. data/sig/bcdd/contract/interface.rbs +12 -0
  63. data/sig/bcdd/contract/list.rbs +21 -0
  64. data/sig/bcdd/contract/map.rbs +45 -0
  65. data/sig/bcdd/contract/proxy.rbs +8 -0
  66. data/sig/bcdd/contract/registry.rbs +25 -0
  67. data/sig/bcdd/contract/unit.rbs +39 -0
  68. data/sig/bcdd/contract.rbs +31 -0
  69. 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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module User
4
+ class Record < ActiveRecord::Base
5
+ self.table_name = 'users'
6
+ end
7
+ 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module User
4
+ Data = ::Struct.new(:id, :name, :email, keyword_init: true)
5
+ 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