clean-architecture 5.0.2 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (166) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +15 -19
  3. data/.gitignore +4 -0
  4. data/CHANGELOG.md +4 -0
  5. data/Gemfile +1 -0
  6. data/README.md +1 -585
  7. data/bin/tapioca +29 -0
  8. data/clean-architecture.gemspec +4 -4
  9. data/lib/clean-architecture.rb +4 -1
  10. data/lib/clean_architecture/adapters/all.rb +1 -1
  11. data/lib/clean_architecture/adapters/attribute_hash_base.rb +47 -20
  12. data/lib/clean_architecture/all.rb +1 -4
  13. data/lib/clean_architecture/builders/abstract_active_record_entity_builder.rb +43 -13
  14. data/lib/clean_architecture/builders/all.rb +1 -1
  15. data/lib/clean_architecture/checks/all.rb +1 -1
  16. data/lib/clean_architecture/checks/authorization.rb +11 -9
  17. data/lib/clean_architecture/entities/all.rb +1 -3
  18. data/lib/clean_architecture/entities/failure_details.rb +27 -17
  19. data/lib/clean_architecture/matchers/all.rb +1 -1
  20. data/lib/clean_architecture/matchers/use_case_result.rb +9 -3
  21. data/lib/clean_architecture/queries/all.rb +1 -1
  22. data/lib/clean_architecture/queries/http_failure_code.rb +8 -20
  23. data/lib/clean_architecture/queries/http_success_code.rb +14 -7
  24. data/lib/clean_architecture/serializers/all.rb +1 -1
  25. data/lib/clean_architecture/serializers/html_response_from_result.rb +7 -1
  26. data/lib/clean_architecture/serializers/json_response_from_result.rb +4 -4
  27. data/lib/clean_architecture/version.rb +1 -1
  28. data/nix/sources.json +14 -0
  29. data/nix/sources.nix +174 -0
  30. data/run_ci.sh +7 -0
  31. data/shell.nix +17 -12
  32. data/sorbet/config +4 -0
  33. data/sorbet/{rbi/gems → dry-monads-sorbet}/dry-monads.rbi +176 -94
  34. data/sorbet/rbi/gems/activemodel@6.1.4.1.rbi +1292 -0
  35. data/sorbet/rbi/gems/activerecord@6.1.4.1.rbi +8092 -0
  36. data/sorbet/rbi/gems/activesupport@6.1.4.1.rbi +3531 -0
  37. data/sorbet/rbi/gems/{ast.rbi → ast@2.4.2.rbi} +28 -22
  38. data/sorbet/rbi/gems/byebug@11.1.3.rbi +1568 -0
  39. data/sorbet/rbi/gems/coderay@1.1.3.rbi +1005 -0
  40. data/sorbet/rbi/gems/concurrent-ruby@1.1.9.rbi +915 -0
  41. data/sorbet/rbi/gems/diff-lcs@1.4.4.rbi +8 -0
  42. data/sorbet/rbi/gems/docile@1.4.0.rbi +54 -0
  43. data/sorbet/rbi/gems/dry-core@0.7.1.rbi +92 -0
  44. data/sorbet/rbi/gems/dry-equalizer@0.3.0.rbi +28 -0
  45. data/sorbet/rbi/gems/dry-matcher@0.9.0.rbi +56 -0
  46. data/sorbet/rbi/gems/dry-monads-sorbet@1.1.7.rbi +41 -0
  47. data/sorbet/rbi/gems/dry-monads@1.4.0.rbi +697 -0
  48. data/sorbet/rbi/gems/em-websocket@0.5.2.rbi +8 -0
  49. data/sorbet/rbi/gems/eventmachine@1.2.7.rbi +45 -0
  50. data/sorbet/rbi/gems/ffi@1.15.4.rbi +8 -0
  51. data/sorbet/rbi/gems/formatador@0.3.0.rbi +8 -0
  52. data/sorbet/rbi/gems/guard-compat@1.2.1.rbi +31 -0
  53. data/sorbet/rbi/gems/guard-livereload@2.5.2.rbi +8 -0
  54. data/sorbet/rbi/gems/guard-rspec@4.7.3.rbi +211 -0
  55. data/sorbet/rbi/gems/guard@2.18.0.rbi +8 -0
  56. data/sorbet/rbi/gems/http_parser.rb@0.6.0.rbi +8 -0
  57. data/sorbet/rbi/gems/i18n@1.8.10.rbi +8 -0
  58. data/sorbet/rbi/gems/listen@3.7.0.rbi +8 -0
  59. data/sorbet/rbi/gems/lumberjack@1.2.8.rbi +8 -0
  60. data/sorbet/rbi/gems/method_source@1.0.0.rbi +72 -0
  61. data/sorbet/rbi/gems/minitest@5.14.4.rbi +344 -0
  62. data/sorbet/rbi/gems/multi_json@1.15.0.rbi +8 -0
  63. data/sorbet/rbi/gems/nenv@0.3.0.rbi +8 -0
  64. data/sorbet/rbi/gems/notiffany@0.1.3.rbi +8 -0
  65. data/sorbet/rbi/gems/parallel@1.21.0.rbi +113 -0
  66. data/sorbet/rbi/gems/{parser.rbi → parser@3.0.2.0.rbi} +966 -699
  67. data/sorbet/rbi/gems/pry-byebug@3.9.0.rbi +461 -0
  68. data/sorbet/rbi/gems/{pry.rbi → pry@0.13.1.rbi} +2191 -1605
  69. data/sorbet/rbi/gems/{rainbow.rbi → rainbow@3.0.0.rbi} +90 -55
  70. data/sorbet/rbi/gems/{rake.rbi → rake@13.0.6.rbi} +578 -427
  71. data/sorbet/rbi/gems/rb-fsevent@0.11.0.rbi +8 -0
  72. data/sorbet/rbi/gems/rb-inotify@0.10.1.rbi +8 -0
  73. data/sorbet/rbi/gems/rb-readline@0.5.5.rbi +884 -0
  74. data/sorbet/rbi/gems/rbi@0.0.6.rbi +1405 -0
  75. data/sorbet/rbi/gems/regexp_parser@2.1.1.rbi +1120 -0
  76. data/sorbet/rbi/gems/{rexml.rbi → rexml@3.2.5.rbi} +562 -479
  77. data/sorbet/rbi/gems/{rspec-core.rbi → rspec-core@3.10.1.rbi} +2317 -1533
  78. data/sorbet/rbi/gems/rspec-expectations@3.10.1.rbi +1574 -0
  79. data/sorbet/rbi/gems/rspec-mocks@3.10.2.rbi +1462 -0
  80. data/sorbet/rbi/gems/rspec-support@3.10.2.rbi +509 -0
  81. data/sorbet/rbi/gems/rspec@3.10.0.rbi +38 -0
  82. data/sorbet/rbi/gems/rubocop-ast@1.12.0.rbi +1938 -0
  83. data/sorbet/rbi/gems/rubocop-rspec@2.5.0.rbi +1786 -0
  84. data/sorbet/rbi/gems/rubocop@1.22.1.rbi +13252 -0
  85. data/sorbet/rbi/gems/ruby-progressbar@1.11.0.rbi +405 -0
  86. data/sorbet/rbi/gems/shellany@0.0.1.rbi +8 -0
  87. data/sorbet/rbi/gems/simplecov-html@0.12.3.rbi +89 -0
  88. data/sorbet/rbi/gems/simplecov@0.21.2.rbi +577 -0
  89. data/sorbet/rbi/gems/simplecov_json_formatter@0.1.3.rbi +8 -0
  90. data/sorbet/rbi/gems/sorbet-struct-comparable@1.1.0.rbi +17 -0
  91. data/sorbet/rbi/gems/spoom@1.1.5.rbi +1241 -0
  92. data/sorbet/rbi/gems/stackprof@0.2.17.rbi +98 -0
  93. data/sorbet/rbi/gems/tapioca@0.5.2.rbi +949 -0
  94. data/sorbet/rbi/gems/thor@1.1.0.rbi +839 -0
  95. data/sorbet/rbi/gems/tzinfo@2.0.4.rbi +8 -0
  96. data/sorbet/rbi/gems/unicode-display_width@2.1.0.rbi +26 -0
  97. data/sorbet/rbi/gems/unparser@0.6.0.rbi +8 -0
  98. data/sorbet/rbi/gems/zeitwerk@2.4.2.rbi +8 -0
  99. data/sorbet/tapioca/require.rb +9 -0
  100. metadata +88 -91
  101. data/.ruby-version +0 -1
  102. data/lib/clean_architecture/entities/targeted_parameters.rb +0 -24
  103. data/lib/clean_architecture/entities/untargeted_parameters.rb +0 -21
  104. data/lib/clean_architecture/interfaces/all.rb +0 -12
  105. data/lib/clean_architecture/interfaces/authorization_parameters.rb +0 -19
  106. data/lib/clean_architecture/interfaces/base_parameters.rb +0 -24
  107. data/lib/clean_architecture/interfaces/jsonable.rb +0 -16
  108. data/lib/clean_architecture/interfaces/targeted_parameters.rb +0 -19
  109. data/lib/clean_architecture/interfaces/use_case.rb +0 -20
  110. data/lib/clean_architecture/interfaces/use_case_actor.rb +0 -20
  111. data/lib/clean_architecture/interfaces/use_case_target.rb +0 -24
  112. data/lib/clean_architecture/types.rb +0 -8
  113. data/lib/clean_architecture/use_cases/abstract_use_case.rb +0 -63
  114. data/lib/clean_architecture/use_cases/all.rb +0 -10
  115. data/lib/clean_architecture/use_cases/contract.rb +0 -9
  116. data/lib/clean_architecture/use_cases/errors.rb +0 -58
  117. data/lib/clean_architecture/use_cases/form.rb +0 -116
  118. data/lib/clean_architecture/use_cases/parameters.rb +0 -43
  119. data/sorbet/rbi/gems/activemodel.rbi +0 -75
  120. data/sorbet/rbi/gems/activesupport.rbi +0 -440
  121. data/sorbet/rbi/gems/byebug.rbi +0 -1040
  122. data/sorbet/rbi/gems/coderay.rbi +0 -92
  123. data/sorbet/rbi/gems/concurrent-ruby.rbi +0 -1586
  124. data/sorbet/rbi/gems/docile.rbi +0 -32
  125. data/sorbet/rbi/gems/dry-configurable.rbi +0 -139
  126. data/sorbet/rbi/gems/dry-container.rbi +0 -89
  127. data/sorbet/rbi/gems/dry-core.rbi +0 -80
  128. data/sorbet/rbi/gems/dry-equalizer.rbi +0 -26
  129. data/sorbet/rbi/gems/dry-inflector.rbi +0 -73
  130. data/sorbet/rbi/gems/dry-initializer.rbi +0 -209
  131. data/sorbet/rbi/gems/dry-logic.rbi +0 -305
  132. data/sorbet/rbi/gems/dry-matcher.rbi +0 -34
  133. data/sorbet/rbi/gems/dry-schema.rbi +0 -786
  134. data/sorbet/rbi/gems/dry-struct.rbi +0 -137
  135. data/sorbet/rbi/gems/dry-types.rbi +0 -709
  136. data/sorbet/rbi/gems/dry-validation.rbi +0 -288
  137. data/sorbet/rbi/gems/duckface-interfaces.rbi +0 -94
  138. data/sorbet/rbi/gems/i18n.rbi +0 -133
  139. data/sorbet/rbi/gems/jaro_winkler.rbi +0 -15
  140. data/sorbet/rbi/gems/method_source.rbi +0 -64
  141. data/sorbet/rbi/gems/parallel.rbi +0 -82
  142. data/sorbet/rbi/gems/pry-byebug.rbi +0 -155
  143. data/sorbet/rbi/gems/rb-readline.rbi +0 -767
  144. data/sorbet/rbi/gems/rspec-expectations.rbi +0 -398
  145. data/sorbet/rbi/gems/rspec-mocks.rbi +0 -816
  146. data/sorbet/rbi/gems/rspec-support.rbi +0 -271
  147. data/sorbet/rbi/gems/rspec.rbi +0 -15
  148. data/sorbet/rbi/gems/rubocop-rspec.rbi +0 -922
  149. data/sorbet/rbi/gems/rubocop.rbi +0 -7319
  150. data/sorbet/rbi/gems/ruby-progressbar.rbi +0 -305
  151. data/sorbet/rbi/gems/simplecov-html.rbi +0 -35
  152. data/sorbet/rbi/gems/simplecov.rbi +0 -361
  153. data/sorbet/rbi/gems/stackprof.rbi +0 -52
  154. data/sorbet/rbi/gems/unicode-display_width.rbi +0 -17
  155. data/sorbet/rbi/hidden-definitions/errors.txt +0 -8580
  156. data/sorbet/rbi/hidden-definitions/hidden.rbi +0 -17036
  157. data/sorbet/rbi/sorbet-typed/lib/activemodel/all/activemodel.rbi +0 -452
  158. data/sorbet/rbi/sorbet-typed/lib/activesupport/>=6.0.0.rc1/activesupport.rbi +0 -23
  159. data/sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi +0 -979
  160. data/sorbet/rbi/sorbet-typed/lib/bundler/all/bundler.rbi +0 -8684
  161. data/sorbet/rbi/sorbet-typed/lib/minitest/all/minitest.rbi +0 -108
  162. data/sorbet/rbi/sorbet-typed/lib/rainbow/all/rainbow.rbi +0 -276
  163. data/sorbet/rbi/sorbet-typed/lib/ruby/all/gem.rbi +0 -4222
  164. data/sorbet/rbi/sorbet-typed/lib/ruby/all/open3.rbi +0 -111
  165. data/sorbet/rbi/sorbet-typed/lib/ruby/all/resolv.rbi +0 -543
  166. data/sorbet/rbi/todo.rbi +0 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '048bebc1a315646deda9635ebf4f6d52a3b7c1350f7d26a542a600c8410fbc44'
4
- data.tar.gz: 1afd42cfc46b98674be6165290806f3621c5921597c03d6cf5b7da9b4cd5a0b3
3
+ metadata.gz: 965f8cb51724ae428566763a1e147a628894aca145a0bd8606f4afd914782216
4
+ data.tar.gz: a0568d6bf58da6241614e6dc233743401a016f68d0bbe941fbc3c67d4573ab95
5
5
  SHA512:
6
- metadata.gz: ca7b6850934dcc47568cc5ce0143b8a5b3cff4c416e136e3450d0a9e10e4d9bd3a64cf41caf493f44157a2b9acfca0daa5184912e8d984c19c9110bfb487080c
7
- data.tar.gz: c15b10110b7eb1ffd0ab96830f48f894c11d27088e8b6b099822b0f02d245052f262908d2b186476def0e8c4afa13edb7c44451a3ff8a20b1b166f900ef0ea1f
6
+ metadata.gz: 585f429567c611b024cec33b918eb9010d8e2db9b2183a4b71865377077918e686eee89ba22e18f7a11afb43ab097ebcf5e8d6d881c6406b156ddba7e57997e9
7
+ data.tar.gz: 7f35db919f5a5cc12abc64079fbc9fc5e41cf2d362753cb69b6e7273a471798e6a3858d08b856f6242ad14ffb98e265c7e7c5aa047b0da2d138f17f3e2ddc4e6
@@ -15,32 +15,28 @@ jobs:
15
15
  shell: bash
16
16
  run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
17
17
  id: extract_branch
18
- - name: Set up Ruby
19
- uses: actions/setup-ruby@v1.1.1
20
- with:
21
- ruby-version: 2.6.6
22
18
  - name: Cache gems
23
- uses: actions/cache@v1
19
+ uses: actions/cache@v2
24
20
  with:
25
21
  path: vendor/bundle
26
- key: "${{ runner.OS }}-gem-cache-${{ hashFiles('**/clean-architecture.gemspec')
22
+ key: "${{ runner.OS }}-gem-cache-${{ hashFiles('**/*.gemspec')
27
23
  }}"
28
24
  restore-keys: "${{ runner.OS }}-gem-cache-\n"
29
- - name: Install bundler
30
- run: (bundler -v | grep "2.1.4") || gem install bundler:2.1.4
31
- - name: Install gems
32
- run: bundle install --jobs $(nproc) --retry 3 --without metrics --path vendor/bundle
33
- - name: Create cache directory
34
- run: mkdir -p tmp/cache
35
- - name: Typecheck with Sorbet
36
- run: bundle exec srb tc . --ignore=/vendor
37
- - name: Run RSpec test suite
38
- run: bundle exec rspec spec
25
+ - uses: "cachix/install-nix-action@8d6d5e949675fbadb765c6b1a975047fa5f09b27"
26
+ with:
27
+ extra_nix_config: |
28
+ post-build-hook = /etc/nix/upload-to-cache.sh
29
+ substituters = https://cache.nixos.org/
30
+ trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
31
+ nix_path: nixpkgs=channel:nixos-21.05
32
+ - name: Run CI through nix-shell
39
33
  env:
40
- METRICS: '1'
34
+ GEMFURY_DEPLOY_TOKEN: ${{ secrets.GEMFURY_DEPLOY_TOKEN }}
35
+ run: nix-shell --run "chmod 755 ./run_ci.sh && ./run_ci.sh"
41
36
  - name: Post to Slack if build fails
42
- if: failure() && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/stable')
43
- uses: pullreminders/slack-action@master
37
+ if: failure() && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main'
38
+ || github.ref == 'refs/heads/stable')
39
+ uses: pullreminders/slack-action@a5a262c896a1cc80dcbae59ba95513e2dfb21439
44
40
  env:
45
41
  SLACK_BOT_TOKEN: "${{ secrets.SLACK_BOT_TOKEN }}"
46
42
  with:
data/.gitignore CHANGED
@@ -1,3 +1,7 @@
1
1
  coverage
2
2
  tags
3
3
  /Gemfile.lock
4
+
5
+ vendor
6
+ .direnv
7
+ .envrc
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ 6.0.0
2
+
3
+ * Tear out everything non-essential
4
+
1
5
  5.0.2
2
6
 
3
7
  * Require the forwardable module where it is used
data/Gemfile CHANGED
@@ -16,6 +16,7 @@ group :development, :test do
16
16
  gem 'rubocop-rspec'
17
17
  gem 'sorbet'
18
18
  gem 'stackprof'
19
+ gem 'tapioca'
19
20
  end
20
21
 
21
22
  group :test do
data/README.md CHANGED
@@ -1,587 +1,3 @@
1
1
  # Clean Architecture
2
2
 
3
- This gem provides helper interfaces and classes to assist in the construction of application with
4
- Clean Architecture, as described in [Robert Martin's seminal book](https://www.amazon.com/gp/product/0134494164).
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
-
33
- ## Installation
34
-
35
- Add this line to your application's Gemfile:
36
-
37
- ```ruby
38
- gem 'clean-architecture'
39
- ```
40
-
41
- And then execute:
42
-
43
- $ bundle install
44
- $ bundle binstubs clean-architecture
45
-
46
- ## Philosophy
47
-
48
- The intention of this gem is to help you build applications that are built from the use case down,
49
- and decisions about I/O can be deferred until the last possible moment. It relies heavily on the
50
- [duckface-interfaces](https://github.com/samuelgiles/duckface) gem to enforce interface
51
- implementation.
52
-
53
- ### Screaming architecture - use cases as an organisational principle
54
-
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
56
- Rails application.
57
-
58
- ```
59
- - lib
60
- - my_banking_application
61
- - use_cases
62
- - retail_customer_opens_bank_account.rb
63
- - retail_customer_makes_a_deposit.rb
64
- - ...
65
- ```
66
-
67
- Note that the use case name contains:
68
-
69
- - the user role
70
- - the action
71
- - the (sometimes implied) subject
72
-
73
- ### Design principles
74
-
75
- #### SRP - The Single Responsibility principle
76
-
77
- > A function should do one, and only one, thing
78
-
79
- We satisfy the SRP by following these rules:
80
-
81
- - An **adapter** is solely responsible for presenting the properties of a business object, or a small number of business objects, in a known interface
82
- - A **command** is solely responsible for completing an atomic I/O operation
83
- - An **entity** is solely responsible for representing, in memory, a business object whos properties do not come from a single source
84
- - An **interface** is a module that represents a contract between two classes
85
- - A **serializer** is solely responsible for taking a business object and turning it into a representation made up of purely primitive values
86
- - A **strategy** is an algorithm used by commands to compose atomic I/O operations
87
- - A **use case** is solely responsible for checking whether an actor has permissions to perform a command, and executing that command if so
88
- - A **validator** is solely responsible for validating a business object and returning a validation result
89
-
90
- #### OCP - The Open/Closed Principle, LSP - The Liskov Substitution Principle and DIP - The Dependency Inversion Principle
91
-
92
- > A software artefact should be open for extension but closed for modification
93
-
94
- > A caller should not have to know the type of an object to interact with it
95
-
96
- > Always depend on or derive from a stable abstraction, rather than a volatile concrete class
97
-
98
- We satisfy the OCP, LSP & DIP by following these rules:
99
-
100
- - We create a clean boundary between our business logic, our gateway and our application-specific classes using interfaces
101
- - We use interfaces wherever possible, allowing concrete implementations of those interfaces to be extended without breaking the contract
102
- - We write unit tests against interfaces, never against concrete implementations (unless interfaces don't exist)
103
-
104
- #### ISP - The Interface Segregation Principle
105
-
106
- > Where some actors only use a subset of methods available from an interface, the interface should be split into sub-interfaces supporting each type of caller
107
-
108
- We satisfy the ISP by following these rules:
109
-
110
- - Each functional area of our code is split into folders (under `lib` in Rails projects)
111
- - Each functional area defines its own interfaces
112
- - Interfaces are not shared between functional areas
113
-
114
- ### Component cohesion
115
-
116
- #### REP - The Reuse/Release Equivalence Principle, CCP - The Common Closure Principle & CRP - The Common Reuse Principle
117
-
118
- > Classes and modules that are grouped together into a component should be releasable together
119
-
120
- > Gather into components those changes the change for the same reasons and at the same times.
121
-
122
- > Classes and modules that tend to be reused together should be placed in the same component
123
-
124
- We satisfy the REP, CCP and CRP by:
125
-
126
- - Having team discussions whenever we make decisions about what a new functional area should be called and what it should contain
127
- - Ensuring that none of our functional areas make direct reference back to the parent application
128
- - Splitting functional areas out into gems when those functional areas change at a different rate than the rest of the codebase
129
- - Splitting functional areas out into standalone applications when it makes sense to do so
130
-
131
- ### Component coupling
132
-
133
- #### ADP - The Acyclic Dependencies Principle
134
-
135
- > Don't create circular dependencies
136
-
137
- I don't think I need to explain this. Just don't do it. I like explicitly including dependencies using `require` because it actually prevents you from doing this. Rails, in so many ways, makes one lazy.
138
-
139
- #### SDP - The Stable Dependencies Principle
140
-
141
- > A component always have less things depending on it than it depends on
142
-
143
- We satisfy the SDP by:
144
-
145
- - Putting sensible abstractions in place that adhere to the Single Responsibility principle
146
- - Not sharing abstractions and entities between multiple functional areas
147
-
148
- #### SAP - The Stable Abstractions Principle
149
-
150
- > A component should be as abstract as it is stable
151
-
152
- We satisfy the SAP by:
153
-
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?
155
-
156
- ## Structure
157
-
158
- ### Practical suggestions for implementation
159
-
160
- * The code that manages your inputs (e.g. a Rails controller) instantiates a gateway
161
- object
162
-
163
- * The code that manages your inputs (e.g. a Rails controller) instantiates a use case actor
164
- object
165
- - Suggest: a class that implements the `UseCaseActor` interface
166
-
167
- ```
168
- use_case_actor = MyUseCaseActorAdapter.new(devise_current_user)
169
- ```
170
-
171
- * The code that manages your inputs (e.g. a Rails controller) instantiates a use case input port
172
- object
173
- - Suggest: a class that implements the `BaseParameters` interface
174
- - Suggest: implement the `AuthorizationParameters` interface if you want to make authorization
175
- part of your use case logic
176
- - Suggest: implement the `TargetedParameters` if your use case operates on a single object
177
- - Suggest: use the `TargetedParameters` entity for an out-of-the-box class that gives you all of
178
- these
179
-
180
- ```
181
- input_port = CleanArchitecture::Entities::TargetedParameters.new(
182
- use_case_actor,
183
- TargetActiveRecordClass.find(params[:id]),
184
- strong_params,
185
- gateway,
186
- other_settings_hash
187
- )
188
- ```
189
-
190
- * The code that manages your inputs (e.g. a Rails controller) instantiates a use case object
191
- - Suggest: a class that implements the `UseCase` interface
192
-
193
- ```
194
- use_case = MyBankingApplication::UseCases::RetailCustomerMakesADeposit.new(input_port)
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_gateway_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_gateway_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_gateway_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_gateway_object: MyGateway.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_gateway_object: MyGateway.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_gateway_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_gateway_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_gateway_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 gateway 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_gateway_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_gateway_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_gateway_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_gateway_object).change_most_wanted_gift(user_id, most_wanted_gift)
583
- end
584
- end
585
- end
586
- end
587
- ```
3
+ This gem is deprecated. It seemed like a good idea at the time.