use_cases 1.0.6 → 1.0.11

Sign up to get free protection for your applications and to get access to all the features.
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