subroutine 2.1.2 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
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