solid-adapters 1.0.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 (39) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +3 -0
  3. data/.standard.yml +5 -0
  4. data/CHANGELOG.md +10 -0
  5. data/CODE_OF_CONDUCT.md +132 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +418 -0
  8. data/Rakefile +14 -0
  9. data/examples/README.md +15 -0
  10. data/examples/anti_corruption_layer/README.md +217 -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 +19 -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/ports_and_adapters/README.md +157 -0
  22. data/examples/ports_and_adapters/Rakefile +66 -0
  23. data/examples/ports_and_adapters/app/models/user/record/repository.rb +13 -0
  24. data/examples/ports_and_adapters/app/models/user/record.rb +7 -0
  25. data/examples/ports_and_adapters/config.rb +32 -0
  26. data/examples/ports_and_adapters/db/setup.rb +16 -0
  27. data/examples/ports_and_adapters/lib/user/creation.rb +19 -0
  28. data/examples/ports_and_adapters/lib/user/data.rb +5 -0
  29. data/examples/ports_and_adapters/lib/user/repository.rb +14 -0
  30. data/examples/ports_and_adapters/test/user_test/repository.rb +21 -0
  31. data/lib/solid/adapters/configurable/options.rb +44 -0
  32. data/lib/solid/adapters/configurable.rb +19 -0
  33. data/lib/solid/adapters/core/config.rb +35 -0
  34. data/lib/solid/adapters/core/proxy.rb +25 -0
  35. data/lib/solid/adapters/interface.rb +57 -0
  36. data/lib/solid/adapters/proxy.rb +11 -0
  37. data/lib/solid/adapters/version.rb +7 -0
  38. data/lib/solid/adapters.rb +28 -0
  39. metadata +85 -0
@@ -0,0 +1,217 @@
1
+ <small>
2
+
3
+ > `MENU` [README](../../README.md) | [Examples](../README.md)
4
+
5
+ </small>
6
+
7
+ ## 🛡️ Anti-Corruption Layer Example <!-- omit from toc -->
8
+
9
+ - [The ACL](#the-acl)
10
+ - [🤔 How does it work?](#-how-does-it-work)
11
+ - [📜 The Contract](#-the-contract)
12
+ - [🔄 The Adapters](#-the-adapters)
13
+ - [⚖️ What is the benefit of doing this?](#️-what-is-the-benefit-of-doing-this)
14
+ - [How much to do this (create ACL)?](#how-much-to-do-this-create-acl)
15
+ - [Is it worth the overhead of contract checking at runtime?](#is-it-worth-the-overhead-of-contract-checking-at-runtime)
16
+ - [🏃‍♂️ How to run the application?](#️-how-to-run-the-application)
17
+
18
+ The **Anti-Corruption Layer**, or ACL, is a pattern that isolates and protects a system from legacy or dependencies out of its control. It acts as a mediator, translating and adapting data between different components, ensuring they communicate without corrupting each other's data or logic.
19
+
20
+ To illustrate this pattern, let's see an example of an application that uses third-party API to charge a credit card.
21
+
22
+ Let's start seeing the code structure of this example:
23
+
24
+ ```
25
+ ├── Rakefile
26
+ ├── config.rb
27
+ ├── app
28
+ │ └── models
29
+ │ └── payment
30
+ │ └── charge_credit_card.rb
31
+ ├── lib
32
+ │ ├── payment_gateways
33
+ │ │ ├── adapters
34
+ │ │ │ ├── circle_up.rb
35
+ │ │ │ └── pay_friend.rb
36
+ │ │ ├── contract.rb
37
+ │ │ └── response.rb
38
+ │ └── payment_gateways.rb
39
+ └── vendor
40
+ ├── circle_up
41
+ │ └── client.rb
42
+ └── pay_friend
43
+ └── client.rb
44
+ ```
45
+
46
+ The files and directories are organized as follows:
47
+
48
+ - `Rakefile` runs the application.
49
+ - `config.rb` file contains the configurations.
50
+ - `app` directory contains the domain model where the business process to charge a credit card is implemented.
51
+ - `lib` directory contains the payment gateways contract and adapters.
52
+ - `vendor` directory contains the third-party API clients.
53
+
54
+ ## The ACL
55
+
56
+ The ACL is implemented in the `PaymentGateways` module (see `lib/payment_gateways.rb`). It translates the third-party APIs (see `vendor`) into something known by the application's domain model. Through this module, the application can charge a credit card without knowing the details/internals of the vendors.
57
+
58
+ ### 🤔 How does it work?
59
+
60
+ The `PaymentGateways::ChargeCreditCard` class (see `app/models/payment/charge_credit_card.rb`) uses`PaymentGateways::Contract` to ensure the `payment_gateway` object implements the required and known interface (input and output) to charge a credit card.
61
+
62
+ ```ruby
63
+ module Payment
64
+ class ChargeCreditCard
65
+ include ::Solid::Output.mixin(config: { addon: { continue: true } })
66
+
67
+ attr_reader :payment_gateway
68
+
69
+ def initialize(payment_gateway)
70
+ @payment_gateway = ::PaymentGateways::Contract.new(payment_gateway)
71
+ end
72
+
73
+ def call(amount:, details: {})
74
+ Given(amount:)
75
+ .and_then(:validate_amount)
76
+ .and_then(:charge_credit_card, details:)
77
+ .and_expose(:payment_charged, %i[payment_id])
78
+ end
79
+
80
+ private
81
+
82
+ def validate_amount(amount:)
83
+ return Continue() if amount.is_a?(::Numeric) && amount.positive?
84
+
85
+ Failure(:invalid_amount, erros: ['amount must be positive'])
86
+ end
87
+
88
+ def charge_credit_card(amount:, details:)
89
+ response = payment_gateway.charge_credit_card(amount:, details:)
90
+
91
+ Continue(payment_id: ::SecureRandom.uuid) if response.success?
92
+ end
93
+ end
94
+ end
95
+ ```
96
+
97
+ #### 📜 The Contract
98
+
99
+ The `PaymentGateways::Contract` defines the interface of the payment gateways. It is implemented by the `PaymentGateways::Adapters::CircleUp` and `PaymentGateways::Adapters::PayFriend` adapters.
100
+
101
+ ```ruby
102
+ module PaymentGateways
103
+ class Contract < ::Solid::Adapters::Proxy
104
+ def charge_credit_card(params)
105
+ params => { amount: Numeric, details: Hash }
106
+
107
+ outcome = object.charge_credit_card(params)
108
+
109
+ outcome => Response[true | false]
110
+
111
+ outcome
112
+ end
113
+ end
114
+ end
115
+ ```
116
+
117
+ In this case, the contract will ensure the input by using the `=>` pattern-matching operator, which will raise an exception if it does not match the expected types. After that, it calls the adapter's `charge_credit_card` method and ensures the output is a `PaymentGateways::Response` by using the `=>` operator again.
118
+
119
+ The response (see `lib/payment_gateways/response.rb`) will ensure the ACL, as it is the object known/exposed to the application.
120
+
121
+ ```ruby
122
+ module PaymentGateways
123
+ Response = ::Struct.new(:success?)
124
+ end
125
+ ```
126
+
127
+ #### 🔄 The Adapters
128
+
129
+ Let's see the payment gateways adapters:
130
+
131
+ `lib/payment_gateways/adapters/circle_up.rb`
132
+
133
+ ```ruby
134
+ module PaymentGateways
135
+ class Adapters::CircleUp
136
+ attr_reader :client
137
+
138
+ def initialize
139
+ @client = ::CircleUp::Client.new
140
+ end
141
+
142
+ def charge_credit_card(params)
143
+ params => { amount:, details: }
144
+
145
+ response = client.charge_cc(amount, details)
146
+
147
+ Response.new(response.ok?)
148
+ end
149
+ end
150
+ end
151
+ ```
152
+
153
+ `lib/payment_gateways/adapters/pay_friend.rb`
154
+
155
+ ```ruby
156
+ module PaymentGateways
157
+ class Adapters::PayFriend
158
+ attr_reader :client
159
+
160
+ def initialize
161
+ @client = ::PayFriend::Client.new
162
+ end
163
+
164
+ def charge_credit_card(params)
165
+ params => { amount:, details: }
166
+
167
+ response = client.charge(amount:, payment_data: details, payment_method: 'credit_card')
168
+
169
+ Response.new(response.status == 'success')
170
+ end
171
+ end
172
+ end
173
+ ```
174
+
175
+ You can see that each third-party API has its way of charging a credit card, so the adapters are responsible for translating the input/output from the third-party APIs to the output known by the application (the `PaymentGateways::Response`).
176
+
177
+ ## ⚖️ What is the benefit of doing this?
178
+
179
+ The benefit of doing this is that the core business logic is decoupled from the legacy/external dependencies, which makes it easier to test and promote changes in the code.
180
+
181
+ Using this example, if the third-party APIs change, we just need to implement a new adapter and make the business processes (`Payment::ChargeCreditCard`) use it. The business processes will not be affected as it is protected by the ACL.
182
+
183
+ ### How much to do this (create ACL)?
184
+
185
+ Use this pattern when there is a real need to decouple the core business logic from external dependencies.
186
+
187
+ You can start with a simple implementation (without ACL) and refactor it to use this pattern when the need arises.
188
+
189
+ ### Is it worth the overhead of contract checking at runtime?
190
+
191
+ You can eliminate the overhead by disabling the `Solid::Adapters::Proxy` class, which is a proxy that forwards all the method calls to the object it wraps.
192
+
193
+ When it is disabled, the `Solid::Adapters::Proxy.new` returns the given object so that the method calls are made directly to it.
194
+
195
+ To disable it, set the configuration to false:
196
+
197
+ ```ruby
198
+ Solid::Adapters.configuration do |config|
199
+ config.proxy_enabled = false
200
+ end
201
+ ```
202
+
203
+ ## 🏃‍♂️ How to run the application?
204
+
205
+ In the same directory as this `README`, run:
206
+
207
+ ```bash
208
+ rake
209
+
210
+ # -- CircleUp --
211
+ #
212
+ # #<Solid::Output::Success type=:payment_charged value={:payment_id=>"2df767d0-af83-4657-b28d-6605044ffe2c"}>
213
+ #
214
+ # -- PayFriend --
215
+ #
216
+ # #<Solid::Output::Success type=:payment_charged value={:payment_id=>"dd2af4cc-8484-4f6a-bc35-f7a5e6917ecc"}>
217
+ ```
@@ -0,0 +1,30 @@
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
+ task :default do
11
+ puts '====================='
12
+ puts 'Anti Corruption Layer'
13
+ puts '====================='
14
+
15
+ puts
16
+ puts '-- CircleUp --'
17
+ puts
18
+
19
+ circle_up_gateway = PaymentGateways::Adapters::CircleUp.new
20
+
21
+ p Payment::ChargeCreditCard.new(circle_up_gateway).call(amount: 100)
22
+
23
+ puts
24
+ puts '-- PayFriend --'
25
+ puts
26
+
27
+ pay_friend_gateway = PaymentGateways::Adapters::PayFriend.new
28
+
29
+ p Payment::ChargeCreditCard.new(pay_friend_gateway).call(amount: 200)
30
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Payment
6
+ class ChargeCreditCard
7
+ include ::Solid::Output.mixin(config: { addon: { continue: true } })
8
+
9
+ attr_reader :payment_gateway
10
+
11
+ def initialize(payment_gateway)
12
+ @payment_gateway = ::PaymentGateways::Contract.new(payment_gateway)
13
+ end
14
+
15
+ def call(amount:, details: {})
16
+ Given(amount:)
17
+ .and_then(:validate_amount)
18
+ .and_then(:charge_credit_card, details:)
19
+ .and_expose(:payment_charged, %i[payment_id])
20
+ end
21
+
22
+ private
23
+
24
+ def validate_amount(amount:)
25
+ return Continue() if amount.is_a?(::Numeric) && amount.positive?
26
+
27
+ Failure(:invalid_amount, erros: ['amount must be positive'])
28
+ end
29
+
30
+ def charge_credit_card(amount:, details:)
31
+ response = payment_gateway.charge_credit_card(amount:, details:)
32
+
33
+ Continue(payment_id: ::SecureRandom.uuid) if response.success?
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,19 @@
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 "solid-result", "~> 2.0"
11
+ gem "solid-adapters", path: "../../"
12
+ end
13
+
14
+ require "vendor/pay_friend/client"
15
+ require "vendor/circle_up/client"
16
+
17
+ require "lib/payment_gateways"
18
+
19
+ require "app/models/payment/charge_credit_card"
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaymentGateways
4
+ class Adapters::CircleUp
5
+ attr_reader :client
6
+
7
+ def initialize
8
+ @client = ::CircleUp::Client.new
9
+ end
10
+
11
+ def charge_credit_card(params)
12
+ params => { amount:, details: }
13
+
14
+ response = client.charge_cc(amount, details)
15
+
16
+ Response.new(response.ok?)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaymentGateways
4
+ class Adapters::PayFriend
5
+ attr_reader :client
6
+
7
+ def initialize
8
+ @client = ::PayFriend::Client.new
9
+ end
10
+
11
+ def charge_credit_card(params)
12
+ params => { amount:, details: }
13
+
14
+ response = client.charge(amount:, payment_data: details, payment_method: 'credit_card')
15
+
16
+ Response.new(response.status == 'success')
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaymentGateways
4
+ class Contract < ::Solid::Adapters::Proxy
5
+ def charge_credit_card(params)
6
+ params => { amount: Numeric, details: Hash }
7
+
8
+ outcome = object.charge_credit_card(params)
9
+
10
+ outcome => Response[true | false]
11
+
12
+ outcome
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaymentGateways
4
+ Response = ::Struct.new(:success?)
5
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaymentGateways
4
+ require_relative 'payment_gateways/contract'
5
+ require_relative 'payment_gateways/response'
6
+
7
+ module Adapters
8
+ require_relative 'payment_gateways/adapters/circle_up'
9
+ require_relative 'payment_gateways/adapters/pay_friend'
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CircleUp
4
+ class Client
5
+ Resp = ::Struct.new(:ok?)
6
+
7
+ def charge_cc(_amount, _credit_card_data)
8
+ Resp.new(true)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PayFriend
4
+ class Client
5
+ APIResponse = ::Struct.new(:status)
6
+
7
+ def charge(amount:, payment_method:, payment_data:)
8
+ APIResponse.new('success')
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,157 @@
1
+ <small>
2
+
3
+ > `MENU` [README](../../README.md) | [Examples](../README.md)
4
+
5
+ </small>
6
+
7
+ ## 🔌 Ports and Adapters Example <!-- omit from toc -->
8
+
9
+ - [⚖️ What is the benefit of doing this?](#️-what-is-the-benefit-of-doing-this)
10
+ - [How much to do this (create Ports and Adapters)?](#how-much-to-do-this-create-ports-and-adapters)
11
+ - [Is it worth the overhead of contract checking at runtime?](#is-it-worth-the-overhead-of-contract-checking-at-runtime)
12
+ - [🏃‍♂️ How to run the application?](#️-how-to-run-the-application)
13
+
14
+ Ports and Adapters is an architectural pattern that separates the application's core logic (Ports) from external dependencies (Adapters).
15
+
16
+ This example shows how to implement a simple application using this pattern and the gem `solid-adapters`.
17
+
18
+ Let's start seeing the code structure:
19
+
20
+ ```
21
+ ├── Rakefile
22
+ ├── config.rb
23
+ ├── db
24
+ ├── app
25
+ │ └── models
26
+ │ └── user
27
+ │ ├── record
28
+ │ │ └── repository.rb
29
+ │ └── record.rb
30
+ ├── lib
31
+ │ └── user
32
+ │ ├── creation.rb
33
+ │ ├── data.rb
34
+ │ └── repository.rb
35
+ └── test
36
+ └── user_test
37
+ └── repository.rb
38
+ ```
39
+
40
+ The files and directories are organized as follows:
41
+
42
+ - `Rakefile` runs the application.
43
+ - `config.rb` file contains the configuration of the application.
44
+ - `db` directory contains the database. It is not part of the application, but it is used by the application.
45
+ - `app` directory contains "Rails" components.
46
+ - `lib` directory contains the core business logic.
47
+ - `test` directory contains the tests.
48
+
49
+ The application is a simple "user management system". It unique core functionality is to create users.
50
+
51
+ Now we understand the code structure, let's see the how the pattern is implemented.
52
+
53
+ ### The Port
54
+
55
+ 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.
56
+
57
+ 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.
58
+
59
+ ```ruby
60
+ module User::Repository
61
+ include Solid::Adapters::Interface
62
+
63
+ module Methods
64
+ def create(name:, email:)
65
+ name => String
66
+ email => String
67
+
68
+ super.tap { _1 => ::User::Data[id: Integer, name: String, email: String] }
69
+ end
70
+ end
71
+ end
72
+ ```
73
+
74
+ ### The Adapters
75
+
76
+ The `User::Repository` is implemented by two adapters:
77
+
78
+ - `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).
79
+
80
+ - `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).
81
+
82
+ ## ⚖️ What is the benefit of doing this?
83
+
84
+ 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.
85
+
86
+ 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.
87
+
88
+ ### How much to do this (create Ports and Adapters)?
89
+
90
+ Use this pattern when there is a real need to decouple the core business logic from external dependencies.
91
+
92
+ You can start with a simple implementation (without Ports and Adapters) and refactor it to use this pattern when the need arises.
93
+
94
+ ### Is it worth the overhead of contract checking at runtime?
95
+
96
+ You can eliminate the overhead by disabling the `Solid::Adapters::Interface`, which is enabled by default.
97
+
98
+ When it is disabled, the `Solid::Adapters::Interface` won't prepend the interface methods module to the adapter, which means that the adapter won't be checked against the interface.
99
+
100
+ To disable it, set the configuration to false:
101
+
102
+ ```ruby
103
+ Solid::Adapters.configuration do |config|
104
+ config.interface_enabled = false
105
+ end
106
+ ```
107
+
108
+ ## 🏃‍♂️ How to run the application?
109
+
110
+ In the same directory as this `README`, run:
111
+
112
+ ```bash
113
+ rake # or rake SOLID_ADAPTERS_ENABLED=enabled
114
+
115
+ # or
116
+
117
+ rake SOLID_ADAPTERS_ENABLED=false
118
+ ```
119
+
120
+ **Proxy enabled**
121
+
122
+ ```bash
123
+ rake # or rake SOLID_ADAPTERS_ENABLED=enabled
124
+
125
+ # Output sample:
126
+ #
127
+ # -- Valid input --
128
+ #
129
+ # Created user: #<struct User::Data id=1, name="Jane", email="jane@foo.com">
130
+ # Created user: #<struct User::Data id=1, name="John", email="john@bar.com">
131
+ #
132
+ # -- Invalid input --
133
+ #
134
+ # rake aborted!
135
+ # NoMatchingPatternError: nil: String === nil does not return true (NoMatchingPatternError)
136
+ # /.../lib/user/repository.rb:9:in `create'
137
+ # /.../lib/user/creation.rb:12:in `call'
138
+ # /.../Rakefile:36:in `block in <top (required)>'
139
+ ```
140
+
141
+ **Proxy disabled**
142
+
143
+ ```bash
144
+ rake SOLID_ADAPTERS_ENABLED=false
145
+
146
+ # Output sample:
147
+ #
148
+ # -- Valid input --
149
+ #
150
+ # Created user: #<struct User::Data id=1, name="Jane", email="jane@foo.com">
151
+ # Created user: #<struct User::Data id=1, name="John", email="john@bar.com">
152
+ #
153
+ # -- Invalid input --
154
+ #
155
+ # Created user: #<struct User::Data id=2, name="Jane", email=nil>
156
+ # Created user: #<struct User::Data id=3, name="", email=nil>
157
+ ```
@@ -0,0 +1,66 @@
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: nil)
37
+
38
+ memory_creation.call(name: "", email: nil)
39
+ end
40
+
41
+ # Output sample: rake SOLID_ADAPTERS_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
+ # NoMatchingPatternError: nil: String === nil does not return true (NoMatchingPatternError)
52
+ # /.../lib/user/repository.rb:9:in `create'
53
+ # /.../lib/user/creation.rb:12:in `call'
54
+ # /.../Rakefile:36:in `block in <top (required)>'
55
+
56
+ # Output sample: rake SOLID_ADAPTERS_ENABLED=false
57
+ #
58
+ # -- Valid input --
59
+ #
60
+ # Created user: #<struct User::Data id=1, name="Jane", email="jane@foo.com">
61
+ # Created user: #<struct User::Data id=1, name="John", email="john@bar.com">
62
+ #
63
+ # -- Invalid input --
64
+ #
65
+ # Created user: #<struct User::Data id=2, name="Jane", email=nil>
66
+ # 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,32 @@
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 "solid-adapters", path: "../../"
13
+ end
14
+
15
+ require "active_support/all"
16
+
17
+ require "db/setup"
18
+
19
+ ::Solid::Adapters.configuration do |config|
20
+ enabled = ENV.fetch("SOLID_ADAPTERS_ENABLED", "true") != "false"
21
+
22
+ config.interface_enabled = enabled
23
+ end
24
+
25
+ module User
26
+ require "lib/user/data"
27
+ require "lib/user/repository"
28
+ require "lib/user/creation"
29
+ end
30
+
31
+ require "app/models/user/record"
32
+ require "app/models/user/record/repository"