subroutine 2.1.2 → 2.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: 63080af90148b6faaff42be6f9753d6f5ef36f3f8ebe650a1372905836f5e208
4
- data.tar.gz: 9522cbaf937f5bdfa85fbb629ad8a1f3f014dce6ea42784ef68b39c4ecebad49
3
+ metadata.gz: a130a9c00d47631ee67621f3772ff5d050c348af94c9ec99a3ef4d90f0164079
4
+ data.tar.gz: d7c099c1c586bfaf516955b1f94aa86188fbbe0194e42021d0f0191341efb482
5
5
  SHA512:
6
- metadata.gz: a76f25b779fd8a9ccc3958febe5b68906d7260f052b5200d6b080680f9f80f79debebe2bf35ee199f6996cccd827f0844e3710d30f0ee367dd248c6900f45c74
7
- data.tar.gz: c3e50c34de7e4f656168cec625bccb1bede1fcd4dbbdd5bef7d9d0dabf9a2695a470dd749b2ec5785448b6cd45c0c3454b469d49a157e9d19fffc2ada78b75ce
6
+ metadata.gz: ffa8be2183e84ad07c523251c499673a169f7b9968d79164f7662082ffc9497027f4344cb12fda6b602aaf2b6884a8be17690a58448f50138f4fb547f9d6e0cf
7
+ data.tar.gz: c6130d737e8860e05d529e679e4a2f2be2de3f7114da5e97c5f8d3844463f3fc6f9e3a2f23da50891282989adcbd08759be9f243ff3aaf25adc978255cd18d43
@@ -0,0 +1,23 @@
1
+ name: build
2
+ on: [push, pull_request]
3
+ jobs:
4
+ build:
5
+ runs-on: ubuntu-latest
6
+ continue-on-error: ${{ matrix.experimental }}
7
+ strategy:
8
+ fail-fast: false
9
+ matrix:
10
+ ruby-version: [2.7.5, 2.7.6]
11
+ experimental: [false]
12
+ include:
13
+ - ruby-version: 3.0
14
+ experimental: true
15
+ - ruby-version: 3.1
16
+ experimental: true
17
+ steps:
18
+ - uses: actions/checkout@v2
19
+ - uses: ruby/setup-ruby@v1
20
+ with:
21
+ ruby-version: ${{ matrix.ruby-version }}
22
+ bundler-cache: true # runs `bundle install` and caches installed gems automatically
23
+ - run: bundle exec rake test
data/CHANGELOG.MD CHANGED
@@ -1,3 +1,7 @@
1
+ ## Subroutine 2.2
2
+
3
+ Add `type` validation for Output.
4
+
1
5
  ## Subroutine 2.0
2
6
 
3
7
  The updates between 1.0 and 2.0 are relatively minor and are focused more on cleaning up the codebase and extending the use of the 0.9->1.0 refactor. There are, however, breaking changes to how associations are loaded. The association is no longer loaded via `find()` but rather `find_by!(id:)`. Given this, a major version was released.
data/README.md CHANGED
@@ -21,7 +21,7 @@ class SignupOp < ::Subroutine::Op
21
21
  validates :company_name, presence: true
22
22
 
23
23
  outputs :user
24
- outputs :business
24
+ outputs :business, type: Business # validate that output type is an instance of Business
25
25
 
26
26
  protected
27
27
 
@@ -60,7 +60,7 @@ module Subroutine
60
60
  end
61
61
 
62
62
  def build_foreign_key_field
63
- build_child_field(foreign_key_method, type: :foreign_key, foreign_key_type: config[:foreign_key_type])
63
+ build_child_field(foreign_key_method, type: :foreign_key, foreign_key_type: determine_foreign_key_type)
64
64
  end
65
65
 
66
66
  def build_foreign_type_field
@@ -84,6 +84,23 @@ module Subroutine
84
84
  ComponentConfiguration.new(name, child_opts)
85
85
  end
86
86
 
87
+ def determine_foreign_key_type
88
+ return config[:foreign_key_type] if config[:foreign_key_type]
89
+
90
+ # TODO: Make this logic work for polymorphic associations.
91
+ return if polymorphic?
92
+
93
+ klass = inferred_foreign_type&.constantize
94
+ if klass && klass.respond_to?(:type_for_attribute)
95
+ case klass.type_for_attribute(find_by)&.type&.to_sym
96
+ when :string
97
+ :string
98
+ else
99
+ :integer
100
+ end
101
+ end
102
+ end
103
+
87
104
  end
88
105
  end
89
106
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Subroutine
4
+ module Outputs
5
+ class InvalidOutputTypeError < StandardError
6
+
7
+ def initialize(name:, expected_type:, actual_type:)
8
+ super("Invalid output type for '#{name}' expected #{expected_type} but got #{actual_type}")
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -4,6 +4,7 @@ require "active_support/concern"
4
4
  require "subroutine/outputs/configuration"
5
5
  require "subroutine/outputs/output_not_set_error"
6
6
  require "subroutine/outputs/unknown_output_error"
7
+ require "subroutine/outputs/invalid_output_type_error"
7
8
 
8
9
  module Subroutine
9
10
  module Outputs
@@ -39,16 +40,6 @@ module Subroutine
39
40
  @outputs = {} # don't do with_indifferent_access because it will turn provided objects into with_indifferent_access objects, which may not be the desired behavior
40
41
  end
41
42
 
42
- def output_provided?(name)
43
- name = name.to_sym
44
-
45
- unless output_configurations.key?(name)
46
- raise ::Subroutine::Outputs::UnknownOutputError, name
47
- end
48
-
49
- outputs.key?(name)
50
- end
51
-
52
43
  def output(name, value)
53
44
  name = name.to_sym
54
45
  unless output_configurations.key?(name)
@@ -70,8 +61,33 @@ module Subroutine
70
61
  if config.required? && !output_provided?(name)
71
62
  raise ::Subroutine::Outputs::OutputNotSetError, name
72
63
  end
64
+ unless valid_output_type?(name)
65
+ name = name.to_sym
66
+ raise ::Subroutine::Outputs::InvalidOutputTypeError.new(
67
+ name: name,
68
+ actual_type: outputs[name].class,
69
+ expected_type: output_configurations[name][:type]
70
+ )
71
+ end
73
72
  end
74
73
  end
75
74
 
75
+ def output_provided?(name)
76
+ name = name.to_sym
77
+
78
+ outputs.key?(name)
79
+ end
80
+
81
+ def valid_output_type?(name)
82
+ name = name.to_sym
83
+
84
+ return true unless output_configurations.key?(name)
85
+
86
+ output_configuration = output_configurations[name]
87
+ return true unless output_configuration[:type]
88
+ return true if !output_configuration.required? && outputs[name].nil?
89
+
90
+ outputs[name].is_a?(output_configuration[:type])
91
+ end
76
92
  end
77
93
  end
@@ -3,8 +3,8 @@
3
3
  module Subroutine
4
4
 
5
5
  MAJOR = 2
6
- MINOR = 1
7
- PATCH = 2
6
+ MINOR = 3
7
+ PATCH = 0
8
8
  PRE = nil
9
9
 
10
10
  VERSION = [MAJOR, MINOR, PATCH, PRE].compact.join(".")
@@ -13,6 +13,10 @@ module Subroutine
13
13
  @fred ||= ::User.new(id: 2, email_address: "fred@example.com")
14
14
  end
15
15
 
16
+ def murphy
17
+ @murphy ||= ::StringIdUser.new(id: "ABACABADABACABA", email_address: "murphy@example.com")
18
+ end
19
+
16
20
  def account
17
21
  @account ||= ::Account.new(id: 1)
18
22
  end
@@ -43,6 +47,16 @@ module Subroutine
43
47
  assert_equal doug, op.user
44
48
  end
45
49
 
50
+ def test_it_looks_up_an_association_with_string_ids
51
+ all_mock = mock
52
+
53
+ ::StringIdUser.expects(:all).returns(all_mock)
54
+ all_mock.expects(:find_by!).with(id: "ABACABADABACABA").returns(murphy)
55
+
56
+ op = ::SimpleAssociationWithStringIdOp.new(string_id_user_id: murphy.id)
57
+ assert_equal murphy, op.string_id_user
58
+ end
59
+
46
60
  def test_it_sanitizes_types
47
61
  all_mock = mock
48
62
 
@@ -122,6 +136,16 @@ module Subroutine
122
136
  assert_equal "email_address", op.field_configurations[:user][:find_by]
123
137
  end
124
138
 
139
+ def test_it_allows_a_find_by_to_be_set_with_implicit_string
140
+ all_mock = mock
141
+ ::User.expects(:all).returns(all_mock)
142
+ all_mock.expects(:find_by!).with(email_address: doug.email_address).returns(doug)
143
+
144
+ op = ::AssociationWithImplicitStringFindByOp.new(user_id: doug.email_address)
145
+ assert_equal doug, op.user
146
+ assert_equal "email_address", op.field_configurations[:user][:find_by]
147
+ end
148
+
125
149
  def test_values_are_correct_for_find_by_usage
126
150
  op = ::AssociationWithFindByKeyOp.new(user: doug)
127
151
  assert_equal doug, op.user
@@ -264,30 +264,6 @@ module Subroutine
264
264
  assert_equal Hash, value.class
265
265
  end
266
266
 
267
- def test_it_raises_an_error_if_an_output_is_not_defined_but_is_set
268
- op = ::MissingOutputOp.new
269
- assert_raises ::Subroutine::Outputs::UnknownOutputError do
270
- op.submit
271
- end
272
- end
273
-
274
- def test_it_raises_an_error_if_not_all_outputs_were_set
275
- op = ::MissingOutputSetOp.new
276
- assert_raises ::Subroutine::Outputs::OutputNotSetError do
277
- op.submit
278
- end
279
- end
280
-
281
- def test_it_does_not_raise_an_error_if_output_is_not_set_and_is_not_required
282
- op = ::OutputNotRequiredOp.new
283
- op.submit
284
- end
285
-
286
- def test_it_does_not_raise_an_error_if_the_perform_is_not_a_success
287
- op = ::NoOutputNoSuccessOp.new
288
- refute op.submit
289
- end
290
-
291
267
  def test_it_does_not_omit_the_backtrace_from_the_original_error
292
268
  op = ::ErrorTraceOp.new
293
269
  begin
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ module Subroutine
6
+ class OutputsTest < TestCase
7
+ class MissingOutputOp < ::Subroutine::Op
8
+ def perform
9
+ output :foo, 'bar'
10
+ end
11
+ end
12
+
13
+ class MissingOutputSetOp < ::Subroutine::Op
14
+ outputs :foo
15
+ def perform
16
+ true
17
+ end
18
+ end
19
+
20
+ class OutputNotRequiredOp < ::Subroutine::Op
21
+ outputs :foo, required: false
22
+ def perform
23
+ true
24
+ end
25
+ end
26
+
27
+ class NoOutputNoSuccessOp < ::Subroutine::Op
28
+ outputs :foo
29
+
30
+ def perform
31
+ errors.add(:foo, 'bar')
32
+ end
33
+ end
34
+
35
+ class OutputWithTypeValidationNotRequired < ::Subroutine::Op
36
+ outputs :value, type: String, required: false
37
+
38
+ def perform; end
39
+ end
40
+
41
+ class OutputWithTypeValidationRequired < ::Subroutine::Op
42
+ outputs :value, type: String, required: true
43
+
44
+ def perform; end
45
+ end
46
+
47
+ def test_it_raises_an_error_if_an_output_is_not_defined_but_is_set
48
+ op = MissingOutputOp.new
49
+ assert_raises ::Subroutine::Outputs::UnknownOutputError do
50
+ op.submit
51
+ end
52
+ end
53
+
54
+ def test_it_raises_an_error_if_not_all_outputs_were_set
55
+ op = MissingOutputSetOp.new
56
+ assert_raises ::Subroutine::Outputs::OutputNotSetError do
57
+ op.submit
58
+ end
59
+ end
60
+
61
+ def test_it_does_not_raise_an_error_if_output_is_not_set_and_is_not_required
62
+ op = OutputNotRequiredOp.new
63
+ op.submit
64
+ end
65
+
66
+ def test_it_does_not_raise_an_error_if_the_perform_is_not_a_success
67
+ op = NoOutputNoSuccessOp.new
68
+ refute op.submit
69
+ end
70
+
71
+ ###################
72
+ # type validation #
73
+ ###################
74
+
75
+ def test_it_does_not_raise_an_error_if_output_is_set_to_the_right_type
76
+ op = OutputWithTypeValidationNotRequired.new
77
+ op.send(:output, :value, 'foo')
78
+ assert op.submit
79
+ end
80
+
81
+ def test_it_raises_an_error_if_output_is_not_set_to_the_right_type
82
+ op = OutputWithTypeValidationNotRequired.new
83
+ op.send(:output, :value, 1)
84
+ assert_raises ::Subroutine::Outputs::InvalidOutputTypeError do
85
+ op.submit
86
+ end
87
+ end
88
+
89
+ def test_it_does_not_raise_an_error_if_output_is_set_to_nil_when_there_is_type_validation_and_not_required
90
+ op = OutputWithTypeValidationNotRequired.new
91
+ op.send(:output, :value, nil)
92
+ op.submit
93
+ end
94
+
95
+ def test_it_raises_an_error_if_output_is_set_to_nil_when_there_is_type_validation_and_is_required
96
+ op = OutputWithTypeValidationRequired.new
97
+ op.send(:output, :value, nil)
98
+ assert_raises ::Subroutine::Outputs::InvalidOutputTypeError do
99
+ op.submit
100
+ end
101
+ end
102
+ end
103
+ end
data/test/support/ops.rb CHANGED
@@ -31,6 +31,23 @@ class User
31
31
  find_by(params) || raise
32
32
  end
33
33
 
34
+ def self.type_for_attribute(attribute)
35
+ case attribute
36
+ when :id
37
+ Struct.new(:type).new(:integer)
38
+ else
39
+ Struct.new(:type).new(:string)
40
+ end
41
+ end
42
+
43
+ end
44
+
45
+ class StringIdUser < ::User
46
+
47
+ def self.type_for_attribute(attribute)
48
+ Struct.new(:type).new(:string)
49
+ end
50
+
34
51
  end
35
52
 
36
53
  class AdminUser < ::User
@@ -325,6 +342,12 @@ class SimpleAssociationOp < ::OpWithAssociation
325
342
 
326
343
  end
327
344
 
345
+ class SimpleAssociationWithStringIdOp < ::OpWithAssociation
346
+
347
+ association :string_id_user
348
+
349
+ end
350
+
328
351
  class UnscopedSimpleAssociationOp < ::OpWithAssociation
329
352
 
330
353
  association :user, unscoped: true, allow_overwrite: true
@@ -355,6 +378,12 @@ class AssociationWithFindByKeyOp < ::OpWithAssociation
355
378
 
356
379
  end
357
380
 
381
+ class AssociationWithImplicitStringFindByOp < ::OpWithAssociation
382
+
383
+ association :user, find_by: "email_address"
384
+
385
+ end
386
+
358
387
  class AssociationWithFindByAndForeignKeyOp < ::OpWithAssociation
359
388
 
360
389
  association :user, foreign_key: "email_address", find_by: "email_address"
@@ -417,41 +446,6 @@ class FalsePerformOp < ::Subroutine::Op
417
446
 
418
447
  end
419
448
 
420
- class MissingOutputOp < ::Subroutine::Op
421
-
422
- def perform
423
- output :foo, "bar"
424
- end
425
-
426
- end
427
-
428
- class MissingOutputSetOp < ::Subroutine::Op
429
-
430
- outputs :foo
431
- def perform
432
- true
433
- end
434
-
435
- end
436
-
437
- class OutputNotRequiredOp < ::Subroutine::Op
438
-
439
- outputs :foo, required: false
440
- def perform
441
- true
442
- end
443
-
444
- end
445
-
446
- class NoOutputNoSuccessOp < ::Subroutine::Op
447
-
448
- outputs :foo
449
- def perform
450
- errors.add(:foo, "bar")
451
- end
452
-
453
- end
454
-
455
449
  class ErrorTraceOp < ::Subroutine::Op
456
450
 
457
451
  class SomeObject
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: 2.1.2
4
+ version: 2.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: 2022-05-07 00:00:00.000000000 Z
11
+ date: 2023-03-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -157,10 +157,10 @@ executables: []
157
157
  extensions: []
158
158
  extra_rdoc_files: []
159
159
  files:
160
+ - ".github/workflows/build.yml"
160
161
  - ".gitignore"
161
162
  - ".ruby-gemset"
162
163
  - ".ruby-version"
163
- - ".travis.yml"
164
164
  - CHANGELOG.MD
165
165
  - Gemfile
166
166
  - LICENSE.txt
@@ -187,6 +187,7 @@ files:
187
187
  - lib/subroutine/op.rb
188
188
  - lib/subroutine/outputs.rb
189
189
  - lib/subroutine/outputs/configuration.rb
190
+ - lib/subroutine/outputs/invalid_output_type_error.rb
190
191
  - lib/subroutine/outputs/output_not_set_error.rb
191
192
  - lib/subroutine/outputs/unknown_output_error.rb
192
193
  - lib/subroutine/type_caster.rb
@@ -196,6 +197,7 @@ files:
196
197
  - test/subroutine/auth_test.rb
197
198
  - test/subroutine/base_test.rb
198
199
  - test/subroutine/fields_test.rb
200
+ - test/subroutine/outputs_test.rb
199
201
  - test/subroutine/type_caster_test.rb
200
202
  - test/support/ops.rb
201
203
  - test/test_helper.rb
@@ -218,7 +220,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
218
220
  - !ruby/object:Gem::Version
219
221
  version: '0'
220
222
  requirements: []
221
- rubygems_version: 3.1.6
223
+ rubygems_version: 3.3.23
222
224
  signing_key:
223
225
  specification_version: 4
224
226
  summary: Feature-driven operation objects.
@@ -233,6 +235,7 @@ test_files:
233
235
  - test/subroutine/auth_test.rb
234
236
  - test/subroutine/base_test.rb
235
237
  - test/subroutine/fields_test.rb
238
+ - test/subroutine/outputs_test.rb
236
239
  - test/subroutine/type_caster_test.rb
237
240
  - test/support/ops.rb
238
241
  - test/test_helper.rb
data/.travis.yml DELETED
@@ -1,20 +0,0 @@
1
- language: ruby
2
- sudo: false
3
-
4
- rvm:
5
- - 2.4.6
6
- - 2.5.5
7
- - 2.6.3
8
-
9
- gemfile:
10
- - gemfiles/am41.gemfile
11
- - gemfiles/am42.gemfile
12
- - gemfiles/am50.gemfile
13
- - gemfiles/am51.gemfile
14
- - gemfiles/am52.gemfile
15
- - gemfiles/am60.gemfile
16
-
17
- matrix:
18
- exclude:
19
- - rvm: 2.4.6
20
- gemfile: gemfiles/am60.gemfile