use_cases 1.0.6 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b7c9560f067bb15b9e9a94d5e89b2840e438486452c640f9b2b2b2b828529850
4
- data.tar.gz: 7c5a1beaedb5d0c4c159860c059513d4da8018e6864e9bb71d0ea855170b8768
3
+ metadata.gz: 2f810a00826d0e18fb1defbddb6a54055f4b77b43141a943d48d8a56273b7bf9
4
+ data.tar.gz: b6f1724c28186e1e8f4ca3b0b79807d4db534903b5f0ee5ace23363b180b8fc2
5
5
  SHA512:
6
- metadata.gz: 5a88ed6dfdacb0d141aa1a782f6f0219f131a52b3c800b86ca62fa6d8a1e77c139e5bfa6fd3bd726d10f278d06ab0e848edd31a1542797f70a405e35e93342ba
7
- data.tar.gz: aacfe4f3862a363757534b259f6ca332f998c8dec8bf7be26568b6db6789d893794a933eaa55cba640ad5b769dd278b94b7ea9419216470fccd3b0abce3f9e95
6
+ metadata.gz: 8973b6cc8d4b89d16af39f010af3a8bf8cd9aea88f9c5c3679356955fc566ee1596711eed422beb9bd928364645640689ec03efd4e8cde11ec7b9d5c3e5c1357
7
+ data.tar.gz: b7c595a0645bf0b8938aa09f99d9fe8167b34c0ca6d9c6e9dfa4ed652a9c8341dfcf216fe70c222ad2321fb094f4667417a6bf1efb7cc1c8c2f2b79b8fe5c0a4
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- use_cases (1.0.5)
4
+ use_cases (1.0.10)
5
5
  activesupport
6
6
  dry-events
7
7
  dry-matcher
data/README.md CHANGED
@@ -8,254 +8,174 @@
8
8
 
9
9
  It's concept is largely based on `dry-transaction` but does not use it behind the scenes. Instead it relies on other `dry` libraries like [dry-validation](https://dry-rb.org/gems/dry-validation/), [dry-events](https://dry-rb.org/gems/dry-validation/) and [dry-monads](https://dry-rb.org/gems/dry-validation/) to implement a DSL that can be flexible enough for your needs.
10
10
 
11
- ## Why `UseCases` came about:
11
+ ### Including UseCase
12
12
 
13
- 1. It allows you to use `dry-validation` without much gymastics.
14
- 2. It abstracts common steps like **authorization** and **validation** into macros.
15
- 3. It solves what we consider a problem of `dry-transaction`. The way it funnels down `input` through `Dry::Monads` payloads alone. `UseCases` offers more flexibility in a way that still promotes functional programming values.
16
- 4. It implements a simple pub/sub mechanism which can be async when `ActiveJob` is a project dependency.
17
- 5. It implements an `enqueue` mechanism to delay execution of steps, also using `ActiveJob` as a dependency.
13
+ Including the `UseCase` module ensures that your class implements the base use case [Base DSL](#the-base-dsl).
18
14
 
19
- ## Installation
15
+ ```ruby
16
+ class Users::Create
17
+ include UseCase
18
+ end
19
+ ````
20
20
 
21
- Add this line to your application's Gemfile:
21
+ In order to add optional modules (optins), use the following notation:
22
22
 
23
23
  ```ruby
24
- gem 'use_cases'
24
+ class Users::Create
25
+ include UseCase[:validated, :transactional, :publishing]
26
+ end
25
27
  ```
26
28
 
27
- And then execute:
28
-
29
- $ bundle install
30
-
31
- Or install it yourself as:
29
+ ### Using a UseCase
32
30
 
33
- $ gem install use_cases
34
-
35
- ## Usage
31
+ ```ruby
32
+ create_user = Users::Create.new
33
+ params = { first_name: 'Don', last_name: 'Quixote' }
36
34
 
37
- To get a good basis to get started on `UseCases`, make sure to read [dry-transaction](https://dry-rb.org/gems/dry-transaction/0.13/)'s documentation first.
35
+ result = create_user.call(params, current_user)
38
36
 
39
- ### Validations
37
+ # Checking if succeeded
38
+ result.success?
40
39
 
41
- See [dry-validation](https://dry-rb.org/gems/dry-validation/)
40
+ # Checking if failed
41
+ result.failure?
42
42
 
43
- ### Creating a Use Case
43
+ # Getting return value
44
+ result.value!
45
+ ```
44
46
 
45
- **Basic Example**
47
+ Or with using dry-matcher by passing a block:
46
48
 
47
49
  ```ruby
48
- class DeleteUser
49
- include UseCase
50
-
51
- check :current_user_is_user?
52
- step :build_user
53
- map :persist_user
50
+ create_user = Users::Create.new
51
+ params = { first_name: 'Don', last_name: 'Quixote' }
54
52
 
55
- private
56
-
57
- def current_user_is_user?(params, current_user)
53
+ create_user.call(params, current_user) do |on|
54
+ on.success do |user|
55
+ puts "#{user.first_name} created!"
58
56
  end
59
57
 
60
- def build_user(params, current_user)
58
+ on.failure do |(code, message)|
59
+ puts "Failure (#{code}): #{message}"
61
60
  end
61
+ end
62
+ ```
62
63
 
64
+ ### Available Optins
63
65
 
64
- def do_something(user, params, current_user)
65
- params[:should_fail] ? Failure([:failed, "failed"]) : Success("it succeeds!")
66
- end
67
- end
68
66
 
69
- params = { should_fail: true }
67
+ | Optin | Description |
68
+ |---|---|
69
+ | `:authorized` | Adds an extra `authorize` step macro, used to check user permissions. |
70
+ | `:prepared` | Adds an extra `prepare` step macro, used to run some code before the use case runs. |
71
+ | `:publishing` | Adds extra extra `publish` option to all steps, which allows a step to broadcast an event after executing |
72
+ | `:transactional` | Calls `#transaction` on a given `transaction_handler` object around the use case execution |
73
+ | `:validated` | Adds all methods of `dry-transaction` to the use case DSL, which run validations on the received `params` object. |
70
74
 
71
- YourCase.new.call(params, nil) do |match|
72
- match.failure :failed do |(code, result)|
73
- puts code
74
- end
75
+ ### The base DSL
75
76
 
76
- match.success do |message|
77
- puts message
78
- end
79
- end
80
- # => failed
77
+ Use cases implements a DSL similar to dry-transaction, using the [Railway programming paradigm](https://fsharpforfunandprofit.com/rop/).
81
78
 
82
- params = { should_fail: false }
83
79
 
84
- YourCase.new.call(params, nil) do |match|
85
- match.failure :failed do |(code, result)|
86
- puts code
87
- end
80
+ Each step macro has a different use case, and so a different subset of available options, different expectations in return values, and interaction with the following step.
88
81
 
89
- match.success do |message|
90
- puts message
91
- end
92
- end
93
- # => it succeeds!
94
- ```
82
+ By taking a simple look at the definition of a use case, anyone should be able to understand the business rules it emcompasses. For that it is necessary to understand the following matrix.
95
83
 
96
- **Complex Example**
97
84
 
98
- ```ruby
99
- class YourCase < UseCases::Base
100
- params {}
85
+ | | Rationale for use | Accepted Options | Expected return | Passes return value |
86
+ |---|---|---|---|---|
87
+ | **step** | This step has some complexity, and it can fail or succeed. | `with`, `pass` | `Success`/ `Failure` | ✅ |
88
+ | **check** | This step checks sets some rules for the operation, usually verifying that domain models fulfil some conditions. | `with`, `pass`, `failure`, `failure_message` | `boolean` | ❌ |
89
+ | **map** | Nothing should go wrong within this step. If it does, it's an unexpected application error. | `with`, `pass` | `any` | ✅ |
90
+ | **try** | We expect that, in some cases, errors will occur, and the operation fails in that case. | `catch`, `with`, `pass`, `failure`, `failure_message` | `any` | ✅ |
91
+ | **tee** | We don't care if this step succeeds or fails, it's used for non essential side effects. | `with`, `pass` | `any` | ❌ |
101
92
 
102
- try :load_some_resource
93
+ #### Optional steps
103
94
 
104
- step :change_this_resource
95
+ | | Rationale for use | Accepted Options | Expected return | Passes return value |
96
+ |---|---|---|---|---|
97
+ | **enqueue** *(requires ActiveJob defined) | The same as a `tee`, but executed later to perform non-essential expensive operations. | `with`, `pass`, and sidekiq options | `any` | ❌ |
98
+ | **authorize**<br> *(requires authorized) | Performs authorization on the current user, by running a `check` which, in case of failure, always returns an `unauthorized` failure. | `with`, `pass`, `failure_message` | `boolean` | ❌ |
99
+ | **prepare**<br> *(requires prepared) | Adds a `tee` step that always runs first. Used to mutate params if necessary. | `with`, `pass` | `any` | ❌ |
105
100
 
106
- tee :log_a_message
107
101
 
108
- check :user_can_create_another_resource?
102
+ ### Defining Steps
109
103
 
110
- map :create_some_already_validated_resource
104
+ Defining a step can be done in the body of the use case.
111
105
 
112
- enqueue :send_email_to_user
106
+ ```ruby
107
+ class Users::DeleteAccount
108
+ include UseCases[:validated, :transactional, :publishing, :validated]
113
109
 
114
- private
110
+ step :do_something, {}
111
+ ```
115
112
 
116
- def load_some_resource(_, params)
117
- Resource.find(params[:id])
118
- end
113
+ In real life, a simple use case would look something like:
119
114
 
120
- def change_this_resource(resource, params)
121
- resource.text = params[:new_text]
115
+ ```ruby
116
+ class Users::DeleteAccount
117
+ include UseCases[:validated, :transactional, :publishing, :validated]
122
118
 
123
- if resource.text == params[:new_text]
124
- Success(resource)
125
- else
126
- Failure([:failed, "could not update resource"])
127
- end
119
+ params do
120
+ required(:id).filled(:str?)
128
121
  end
129
122
 
130
- def log_a_message(resource)
131
- Logger.info('Resource updated')
132
- end
123
+ authorize :user_owns_account?, failure_message: 'Cannot delete account'
124
+ try :load_account, catch: ActiveRecord::RecordNotFound, failure: :account_not_found, failure_message: 'Account not found'
125
+ map :delete_account, publish: :account_deleted
126
+ enqueue :send_farewell_email
133
127
 
134
- def user_can_create_another_resource?(_, _, user)
135
- user.can_create?(Resource)
136
- end
128
+ private
137
129
 
138
- def create_some_already_validated_resource(resource, params, user)
139
- new_resource = Resource.create(text: params[:text])
130
+ def user_owns_account?(_previous_step_input, params, current_user)
131
+ current_user.account_id == params[:id]
140
132
  end
141
133
 
142
- def send_email_to_user(new_resource, _, user)
143
- ResourcEMailer.notify_user(user, new_resource).deliver!
134
+ def load_account(_previous_step_input, params, _current_user)
135
+ Account.find_by!(user_id: params[:id])
144
136
  end
145
- end
146
- ```
147
-
148
- ### Authorization
149
-
150
- `authorize` is a `check` step that returns an `:unauthorized` code in it's `Failure`.
151
137
 
152
- **Example**
153
-
154
- ```ruby
155
- class YourCase < UseCases::Base
156
- authorize 'User must be over 18' do |user|
157
- user.age >= 18
138
+ def delete_account(account, _params, _current_user)
139
+ account.destroy!
158
140
  end
159
- end
160
141
 
161
- user = User.where('age = 15').first
162
-
163
- YourCase.new.call({}, user) do |match|
164
- match.failure :unauthorized do |(code, result)|
165
- puts code
142
+ # since this executed async, all args are serialized
143
+ def send_farewell_email(account_attrs, params, current_user_attrs)
144
+ user = User.find(params[:id])
145
+ UserMailer.farewell(user).deliver_now!
166
146
  end
167
147
  end
168
- # => User must be over 18
169
148
  ```
170
149
 
171
- ### Example
150
+ #### Available Options
172
151
 
173
- For the case of creating posts within a thread.
152
+ | Name | Description | Expected Usage |
153
+ |---|---|---|
154
+ | `with` | Retrieves the callable object used to perform the step. |<em><br> Symbol: `send(options[:with])` <br> String: `UseCases.config.container[options[:with]]` <br> Class: `options[:with]`</em> |
155
+ | `pass` | An array of the arguments to pass to the object set by `with`. <br> _options: params, current_user & previous_step_result_ | Array\<Symbol> |
156
+ | `failure` | The code passed to the Failure object. | Symbol / String |
157
+ | `failure_message` | The string message passed to the Failure object. | Symbol / String |
158
+ | `catch` | Array of error classes to rescue from. | Array\<Exception>
174
159
 
175
- **Specs**
160
+ ## Installation
176
161
 
177
- - Only active users or the thread owner can post.
178
- - The post must be between 25 and 150 characters.
179
- - The post must be sanitized to remove any sensitive or explicit content.
180
- - The post must be saved to the database in the end.
181
- - In case any conditions are not met, an failure should be returned with it's own error code.
162
+ Add this line to your application's Gemfile:
182
163
 
183
164
  ```ruby
184
- class Posts::Create < UseCases::Base
185
-
186
- params do
187
- required(:body).filled(:string).value(size?: 25..150)
188
- required(:thread_id).filled(:integer)
189
- end
190
-
191
- try :load_post_thread, failure: :not_found
192
-
193
- authorize 'User is not active' do |user, params, thread|
194
- user.active?
195
- end
196
-
197
- authorize 'User must be active or the thread author.' do |user, params, thread|
198
- user.active? || thread.author == user
199
- end
200
-
201
- step :sanitize_body
202
-
203
- step :create_post
204
-
205
- private
165
+ gem 'use_cases'
166
+ ```
206
167
 
207
- def load_post_thread(params, user)
208
- Thread.find(params[:thread_id])
209
- end
168
+ And then execute:
210
169
 
211
- def sanitize_body(params)
212
- SanitizeText.new.call(params[:body]).to_monad
213
- end
170
+ $ bundle install
214
171
 
215
- def create_post(body, params, user)
216
- post = Post.new(body: body, user_id: user.id, thread_id: params[:thread_id])
172
+ Or install it yourself as:
217
173
 
218
- post.save ? Success(post) : Failure([:failed_to_save, post.errors.details])
219
- end
220
- end
221
- ```
174
+ $ gem install use_cases
222
175
 
223
- And in your controller action
176
+ ## Usage
224
177
 
225
- ```ruby
226
- # app/controllers/posts_controller.rb
227
- class PostsController < ApplicationController
228
- def create
229
- Posts::Create.new.call(params, current_user) do |match|
230
-
231
- # in success, the return value is the Success payload of the last step (#create_post)
232
- match.success do |post|
233
- # result => <Post:>
234
- end
235
-
236
- # in case ::params or any other dry-validation fails.
237
- match.failure :validation_error do |result|
238
- # result => [:validation_error, ['validation_error', { thread_id: 'is missing' }]
239
- end
240
-
241
- # in case ::try raises an error (ActiveRecord::NotFound in this case)
242
- match.failure :not_found do |result|
243
- # result => [:not_found, ['not_found', 'Could not find thread with id='<params[:thread_id]>'']
244
- end
245
-
246
- # in case any of the ::authorize blocks returns false
247
- match.failure :unauthorized do |result|
248
- # result => [:unauthorized, ['unauthorized', 'User is not active']
249
- end
250
-
251
- # in case #create_post returns a Failure
252
- match.failure :failed_to_save do |result|
253
- # result => [:failed_to_save, ['failed_to_save', { user_id: 'some error' }]
254
- end
255
- end
256
- end
257
- end
258
- ```
178
+ To get a good basis to get started on `UseCases`, make sure to read [dry-transaction](https://dry-rb.org/gems/dry-transaction/0.13/)'s documentation first.
259
179
 
260
180
  ## Development
261
181
 
@@ -5,14 +5,8 @@ module UseCases
5
5
  module Events
6
6
  class PublishJob < ActiveJob::Base
7
7
  def perform(publish_key, payload)
8
- publish_key += '.async'
9
- UseCases.publisher.publish(publish_key, payload)
10
- end
11
-
12
- private
13
-
14
- def publisher
15
- UseCases.publisher
8
+ publish_key += ".async"
9
+ UseCases.publisher.register_and_publish_event(publish_key, payload)
16
10
  end
17
11
  end
18
12
  end
@@ -1,9 +1,50 @@
1
- require 'dry/events/publisher'
1
+ require "dry/events/publisher"
2
2
 
3
3
  module UseCases
4
4
  module Events
5
5
  class Publisher
6
6
  include Dry::Events::Publisher[:use_cases]
7
+
8
+ def self.register_and_publish_event(event_name, payload)
9
+ register_events(event_name)
10
+
11
+ new.tap do |publisher|
12
+ publisher.subscribe_to_event(event_name)
13
+ publisher.publish(event_name, payload)
14
+ end
15
+
16
+ return unless defined? UseCases::Events::PublishJob
17
+
18
+ UseCases::Events::PublishJob.perform_later(event_name, payload)
19
+ end
20
+
21
+ def self.extract_payload(use_case_args)
22
+ {
23
+ return_value: use_case_args[0],
24
+ params: use_case_args[1],
25
+ current_user: use_case_args[2]
26
+ }
27
+ end
28
+
29
+ def self.register_events(event_name)
30
+ [event_name, "#{event_name}.aync"].each do |key|
31
+ register_event(key) unless events[key]
32
+ end
33
+ end
34
+
35
+ def subscribe_to_event(event_name)
36
+ subscribers_for(event_name).each(&method(:subscribe))
37
+ end
38
+
39
+ def subscribers_for(event_name)
40
+ available_subscribers.select do |subscriber|
41
+ subscriber.subscribed_to?(event_name)
42
+ end
43
+ end
44
+
45
+ def available_subscribers
46
+ UseCases::Events::Subscriber.descendants.map(&:new) + UseCases.subscribers
47
+ end
7
48
  end
8
49
  end
9
50
  end
@@ -1,6 +1,13 @@
1
+ require "active_support/core_ext/class/subclasses"
2
+
1
3
  module UseCases
2
4
  module Events
3
5
  class Subscriber
6
+ def subscribed_to?(event_name)
7
+ event_handler_name = "on_#{event_name.gsub('.', '_')}"
8
+
9
+ respond_to?(event_handler_name)
10
+ end
4
11
  end
5
12
  end
6
13
  end
@@ -9,35 +9,37 @@ module UseCases
9
9
  module Publishing
10
10
  def self.included(base)
11
11
  super
12
- base.prepend DoCallPatch
12
+ StepAdapters::Abstract.prepend StepCallPatch
13
13
  end
14
14
 
15
- module DoCallPatch
16
- def do_call(*args)
17
- super(*args, method(:publish_step_result).to_proc)
15
+ module StepCallPatch
16
+ def call(*args)
17
+ super(*args).tap do |result|
18
+ publish_step_result(result, args)
19
+ end
18
20
  end
19
21
 
20
- def publish_step_result(step_result, step_object)
21
- publish_key = step_object.options[:publish]
22
- publish_key += step_result.success? ? ".success" : ".failure"
23
- payload = step_result.value!
24
- return unless publish_key
22
+ def publish_step_result(step_result, args)
23
+ return unless options[:publish]
25
24
 
26
- register_event(publish_key)
27
- peform_publish(publish_key, payload)
25
+ key = extract_event_key(step_result)
26
+ payload = extract_payload(step_result, args)
27
+
28
+ UseCases.publisher.register_and_publish_event(key, payload)
28
29
  end
29
30
 
30
- def register_event(publish_key)
31
- return if UseCases.publisher.class.events[publish_key]
31
+ private
32
32
 
33
- UseCases.publisher.class.register_event(publish_key)
33
+ def extract_payload(step_result, args)
34
+ {
35
+ return_value: step_result.value!,
36
+ params: args[-2],
37
+ current_user: args[-1]
38
+ }
34
39
  end
35
40
 
36
- def peform_publish(publish_key, payload)
37
- UseCases.publisher.publish(publish_key, payload)
38
- return unless defined? UseCases::Events::PublishJob
39
-
40
- UseCases::Events::PublishJob.perform_later(publish_key, payload.to_json)
41
+ def extract_event_key(step_result)
42
+ options[:publish].to_s + (step_result.success? ? ".success" : ".failure")
41
43
  end
42
44
  end
43
45
  end
@@ -17,8 +17,6 @@ module UseCases
17
17
  def do_call(*args)
18
18
  stack.call do
19
19
  result = _run_step(stack, args)
20
- args.last.call(result, stack.current_step) if args.last.is_a? Proc
21
-
22
20
  return result if result.failure?
23
21
 
24
22
  result
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "dry/monads/all"
4
- require "byebug"
5
4
 
6
5
  module UseCases
7
6
  module StepAdapters
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module UseCases
4
- VERSION = "1.0.6"
4
+ VERSION = "1.0.11"
5
5
  end
data/lib/use_cases.rb CHANGED
@@ -15,24 +15,6 @@ module UseCases
15
15
  extend Dry::Configurable
16
16
 
17
17
  setting :container, reader: true
18
- setting :publisher, ::UseCases::Events::Publisher.new, reader: true
18
+ setting :publisher, ::UseCases::Events::Publisher, reader: true
19
19
  setting :subscribers, [], reader: true
20
-
21
- def self.configure(&blk)
22
- super
23
- finalize_subscriptions!
24
- end
25
-
26
- def self.finalize_subscriptions!
27
- subscribe(*subscribers)
28
- return unless UseCases::Events::Subscriber.respond_to?(:descendants)
29
-
30
- usecase_subscriber_children = UseCases::Events::Subscriber.descendants
31
- subscribe(*usecase_subscriber_children)
32
- subscribers.concat(usecase_subscriber_children)
33
- end
34
-
35
- def self.subscribe(*subscribers)
36
- subscribers.each { |subscriber| publisher.subscribe(subscriber) }
37
- end
38
20
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: use_cases
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.6
4
+ version: 1.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ring Twice
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-01-05 00:00:00.000000000 Z
11
+ date: 2022-01-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport