the_help 2.0.0 → 3.3.0

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: f11ddd3d1fc5d239938188bda0cba4c3e57d86433fc39f5214dc9ae6aee875af
4
- data.tar.gz: e1a80305b54998dfb4c84e6ec8c1b0c49ccaedc0b21c969becfaca90a671ac60
3
+ metadata.gz: a75d7a8941930a8c0364a9ab46feb9d84ed033854f6f00f7c94b2edd7835a37a
4
+ data.tar.gz: 7a1155356c2e5d7b41aa7abae90cbf75debd78cfb2213cd1a73bf8e68250dc3d
5
5
  SHA512:
6
- metadata.gz: 0af2dbe4d5979ed8d525794f9797c0011a4535ebe7127afd3bddd2a5c11911c0b919c86b8527916a49ee0e29decf662a28c9317cdea935cd8366e1bf56d781e7
7
- data.tar.gz: '019aaca0b3c14e5d96ac5e0f661f5fb9cb8c04ddcbff254ae27f9c380c19a44019037ae80026dbb24604c0a2d09e8b50331f9e94b325515083ab1c67cb083594'
6
+ metadata.gz: 8b41c62f81ccca29c73cbdce4973c1fc7cbb9fd51c55e6b387cd7015c0280e5db1d38fd34d6b288ecca318782e53144364c0486df73ea15cebf66269a2b1c2dd
7
+ data.tar.gz: 339df6294d38b60184704eabdc219e8f5c960a3fd898d461aa4d7f885e0e13de0a2db1be9098a87510e9f2ce4e2c84f741f3d316ce5281c18c875bdf13fdcd46
@@ -0,0 +1,11 @@
1
+ # TheHelp Changelog #
2
+
3
+ ## 3.3.0 ##
4
+
5
+ * Calling `#stop!` with no arguments in a service definition will now check that a result was set
6
+ with either `#result.success` or `#result.error` and raise `TheHelp::NoResultError` if no result
7
+ was set.
8
+
9
+ * You can now call `#stop!` with both a `type:` and `value:` argument. `type:` can be either
10
+ `:error` (the default) or `:success`, and `value:` can be any object. Calling in this manner
11
+ will set the service result to the specified type and value.
@@ -1,50 +1,55 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- the_help (2.0.0)
4
+ the_help (3.3.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
- ast (2.4.0)
10
- byebug (11.0.1)
11
- diff-lcs (1.3)
12
- jaro_winkler (1.5.3)
13
- parallel (1.18.0)
14
- parser (2.6.5.0)
15
- ast (~> 2.4.0)
9
+ ast (2.4.1)
10
+ byebug (11.1.3)
11
+ diff-lcs (1.4.4)
12
+ parallel (1.19.2)
13
+ parser (2.7.2.0)
14
+ ast (~> 2.4.1)
16
15
  rainbow (3.0.0)
17
- rake (10.5.0)
16
+ rake (13.0.1)
17
+ regexp_parser (1.8.2)
18
+ rexml (3.2.4)
18
19
  rspec (3.9.0)
19
20
  rspec-core (~> 3.9.0)
20
21
  rspec-expectations (~> 3.9.0)
21
22
  rspec-mocks (~> 3.9.0)
22
- rspec-core (3.9.0)
23
- rspec-support (~> 3.9.0)
24
- rspec-expectations (3.9.0)
23
+ rspec-core (3.9.3)
24
+ rspec-support (~> 3.9.3)
25
+ rspec-expectations (3.9.3)
25
26
  diff-lcs (>= 1.2.0, < 2.0)
26
27
  rspec-support (~> 3.9.0)
27
- rspec-mocks (3.9.0)
28
+ rspec-mocks (3.9.1)
28
29
  diff-lcs (>= 1.2.0, < 2.0)
29
30
  rspec-support (~> 3.9.0)
30
- rspec-support (3.9.0)
31
- rubocop (0.75.1)
32
- jaro_winkler (~> 1.5.1)
31
+ rspec-support (3.9.4)
32
+ rubocop (0.93.1)
33
33
  parallel (~> 1.10)
34
- parser (>= 2.6)
34
+ parser (>= 2.7.1.5)
35
35
  rainbow (>= 2.2.2, < 4.0)
36
+ regexp_parser (>= 1.8)
37
+ rexml
38
+ rubocop-ast (>= 0.6.0)
36
39
  ruby-progressbar (~> 1.7)
37
- unicode-display_width (>= 1.4.0, < 1.7)
40
+ unicode-display_width (>= 1.4.0, < 2.0)
41
+ rubocop-ast (1.1.0)
42
+ parser (>= 2.7.1.5)
38
43
  ruby-progressbar (1.10.1)
39
- unicode-display_width (1.6.0)
40
- yard (0.9.20)
44
+ unicode-display_width (1.7.0)
45
+ yard (0.9.25)
41
46
 
42
47
  PLATFORMS
43
48
  ruby
44
49
 
45
50
  DEPENDENCIES
46
51
  byebug
47
- rake (~> 10.0)
52
+ rake (~> 13.0)
48
53
  rspec (~> 3.0)
49
54
  rubocop (~> 0.50)
50
55
  the_help!
data/README.md CHANGED
@@ -28,10 +28,128 @@ them.
28
28
  Make it easier to call a service by including
29
29
  [`TheHelp::ServiceCaller`](lib/the_help/service_caller.rb).
30
30
 
31
+ ### Service Results
32
+
33
+ Every service call will return an instance of `TheHelp::Service::Result`. Your service
34
+ implementation MUST set a result using either the `#success` or the `#error` methods, for example:
35
+
36
+ ```ruby
37
+ class MyService < TheHelp::Service
38
+ authorization_policy allow_all: true
39
+
40
+ input :foo
41
+
42
+ main do
43
+ if foo
44
+ result.success 'bar'
45
+ else
46
+ result.error 'sorry, that did not work'
47
+ end
48
+ end
49
+ end
50
+
51
+ result = MyService.call(context: {}, logger: logger, foo: false)
52
+ result.success?
53
+ #=> false
54
+ result.error?
55
+ #=> true
56
+ result.value
57
+ #=> 'sorry, that did not work'
58
+ result.value!
59
+ # raises the exception TheHelp::Service::ResultError with the message 'sorry, that did not work'
60
+
61
+ result = MyService.call(context: {}, logger: logger, foo: true)
62
+ result.success?
63
+ #=> true
64
+ result.error?
65
+ #=> false
66
+ result.value
67
+ #=> 'bar'
68
+ result.value!
69
+ #=> 'bar'
70
+
71
+ MyService.call(context: {}, logger: logger, foo: true) { |result|
72
+ break 'oops' if result.error?
73
+
74
+ result.value + ' baz'
75
+ }
76
+ #=> 'bar baz'
77
+ ```
78
+
79
+ When using the ServiceCaller interface, unless a block is provided, the `#call_service` will call
80
+ the `TheHelp::Service::Result#value!` method internally, and will either return the succesful
81
+ result value or raise an exception as appropriate.
82
+
83
+ ```ruby
84
+ call_service(MyService, foo: true)
85
+ #=> 'bar'
86
+
87
+ call_service(MyService, foo: false)
88
+ # raises the exception TheHelp::Service::ResultError with the message 'sorry, that did not work'
89
+
90
+ call_service(MyService, foo: true) { |result|
91
+ break 'oops' if result.error?
92
+
93
+ result.value + ' baz'
94
+ }
95
+ #=> 'bar baz'
96
+ ```
97
+
98
+ Finally, you can change the type of the exception that is raised when
99
+ `TheHelp::Service::Result#value!` is called on an error result by providing the exception itself
100
+ as the result value:
101
+
102
+ ```ruby
103
+ class MyService < TheHelp::Service
104
+ authorization_policy allow_all: true
105
+
106
+ input :foo
107
+
108
+ main do
109
+ if foo
110
+ result.success 'bar'
111
+ else
112
+ result.error ArgumentError.new('foo must be true')
113
+ end
114
+ end
115
+ end
116
+
117
+ call_service(MyService, foo: false)
118
+ # raises the exception ArgumentError with the message 'foo must be true'
119
+ ```
120
+
121
+ If you want to make sure the exception's backtrace points to the correct line of code, raise the
122
+ exception in a block provided to the `#error` method:
123
+
124
+ ```ruby
125
+ class MyService < TheHelp::Service
126
+ authorization_policy allow_all: true
127
+
128
+ input :foo
129
+
130
+ main do
131
+ if foo
132
+ result.success 'bar'
133
+ else
134
+ result.error { raise ArgumentError.new('foo must be true') }
135
+ end
136
+ end
137
+ end
138
+
139
+ call_service(MyService, foo: false)
140
+ # raises the exception ArgumentError with the message 'foo must be true'
141
+ ```
142
+
143
+ With the block form, the backtrace will point to the line where the exception was first raised
144
+ rather than to the `#value!` method, however all other code execution will continue until the
145
+ point where the `#value!` method is called (as long as the exception is a subtype of
146
+ `StandardError`.)
147
+
31
148
  ### Running Callbacks
32
149
 
33
- This library encourages you to pass in callbacks to a service rather than
34
- relying on a return value from the service call. For example:
150
+ In some cases a simple success or error result is not sufficient to describe the various results
151
+ about which a service may need to be able to inform its callers. In these cases, a callback style
152
+ of programming can be useful:
35
153
 
36
154
  ```ruby
37
155
  class Foo < TheHelp::Service
@@ -89,8 +207,10 @@ class GetSomeWidgets < TheHelp::Service
89
207
  invalid_customer.call
90
208
  no_widgets_found.call
91
209
  do_some_important_cleanup_for_invalid_customers
210
+ result.error 'invalid customer'
92
211
  else
93
212
  #...
213
+ result.success some_widgets
94
214
  end
95
215
  end
96
216
 
@@ -122,8 +242,10 @@ class GetSomeWidgets < TheHelp::Service
122
242
  run_callback(invalid_customer)
123
243
  run_callback(no_widgets_found)
124
244
  do_some_important_cleanup_for_invalid_customers
245
+ result.error 'invalid customer'
125
246
  else
126
247
  #...
248
+ result.success some_widgets
127
249
  end
128
250
  end
129
251
 
@@ -6,5 +6,6 @@ module TheHelp
6
6
  class ServiceNotImplementedError < StandardError; end
7
7
  class NotAuthorizedError < RuntimeError; end
8
8
  class NoResultError < StandardError; end
9
+ class ResultError < StandardError; end
9
10
  end
10
11
  end
@@ -150,18 +150,42 @@ module TheHelp
150
150
  self
151
151
  end
152
152
 
153
- def input(name, **options)
154
- attr_accessor name, make_private: true
155
- if options.key?(:default)
156
- required_inputs.delete(name)
157
- define_method(name) do
158
- instance_variable_get("@#{name}") || options[:default]
159
- end
153
+ # Defines a service input
154
+ #
155
+ # The specified input becomes a named parameter for the service's `#call` method.
156
+ #
157
+ # @param name [Symbol] This becomes the name of the input parameter
158
+ #
159
+ # @param block [Proc] If a block is provided, the contents of the block will be executed in
160
+ # the scope of the service instance in order to provide the default
161
+ # value of the input. This is different than providing a Proc to the
162
+ # `:default` option, which would simply return the Proc itself as the
163
+ # default value rather than calling it.
164
+ #
165
+ # @option options [Object] :default If specified (and no block is given), this becomes the
166
+ # literal default value for the input.
167
+ def input(name, **options, &block)
168
+ if options.key?(:default) || block
169
+ make_optional_input(name, options[:default], &block)
160
170
  else
171
+ attr_accessor name, make_private: true
161
172
  required_inputs << name
162
173
  end
163
174
  self
164
175
  end
176
+
177
+ private
178
+
179
+ def make_optional_input(name, default, &block)
180
+ attr_writer name
181
+ private "#{name}="
182
+ default_routine = block || -> { default }
183
+ define_method("_#{name}", &default_routine)
184
+ define_method(name) do
185
+ instance_variable_get("@#{name}") || send("_#{name}")
186
+ end
187
+ required_inputs.delete(name)
188
+ end
165
189
  end
166
190
 
167
191
  # Holds the result of running a service as well as the execution status
@@ -194,12 +218,30 @@ module TheHelp
194
218
  freeze
195
219
  end
196
220
 
197
- def error(value)
198
- self.value = value
221
+ def error(value = nil, &block)
222
+ self.value = if block_given?
223
+ begin
224
+ self.value = block.call
225
+ rescue StandardError => e
226
+ e
227
+ end
228
+ else
229
+ value
230
+ end
199
231
  self.status = :error
200
232
  freeze
201
233
  end
202
234
 
235
+ def value!
236
+ raise TheHelp::NoResultError if pending?
237
+
238
+ raise value if error? && value.is_a?(Exception)
239
+
240
+ raise TheHelp::ResultError.new(value) if error?
241
+
242
+ value
243
+ end
244
+
203
245
  private
204
246
 
205
247
  attr_writer :status, :value
@@ -221,16 +263,21 @@ module TheHelp
221
263
  # @return [TheHelp::Service::Result]
222
264
  def call
223
265
  validate_service_definition
266
+
224
267
  catch(:stop) do
225
268
  authorize
226
269
  log_service_call
227
270
  main
228
271
  check_result!
229
- self.block_result = yield result if block_given?
230
272
  end
273
+
274
+ self.block_result = yield result if block_given?
275
+
231
276
  throw :stop if stop_caller
277
+
232
278
  return block_result if block_given?
233
- return result
279
+
280
+ result
234
281
  end
235
282
 
236
283
  private
@@ -273,11 +320,18 @@ module TheHelp
273
320
  return if authorized?
274
321
  logger.warn("Unauthorized attempt to access #{self.class.name}/#{__id__} " \
275
322
  "as #{context.inspect}")
276
- run_callback(not_authorized, service: self.class, context: context)
323
+ result.error run_callback(not_authorized, service: self.class, context: context)
277
324
  stop!
278
325
  end
279
326
 
280
- def stop!
327
+ def stop!(type: :error, value: nil)
328
+ if value.nil?
329
+ check_result!
330
+ elsif type == :success
331
+ result.success value
332
+ else
333
+ result.error value
334
+ end
281
335
  throw :stop
282
336
  end
283
337
 
@@ -39,7 +39,9 @@ module TheHelp
39
39
  }.merge(args)
40
40
  service_logger.debug("#{self.class.name}/#{__id__} called service " \
41
41
  "#{service.name}")
42
- service.call(**service_args, &block)
42
+ return service.call(**service_args, &block) if block_given?
43
+
44
+ service.call(**service_args).value!
43
45
  end
44
46
  end
45
47
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TheHelp
4
- VERSION = '2.0.0'
4
+ VERSION = '3.3.0'
5
5
  end
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
23
23
  spec.require_paths = ['lib']
24
24
 
25
25
  spec.add_development_dependency 'byebug'
26
- spec.add_development_dependency 'rake', '~> 10.0'
26
+ spec.add_development_dependency 'rake', '~> 13.0'
27
27
  spec.add_development_dependency 'rspec', '~> 3.0'
28
28
  spec.add_development_dependency 'rubocop', '~> 0.50'
29
29
  spec.add_development_dependency 'yard'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: the_help
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Wilger
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-10-28 00:00:00.000000000 Z
11
+ date: 2020-10-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: byebug
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '10.0'
33
+ version: '13.0'
34
34
  type: :development
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: '10.0'
40
+ version: '13.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -93,6 +93,7 @@ files:
93
93
  - ".rubocop.yml"
94
94
  - ".tool-versions"
95
95
  - ".travis.yml"
96
+ - CHANGELOG.md
96
97
  - CODE_OF_CONDUCT.md
97
98
  - Gemfile
98
99
  - Gemfile.lock