the_help 1.4.2 → 1.5.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: 16fd1e45c719b154afcdca402e8c5a049fd521e9a11c6a1c54f7abc9fc3e13fc
4
- data.tar.gz: 9b9cac4ed3fdfcb4ad9f5359aa76f9a3298592017a9a7733be5d64d1712e6a0b
3
+ metadata.gz: 6fdee5c4c5d8c3c2d67bbb8abf7fb42379961924c59faa7f36da7575a085d325
4
+ data.tar.gz: 96464f2494174451d4581f984050fc258901b010a27556a3dc19a555ba2d99ab
5
5
  SHA512:
6
- metadata.gz: ea9b7907840050fa2017b2bc4d055f948524d52141c71d70dcbbd04800eb0a911ff2744f4278a777a6a4509f5273cb68f8c9685079b14cc027bdaa6ed5c687c5
7
- data.tar.gz: ca5962b02c675ae30a2de84c1c3b782ed56f1a672f0be39256af50273eca28fa6ac8e488e336ab705bf42ccc9501a70dec73ceaca0b774f52d4713d32181ba7d
6
+ metadata.gz: 800152351fd6ca49eab793748e48030cdb73f14c53f8ad4cd419be8a28e1fc20291a40948ee764744b924b53b7a96a0f13c109024cd1f5ebf0cb2bc09326c1bc
7
+ data.tar.gz: 3c11bcb7dbcf769a5045f3a42bcf52cec4b5a06ecee8015eda56554ae67d7f4604bb1df15914f09bf4c4991eb74201bc47120c675659f85e0c4c05b6cb576b2b
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- the_help (1.4.2)
4
+ the_help (1.5.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # TheHelp
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/the_help`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
3
+ TheHelp is a framework for developing service objects in a way that encourages
4
+ adherence to the [Single Responsibility Principle][SRP] and [Tell Don't
5
+ Ask][TDA]
6
6
 
7
7
  ## Installation
8
8
 
@@ -22,7 +22,122 @@ Or install it yourself as:
22
22
 
23
23
  ## Usage
24
24
 
25
- TODO: Write usage instructions here
25
+ Create subclasses of [`TheHelp::Service`][lib/the_help/service.rb] and call
26
+ them.
27
+
28
+ Make it easier to call a service by including
29
+ [`TheHelp::ServiceCaller`][lib/the_help/service_caller.rb].
30
+
31
+ ### Running Callbacks
32
+
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:
35
+
36
+ ```ruby
37
+ class Foo < TheHelp::Service
38
+ authorization_policy allow_all: true
39
+
40
+ main do
41
+ call_service(GetSomeWidgets,
42
+ customer_id: 12345,
43
+ each_widget: method(:process_widget),
44
+ invalid_customer: method(:no_customer),
45
+ no_widgets_found: method(:no_widgets))
46
+ do_something_else
47
+ end
48
+
49
+ private
50
+
51
+ def process_widget(widget)
52
+ # do something with it
53
+ end
54
+
55
+ def invalid_customer
56
+ # handle this case
57
+ stop!
58
+ end
59
+
60
+ def no_widgets
61
+ # handle this case
62
+ end
63
+
64
+ def do_something_else
65
+ # ...
66
+ end
67
+ end
68
+ ```
69
+
70
+ When writing a service that accepts callbacks like this, do not simply run
71
+ `#call` on the callback that was passed in. Instead you must use the
72
+ `#run_callback` method. This ensures that, if the callback method you pass in
73
+ tries to halt the execution of the service, it will behave as expected.
74
+
75
+ In the above service, it is clear that the intention is to stop executing the
76
+ `Foo` service in the case where `GetSomeWidgets` reports back that the customer
77
+ was invalid. However, if `GetSomeWidgets` is implemented as:
78
+
79
+ ```ruby
80
+ class GetSomeWidgets < TheHelp::Service
81
+ input :customer_id
82
+ input :each_widget
83
+ input :invalid_customer
84
+ input :no_widgets_found
85
+
86
+ authorization_policy allow_all: true
87
+
88
+ main do
89
+ set_some_stuff_up
90
+ if customer_invalid?
91
+ invalid_customer.call
92
+ no_widgets_found.call
93
+ do_some_important_cleanup_for_invalid_customers
94
+ else
95
+ #...
96
+ end
97
+ end
98
+
99
+ #...
100
+ end
101
+ ```
102
+
103
+ then the problem is that the call to `#stop!` in the `Foo#invalid_customer`
104
+ callback will not just stop the `Foo` service, it will also stop the
105
+ `GetSomeWidgets` service at the point where the callback is executed (because it
106
+ uses `throw` behind the scenes.) This would cause the
107
+ `do_some_important_cleanup_for_invalid_customers` method to never be called.
108
+
109
+ You can protect yourself from this by implementing `GetSomeWidgets` like this,
110
+ instead:
111
+
112
+ ```ruby
113
+ class GetSomeWidgets < TheHelp::Service
114
+ input :customer_id
115
+ input :each_widget
116
+ input :invalid_customer
117
+ input :no_widgets_found
118
+
119
+ authorization_policy allow_all: true
120
+
121
+ main do
122
+ set_some_stuff_up
123
+ if customer_invalid?
124
+ run_callback(invalid_customer)
125
+ run_callback(no_widgets_found)
126
+ do_some_important_cleanup_for_invalid_customers
127
+ else
128
+ #...
129
+ end
130
+ end
131
+
132
+ #...
133
+ end
134
+ ```
135
+
136
+ This will ensure that callbacks only stop the service that provides them, not
137
+ the service that calls them. (If you really do need to allow the calling service
138
+ to stop the execution of the inner service, you could raise an exception or
139
+ throw a symbol other than `:stop`; but do so with caution, since it may have
140
+ unintended consequences further down the stack.)
26
141
 
27
142
  ## Development
28
143
 
@@ -41,3 +156,6 @@ The gem is available as open source under the terms of the [MIT License](https:/
41
156
  ## Code of Conduct
42
157
 
43
158
  Everyone interacting in the TheHelp project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/jwilger/the_help/blob/master/CODE_OF_CONDUCT.md).
159
+
160
+ [SRP]: https://en.wikipedia.org/wiki/Single_responsibility_principle
161
+ [TDA]: https://martinfowler.com/bliki/TellDontAsk.html
@@ -29,8 +29,8 @@ module TheHelp
29
29
  # end
30
30
  # end
31
31
  #
32
- # callback(:message_sent) do
33
- # # do something really important, I'm sure
32
+ # callback(:message_sent) do |message|
33
+ # # do something really important with `message`, I'm sure
34
34
  # end
35
35
  # end
36
36
  #
@@ -49,11 +49,12 @@ module TheHelp
49
49
  #
50
50
  # class SendWelcomeMessage < TheHelp::Service
51
51
  # input :user
52
- # input :success, default: ->() { }
52
+ # input :success, default: ->(message) { }
53
53
  #
54
54
  # main do
55
- # # whatever
56
- # success.call
55
+ # message = 'Hello, world!'
56
+ # # do something with message...
57
+ # run_callback(success, message)
57
58
  # end
58
59
  # end
59
60
  #
@@ -96,6 +97,7 @@ module TheHelp
96
97
  # service_result
97
98
  # #=> :the_service_result
98
99
  #
100
+ # @note See README section "Running Callbacks"
99
101
  class Service
100
102
  include ProvidesCallbacks
101
103
  include ServiceCaller
@@ -189,6 +191,7 @@ module TheHelp
189
191
  self.logger = logger
190
192
  self.not_authorized = not_authorized
191
193
  self.inputs = inputs
194
+ self.stop_caller = false
192
195
  end
193
196
 
194
197
  def call
@@ -199,6 +202,7 @@ module TheHelp
199
202
  main
200
203
  self.block_result = yield result if block_given?
201
204
  end
205
+ throw :stop if stop_caller
202
206
  return block_result if block_given?
203
207
  return result if result_set?
204
208
  self
@@ -206,7 +210,8 @@ module TheHelp
206
210
 
207
211
  private
208
212
 
209
- attr_accessor :context, :logger, :not_authorized, :block_result
213
+ attr_accessor :context, :logger, :not_authorized, :block_result,
214
+ :stop_caller
210
215
  attr_writer :result
211
216
  attr_reader :inputs
212
217
 
@@ -243,8 +248,8 @@ module TheHelp
243
248
  return if authorized?
244
249
  logger.warn("Unauthorized attempt to access #{self.class.name} " \
245
250
  "as #{context.inspect}")
246
- not_authorized.call(service: self.class, context: context)
247
- throw :stop
251
+ run_callback(not_authorized, service: self.class, context: context)
252
+ stop!
248
253
  end
249
254
 
250
255
  def stop!
@@ -259,5 +264,14 @@ module TheHelp
259
264
  def result_set?
260
265
  defined?(@result)
261
266
  end
267
+
268
+ def run_callback(callback, *args)
269
+ continue = false
270
+ continue = catch(:stop) do
271
+ callback.call(*args)
272
+ true
273
+ end
274
+ self.stop_caller ||= !continue
275
+ end
262
276
  end
263
277
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TheHelp
4
- VERSION = '1.4.2'
4
+ VERSION = '1.5.0'
5
5
  end
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: 1.4.2
4
+ version: 1.5.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: 2018-08-31 00:00:00.000000000 Z
11
+ date: 2018-09-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake