clean-architecture 2.0.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/rspec.yml +21 -0
- data/.gitignore +1 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +42 -0
- data/Gemfile +1 -0
- data/README.md +423 -4
- data/clean-architecture.gemspec +9 -5
- data/generate_require_files.rb +1 -0
- data/lib/clean-architecture.rb +1 -0
- data/lib/clean_architecture/adapters/all.rb +1 -0
- data/lib/clean_architecture/adapters/attribute_hash_base.rb +1 -0
- data/lib/clean_architecture/all.rb +3 -0
- data/lib/clean_architecture/builders/abstract_active_record_entity_builder.rb +124 -0
- data/lib/clean_architecture/builders/all.rb +6 -0
- data/lib/clean_architecture/checks/all.rb +1 -0
- data/lib/clean_architecture/checks/authorization.rb +1 -0
- data/lib/clean_architecture/entities/all.rb +1 -0
- data/lib/clean_architecture/entities/failure_details.rb +2 -1
- data/lib/clean_architecture/entities/targeted_parameters.rb +1 -0
- data/lib/clean_architecture/entities/untargeted_parameters.rb +1 -0
- data/lib/clean_architecture/interfaces/all.rb +1 -0
- data/lib/clean_architecture/interfaces/authorization_parameters.rb +1 -0
- data/lib/clean_architecture/interfaces/base_parameters.rb +1 -0
- data/lib/clean_architecture/interfaces/jsonable.rb +1 -0
- data/lib/clean_architecture/interfaces/success_payload.rb +1 -0
- data/lib/clean_architecture/interfaces/targeted_parameters.rb +1 -0
- data/lib/clean_architecture/interfaces/use_case.rb +1 -0
- data/lib/clean_architecture/interfaces/use_case_actor.rb +1 -0
- data/lib/clean_architecture/interfaces/use_case_target.rb +1 -0
- data/lib/clean_architecture/matchers/all.rb +1 -0
- data/lib/clean_architecture/matchers/use_case_result.rb +1 -0
- data/lib/clean_architecture/queries/all.rb +1 -0
- data/lib/clean_architecture/queries/http_failure_code.rb +1 -0
- data/lib/clean_architecture/queries/http_success_code.rb +1 -0
- data/lib/clean_architecture/serializers/all.rb +1 -0
- data/lib/clean_architecture/serializers/html_response_from_result.rb +1 -0
- data/lib/clean_architecture/serializers/json_response_from_result.rb +1 -0
- data/lib/clean_architecture/serializers/success_collection_payload.rb +1 -0
- data/lib/clean_architecture/serializers/success_payload.rb +1 -0
- data/lib/clean_architecture/types.rb +2 -1
- data/lib/clean_architecture/use_cases/abstract_use_case.rb +62 -0
- data/lib/clean_architecture/use_cases/all.rb +10 -0
- data/lib/clean_architecture/use_cases/contract.rb +9 -0
- data/lib/clean_architecture/use_cases/errors.rb +57 -0
- data/lib/clean_architecture/use_cases/form.rb +116 -0
- data/lib/clean_architecture/use_cases/parameters.rb +42 -0
- data/lib/clean_architecture/version.rb +2 -1
- data/sorbet/config +2 -0
- data/sorbet/rbi/gems/activemodel.rbi +74 -0
- data/sorbet/rbi/gems/activesupport.rbi +440 -0
- data/sorbet/rbi/gems/ast.rbi +47 -0
- data/sorbet/rbi/gems/axiom-types.rbi +159 -0
- data/sorbet/rbi/gems/byebug.rbi +1039 -0
- data/sorbet/rbi/gems/codeclimate-engine-rb.rbi +123 -0
- data/sorbet/rbi/gems/coderay.rbi +91 -0
- data/sorbet/rbi/gems/coercible.rbi +156 -0
- data/sorbet/rbi/gems/concurrent-ruby.rbi +1587 -0
- data/sorbet/rbi/gems/descendants_tracker.rbi +17 -0
- data/sorbet/rbi/gems/docile.rbi +31 -0
- data/sorbet/rbi/gems/dry-configurable.rbi +89 -0
- data/sorbet/rbi/gems/dry-container.rbi +88 -0
- data/sorbet/rbi/gems/dry-core.rbi +79 -0
- data/sorbet/rbi/gems/dry-equalizer.rbi +25 -0
- data/sorbet/rbi/gems/dry-inflector.rbi +72 -0
- data/sorbet/rbi/gems/dry-initializer.rbi +209 -0
- data/sorbet/rbi/gems/dry-logic.rbi +304 -0
- data/sorbet/rbi/gems/dry-matcher.rbi +33 -0
- data/sorbet/rbi/gems/dry-monads.rbi +508 -0
- data/sorbet/rbi/gems/dry-schema.rbi +790 -0
- data/sorbet/rbi/gems/dry-struct.rbi +165 -0
- data/sorbet/rbi/gems/dry-types.rbi +688 -0
- data/sorbet/rbi/gems/dry-validation.rbi +284 -0
- data/sorbet/rbi/gems/duckface-interfaces.rbi +93 -0
- data/sorbet/rbi/gems/equalizer.rbi +22 -0
- data/sorbet/rbi/gems/i18n.rbi +132 -0
- data/sorbet/rbi/gems/ice_nine.rbi +66 -0
- data/sorbet/rbi/gems/jaro_winkler.rbi +14 -0
- data/sorbet/rbi/gems/kwalify.rbi +339 -0
- data/sorbet/rbi/gems/method_source.rbi +63 -0
- data/sorbet/rbi/gems/parallel.rbi +81 -0
- data/sorbet/rbi/gems/parser.rbi +1293 -0
- data/sorbet/rbi/gems/pry-byebug.rbi +149 -0
- data/sorbet/rbi/gems/pry.rbi +1964 -0
- data/sorbet/rbi/gems/psych.rbi +462 -0
- data/sorbet/rbi/gems/rainbow.rbi +117 -0
- data/sorbet/rbi/gems/rake.rbi +634 -0
- data/sorbet/rbi/gems/rb-readline.rbi +766 -0
- data/sorbet/rbi/gems/reek.rbi +1066 -0
- data/sorbet/rbi/gems/rspec-core.rbi +1658 -0
- data/sorbet/rbi/gems/rspec-expectations.rbi +430 -0
- data/sorbet/rbi/gems/rspec-mocks.rbi +815 -0
- data/sorbet/rbi/gems/rspec-support.rbi +268 -0
- data/sorbet/rbi/gems/rspec.rbi +14 -0
- data/sorbet/rbi/gems/rubocop-rspec.rbi +875 -0
- data/sorbet/rbi/gems/rubocop.rbi +7014 -0
- data/sorbet/rbi/gems/ruby-progressbar.rbi +304 -0
- data/sorbet/rbi/gems/simplecov-html.rbi +30 -0
- data/sorbet/rbi/gems/simplecov.rbi +225 -0
- data/sorbet/rbi/gems/stackprof.rbi +51 -0
- data/sorbet/rbi/gems/thread_safe.rbi +81 -0
- data/sorbet/rbi/gems/timecop.rbi +97 -0
- data/sorbet/rbi/gems/unicode-display_width.rbi +16 -0
- data/sorbet/rbi/gems/virtus.rbi +421 -0
- data/sorbet/rbi/hidden-definitions/errors.txt +7332 -0
- data/sorbet/rbi/hidden-definitions/hidden.rbi +17521 -0
- data/sorbet/rbi/sorbet-typed/lib/activemodel/all/activemodel.rbi +422 -0
- data/sorbet/rbi/sorbet-typed/lib/activesupport/>=6.0.0.rc1/activesupport.rbi +23 -0
- data/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +625 -0
- data/sorbet/rbi/sorbet-typed/lib/bundler/all/bundler.rbi +8684 -0
- data/sorbet/rbi/sorbet-typed/lib/minitest/all/minitest.rbi +99 -0
- data/sorbet/rbi/sorbet-typed/lib/rainbow/all/rainbow.rbi +254 -0
- data/sorbet/rbi/sorbet-typed/lib/ruby/all/gem.rbi +4222 -0
- data/sorbet/rbi/sorbet-typed/lib/ruby/all/open3.rbi +111 -0
- data/sorbet/rbi/sorbet-typed/lib/ruby/all/resolv.rbi +543 -0
- data/sorbet/rbi/todo.rbi +12 -0
- metadata +156 -24
- data/Gemfile.lock +0 -187
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 79fccd9c446b769e45c8e7c4a6f378789cd33ae10a299e7966186c6b8f8161e1
|
4
|
+
data.tar.gz: 566434a7e22887b2228811569bb95c4dd6e252453bec836860f5437584c8850e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 121377ec84ca3bc904248256b38022d206edb19e149c025be35d66296df48ac9fd4f54ee1c1924d52140a07533bd5384f9d0a2825b897a5b603f9c51414bca6e
|
7
|
+
data.tar.gz: 4e5cb96519180360d657833b2c853e41023bf363d249e3eaf2468cf72f1fcccc541706602dfe251838cae46cf39a1addea9a99c6e21812c06e3d81255f3b0003
|
@@ -0,0 +1,21 @@
|
|
1
|
+
name: RSpec Test Suite
|
2
|
+
|
3
|
+
on: [push]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
build:
|
7
|
+
runs-on: ubuntu-latest
|
8
|
+
steps:
|
9
|
+
- name: Checkout branch
|
10
|
+
uses: actions/checkout@v1
|
11
|
+
- name: Set up Ruby
|
12
|
+
uses: actions/setup-ruby@v1
|
13
|
+
with:
|
14
|
+
ruby-version: 2.5.5
|
15
|
+
- name: Install system packages
|
16
|
+
run: |
|
17
|
+
command -v bundler || gem install bundler
|
18
|
+
- name: Install gems
|
19
|
+
run: bundle install --jobs $(nproc) --retry 3
|
20
|
+
- name: Run RSpec test suite
|
21
|
+
run: bundle exec rspec spec
|
data/.gitignore
CHANGED
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.5.1
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,45 @@
|
|
1
|
+
3.0.0
|
2
|
+
|
3
|
+
* Add sorbet and support for Sorbet T::Struct
|
4
|
+
|
5
|
+
2.6.1
|
6
|
+
|
7
|
+
* Update dependency on dry-validation
|
8
|
+
|
9
|
+
2.6.0
|
10
|
+
|
11
|
+
* Bring `UseCases::AbstractUseCase` into line with dry-validation 1.0 & co.
|
12
|
+
* Use cases now have a 'contract', params act as per normal
|
13
|
+
* Predicates have been replaced with macros, they can still be shared
|
14
|
+
* Context variables passed into the use case parameters are accessible within the use case as `context(:my_context)`
|
15
|
+
* There is no need to redefine `initialize` in use cases, you can access context with `#context` and params via `#result_of_validating_params`
|
16
|
+
|
17
|
+
2.5.0
|
18
|
+
|
19
|
+
* Remove restrictions on dry-rb gem
|
20
|
+
|
21
|
+
2.4.0
|
22
|
+
|
23
|
+
* Introduce `UseCases::AbstractUseCase` to make it easier to create use cases with built in
|
24
|
+
validation.
|
25
|
+
* Add `UseCases::Form` to help map HTTP parameters to a use case's parameter object.
|
26
|
+
|
27
|
+
2.3.1
|
28
|
+
|
29
|
+
* Fixed an issue with optional belongs_to relations in `AbstractActiveRecordEntityBuilder`
|
30
|
+
|
31
|
+
2.3.0
|
32
|
+
|
33
|
+
* `AbstractActiveRecordEntityBuilder` now has a DSL for streamlining the use of builders for `has_many` and `belongs_to` relations.
|
34
|
+
|
35
|
+
2.2.0
|
36
|
+
|
37
|
+
* Made `AbstractActiveRecordEntityBuilder` work with dry-struct >= 0.0.6.
|
38
|
+
|
39
|
+
2.1.0
|
40
|
+
|
41
|
+
* Added `AbstractActiveRecordEntityBuilder` to help map ActiveRecord instances to `Dry::Struct` based entities.
|
42
|
+
|
1
43
|
2.0.0
|
2
44
|
|
3
45
|
* Support collections for use case targets and success payloads
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -3,6 +3,33 @@
|
|
3
3
|
This gem provides helper interfaces and classes to assist in the construction of application with
|
4
4
|
Clean Architecture, as described in [Robert Martin's seminal book](https://www.amazon.com/gp/product/0134494164).
|
5
5
|
|
6
|
+
Table of Contents
|
7
|
+
=================
|
8
|
+
|
9
|
+
Generated by https://github.com/ekalinin/github-markdown-toc/blob/master/gh-md-toc
|
10
|
+
|
11
|
+
* [Installation](#installation)
|
12
|
+
* [Philosophy](#philosophy)
|
13
|
+
* [Screaming architecture - use cases as an organisational principle](#screaming-architecture---use-cases-as-an-organisational-principle)
|
14
|
+
* [Design principles](#design-principles)
|
15
|
+
* [SRP - The Single Responsibility principle](#srp---the-single-responsibility-principle)
|
16
|
+
* [OCP - The Open/Closed Principle, LSP - The Liskov Substitution Principle and DIP - The Dependency Inversion Principle](#ocp---the-openclosed-principle-lsp---the-liskov-substitution-principle-and-dip---the-dependency-inversion-principle)
|
17
|
+
* [ISP - The Interface Segregation Principle](#isp---the-interface-segregation-principle)
|
18
|
+
* [Component cohesion](#component-cohesion)
|
19
|
+
* [REP - The Reuse/Release Equivalence Principle, CCP - The Common Closure Principle & CRP - The Common Reuse Principle](#rep---the-reuserelease-equivalence-principle-ccp---the-common-closure-principle--crp---the-common-reuse-principle)
|
20
|
+
* [Component coupling](#component-coupling)
|
21
|
+
* [ADP - The Acyclic Dependencies Principle](#adp---the-acyclic-dependencies-principle)
|
22
|
+
* [SDP - The Stable Dependencies Principle](#sdp---the-stable-dependencies-principle)
|
23
|
+
* [SAP - The Stable Abstractions Principle](#sap---the-stable-abstractions-principle)
|
24
|
+
* [Structure](#structure)
|
25
|
+
* [Practical suggestions for implementation](#practical-suggestions-for-implementation)
|
26
|
+
* [Conventions](#conventions)
|
27
|
+
* [Result objects](#result-objects)
|
28
|
+
* [Idiomatic FP](#idiomatic-fp)
|
29
|
+
* [Multiple bind operations](#multiple-bind-operations)
|
30
|
+
* [Transactions](#transactions)
|
31
|
+
* [Helper classes](#helper-classes)
|
32
|
+
|
6
33
|
## Installation
|
7
34
|
|
8
35
|
Add this line to your application's Gemfile:
|
@@ -16,15 +43,13 @@ And then execute:
|
|
16
43
|
$ bundle install
|
17
44
|
$ bundle binstubs clean-architecture
|
18
45
|
|
19
|
-
##
|
46
|
+
## Philosophy
|
20
47
|
|
21
48
|
The intention of this gem is to help you build applications that are built from the use case down,
|
22
49
|
and decisions about I/O can be deferred until the last possible moment. It relies heavily on the
|
23
50
|
[duckface-interfaces](https://github.com/samuelgiles/duckface) gem to enforce interface
|
24
51
|
implementation.
|
25
52
|
|
26
|
-
## Clean architecture principles
|
27
|
-
|
28
53
|
### Screaming architecture - use cases as an organisational principle
|
29
54
|
|
30
55
|
Uncle Bob suggests that your source code organisation should allow developers to easily find a listing of all use cases your application provides. Here's an example of how this might look in a
|
@@ -128,7 +153,9 @@ We satisfy the SAP by:
|
|
128
153
|
|
129
154
|
- Thinking hard about the methods and parameters we specify in our interfaces. Are they solving for a general problem? Are we likely to have to change them when requirements change, and how we can avoid that?
|
130
155
|
|
131
|
-
##
|
156
|
+
## Structure
|
157
|
+
|
158
|
+
### Practical suggestions for implementation
|
132
159
|
|
133
160
|
* The code that manages your inputs (e.g. a Rails controller) instantiates a persistence layer
|
134
161
|
object
|
@@ -166,3 +193,395 @@ We satisfy the SAP by:
|
|
166
193
|
```
|
167
194
|
use_case = MyBankingApplication::UseCases::RetailCustomerMakesADeposit.new(input_port)
|
168
195
|
```
|
196
|
+
|
197
|
+
## Conventions
|
198
|
+
|
199
|
+
### Result objects
|
200
|
+
|
201
|
+
We make use of the [Dry-Rb](https://dry-rb.org/) collection of Gems to
|
202
|
+
provide better control flow instead of relying on `raise` and `rescue`.
|
203
|
+
Specifically, we use:
|
204
|
+
|
205
|
+
- https://dry-rb.org/gems/dry-matcher/result-matcher/
|
206
|
+
- https://dry-rb.org/gems/dry-monads/1.0/result/
|
207
|
+
|
208
|
+
### Idiomatic FP
|
209
|
+
|
210
|
+
#### Multiple `bind` operations
|
211
|
+
|
212
|
+
When you want to bind or chain multiple method calls using the
|
213
|
+
previous return value, consider using [Do
|
214
|
+
Notation](https://dry-rb.org/gems/dry-monads/1.0/do-notation/)
|
215
|
+
|
216
|
+
This is inspired by the Haskell do-notation which lets you go from
|
217
|
+
writing this:
|
218
|
+
|
219
|
+
```haskell
|
220
|
+
action1
|
221
|
+
>>=
|
222
|
+
(\ x1 -> action2
|
223
|
+
>>=
|
224
|
+
(\ x2 -> mk_action3 x1 x2 ))
|
225
|
+
```
|
226
|
+
|
227
|
+
to this:
|
228
|
+
|
229
|
+
```haskell
|
230
|
+
do
|
231
|
+
x1 <- action1
|
232
|
+
x2 <- action2
|
233
|
+
mk_action3 x1 x2
|
234
|
+
```
|
235
|
+
|
236
|
+
#### Transactions
|
237
|
+
|
238
|
+
If you don't want to manually handle the wiring between multiple
|
239
|
+
Success/Failure objects, you can use the
|
240
|
+
[dry-transaction](https://dry-rb.org/gems/dry-transaction/) gem which
|
241
|
+
abstracts this away so that you just need to define steps, and deal with
|
242
|
+
the input from the output of the previous result.
|
243
|
+
|
244
|
+
```ruby
|
245
|
+
require "dry/transaction"
|
246
|
+
|
247
|
+
class CreateUser
|
248
|
+
include Dry::Transaction
|
249
|
+
|
250
|
+
step :validate
|
251
|
+
step :create
|
252
|
+
|
253
|
+
private
|
254
|
+
|
255
|
+
def validate(input)
|
256
|
+
# returns Success(valid_data) or Failure(validation)
|
257
|
+
end
|
258
|
+
|
259
|
+
def create(input)
|
260
|
+
# returns Success(user)
|
261
|
+
end
|
262
|
+
end
|
263
|
+
```
|
264
|
+
|
265
|
+
# Helper classes
|
266
|
+
|
267
|
+
The gem comes with some useful classes that can help you achieve a cleaner architecture with less work.
|
268
|
+
|
269
|
+
## Active Record Entity Builder
|
270
|
+
|
271
|
+
Maintain a separation between your business entities and your database requires the use of gateways that build your entities from records in the database.
|
272
|
+
|
273
|
+
For Rails applications using ActiveRecord this can involve a bunch of boilerplate code where your simply creating a hash from the attributes of the database record & using those to create a new instance of your struct based entity.
|
274
|
+
|
275
|
+
The `CleanArchitecture::Builders::AbstractActiveRecordEntityBuilder` can help remove this boilerplate by handling 99% of the mapping for you.
|
276
|
+
|
277
|
+
### Usage:
|
278
|
+
|
279
|
+
Create a builder class and have it inherit from `CleanArchitecture::Builders::AbstractActiveRecordEntityBuilder`, from here you need to point the builder at the entity you wish for it to create instances of with `.acts_as_builder_for_entity`, from there its just a case of instantiating the builder with an instance of your AR model and calling `#build`.
|
280
|
+
|
281
|
+
Relations are handled easily, just define a builder for said entity and then declare the relation with `has_many :relation_name, use: MyBuilderClass` and `belongs_to :relation_name, use: MyBuilderClass`.
|
282
|
+
|
283
|
+
If you wish to override the attributes used to construct the entity you can define a `#attributes_for_entity` method with said attributes in a hash, this can be useful for complex relations, files and other attributes that don't map perfectly from the database to your struct based entity.
|
284
|
+
|
285
|
+
```ruby
|
286
|
+
class Person < ApplicationRecord
|
287
|
+
has_many :interests, autosave: true, dependent: :destroy
|
288
|
+
belongs_to :father
|
289
|
+
end
|
290
|
+
|
291
|
+
class Entities::Person < Dry::Struct
|
292
|
+
attribute :forename, Types::Strict::String
|
293
|
+
attribute :surname, Types::Strict::String
|
294
|
+
attribute :father, Types.Instance(Person)
|
295
|
+
attribute :interests, Types.Array(Types.Instance(Interest))
|
296
|
+
attribute :birth_month, Types::Strict::String
|
297
|
+
end
|
298
|
+
|
299
|
+
class PersonBuilder < CleanArchitecture::Builders::AbstractActiveRecordEntityBuilder
|
300
|
+
acts_as_builder_for_entity Entities::Person
|
301
|
+
|
302
|
+
has_many :interests, use: InterestBuilder
|
303
|
+
belongs_to :father, use: PersonBuilder
|
304
|
+
|
305
|
+
def attributes_for_entity
|
306
|
+
{ birth_month: @ar_model_instance.birth_date.month }
|
307
|
+
end
|
308
|
+
end
|
309
|
+
```
|
310
|
+
|
311
|
+
## Use cases with contracts, errors & form objects
|
312
|
+
|
313
|
+
Finding a way to map HTTP parameters to parameters within your use case, pass back & display validation errors and coerce types are difficult to replace when moving away from the typical `MyMode.update(params.permit(:some_param))` that a standard Rails app might use.
|
314
|
+
|
315
|
+
The `CleanArchitecture::UseCases` component contains some useful classes for helping to make replacing these functions a little easier whilst still maintaining good boundaries.
|
316
|
+
|
317
|
+
The 'contracts' use `dry-validation` and support all options included the sharing of contracts between use cases, more information can be found here: https://dry-rb.org/gems/dry-validation/. Don't be afraid of the seemingly magical `.contract` method that use cases have, all its doing is creating an anonymous `Class` and storing it in a class variable, the methods existence is justified by how it enables form objects & helps to standardise the process a little.
|
318
|
+
|
319
|
+
`dry-validation` itself is actually built on top of `dry-schema`, as such most of the useful information on predicates can be found here: https://dry-rb.org/gems/dry-schema/basics/built-in-predicates/
|
320
|
+
|
321
|
+
### Usage:
|
322
|
+
|
323
|
+
Usage is fairly simple, use cases define a contract, parameters handed to a use case are validated, at which point if the parameters aren't valid you'll get an `Errors` object back within a `Failure`, if they are you'll get a success with a `Parameters`.
|
324
|
+
|
325
|
+
Here is an example use case for a user updating their username that does a pre-flight check to ensure the username is available:
|
326
|
+
|
327
|
+
```ruby
|
328
|
+
module MyBusinessDomain
|
329
|
+
module UseCases
|
330
|
+
class UserUpdatesNickname < CleanArchitecture::UseCases::AbstractUseCase
|
331
|
+
contract do
|
332
|
+
option :my_persistence_object
|
333
|
+
|
334
|
+
params do
|
335
|
+
required(:user_id).filled(:id)
|
336
|
+
required(:nickname).filled(:str)
|
337
|
+
end
|
338
|
+
|
339
|
+
rule(:nickname).validate(:not_already_taken)
|
340
|
+
|
341
|
+
register_macro(:not_already_taken) do
|
342
|
+
unless my_persistence_object.username_is_available?(values[key_name])
|
343
|
+
key.failure('is already taken')
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
extend Forwardable
|
349
|
+
include Dry::Monads::Do.for(:result)
|
350
|
+
|
351
|
+
def result
|
352
|
+
valid_params = yield result_of_validating_params
|
353
|
+
context(:my_persistence_object).result_of_updating_nickname(
|
354
|
+
valid_params[:id],
|
355
|
+
valid_params[:nickname]
|
356
|
+
)
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
```
|
362
|
+
|
363
|
+
You could imagine a page with a simple form asking the user to enter their new username and you may want this form to display that message if the username isn't available. The `Form` class can be used to assist with the mapping of http parameters to the use case parameters. Best of all since the forms aren't tied to the use cases they can live within your web app far away from your business logic.
|
364
|
+
|
365
|
+
```ruby
|
366
|
+
module MyWebApp
|
367
|
+
class NicknameUpdateForm < CleanArchitecture::UseCases::Form
|
368
|
+
acts_as_form_for MyBusinessDomain::UseCases::UserUpdatesNickname
|
369
|
+
end
|
370
|
+
end
|
371
|
+
```
|
372
|
+
|
373
|
+
The standard Rails form builder works with instances of `Form`.
|
374
|
+
|
375
|
+
Putting these both together a controller action would look like the below example.
|
376
|
+
|
377
|
+
- A new instance of the use case is passed a parameter object built from `params`.
|
378
|
+
- If the use case is successful we'll show a flash message.
|
379
|
+
- If unsuccessful we'll take the returned `Errors` (`Entities::FailureDetails` and plain strings are also handled by `#with_errors`) and add them to the form with `#with_errors` and re-render the `edit` action.
|
380
|
+
|
381
|
+
```ruby
|
382
|
+
module MyWebApp
|
383
|
+
class NicknamesController < ApplicationController
|
384
|
+
def update
|
385
|
+
Dry::Matcher::ResultMatcher.call(user_updates_nickname.result) do |matcher|
|
386
|
+
matcher.success do |_|
|
387
|
+
flash[:success] = 'Nickname successfully updated'
|
388
|
+
redirect_to action: :edit
|
389
|
+
end
|
390
|
+
|
391
|
+
matcher.failure do |errors|
|
392
|
+
@form = nickname_update_form.with_errors(errors)
|
393
|
+
render :edit
|
394
|
+
end
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
private
|
399
|
+
|
400
|
+
def user_updates_nickname
|
401
|
+
MyBusinessDomain::UseCases::UserUpdatesNickname.new(nickname_update_form.to_parameter_object)
|
402
|
+
end
|
403
|
+
|
404
|
+
def nickname_update_form
|
405
|
+
@nickname_update_form ||= NicknameUpdateForm.new(
|
406
|
+
params: params.permit(:user_id, :nickname),
|
407
|
+
context: { my_persistence_object: MyPersistence.new }
|
408
|
+
)
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|
412
|
+
```
|
413
|
+
|
414
|
+
There won't always be a complex form in front of a use case, sometimes its just one parameter, using the above example example you could easily execute the use case with a manually constructed parameter object if it was say an API only endpoint:
|
415
|
+
|
416
|
+
```ruby
|
417
|
+
module MyWebApp
|
418
|
+
class NicknamesController < ApplicationController
|
419
|
+
def update
|
420
|
+
Dry::Matcher::ResultMatcher.call(user_updates_nickname.result) do |matcher|
|
421
|
+
matcher.success do |_|
|
422
|
+
render json: { success: true }
|
423
|
+
end
|
424
|
+
|
425
|
+
matcher.failure do |errors|
|
426
|
+
render json: { errors: errors.full_messages }
|
427
|
+
end
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
private
|
432
|
+
|
433
|
+
def user_updates_nickname
|
434
|
+
MyBusinessDomain::UseCases::UserUpdatesNickname.new(user_updates_nickname_parameters)
|
435
|
+
end
|
436
|
+
|
437
|
+
def user_updates_nickname_parameters
|
438
|
+
MyBusinessDomain::UseCases::UserUpdatesNickname.parameters(
|
439
|
+
context: { my_persistence_object: MyPersistence.new },
|
440
|
+
user_id: params[:user_id],
|
441
|
+
nickname: params[:nickname]
|
442
|
+
)
|
443
|
+
end
|
444
|
+
end
|
445
|
+
end
|
446
|
+
```
|
447
|
+
|
448
|
+
Elements of contracts can be shared amongst use cases, this can be very helpful for `options` (context) that you know every use case in a domain may require or validation rules that you know will be used in multiple use cases. Shared contracts can help tidy up your specs too by allowing you to test all your validation logic separately to what the use case itself does.
|
449
|
+
|
450
|
+
```ruby
|
451
|
+
module MyBusinessDomain
|
452
|
+
module UseCases
|
453
|
+
class SharedContract < CleanArchitecture::UseCases::Contract
|
454
|
+
option :my_persistence_object
|
455
|
+
|
456
|
+
register_macro(:not_already_taken?) do
|
457
|
+
unless not_already_taken?(values[key_name])
|
458
|
+
key.failure('is already taken')
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
private
|
463
|
+
|
464
|
+
def not_already_taken?(username)
|
465
|
+
my_persistence_object.username_is_available?(values[key_name])
|
466
|
+
end
|
467
|
+
end
|
468
|
+
end
|
469
|
+
end
|
470
|
+
```
|
471
|
+
|
472
|
+
Using a shared contract is simple; when you define the contract for a use case just specify the shared contract as an argument to `.contract`:
|
473
|
+
|
474
|
+
```ruby
|
475
|
+
module MyBusinessDomain
|
476
|
+
module UseCases
|
477
|
+
class UserUpdatesNickname < CleanArchitecture::UseCases::AbstractUseCase
|
478
|
+
contract(SharedContract) do
|
479
|
+
option :my_persistence_object
|
480
|
+
|
481
|
+
params do
|
482
|
+
required(:user_id).filled(:id)
|
483
|
+
required(:nickname).filled(:str)
|
484
|
+
end
|
485
|
+
|
486
|
+
rule(:nickname).validate(:not_already_taken)
|
487
|
+
```
|
488
|
+
|
489
|
+
Use cases themselves are outside of their params just plain old ruby objects. There are only a few methods you'll use composing use cases:
|
490
|
+
|
491
|
+
### `#result_of_validating_params`
|
492
|
+
|
493
|
+
This methods gives you a Result monad with either `Success` containing a hash of the valid params or `Failure` with an `Errors` instance containing the validation errors. The `Do` syntax from `dry-monads` helps to tidy the usage of this method up:
|
494
|
+
|
495
|
+
```ruby
|
496
|
+
module MyBusinessDomain
|
497
|
+
module UseCases
|
498
|
+
class UserUpdatesAge < CleanArchitecture::UseCases::AbstractUseCase
|
499
|
+
contract do
|
500
|
+
params do
|
501
|
+
required(:user_id).filled(:int)
|
502
|
+
required(:age).filled(:int)
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
include Dry::Monads::Do.for(:result)
|
507
|
+
|
508
|
+
def result
|
509
|
+
valid_params = yield result_of_validating_params
|
510
|
+
|
511
|
+
Dry::Monads::Success(valid_params[:age] * 365)
|
512
|
+
end
|
513
|
+
end
|
514
|
+
end
|
515
|
+
end
|
516
|
+
```
|
517
|
+
|
518
|
+
### `#context`
|
519
|
+
|
520
|
+
Any context variables defined as `option`'s in your use case contract have to be specified whenever creating an instance of the parameter objects for your use case. In practice this means you can't accidentally forget to pass in say a persistence object / repository / factory / etc.
|
521
|
+
|
522
|
+
These context variables can be used within the use case using the `context` method:
|
523
|
+
|
524
|
+
```ruby
|
525
|
+
module MyBusinessDomain
|
526
|
+
module UseCases
|
527
|
+
class UserUpdatesAge < CleanArchitecture::UseCases::AbstractUseCase
|
528
|
+
contract do
|
529
|
+
option :required_persistence_object
|
530
|
+
|
531
|
+
params do
|
532
|
+
required(:user_id).filled(:int)
|
533
|
+
required(:age).filled(:int)
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|
537
|
+
include Dry::Monads::Do.for(:result)
|
538
|
+
|
539
|
+
def result
|
540
|
+
valid_params = yield result_of_validating_params
|
541
|
+
|
542
|
+
context(:required_persistence_object).update_user_age_result(
|
543
|
+
valid_params[:user_id],
|
544
|
+
valid_params[:age]
|
545
|
+
)
|
546
|
+
end
|
547
|
+
end
|
548
|
+
end
|
549
|
+
end
|
550
|
+
```
|
551
|
+
|
552
|
+
You may wish to tidy access to context variables away into private methods to mask the implementation details.
|
553
|
+
|
554
|
+
### `#fail_with_error_message`
|
555
|
+
|
556
|
+
This method can be used for returning a simple message wrapped in an instance of `Errors`. Optionally you can specify the type of error should you wish for your controller to react different for say a record not being found vs an API connection error.
|
557
|
+
|
558
|
+
```ruby
|
559
|
+
module MyBusinessDomain
|
560
|
+
module UseCases
|
561
|
+
class UserUpdatesChristmasWishlist < CleanArchitecture::UseCases::AbstractUseCase
|
562
|
+
contract do
|
563
|
+
option :required_persistence_object
|
564
|
+
|
565
|
+
params do
|
566
|
+
required(:user_id).filled(:int)
|
567
|
+
required(:most_wanted_gift).filled(:str)
|
568
|
+
end
|
569
|
+
end
|
570
|
+
|
571
|
+
include Dry::Monads::Do.for(:result)
|
572
|
+
|
573
|
+
CHRISTMAS_DAY = Date.new('2019', '12', '25')
|
574
|
+
|
575
|
+
def result
|
576
|
+
valid_params = yield result_of_validating_params
|
577
|
+
|
578
|
+
if Date.today == CHRISTMAS_DAY
|
579
|
+
return fail_with_error_message('Uh oh, Santa has already left the North Pole!')
|
580
|
+
end
|
581
|
+
|
582
|
+
context(:required_persistence_object).change_most_wanted_gift(user_id, most_wanted_gift)
|
583
|
+
end
|
584
|
+
end
|
585
|
+
end
|
586
|
+
end
|
587
|
+
```
|