subroutine 0.2.1 → 0.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
  SHA1:
3
- metadata.gz: 4f44648d17e8af9de061d865e831e95811de6ff4
4
- data.tar.gz: b5347035ebf89c8e17a45b49333fb9b1a0812555
3
+ metadata.gz: a1eb7328166962d1dfee177ad4311926c7958217
4
+ data.tar.gz: f5133fc409be40473d1c14a0ae1880b1d007a35d
5
5
  SHA512:
6
- metadata.gz: 9e8cb47942615ef6cee08df82ff32c026a1824aac18e3e07dacc74e71bb23c30f9959493491d3cb358ec6e4daccbe155c0562b4fa0794efd85b29a9ffa39a5cd
7
- data.tar.gz: aedf7e3533db2a99570634e97a74cc82bb95673ed9f256153b7844a94638ce49436717c7912b7a552dbe13985c603078977270ff3f527128379b28cc03be5680
6
+ metadata.gz: a9bb3b82b53ef32a8f0d236c4e2561d7c56ce5c233f520a06bded564017e32e7d017758f43e0dff4d80c425b476e5ea95562d1f0414b7c26bd4e0d426efa8244
7
+ data.tar.gz: d4752345399d5ed62c138438ac80050c55de4ca84163d3c2121aed0bd7b404ffd36cc1caf6447d83a28bf570602f007bb5c55d59b70965e7dd6224831f350702
data/.gitignore CHANGED
@@ -13,3 +13,4 @@
13
13
  *.a
14
14
  mkmf.log
15
15
  *.gem
16
+ .byebug_history
data/README.md CHANGED
@@ -343,93 +343,123 @@ op.submit!
343
343
  # if the op succeeds nothing will be raised, otherwise a ::Subroutine::Failure will be raised.
344
344
  ```
345
345
 
346
- ## Extending Subroutine::Op
346
+ ## Built-in Extensions
347
347
 
348
- Great, so you're sold on using ops. Let's talk about how I usually standardize their usage in my apps. The most common thing needed is `current_user`. For this reason I usually follow the rails convention of declaring an "Application" op which declares all of my common needs. I hate writing `ApplicationOp` all the time so I usually call it `BaseOp`.
348
+ ### Subroutine::Association
349
+
350
+ The `Subroutine::Association` module provides an interface for loading ActiveRecord instances easily.
349
351
 
350
352
  ```ruby
351
- class BaseOp < ::Subroutine::Op
353
+ class UserUpdateOp < ::Subroutine::Op
354
+ include ::Subroutine::Association
352
355
 
353
- attr_reader :current_user
356
+ association :user
354
357
 
355
- def initialize(*args)
356
- params = args.extract_options!
357
- @current_user = args[0]
358
- super(params)
359
- end
358
+ string :first_name, :last_name
360
359
 
361
- end
362
- ```
360
+ protected
363
361
 
364
- So now I can pass the current user as my first argument to any op constructor. The next most common case is permissions. In a common role-based system things become pretty easy. I usually just add a class method which declares the minimum required role.
362
+ def perform
363
+ user.update_attributes(
364
+ first_name: first_name,
365
+ last_name: last_name
366
+ )
365
367
 
366
- ```ruby
367
- class SendInvitationOp < BaseOp
368
- require_role :admin
368
+ true
369
+ end
369
370
  end
370
371
  ```
371
372
 
372
- In the case of a more complex permission system, I'll usually utilize pundit but still standardize the check as a validation.
373
-
374
373
  ```ruby
375
- class BaseOp < ::Subroutine::Op
374
+ class RecordTouchOp < ::Subroutine::Op
375
+ include ::Subroutine::Association
376
376
 
377
- validate :validate_permissions
377
+ association :record, polymorphic: true
378
378
 
379
379
  protected
380
380
 
381
- # default implementation is to allow access.
382
- def validate_permissions
383
- true
384
- end
381
+ def perform
382
+ record.touch
385
383
 
386
- def not_authorized!
387
- errors.add(:current_user, :not_authorized)
388
- false
384
+ true
389
385
  end
390
386
  end
387
+ ```
388
+
389
+ ### Subroutine::Auth
390
+
391
+ The `Subroutine::Auth` module provides basic bindings for application authorization. It assumes that, optionally, a `User` will be provided as the first argument to an Op. It forces authorization to be declared on each class it's included in.
392
+
393
+ ```ruby
394
+ class SayHiOp < ::Subroutine::Op
395
+ include ::Subroutine::Auth
396
+
397
+ require_user!
391
398
 
392
- class SendInvitationOp < BaseOp
399
+ string :say_what, default: "hi"
393
400
 
394
401
  protected
395
402
 
396
- def validate_permissions
397
- unless UserPolicy.new(current_user).send_invitations?
398
- return not_authorized!
399
- end
403
+ def perform
404
+ puts "#{current_user.name} says: #{say_what}"
400
405
 
401
406
  true
402
407
  end
403
-
404
408
  end
405
409
  ```
406
410
 
407
- Clearly there are a ton of ways this could be implemented but that should be a good jumping-off point.
411
+ ```ruby
412
+ user = User.find("john")
413
+ SayHiOp.submit!(user)
414
+ # => John says: hi
415
+
416
+ SayHiOp.submit!(user, say_what: "hello")
417
+ # => John says: hello
418
+
419
+
420
+ SayHiOp.submit!
421
+ # => raises Subroutine::Auth::NotAuthorizedError
422
+ ```
423
+
424
+ There are a handful of authorization configurations:
425
+
426
+ 1. `require_user!` - ensures that a user is provided
427
+ 2. `require_no_user!` - ensures that a user is not present
428
+ 3. `no_user_requirements!` - explicitly doesn't matter
408
429
 
409
- Performance monitoring is also important to me so I've added a few hooks to observe what's going on during an op's submission. I'm primarily using Skylight at the moment.
430
+ In addition to these top-level authorization declarations you can provide custom authorizations like so:
410
431
 
411
432
  ```ruby
412
- class BaseOp < ::Subroutine::Op
433
+ class AccountSetSecretOp < ::Subroutine::Op
434
+ include ::Subroutine::Auth
435
+
436
+ require_user!
437
+ authorize :authorize_first_name_is_john
438
+
439
+ policy :can_set_secret?
440
+
441
+ string :secret
442
+ belongs_to :account
413
443
 
414
444
  protected
415
445
 
416
- def observe_submission
417
- Skylight.instrument category: 'op.submission', title: "#{self.class.name}#submit" do
418
- yield
419
- end
446
+ def perform
447
+ account.secret = secret
448
+ current_user.save!
449
+
450
+ true
420
451
  end
421
452
 
422
- def observe_validation
423
- Skylight.instrument category: 'op.validation', title: "#{self.class.name}#valid?" do
424
- yield
453
+ def authorize_first_name_is_john
454
+ unless current_user.first_name == "john"
455
+ unauthorized!
425
456
  end
426
457
  end
427
458
 
428
- def observe_perform
429
- Skylight.instrument category: 'op.perform', title: "#{self.class.name}#perform" do
430
- yield
431
- end
459
+ def policy
460
+ ::UserPolicy.new(current_user, current_user)
432
461
  end
462
+
433
463
  end
434
464
  ```
435
465
 
@@ -0,0 +1,132 @@
1
+ module Subroutine
2
+ module Association
3
+
4
+ def self.included(base)
5
+ base.send :extend, ::Subroutine::Association::ClassMethods
6
+ class << base
7
+ alias_method :field_without_associations, :field
8
+ alias_method :field, :field_with_associations
9
+ end
10
+
11
+ end
12
+
13
+ module ClassMethods
14
+
15
+
16
+ def field_with_associations(*args)
17
+ opts = args.extract_options!
18
+ if opts[:association]
19
+ args.each do |arg|
20
+ association(arg, opts)
21
+ end
22
+ else
23
+ field_without_associations(*args, opts)
24
+ end
25
+ end
26
+
27
+ # association :user
28
+ # - user_id
29
+ # - user_type => "User"
30
+
31
+ # association :user, polymorphic: true
32
+ # - user_id
33
+ # - user_type
34
+ # - user => polymorphic_lookup(user_type, user_id)
35
+
36
+ # association :inbound_user_request, as: :request
37
+ # - inbound_user_request_id
38
+ # - inbound_user_request_type => "InboundUserRequest"
39
+ # - request => polymorphic_lookup(inbound_user_request_type, inbound_user_request_id)
40
+
41
+ # association :inbound_user_request, foreign_key: :request_id
42
+ # - request_id
43
+ # - request_type
44
+ # - inbound_user_request => polymorphic_lookup(request_type, request_id)
45
+
46
+ # Other options:
47
+ # - unscoped => set true if the record should be looked up via Type.unscoped
48
+
49
+ def association(field, options = {})
50
+
51
+ if options[:as] && options[:foreign_key]
52
+ raise ArgumentError.new(":as and :foreign_key options should be provided together to an association invocation")
53
+ end
54
+
55
+ class_name = options[:class_name]
56
+
57
+ poly = options[:polymorphic] || !class_name.nil?
58
+ as = options[:as] || field
59
+ unscoped = !!options[:unscoped]
60
+
61
+ klass = class_name.to_s if class_name
62
+
63
+ foreign_key_method = (options[:foreign_key] || "#{field}_id").to_s
64
+ foreign_type_method = foreign_key_method.gsub(/_id$/, "_type")
65
+
66
+
67
+ if poly
68
+ string foreign_type_method
69
+ else
70
+ class_eval <<-EV, __FILE__, __LINE__+1
71
+ def #{foreign_type_method}
72
+ #{as.to_s.camelize.inspect}
73
+ end
74
+ EV
75
+ end
76
+
77
+ integer foreign_key_method
78
+
79
+ field_without_associations as, options.merge(association: true)
80
+
81
+ class_eval <<-EV, __FILE__, __LINE__+1
82
+ def #{as}_with_association
83
+ return @#{as} if defined?(@#{as})
84
+ @#{as} = begin
85
+ #{as}_without_association ||
86
+ polymorphic_instance(#{klass.nil? ? foreign_type_method : klass.to_s}, #{foreign_key_method}, #{unscoped.inspect})
87
+ end
88
+ end
89
+
90
+ def #{as}_with_association=(r)
91
+ @#{as} = r
92
+ #{poly || klass ? "params['#{foreign_type_method}'] = r.nil? ? nil : #{klass.nil? ? "r.class.name" : klass.to_s.inspect}" : ""}
93
+ params['#{foreign_key_method}'] = r.nil? ? nil : r.id
94
+ r
95
+ end
96
+ EV
97
+
98
+ alias_method :"#{as}_without_association", :"#{as}"
99
+ alias_method :"#{as}", :"#{as}_with_association"
100
+
101
+ alias_method :"#{as}_without_association=", :"#{as}="
102
+ alias_method :"#{as}=", :"#{as}_with_association="
103
+
104
+ end
105
+ end
106
+
107
+ def initialize(*args)
108
+ super(*args)
109
+
110
+ self._fields.each_pair do |field, config|
111
+ next unless config[:association]
112
+ next unless @original_params.has_key?(field)
113
+ send("#{field}=", @original_params[field]) # this gets the _id and _type into the params hash
114
+ end
115
+ end
116
+
117
+ def polymorphic_instance(_type, _id, _unscoped = false)
118
+ return nil unless _type && _id
119
+
120
+ klass = _type
121
+ klass = klass.camelize.constantize if klass.is_a?(String)
122
+
123
+ return nil unless klass
124
+
125
+ scope = klass.all
126
+ scope = scope.unscoped if _unscoped
127
+
128
+ scope.find(_id)
129
+ end
130
+
131
+ end
132
+ end
@@ -0,0 +1,97 @@
1
+ module Subroutine
2
+ module Auth
3
+
4
+ class NotAuthorizedError < ::StandardError
5
+
6
+ def initialize(msg = nil)
7
+ msg = I18n.t("errors.#{msg}", default: "Sorry, you are not authorized to perform this action.") if msg.is_a?(Symbol)
8
+ msg ||= I18n.t('errors.unauthorized', default: "Sorry, you are not authorized to perform this action.")
9
+ super msg
10
+ end
11
+
12
+ def status
13
+ 401
14
+ end
15
+
16
+ end
17
+
18
+ def self.included(base)
19
+ base.instance_eval do
20
+ extend ::Subroutine::Auth::ClassMethods
21
+
22
+ class_attribute :authorization_declared
23
+ self.authorization_declared = false
24
+ end
25
+ end
26
+
27
+
28
+ module ClassMethods
29
+
30
+ def authorize(validation_name)
31
+ validate validation_name, unless: :skip_auth_checks?
32
+ end
33
+
34
+ def no_user_requirements!
35
+ self.authorization_declared = true
36
+ end
37
+
38
+ def require_user!
39
+ self.authorization_declared = true
40
+
41
+ validate unless: :skip_auth_checks? do
42
+ unauthorized! unless current_user.present?
43
+ end
44
+ end
45
+
46
+ def require_no_user!
47
+ self.authorization_declared = true
48
+
49
+ validate unless: :skip_auth_checks? do
50
+ unauthorized! :empty_unauthorized if current_user.present?
51
+ end
52
+ end
53
+
54
+ # policy :can_update_user
55
+ # policy :can_do_whatever, policy: :foo_policy
56
+ def policy(*meths)
57
+ opts = meths.extract_options!
58
+ policy_name = opts[:policy] || :policy
59
+ validate unless: :skip_auth_checks? do
60
+ p = self.send(policy_name)
61
+ if !p || meths.any?{|m| !(p.respond_to?("#{m}?") ? p.send("#{m}?") : p.send(m)) }
62
+ unauthorized! opts[:error]
63
+ end
64
+ end
65
+ end
66
+
67
+ end
68
+
69
+ def initialize(*args)
70
+ raise "Authorization management has not been declared on this class" if(!self.class.authorization_declared)
71
+
72
+ super(args.extract_options!)
73
+ @skip_auth_checks = false
74
+ @current_user = args.shift
75
+ end
76
+
77
+ def skip_auth_checks!
78
+ @skip_auth_checks = true
79
+ self
80
+ end
81
+
82
+ def skip_auth_checks?
83
+ !!@skip_auth_checks
84
+ end
85
+
86
+ def current_user
87
+ @current_user = ::User.find(@current_user) if Fixnum === @current_user
88
+ @current_user
89
+ end
90
+
91
+ def unauthorized!(reason = nil)
92
+ reason ||= :unauthorized
93
+ raise ::Subroutine::Auth::NotAuthorizedError.new(reason)
94
+ end
95
+
96
+ end
97
+ end
@@ -1,8 +1,8 @@
1
1
  module Subroutine
2
2
 
3
3
  MAJOR = 0
4
- MINOR = 2
5
- PATCH = 1
4
+ MINOR = 3
5
+ PATCH = 0
6
6
  PRE = nil
7
7
 
8
8
  VERSION = [MAJOR, MINOR, PATCH, PRE].compact.join('.')
data/subroutine.gemspec CHANGED
@@ -25,4 +25,5 @@ Gem::Specification.new do |spec|
25
25
 
26
26
  spec.add_development_dependency "minitest"
27
27
  spec.add_development_dependency "minitest-reporters"
28
+ spec.add_development_dependency "mocha"
28
29
  end
@@ -0,0 +1,58 @@
1
+ require 'test_helper'
2
+
3
+ module Subroutine
4
+ class AuthTest < TestCase
5
+
6
+ def doug
7
+ @doug ||= ::User.new(id: 1, email_address: "doug@example.com")
8
+ end
9
+
10
+ def fred
11
+ @fred ||= ::User.new(id: 2, email_address: "fred@example.com")
12
+ end
13
+
14
+ def test_it_sets_accessors_on_init
15
+ op = SimpleAssociationOp.new user: doug
16
+ assert_equal "User", op.user_type
17
+ assert_equal doug.id, op.user_id
18
+ end
19
+
20
+ def test_it_looks_up_an_association
21
+ all_mock = mock
22
+
23
+ ::User.expects(:all).returns(all_mock)
24
+ all_mock.expects(:find).with(1).returns(doug)
25
+
26
+ op = SimpleAssociationOp.new user_type: "User", user_id: doug.id
27
+ assert_equal doug, op.user
28
+ end
29
+
30
+ def test_it_allows_an_association_to_be_looked_up_without_default_scoping
31
+ all_mock = mock
32
+ unscoped_mock = mock
33
+
34
+ ::User.expects(:all).returns(all_mock)
35
+ all_mock.expects(:unscoped).returns(unscoped_mock)
36
+ unscoped_mock.expects(:find).with(1).returns(doug)
37
+
38
+ op = UnscopedSimpleAssociationOp.new user_type: "User", user_id: doug.id
39
+ assert_equal doug, op.user
40
+ end
41
+
42
+ def test_it_allows_polymorphic_associations
43
+ all_mock = mock
44
+ ::User.expects(:all).never
45
+ ::AdminUser.expects(:all).returns(all_mock)
46
+ all_mock.expects(:find).with(1).returns(doug)
47
+
48
+ op = PolymorphicAssociationOp.new(admin_type: "AdminUser", admin_id: doug.id)
49
+ assert_equal doug, op.admin
50
+ end
51
+
52
+ def test_it_allows_the_class_to_be_set
53
+ op = ::AssociationWithClassOp.new(admin: doug)
54
+ assert_equal "AdminUser", op.admin_type
55
+ end
56
+
57
+ end
58
+ end
@@ -0,0 +1,69 @@
1
+ require 'test_helper'
2
+
3
+ module Subroutine
4
+ class AuthTest < TestCase
5
+
6
+ def user
7
+ @user ||= ::User.new(email_address: "doug@example.com")
8
+ end
9
+
10
+ def test_it_throws_an_error_if_authorization_is_not_defined
11
+ assert_raises "Authorization management has not been declared on this class" do
12
+ MissingAuthOp.new
13
+ end
14
+ end
15
+
16
+ def test_it_throws_an_error_if_require_user_but_none_is_provided
17
+ assert_raises ::Subroutine::Auth::NotAuthorizedError do
18
+ RequireUserOp.submit!
19
+ end
20
+ end
21
+
22
+ def test_it_does_not_throw_an_error_if_require_user_but_none_is_provided
23
+ RequireUserOp.submit! user
24
+ end
25
+
26
+ def test_it_throws_an_error_if_require_no_user_but_one_is_present
27
+ assert_raises ::Subroutine::Auth::NotAuthorizedError do
28
+ RequireNoUserOp.submit! user
29
+ end
30
+ end
31
+
32
+ def test_it_does_not_throw_an_error_if_require_no_user_and_none_is_provided
33
+ RequireNoUserOp.submit!
34
+ end
35
+
36
+ def test_it_does_not_throw_an_error_if_no_user_requirements_and_one_is_provided
37
+ NoUserRequirementsOp.submit! user
38
+ end
39
+
40
+ def test_it_does_not_throw_an_error_if_no_user_requirements_and_none_is_provided
41
+ NoUserRequirementsOp.submit!
42
+ end
43
+
44
+ def test_it_runs_custom_authorizations
45
+ CustomAuthorizeOp.submit! user
46
+
47
+ assert_raises ::Subroutine::Auth::NotAuthorizedError do
48
+ CustomAuthorizeOp.submit! User.new(email_address: "foo@bar.com")
49
+ end
50
+ end
51
+
52
+ def test_it_does_not_run_authorizations_if_explicitly_bypassed
53
+ op = CustomAuthorizeOp.new User.new(email_address: "foo@bar.com")
54
+ op.skip_auth_checks!
55
+ op.submit!
56
+ end
57
+
58
+ def test_it_runs_policies_as_part_of_authorization
59
+ assert_raises ::Subroutine::Auth::NotAuthorizedError do
60
+ PolicyOp.submit! user
61
+ end
62
+
63
+ op = PolicyOp.new
64
+ op.skip_auth_checks!
65
+ op.submit!
66
+ end
67
+
68
+ end
69
+ end
@@ -220,5 +220,19 @@ module Subroutine
220
220
  assert_equal "2022-12-22T10:30:24Z", op.iso_time_input
221
221
  end
222
222
 
223
+ def test_field_provided
224
+ op = ::SignupOp.new()
225
+ assert_equal false, op.send(:field_provided?, :email)
226
+
227
+ op = ::SignupOp.new(email: "foo")
228
+ assert_equal true, op.send(:field_provided?, :email)
229
+
230
+ op = ::DefaultsOp.new()
231
+ assert_equal false, op.send(:field_provided?, :foo)
232
+
233
+ op = ::DefaultsOp.new(foo: "foo")
234
+ assert_equal true, op.send(:field_provided?, :foo)
235
+ end
236
+
223
237
  end
224
238
  end
data/test/support/ops.rb CHANGED
@@ -1,8 +1,12 @@
1
+ require "subroutine/auth"
2
+ require "subroutine/association"
3
+
1
4
  ## Models ##
2
5
 
3
6
  class User
4
7
  include ::ActiveModel::Model
5
8
 
9
+ attr_accessor :id
6
10
  attr_accessor :email_address
7
11
  attr_accessor :password
8
12
 
@@ -109,3 +113,78 @@ class TypeCastOp < ::Subroutine::Op
109
113
  array :array_input, :default => 'foo'
110
114
 
111
115
  end
116
+
117
+ class OpWithAuth < ::Subroutine::Op
118
+ include ::Subroutine::Auth
119
+ def perform
120
+ true
121
+ end
122
+ end
123
+
124
+ class MissingAuthOp < OpWithAuth
125
+ end
126
+
127
+ class RequireUserOp < OpWithAuth
128
+ require_user!
129
+ end
130
+
131
+ class RequireNoUserOp < OpWithAuth
132
+ require_no_user!
133
+ end
134
+
135
+ class NoUserRequirementsOp < OpWithAuth
136
+ no_user_requirements!
137
+ end
138
+
139
+ class CustomAuthorizeOp < OpWithAuth
140
+
141
+ require_user!
142
+ authorize :authorize_user_is_correct
143
+
144
+ protected
145
+
146
+ def authorize_user_is_correct
147
+ unless current_user.email_address.to_s =~ /example\.com$/
148
+ unauthorized!
149
+ end
150
+ end
151
+ end
152
+
153
+ class PolicyOp < OpWithAuth
154
+
155
+ class FakePolicy
156
+ def user_can_access?
157
+ false
158
+ end
159
+ end
160
+
161
+ require_user!
162
+ policy :user_can_access?
163
+
164
+ def policy
165
+ @policy ||= FakePolicy.new
166
+ end
167
+ end
168
+
169
+
170
+ class OpWithAssociation < ::Subroutine::Op
171
+ include ::Subroutine::Association
172
+ end
173
+
174
+ class SimpleAssociationOp < ::OpWithAssociation
175
+
176
+ association :user
177
+
178
+ end
179
+
180
+ class UnscopedSimpleAssociationOp < ::OpWithAssociation
181
+ association :user, unscoped: true
182
+ end
183
+
184
+ class PolymorphicAssociationOp < ::OpWithAssociation
185
+ association :admin, polymorphic: true
186
+ end
187
+
188
+ class AssociationWithClassOp < ::OpWithAssociation
189
+ association :admin, class_name: "AdminUser"
190
+ end
data/test/test_helper.rb CHANGED
@@ -3,11 +3,10 @@ require 'minitest/autorun'
3
3
  require 'minitest/unit'
4
4
 
5
5
  require 'minitest/reporters'
6
+ require 'mocha/mini_test'
6
7
 
7
8
  Minitest::Reporters.use!([Minitest::Reporters::DefaultReporter.new])
8
9
 
9
10
  class TestCase < (MiniTest::Unit::TestCase rescue ::MiniTest::Test); end
10
11
 
11
12
  require_relative 'support/ops'
12
-
13
-
metadata CHANGED
@@ -1,83 +1,97 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: subroutine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Nelson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-08-22 00:00:00.000000000 Z
11
+ date: 2017-02-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - '>='
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: 4.0.0
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
26
  version: 4.0.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ~>
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
33
  version: '1.7'
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
40
  version: '1.7'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ~>
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
47
  version: '10.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ~>
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '10.0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: minitest
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - '>='
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - '>='
66
+ - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: minitest-reporters
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - '>='
73
+ - - ">="
74
74
  - !ruby/object:Gem::Version
75
75
  version: '0'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - '>='
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: mocha
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
81
95
  - !ruby/object:Gem::Version
82
96
  version: '0'
83
97
  description: An interface for creating feature-driven operations.
@@ -87,8 +101,8 @@ executables: []
87
101
  extensions: []
88
102
  extra_rdoc_files: []
89
103
  files:
90
- - .gitignore
91
- - .travis.yml
104
+ - ".gitignore"
105
+ - ".travis.yml"
92
106
  - Gemfile
93
107
  - LICENSE.txt
94
108
  - README.md
@@ -100,12 +114,16 @@ files:
100
114
  - gemfiles/am41.gemfile
101
115
  - gemfiles/am42.gemfile
102
116
  - lib/subroutine.rb
117
+ - lib/subroutine/association.rb
118
+ - lib/subroutine/auth.rb
103
119
  - lib/subroutine/failure.rb
104
120
  - lib/subroutine/filtered_errors.rb
105
121
  - lib/subroutine/op.rb
106
122
  - lib/subroutine/type_caster.rb
107
123
  - lib/subroutine/version.rb
108
124
  - subroutine.gemspec
125
+ - test/subroutine/association_test.rb
126
+ - test/subroutine/auth_test.rb
109
127
  - test/subroutine/base_test.rb
110
128
  - test/subroutine/type_caster_test.rb
111
129
  - test/support/ops.rb
@@ -120,17 +138,17 @@ require_paths:
120
138
  - lib
121
139
  required_ruby_version: !ruby/object:Gem::Requirement
122
140
  requirements:
123
- - - '>='
141
+ - - ">="
124
142
  - !ruby/object:Gem::Version
125
143
  version: '0'
126
144
  required_rubygems_version: !ruby/object:Gem::Requirement
127
145
  requirements:
128
- - - '>='
146
+ - - ">="
129
147
  - !ruby/object:Gem::Version
130
148
  version: '0'
131
149
  requirements: []
132
150
  rubyforge_project:
133
- rubygems_version: 2.0.14.1
151
+ rubygems_version: 2.6.8
134
152
  signing_key:
135
153
  specification_version: 4
136
154
  summary: Feature-driven operation objects.
@@ -141,6 +159,8 @@ test_files:
141
159
  - gemfiles/am40.gemfile
142
160
  - gemfiles/am41.gemfile
143
161
  - gemfiles/am42.gemfile
162
+ - test/subroutine/association_test.rb
163
+ - test/subroutine/auth_test.rb
144
164
  - test/subroutine/base_test.rb
145
165
  - test/subroutine/type_caster_test.rb
146
166
  - test/support/ops.rb