rails_use_case 0.0.8 → 0.0.12

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: 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: []