the_help 2.0.0 → 3.3.0

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