rails_use_case 0.0.7 → 0.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: 26b3c53326dd3bd10cd2b119eba354e2cebe8333deeb0781eca31dfea6ba6281
4
- data.tar.gz: 75b6d7e2dbc461a6fef31f2ea8bfb0a3c65edfbb52892a4dc93bec2f5dd0bcdb
3
+ metadata.gz: 7ede9e2243fddfc8ed59a395ca97b7f14062c4180e27d1f956f793a435559e90
4
+ data.tar.gz: 3f56c4fe1f24f74af0226aa5adacbb1341ac042e52b0caf767d61005c8d04b9a
5
5
  SHA512:
6
- metadata.gz: 5e2a68e4b6939116696dfb69ed347d131eccf05e029b4ff97b26ae563726c8239faa76d2ef07be3025ba642ce4932ebd4e522dc689da50b3f618d76a613cef76
7
- data.tar.gz: 03fbc54da3a23e932fab54359c222a07d988297ffdd57c1e8832526d8affefcc1bf3b9870c26929ac5067a0a61b9f7bdd7a563653865592dba2d33d37f64635f
6
+ metadata.gz: e21f3ab1cc85466ec37bccfcc1dae363c478bb4d49a15bdcd5b1e093e5d63d270d5be3befccd66f41f0b81cd79015cdcf41f124f9321fa3d968f17300d7ba1a8
7
+ data.tar.gz: 34ec8bbeb3e95a0ffce45ef6945d859e71b3122b53c1a01b7d7ad122b9c2d884f681c67a47a355bd0905b4e19ca482defcd7ba572b2aaf209f29920911a53862
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Rails Use Case gem
2
2
 
3
- Opinionated gem for UseCases and Services in rails to keep models and controllers slim.
3
+ Opinionated gem for UseCases and Services in Rails to keep your Models and Controllers slim.
4
+
5
+ Read more: https://dev.to/phortx/pimp-your-rails-application-32d0
4
6
 
5
7
  The purpose of a UseCase is to contain reusable high level business logic which would normally be
6
8
  located in the controller. Examples are: Place an item in the cart, create a new user or delete a comment.
@@ -20,33 +22,71 @@ gem 'rails_use_case'
20
22
 
21
23
  The purpose of a UseCase is to contain reusable high level business logic which would normally be
22
24
  located in the controller. It defines a process via `step` definitions. A UseCase takes params
23
- and has a outcome, which is successfully or failed. It doesn't have a configuration file and doesn't
25
+ and has a outcome, which is either successful or failed. It doesn't have a configuration file and doesn't
24
26
  log anything. Examples are: Place an item in the cart, create a new user or delete a comment.
25
27
 
28
+ The params should always passed as hash and are automatically assigned to instance variables.
29
+
30
+ Use Cases should be placed in the `app/use_cases/` directory and the file and class name should start with a verb like `create_blog_post.rb`.
31
+
32
+
33
+ ### Steps
34
+
26
35
  Steps are executed in the defined order. Only when a step succeeds (returns true) the next step will
27
36
  be executed. Steps can be skipped via `if` and `unless`.
28
37
 
29
- The UseCase should assign the main record to `@record`. Calling save! without argument will try to
30
- save that record or raises an exception. Also the `@record` will automatically passed into the outcome.
38
+ The step either provides the name of a method within the use case or a block.
39
+ When a block is given, it will be executed. Otherwise the framework will try to
40
+ call a method with the given name.
31
41
 
32
- The params should always passed as hash and are automatically assigned to instance variables.
42
+ You can also have named inline steps: `step :foo, do: -> { ... }` which is
43
+ equivalent to `step { ... }` but with a given name. An existing method `foo`
44
+ will not be called in this case but rather the block be executed.
33
45
 
34
- Use Cases should be placed in the `app/use_cases/` directory and the file and class name should start with a verb like `create_blog_post.rb`.
46
+ There are also two special steps: `success` and `failure`. Both will end the
47
+ step chain immediately. `success` will end the use case successfully (like there
48
+ would be no more steps). And `failure` respectively will end the use case with a
49
+ error. You should pass the error message and/or code via `message:` and/or
50
+ `code:` options.
51
+
52
+
53
+ ### Failing
54
+
55
+ A UseCase fails when a step returns a falsy value or raises an exception.
56
+
57
+ For even better error handling, you should let a UseCase fail via the shortcut
58
+ `fail!()` which actually just raised an `UseCase::Error` but you can provide
59
+ some additional information. This way you can provide a human readable message
60
+ with error details and additionally you can pass an error code as symbol, which
61
+ allows the calling code to do error handling:
62
+
63
+ `fail!(message: 'You have called this wrong. Shame on you!', code: :missing_information)`.
64
+
65
+ The error_code can also passed as first argument to the `failure` step definition.
66
+
67
+
68
+ ### Record
69
+
70
+ The UseCase should assign the main record to `@record`. Calling `save!` without argument will try to
71
+ save that record or raises an exception. Also the `@record` will automatically passed into the outcome.
35
72
 
36
73
 
37
74
  ### Example UseCase
38
75
 
39
76
  ```ruby
40
- class CreateBlogPost < Rails::UseCase
41
- attr_accessor :title, :content, :author, :skip_notifications
77
+ class BlogPosts::Create < Rails::UseCase
78
+ attr_accessor :title, :content, :author, :skip_notifications, :publish
42
79
 
43
80
  validates :title, presence: true
44
81
  validates :content, presence: true
45
82
  validates :author, presence: true
46
83
 
47
- step :build_post
48
- step :save!
49
- step :notify_subscribers, unless: -> { skip_notifications }
84
+ failure :access_denied, message: 'No permission', unless: -> { author.can_publish_blog_posts? }
85
+ step :build_post
86
+ step :save!
87
+ succeed unless: -> { publish }
88
+ step :publish, do: -> { record.publish! }
89
+ step :notify_subscribers, unless: -> { skip_notifications }
50
90
 
51
91
 
52
92
  private def build_post
@@ -67,22 +107,67 @@ end
67
107
  Example usage of that UseCase:
68
108
 
69
109
  ```ruby
70
- result = CreateBlogPost.perform(
110
+ result = BlogPosts::Create.perform(
71
111
  title: 'Super Awesome Stuff!',
72
112
  content: 'Lorem Ipsum Dolor Sit Amet',
73
- created_by: current_user,
113
+ author: current_user,
74
114
  skip_notifications: false
75
115
  )
76
116
 
77
117
  puts result.inspect
78
- # => {
79
- # success: true,
80
- # record: BlogPost(...)
81
- # errors: [],
82
- # error: nil
83
- # }
118
+ => {
119
+ success: false, # Wether the UseCase ended successfully
120
+ record: BlogPost(...) # The value assigned to @record
121
+ errors: [], # List of validation errors
122
+ exception: Rails::UseCase::Error(...), # The exception raised by the UseCase
123
+ message: "...", # Error message
124
+ error_code: :save_failed # Error Code
125
+ }
84
126
  ```
85
127
 
128
+ - You can check whether a UseCase was successful via `result.success?`.
129
+ - You can access the value of `@record` via `result.record`.
130
+ - You can stop the UseCase process with a error message via throwing `Rails::UseCase::Error` exception.
131
+
132
+
133
+ ### Working with the result
134
+
135
+ The `perform` method of a UseCase returns an outcome object which contains a
136
+ `code` field with the error code or `:success` otherwise. This comes handy when
137
+ using in controller actions for example and is a great way to delegate the
138
+ business logic part of a controller action to the respective UseCase.
139
+ Everything the controller has to do, is to setup the params and dispatch the
140
+ result.
141
+
142
+ Given the Example above, here is the same call within a controller action with
143
+ an case statement.
144
+
145
+ ```ruby
146
+ class BlogPostsController < ApplicationController
147
+ # ...
148
+
149
+ def create
150
+ parameters = {
151
+ title: params[:post][:title],
152
+ content: params[:post][:content],
153
+ publish: params[:post][:publish],
154
+ author: current_user
155
+ }
156
+
157
+ case BlogPosts::Create.perform(parameters).code
158
+ when :success then redirect_to(outcome.record)
159
+ when :access_denied then render(:new, flash: { error: "Access Denied!" })
160
+ when :foo then redirect_to('/')
161
+ else render(:new, flash: { error: outcome.message })
162
+ end
163
+ end
164
+
165
+ # ...
166
+ end
167
+ ```
168
+
169
+ However this is not rails specific and can be used in any context.
170
+
86
171
 
87
172
  ## Behavior
88
173
 
@@ -8,8 +8,8 @@ module Rails
8
8
  extend ActiveSupport::Concern
9
9
 
10
10
  class_methods do
11
- def call(*args)
12
- new.call(*args)
11
+ def call(...)
12
+ new.call(...)
13
13
  end
14
14
 
15
15
  alias_method :perform, :call
data/lib/rails/service.rb CHANGED
@@ -70,7 +70,7 @@ module Rails
70
70
  @logger = Logger.new(STDOUT)
71
71
 
72
72
  @logger.formatter = proc do |severity, datetime, progname, msg|
73
- "[@service_name] #{msg}"
73
+ "[#{@service_name}] #{msg}"
74
74
  end
75
75
  end
76
76
 
@@ -121,8 +121,8 @@ module Rails
121
121
 
122
122
 
123
123
  # Allows call syntax on class level: SomeService.(some, args)
124
- def self.call(*args)
125
- new.(*args)
124
+ def self.call(...)
125
+ new.(...)
126
126
  end
127
127
 
128
128
  # Allows to use rails view helpers
@@ -4,19 +4,28 @@ module Rails
4
4
  class UseCase
5
5
  # Outcome of a UseCase
6
6
  class Outcome
7
- attr_reader :success, :errors, :record, :exception
7
+ attr_reader :success, :errors, :record, :exception, :message, :code
8
8
 
9
9
  # Constructor.
10
10
  # @param success [Boolean] Wether the UseCase was successful.
11
11
  # @param errors [Array|nil] ActiveModel::Validations error.
12
12
  # @param record [ApplicationRecord|nil] The main record of the use case.
13
13
  # @param exception [Rails::UseCase::Error|nil] The error which was raised.
14
- def initialize(success:, errors: nil, record: nil, exception: nil)
14
+ # @param message [String|nil] The error message.
15
+ # @param code [Symbol|String|nil] The error code.
16
+ # rubocop:disable Metrics/ParameterLists
17
+ def initialize(
18
+ success:, errors: nil, record: nil, exception: nil,
19
+ message: nil, code: nil
20
+ )
15
21
  @success = success
16
22
  @errors = errors
17
23
  @record = record
18
24
  @exception = exception
25
+ @message = message || exception&.message || errors&.full_messages
26
+ @code = success ? :success : code&.to_sym
19
27
  end
28
+ # rubocop:enable Metrics/ParameterLists
20
29
 
21
30
 
22
31
  # @return [Boolean] Whether the UseCase was successful.
@@ -34,14 +34,32 @@ module Rails
34
34
  # DSL to define a process step of the UseCase.
35
35
  # You can use if/unless with a lambda in the options
36
36
  # to conditionally skip the step.
37
+ #
37
38
  # @param name [Symbol]
38
39
  # @param options [Hash]
39
- def self.step(name, options = {})
40
+ def self.step(name = :inline, options = {}, &block)
40
41
  @steps ||= []
42
+
43
+ if block_given?
44
+ options[:do] = block
45
+ name = :inline
46
+ end
47
+
41
48
  @steps << { name: name.to_sym, options: options }
42
49
  end
43
50
 
44
51
 
52
+ def self.success(options = {})
53
+ step :success, options
54
+ end
55
+
56
+
57
+ def self.failure(code = nil, options = {})
58
+ options[:code] = code || options[:code] || :failure
59
+ step :failure, options
60
+ end
61
+
62
+
45
63
  # DSL to include a behavior.
46
64
  # @param mod [Module]
47
65
  def self.with(mod)
@@ -52,14 +70,32 @@ module Rails
52
70
  # Will run the steps of the use case.
53
71
  def process
54
72
  self.class.steps.each do |step|
73
+ # Check wether to skip when :if or :unless are set.
55
74
  next if skip_step?(step)
56
- next if send(step[:name])
57
75
 
58
- raise UseCase::Error, "Step #{step[:name]} returned false"
76
+ opts = step[:options]
77
+ name = step[:name]
78
+
79
+ # Handle failure and success steps.
80
+ return true if name == :success
81
+
82
+ fail!(code: opts[:code], message: opts[:message]) if name == :failure
83
+
84
+ # Run the lambda, when :do is set. Otherwise call the method.
85
+ next if opts[:do] ? instance_eval(&opts[:do]) : send(name)
86
+
87
+ # result is false, so we have a failure.
88
+ fail! code: :step_false, message: "Step '#{name}' returned false"
59
89
  end
60
90
  end
61
91
 
62
92
 
93
+ def fail!(code: nil, message: 'Failed')
94
+ @error_code = code
95
+ raise UseCase::Error, message
96
+ end
97
+
98
+
63
99
  # Checks whether to skip a step.
64
100
  # @param step [Hash]
65
101
  def skip_step?(step)
@@ -93,7 +129,7 @@ module Rails
93
129
  def break_when_invalid!
94
130
  return true if valid?
95
131
 
96
- raise UseCase::Error, errors.full_messages.join(', ')
132
+ fail! code: :validation_failed, message: errors.full_messages.join(', ')
97
133
  end
98
134
 
99
135
 
@@ -119,7 +155,7 @@ module Rails
119
155
  message: record.errors.full_messages.join(', ')
120
156
  )
121
157
 
122
- raise UseCase::Error, "#{record.class.name} is not valid"
158
+ fail! code: :save_failed, message: errors.full_messages.join(', ')
123
159
  end
124
160
 
125
161
 
@@ -140,7 +176,9 @@ module Rails
140
176
  success: false,
141
177
  record: @record,
142
178
  errors: errors,
143
- exception: error
179
+ exception: error,
180
+ message: error.message,
181
+ code: @error_code
144
182
  )
145
183
  end
146
184
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_use_case
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.7
4
+ version: 0.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Benjamin Klein
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-04-05 00:00:00.000000000 Z
11
+ date: 2021-10-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 4.1.0
19
+ version: 6.1.3
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 4.1.0
26
+ version: 6.1.3
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: railties
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 4.1.0
33
+ version: 6.1.3
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 4.1.0
40
+ version: 6.1.3
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: bundler-audit
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -195,7 +195,7 @@ homepage: https://github.com/phortx/rails-use-case
195
195
  licenses:
196
196
  - MIT
197
197
  metadata: {}
198
- post_install_message:
198
+ post_install_message:
199
199
  rdoc_options: []
200
200
  require_paths:
201
201
  - lib
@@ -210,8 +210,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
210
210
  - !ruby/object:Gem::Version
211
211
  version: '0'
212
212
  requirements: []
213
- rubygems_version: 3.0.3
214
- signing_key:
213
+ rubygems_version: 3.1.2
214
+ signing_key:
215
215
  specification_version: 4
216
216
  summary: Rails UseCase and Service classes
217
217
  test_files: []