subroutine 0.5.0 → 0.5.1
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 +4 -4
- data/.travis.yml +1 -17
- data/README.md +23 -41
- data/lib/subroutine/auth.rb +7 -1
- data/lib/subroutine/op.rb +9 -2
- data/lib/subroutine/version.rb +1 -1
- data/test/subroutine/auth_test.rb +1 -1
- data/test/subroutine/base_test.rb +33 -12
- data/test/subroutine/type_caster_test.rb +10 -10
- data/test/support/ops.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3acb651d981b9e158e41c5e36b9fcf2fc644fb51
|
4
|
+
data.tar.gz: ff3571355496f5202702a26ab935a1d7944c73a3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e651e0bbbcb6a355d6f07613becee56d67c08b7016b72660cda62608956a0b7db58006ed8ff45b58346ec827b02b8af2d1b2b6ad191d55ccb42c9935f24a4dff
|
7
|
+
data.tar.gz: 9fbb7f7d91c60a1c71ac4a965179efd9f45a665e93bc9c9bfb70da2ef0d07d06cdebeb2ef3526f968477c71c7eaa4515b20c097aa5bf66d8cdbd5141150a11f9
|
data/.travis.yml
CHANGED
@@ -2,7 +2,6 @@ language: ruby
|
|
2
2
|
sudo: false
|
3
3
|
|
4
4
|
rvm:
|
5
|
-
- 1.9.3
|
6
5
|
- 2.0.0
|
7
6
|
- 2.1.1
|
8
7
|
- jruby
|
@@ -10,19 +9,4 @@ rvm:
|
|
10
9
|
gemfile:
|
11
10
|
- gemfiles/am40.gemfile
|
12
11
|
- gemfiles/am41.gemfile
|
13
|
-
- gemfiles/am42.gemfile
|
14
|
-
|
15
|
-
matrix:
|
16
|
-
exclude:
|
17
|
-
- rvm: 1.8.7
|
18
|
-
gemfile: gemfiles/am40.gemfile
|
19
|
-
- rvm: 1.8.7
|
20
|
-
gemfile: gemfiles/am41.gemfile
|
21
|
-
- rvm: 1.8.7
|
22
|
-
gemfile: gemfiles/am42.gemfile
|
23
|
-
- rvm: 1.9.2
|
24
|
-
gemfile: gemfiles/am40.gemfile
|
25
|
-
- rvm: 1.9.2
|
26
|
-
gemfile: gemfiles/am41.gemfile
|
27
|
-
- rvm: 1.9.2
|
28
|
-
gemfile: gemfiles/am42.gemfile
|
12
|
+
- gemfiles/am42.gemfile
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Subroutine
|
2
2
|
|
3
|
-
A gem that provides an interface for creating feature-driven operations.
|
3
|
+
A gem that provides an interface for creating feature-driven operations. You've probably heard at least one of these terms: "service objects", "form objects", or maybe even "commands". Subroutine calls these "ops" and really it's just about enabling clear, concise, testable, and meaningful code.
|
4
4
|
|
5
5
|
## Examples
|
6
6
|
|
@@ -17,21 +17,18 @@ class SignupOp < ::Subroutine::Op
|
|
17
17
|
validates :email, presence: true
|
18
18
|
validates :password, presence: true
|
19
19
|
|
20
|
-
|
20
|
+
outputs :signed_up_user
|
21
21
|
|
22
22
|
protected
|
23
23
|
|
24
24
|
def perform
|
25
|
-
u =
|
26
|
-
u.save!
|
27
|
-
|
25
|
+
u = create_user!
|
28
26
|
deliver_welcome_email!(u)
|
29
27
|
|
30
|
-
|
31
|
-
true
|
28
|
+
output :signed_up_user, u
|
32
29
|
end
|
33
30
|
|
34
|
-
def
|
31
|
+
def create_user
|
35
32
|
User.new(params)
|
36
33
|
end
|
37
34
|
|
@@ -106,7 +103,7 @@ class Api::Controller < ApplicationController
|
|
106
103
|
end
|
107
104
|
```
|
108
105
|
|
109
|
-
With ops, your controllers are essentially just connections between routes, operations, and
|
106
|
+
With ops, your controllers are essentially just connections between routes, operations, and whatever you use to build responses.
|
110
107
|
|
111
108
|
```ruby
|
112
109
|
class UsersController < ::Api::Controller
|
@@ -201,14 +198,9 @@ class MyOp < ::Subroutine::Op
|
|
201
198
|
end
|
202
199
|
|
203
200
|
def validate_first_name_is_not_bob
|
204
|
-
|
205
|
-
|
206
|
-
if first_name.downcase == 'bob'
|
201
|
+
if field_provided?(:first_name) && first_name.downcase == 'bob'
|
207
202
|
errors.add(:first_name, 'should not be bob')
|
208
|
-
return false
|
209
203
|
end
|
210
|
-
|
211
|
-
true
|
212
204
|
end
|
213
205
|
end
|
214
206
|
```
|
@@ -224,7 +216,6 @@ class MyOp < ::Subroutine::Op
|
|
224
216
|
puts params.inspect
|
225
217
|
puts defaults.inspect
|
226
218
|
puts params_with_defaults.inspect
|
227
|
-
true
|
228
219
|
end
|
229
220
|
end
|
230
221
|
|
@@ -241,20 +232,18 @@ MyOp.submit(name: "foobar")
|
|
241
232
|
|
242
233
|
#### Execution
|
243
234
|
|
244
|
-
Every op must implement a `perform`
|
245
|
-
|
246
|
-
while falsy values are assumed to be failures. In general, returning `true` at the end of the perform method is desired.
|
235
|
+
Every op must implement a `perform` method. This is the method which will be executed if all validations pass.
|
236
|
+
When the the `perform` method is complete, the Op determins success based on whether `errors` is empty.
|
247
237
|
|
248
238
|
```ruby
|
249
|
-
class
|
239
|
+
class MyFailingOp < ::Subroutine::Op
|
250
240
|
field :first_name
|
251
241
|
validates :first_name, presence: true
|
252
242
|
|
253
243
|
protected
|
254
244
|
|
255
245
|
def perform
|
256
|
-
|
257
|
-
true
|
246
|
+
errors.add(:base, "This will never succeed")
|
258
247
|
end
|
259
248
|
|
260
249
|
end
|
@@ -266,10 +255,10 @@ Notice we do not declare `perform` as a public method. This is to ensure the "pu
|
|
266
255
|
|
267
256
|
Reporting errors is very important in Subroutine Ops since these can be used as form objects. Errors can be reported a couple different ways:
|
268
257
|
|
269
|
-
1. `errors.add(:key, :error)` That is, the way you add errors to an ActiveModel object.
|
258
|
+
1. `errors.add(:key, :error)` That is, the way you add errors to an ActiveModel object.
|
270
259
|
2. `inherit_errors(error_object_or_activemodel_object)` Same as `errors.add`, but it iterates an existing error hash and inherits the errors. As part of this iteration,
|
271
|
-
it checks whether the key in the provided error_object matches a field (or
|
272
|
-
that field, but if there is not, the error will be placed on `:base`.
|
260
|
+
it checks whether the key in the provided error_object matches a field (or alias of a field) in our op. If there is a match, the error will be placed on
|
261
|
+
that field, but if there is not, the error will be placed on `:base`.
|
273
262
|
|
274
263
|
```ruby
|
275
264
|
class MyOp < ::Subroutine::Op
|
@@ -283,12 +272,12 @@ class MyOp < ::Subroutine::Op
|
|
283
272
|
|
284
273
|
if first_name == 'bill'
|
285
274
|
errors.add(:first_name, 'cannot be bill')
|
286
|
-
return
|
275
|
+
return
|
287
276
|
end
|
288
277
|
|
289
278
|
if first_name == 'john'
|
290
279
|
errors.add(:first_name, 'cannot be john')
|
291
|
-
|
280
|
+
return
|
292
281
|
end
|
293
282
|
|
294
283
|
unless _user.valid?
|
@@ -296,10 +285,7 @@ class MyOp < ::Subroutine::Op
|
|
296
285
|
# if there are :first_name or :firstname errors on _user, they will be added to our :first_name
|
297
286
|
# if there are :last_name, :lastname, or :surname errors on _user, they will be added to our :last_name
|
298
287
|
inherit_errors(_user)
|
299
|
-
return false
|
300
288
|
end
|
301
|
-
|
302
|
-
true
|
303
289
|
end
|
304
290
|
|
305
291
|
def _user
|
@@ -317,7 +303,7 @@ The `Subroutine::Op` class' `submit` and `submit!` methods have identical signat
|
|
317
303
|
|
318
304
|
```ruby
|
319
305
|
op = MyOp.submit({foo: 'bar'})
|
320
|
-
# if the op succeeds it will be returned, otherwise
|
306
|
+
# if the op succeeds it will be returned, otherwise false will be returned.
|
321
307
|
```
|
322
308
|
|
323
309
|
#### Via the class' `submit!` method
|
@@ -364,8 +350,6 @@ class UserUpdateOp < ::Subroutine::Op
|
|
364
350
|
first_name: first_name,
|
365
351
|
last_name: last_name
|
366
352
|
)
|
367
|
-
|
368
|
-
true
|
369
353
|
end
|
370
354
|
end
|
371
355
|
```
|
@@ -380,8 +364,6 @@ class RecordTouchOp < ::Subroutine::Op
|
|
380
364
|
|
381
365
|
def perform
|
382
366
|
record.touch
|
383
|
-
|
384
|
-
true
|
385
367
|
end
|
386
368
|
end
|
387
369
|
```
|
@@ -402,8 +384,6 @@ class SayHiOp < ::Subroutine::Op
|
|
402
384
|
|
403
385
|
def perform
|
404
386
|
puts "#{current_user.name} says: #{say_what}"
|
405
|
-
|
406
|
-
true
|
407
387
|
end
|
408
388
|
end
|
409
389
|
```
|
@@ -435,7 +415,11 @@ class AccountSetSecretOp < ::Subroutine::Op
|
|
435
415
|
|
436
416
|
require_user!
|
437
417
|
authorize :authorize_first_name_is_john
|
438
|
-
|
418
|
+
|
419
|
+
# If you use a policy-based authorization framework like pundit:
|
420
|
+
# `policy` is a shortcut for the following:
|
421
|
+
# authorize -> { unauthorized! unless policy.can_set_secret? }
|
422
|
+
|
439
423
|
policy :can_set_secret?
|
440
424
|
|
441
425
|
string :secret
|
@@ -446,8 +430,6 @@ class AccountSetSecretOp < ::Subroutine::Op
|
|
446
430
|
def perform
|
447
431
|
account.secret = secret
|
448
432
|
current_user.save!
|
449
|
-
|
450
|
-
true
|
451
433
|
end
|
452
434
|
|
453
435
|
def authorize_first_name_is_john
|
@@ -478,7 +460,7 @@ Subroutine::Factory.define :signup do
|
|
478
460
|
inputs :password, "password123"
|
479
461
|
|
480
462
|
# by default, the op will be returned when the factory is used.
|
481
|
-
# this `output` returns the value of the
|
463
|
+
# this `output` returns the value of the `user` output on the resulting op
|
482
464
|
output :user
|
483
465
|
end
|
484
466
|
|
data/lib/subroutine/auth.rb
CHANGED
@@ -15,6 +15,12 @@ module Subroutine
|
|
15
15
|
|
16
16
|
end
|
17
17
|
|
18
|
+
class AuthorizationNotDeclaredError < ::StandardError
|
19
|
+
def initialize(msg = nil)
|
20
|
+
super(msg || "Authorization management has not been declared on this class")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
18
24
|
def self.included(base)
|
19
25
|
base.instance_eval do
|
20
26
|
extend ::Subroutine::Auth::ClassMethods
|
@@ -67,7 +73,7 @@ module Subroutine
|
|
67
73
|
end
|
68
74
|
|
69
75
|
def initialize(*args)
|
70
|
-
raise
|
76
|
+
raise Subroutine::Auth::AuthorizationNotDeclaredError.new if(!self.class.authorization_declared)
|
71
77
|
|
72
78
|
super(args.extract_options!)
|
73
79
|
@skip_auth_checks = false
|
data/lib/subroutine/op.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
require 'active_support/core_ext/hash/indifferent_access'
|
2
|
+
require 'active_support/core_ext/object/duplicable'
|
3
|
+
require 'active_support/core_ext/object/deep_dup'
|
2
4
|
require 'active_model'
|
3
5
|
|
4
6
|
require "subroutine/failure"
|
@@ -307,7 +309,13 @@ module Subroutine
|
|
307
309
|
self._fields.each_pair do |field, config|
|
308
310
|
unless config[:default].nil?
|
309
311
|
deflt = config[:default]
|
310
|
-
|
312
|
+
if deflt.respond_to?(:call)
|
313
|
+
deflt = deflt.call
|
314
|
+
elsif deflt.duplicable? # from active_support
|
315
|
+
# Some classes of default values need to be duplicated, or the instance field value will end up referencing
|
316
|
+
# the class global default value, and potentially modify it.
|
317
|
+
deflt = deflt.deep_dup # from active_support
|
318
|
+
end
|
311
319
|
defaults[field] = type_caster.cast(deflt, config[:type])
|
312
320
|
end
|
313
321
|
end
|
@@ -315,7 +323,6 @@ module Subroutine
|
|
315
323
|
defaults
|
316
324
|
end
|
317
325
|
|
318
|
-
|
319
326
|
end
|
320
327
|
|
321
328
|
end
|
data/lib/subroutine/version.rb
CHANGED
@@ -8,7 +8,7 @@ module Subroutine
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def test_it_throws_an_error_if_authorization_is_not_defined
|
11
|
-
assert_raises
|
11
|
+
assert_raises ::Subroutine::Auth::AuthorizationNotDeclaredError do
|
12
12
|
MissingAuthOp.new
|
13
13
|
end
|
14
14
|
end
|
@@ -10,7 +10,7 @@ module Subroutine
|
|
10
10
|
|
11
11
|
def test_inherited_fields
|
12
12
|
op = ::AdminSignupOp.new
|
13
|
-
assert_equal [:email, :password, :
|
13
|
+
assert_equal [:email, :password, :privileges], op._fields.keys.sort
|
14
14
|
end
|
15
15
|
|
16
16
|
def test_class_attribute_usage
|
@@ -70,10 +70,10 @@ module Subroutine
|
|
70
70
|
|
71
71
|
assert_nil op.email
|
72
72
|
assert_nil op.password
|
73
|
-
assert_equal 'min', op.
|
73
|
+
assert_equal 'min', op.privileges
|
74
74
|
|
75
|
-
op.
|
76
|
-
assert_equal 'max', op.
|
75
|
+
op.privileges = 'max'
|
76
|
+
assert_equal 'max', op.privileges
|
77
77
|
end
|
78
78
|
|
79
79
|
def test_validations_are_evaluated_before_perform_is_invoked
|
@@ -157,37 +157,58 @@ module Subroutine
|
|
157
157
|
}, op.params)
|
158
158
|
|
159
159
|
assert_equal({
|
160
|
-
"
|
160
|
+
"privileges" => "min",
|
161
161
|
}, op.defaults)
|
162
162
|
|
163
163
|
assert_equal({
|
164
164
|
"email" => "foo",
|
165
|
-
"
|
165
|
+
"privileges" => "min",
|
166
166
|
}, op.params_with_defaults)
|
167
167
|
end
|
168
168
|
|
169
169
|
def test_it_allows_defaults_to_be_overridden
|
170
|
-
op = ::AdminSignupOp.new(email: "foo",
|
170
|
+
op = ::AdminSignupOp.new(email: "foo", privileges: nil)
|
171
171
|
|
172
172
|
assert_equal({
|
173
173
|
"email" => "foo",
|
174
|
-
"
|
174
|
+
"privileges" => nil
|
175
175
|
}, op.params)
|
176
176
|
|
177
177
|
assert_equal({
|
178
|
-
"
|
178
|
+
"privileges" => "min",
|
179
179
|
}, op.defaults)
|
180
180
|
|
181
181
|
assert_equal({
|
182
182
|
"email" => "foo",
|
183
|
-
"
|
183
|
+
"privileges" => nil,
|
184
184
|
}, op.params_with_defaults)
|
185
185
|
end
|
186
186
|
|
187
|
+
def test_it_overriding_default_does_not_alter_default
|
188
|
+
op = ::AdminSignupOp.new(email: "foo")
|
189
|
+
op.privileges << "bangbang"
|
190
|
+
|
191
|
+
op = ::AdminSignupOp.new(email: "foo", privileges: nil)
|
192
|
+
|
193
|
+
assert_equal({
|
194
|
+
"email" => "foo",
|
195
|
+
"privileges" => nil
|
196
|
+
}, op.params)
|
197
|
+
|
198
|
+
assert_equal({
|
199
|
+
"privileges" => "min",
|
200
|
+
}, op.defaults)
|
201
|
+
|
202
|
+
assert_equal({
|
203
|
+
"email" => "foo",
|
204
|
+
"privileges" => nil,
|
205
|
+
}, op.params_with_defaults)
|
206
|
+
end
|
207
|
+
|
187
208
|
def test_it_overrides_defaults_with_nils
|
188
|
-
op = ::AdminSignupOp.new(email: "foo",
|
209
|
+
op = ::AdminSignupOp.new(email: "foo", privileges: nil)
|
189
210
|
assert_equal({
|
190
|
-
"
|
211
|
+
"privileges" => nil,
|
191
212
|
"email" => "foo"
|
192
213
|
}, op.params)
|
193
214
|
end
|
@@ -9,7 +9,7 @@ module Subroutine
|
|
9
9
|
|
10
10
|
def test_integer_inputs
|
11
11
|
op.integer_input = nil
|
12
|
-
|
12
|
+
assert_nil op.integer_input
|
13
13
|
|
14
14
|
op.integer_input = 'foo'
|
15
15
|
assert_equal 0, op.integer_input
|
@@ -29,7 +29,7 @@ module Subroutine
|
|
29
29
|
|
30
30
|
def test_number_inputs
|
31
31
|
op.number_input = nil
|
32
|
-
|
32
|
+
assert_nil op.number_input
|
33
33
|
|
34
34
|
op.number_input = 4
|
35
35
|
assert_equal 4.0, op.number_input
|
@@ -43,7 +43,7 @@ module Subroutine
|
|
43
43
|
|
44
44
|
def test_string_inputs
|
45
45
|
op.string_input = nil
|
46
|
-
|
46
|
+
assert_nil op.string_input
|
47
47
|
|
48
48
|
op.string_input = ""
|
49
49
|
assert_equal '', op.string_input
|
@@ -60,7 +60,7 @@ module Subroutine
|
|
60
60
|
|
61
61
|
def test_boolean_inputs
|
62
62
|
op.boolean_input = nil
|
63
|
-
|
63
|
+
assert_nil op.boolean_input
|
64
64
|
|
65
65
|
op.boolean_input = 'yes'
|
66
66
|
assert_equal true, op.boolean_input
|
@@ -101,7 +101,7 @@ module Subroutine
|
|
101
101
|
|
102
102
|
def test_hash_inputs
|
103
103
|
op.object_input = nil
|
104
|
-
|
104
|
+
assert_nil op.object_input
|
105
105
|
|
106
106
|
op.object_input = ''
|
107
107
|
assert_equal({}, op.object_input)
|
@@ -121,7 +121,7 @@ module Subroutine
|
|
121
121
|
|
122
122
|
def test_array_inputs
|
123
123
|
op.array_input = nil
|
124
|
-
|
124
|
+
assert_nil op.array_input
|
125
125
|
|
126
126
|
op.array_input = ''
|
127
127
|
assert_equal [], op.array_input
|
@@ -138,7 +138,7 @@ module Subroutine
|
|
138
138
|
|
139
139
|
def test_date_inputs
|
140
140
|
op.date_input = nil
|
141
|
-
|
141
|
+
assert_nil op.date_input
|
142
142
|
|
143
143
|
op.date_input = "2022-12-22"
|
144
144
|
assert_equal ::Date, op.date_input.class
|
@@ -172,7 +172,7 @@ module Subroutine
|
|
172
172
|
|
173
173
|
def test_time_inputs
|
174
174
|
op.time_input = nil
|
175
|
-
|
175
|
+
assert_nil op.time_input
|
176
176
|
|
177
177
|
op.time_input = "2022-12-22"
|
178
178
|
assert_equal ::Time, op.time_input.class
|
@@ -199,7 +199,7 @@ module Subroutine
|
|
199
199
|
|
200
200
|
def test_iso_date_inputs
|
201
201
|
op.iso_date_input = nil
|
202
|
-
|
202
|
+
assert_nil op.iso_date_input
|
203
203
|
|
204
204
|
op.iso_date_input = "2022-12-22"
|
205
205
|
assert_equal ::String, op.iso_date_input.class
|
@@ -212,7 +212,7 @@ module Subroutine
|
|
212
212
|
|
213
213
|
def test_iso_time_inputs
|
214
214
|
op.iso_time_input = nil
|
215
|
-
|
215
|
+
assert_nil op.iso_time_input
|
216
216
|
|
217
217
|
op.iso_time_input = "2022-12-22T10:30:24Z"
|
218
218
|
assert_equal ::String, op.iso_time_input.class
|
data/test/support/ops.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: subroutine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.5.
|
4
|
+
version: 0.5.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Nelson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-08-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|