rails_use_case 0.0.8 → 0.0.12

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: 789a956d7cf0a8545c91465e9a664e44aa5ae79481127f954cf4e7a4b2b90ec4
4
- data.tar.gz: 9be655cf0d0c3e16c81ce8700af3b2952ba82e3b6a1dc2b28c87880ba8c6e281
3
+ metadata.gz: c46cd0289d56e4cf86a62f9e1dd780b5fe311c9f7426dfdb942cab5a1b8b8b74
4
+ data.tar.gz: cd02462628e21a6fbda7bed59f237d0b1fdbf1a082d148747404ded5c2b0e365
5
5
  SHA512:
6
- metadata.gz: 0a01d5e6930473a0140ce41ca2b0d6744c4bc4af1a9d425afe6416a639de96b40410a1d67c1858cb439d7813553a6edc32712754fd099d541cf81a6088c05e70
7
- data.tar.gz: 661ae29ab31c2b4b69b3843758c36e5bbab74392dfc621375aa6ac9cb835c15a1f3e83cb5af5e1c04720504593e1d7aff903d997e7d1235e6d68049e52ffe984
6
+ metadata.gz: 6bd525db88d52a6b9563fed40b80e69d9bc035f115fa77ae6a5668688df7576d5426e4f8caba222986e741b4fa61c9790b75751b582913b009217089998fd8ac
7
+ data.tar.gz: 2274ca1dfa0312c2bd9f1e2cc8245665bde540b30919c4eefe0875e3d0eba1a3609d2a55c4acac40849e8d237a20bd228ad6aabcaabac8f2c4c50f79cbd9e1bc
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,37 +22,88 @@ 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
71
+ argument will try to save that record or raises an exception. Also the
72
+ `@record` will automatically passed into the outcome.
73
+
74
+ You can either set the `@record` manually or via the `record` method. This comes
75
+ in two flavors:
76
+
77
+ Either passing the name of a param as symbol. Let's assume the UseCase
78
+ has a parameter called `user` (defined via `attr_accessor`), then you can assign
79
+ the user to `@record` by adding `record :user` to your use case.
80
+
81
+ The alternative way is to pass a block which returns the value for `@record`
82
+ like in the example UseCase below.
35
83
 
36
84
 
37
85
  ### Example UseCase
38
86
 
39
87
  ```ruby
40
- class CreateBlogPost < Rails::UseCase
41
- attr_accessor :title, :content, :author, :skip_notifications
88
+ class BlogPosts::Create < Rails::UseCase
89
+ attr_accessor :title, :content, :author, :skip_notifications, :publish
42
90
 
43
91
  validates :title, presence: true
44
92
  validates :content, presence: true
45
93
  validates :author, presence: true
46
94
 
47
- step :build_post
48
- step :save!
49
- step :notify_subscribers, unless: -> { skip_notifications }
95
+ record { BlogPost.new }
96
+
97
+ failure :access_denied, message: 'No permission', unless: -> { author.can_publish_blog_posts? }
98
+ step :assign_attributes
99
+ step :save!
100
+ succeed unless: -> { publish }
101
+ step :publish, do: -> { record.publish! }
102
+ step :notify_subscribers, unless: -> { skip_notifications }
50
103
 
51
104
 
52
- private def build_post
53
- @record = BlogPost.new(
105
+ private def assign_attributes
106
+ @record.assign_attributes(
54
107
  title: title,
55
108
  content: content,
56
109
  created_by: author,
@@ -67,22 +120,69 @@ end
67
120
  Example usage of that UseCase:
68
121
 
69
122
  ```ruby
70
- result = CreateBlogPost.perform(
123
+ result = BlogPosts::Create.perform(
71
124
  title: 'Super Awesome Stuff!',
72
125
  content: 'Lorem Ipsum Dolor Sit Amet',
73
- created_by: current_user,
126
+ author: current_user,
74
127
  skip_notifications: false
75
128
  )
76
129
 
77
130
  puts result.inspect
78
- # => {
79
- # success: true,
80
- # record: BlogPost(...)
81
- # errors: [],
82
- # error: nil
83
- # }
131
+ => {
132
+ success: false, # Wether the UseCase ended successfully
133
+ record: BlogPost(...) # The value assigned to @record
134
+ errors: [], # List of validation errors
135
+ exception: Rails::UseCase::Error(...), # The exception raised by the UseCase
136
+ message: "...", # Error message
137
+ error_code: :save_failed # Error Code
138
+ }
139
+ ```
140
+
141
+ - You can check whether a UseCase was successful via `result.success?`.
142
+ - You can access the value of `@record` via `result.record`.
143
+ - You can stop the UseCase process with a error message via throwing `Rails::UseCase::Error` exception.
144
+
145
+
146
+ ### Working with the result
147
+
148
+ The `perform` method of a UseCase returns an outcome object which contains a
149
+ `code` field with the error code or `:success` otherwise. This comes handy when
150
+ using in controller actions for example and is a great way to delegate the
151
+ business logic part of a controller action to the respective UseCase.
152
+ Everything the controller has to do, is to setup the params and dispatch the
153
+ result.
154
+
155
+ Given the Example above, here is the same call within a controller action with
156
+ an case statement.
157
+
158
+ ```ruby
159
+ class BlogPostsController < ApplicationController
160
+ # ...
161
+
162
+ def create
163
+ parameters = {
164
+ title: params[:post][:title],
165
+ content: params[:post][:content],
166
+ publish: params[:post][:publish],
167
+ author: current_user
168
+ }
169
+
170
+ outcome = BlogPosts::Create.perform(parameters).code
171
+
172
+ case outcome.code
173
+ when :success then redirect_to(outcome.record)
174
+ when :access_denied then render(:new, flash: { error: "Access Denied!" })
175
+ when :foo then redirect_to('/')
176
+ else render(:new, flash: { error: outcome.message })
177
+ end
178
+ end
179
+
180
+ # ...
181
+ end
84
182
  ```
85
183
 
184
+ However this is not rails specific and can be used in any context.
185
+
86
186
 
87
187
  ## Behavior
88
188
 
@@ -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
@@ -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)
@@ -49,17 +67,51 @@ module Rails
49
67
  end
50
68
 
51
69
 
70
+ # DSL to set the record source.
71
+ # @param [Symbol|nil] Name of the param.
72
+ # @yields
73
+ def self.record(param = nil, &block)
74
+ block = -> { send(param.to_sym) } unless block_given?
75
+
76
+ define_method(:determine_record, &block)
77
+ end
78
+
79
+
52
80
  # Will run the steps of the use case.
53
81
  def process
82
+ @record = determine_record if respond_to?(:determine_record)
83
+ run_steps
84
+ end
85
+
86
+
87
+ def run_steps
54
88
  self.class.steps.each do |step|
89
+ # Check wether to skip when :if or :unless are set.
55
90
  next if skip_step?(step)
56
- next if send(step[:name])
57
91
 
58
- raise UseCase::Error, "Step #{step[:name]} returned false"
92
+ opts = step[:options]
93
+ name = step[:name]
94
+
95
+ # Handle failure and success steps.
96
+ return true if name == :success
97
+
98
+ fail!(code: opts[:code], message: opts[:message]) if name == :failure
99
+
100
+ # Run the lambda, when :do is set. Otherwise call the method.
101
+ next if opts[:do] ? instance_eval(&opts[:do]) : send(name)
102
+
103
+ # result is false, so we have a failure.
104
+ fail! code: :step_false, message: "Step '#{name}' returned false"
59
105
  end
60
106
  end
61
107
 
62
108
 
109
+ def fail!(code: nil, message: 'Failed')
110
+ @error_code = code
111
+ raise UseCase::Error, message
112
+ end
113
+
114
+
63
115
  # Checks whether to skip a step.
64
116
  # @param step [Hash]
65
117
  def skip_step?(step)
@@ -93,7 +145,7 @@ module Rails
93
145
  def break_when_invalid!
94
146
  return true if valid?
95
147
 
96
- raise UseCase::Error, errors.full_messages.join(', ')
148
+ fail! code: :validation_failed, message: errors.full_messages.join(', ')
97
149
  end
98
150
 
99
151
 
@@ -119,7 +171,7 @@ module Rails
119
171
  message: record.errors.full_messages.join(', ')
120
172
  )
121
173
 
122
- raise UseCase::Error, "#{record.class.name} is not valid"
174
+ fail! code: :save_failed, message: errors.full_messages.join(', ')
123
175
  end
124
176
 
125
177
 
@@ -140,7 +192,9 @@ module Rails
140
192
  success: false,
141
193
  record: @record,
142
194
  errors: errors,
143
- exception: error
195
+ exception: error,
196
+ message: error.message,
197
+ code: @error_code
144
198
  )
145
199
  end
146
200
  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.8
4
+ version: 0.0.12
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-06 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: []