subroutine 1.0.1 → 2.0.0.beta

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: a8871d701ab81402f275e38e7bf632c629bffc4320d31040119d99b12db05caf
4
- data.tar.gz: edba79a803523a74e4e53d5e9cf62bf0dbdb2087592e7503b7b2d18395e5a8a6
3
+ metadata.gz: d0ade77ec5abb9601e1dce7bc53e34baac35c978a41aef3268b57ee5b9def6e5
4
+ data.tar.gz: c450947886efc34b26b202b52202909f44735a2d5f26a3c1af51fdf93be509c6
5
5
  SHA512:
6
- metadata.gz: 9953dd7a8ac11f4c842773c88a338e255c254bbf38f50a3437f204406a8136eca15544cd87aa9641395faf933ca9a00ddf9b3951a2a76326302e677988240143
7
- data.tar.gz: 900b42b891947630463f3700a5df207d5e689a91dbe2e1970987f41b565fac7889083161d9d39bfd36082a7d0f4d1667d9b8bd549e7804371c58ad2987d238a0
6
+ metadata.gz: c171a9bb82076ed4f5e407375d19e423a5e6bba0211192109d300e3b7372d537a0bac3d67ef5c480ef9af34ddd2a849a07fef79459f5597ea97e195cad239fe7
7
+ data.tar.gz: 52502e8dd504de2d59faa4665ab7c755c9ff6ba8b951e79acaa264c4d257f09a20da58977f1565e8d121023d6a2f5291cd4528eca143d697252b2f6a27d4197b
@@ -74,7 +74,7 @@ module Subroutine
74
74
  class_eval <<-EV, __FILE__, __LINE__ + 1
75
75
  try(:silence_redefinition_of_method, :#{config.foreign_type_method})
76
76
  def #{config.foreign_type_method}
77
- #{config.inferred_class_name.inspect}
77
+ #{config.inferred_foreign_type.inspect}
78
78
  end
79
79
  EV
80
80
  end
@@ -119,7 +119,7 @@ module Subroutine
119
119
  if config&.behavior == :association
120
120
  maybe_raise_on_association_type_mismatch!(config, value)
121
121
  set_field(config.foreign_type_method, value&.class&.name, opts) if config.polymorphic?
122
- set_field(config.foreign_key_method, value&.id, opts)
122
+ set_field(config.foreign_key_method, value&.send(config.find_by), opts)
123
123
  association_cache[config.field_name] = value
124
124
  else
125
125
  if config&.behavior == :association_component
@@ -137,10 +137,7 @@ module Subroutine
137
137
  stored_result = association_cache[config.field_name]
138
138
  return stored_result unless stored_result.nil?
139
139
 
140
- fk = config.field_reader? ? send(config.foreign_key_method) : get_field(config.foreign_id_method)
141
- type = config.field_reader? || !config.polymorphic? ? send(config.foreign_type_method) : get_field(config.foreign_type_method)
142
-
143
- result = fetch_association_instance(type, fk, config.unscoped?)
140
+ result = fetch_association_instance(config)
144
141
  association_cache[config.field_name] = result
145
142
  else
146
143
  get_field_without_association(field_name)
@@ -175,25 +172,32 @@ module Subroutine
175
172
  end
176
173
  end
177
174
 
178
- def fetch_association_instance(_type, _fk, _unscoped = false)
179
- return nil unless _type && _fk
175
+ def fetch_association_instance(config)
176
+ klass =
177
+ if config.field_reader?
178
+ config.polymorphic? ? send(config.foreign_type_method) : config.inferred_foreign_type
179
+ else
180
+ get_field(config.foreign_type_method)
181
+ end
180
182
 
181
- klass = _type
182
183
  klass = klass.classify.constantize if klass.is_a?(String)
183
-
184
184
  return nil unless klass
185
185
 
186
+ foreign_key = config.foreign_key_method
187
+ value = send(foreign_key)
188
+ return nil unless value
189
+
186
190
  scope = klass.all
187
- scope = scope.unscoped if _unscoped
191
+ scope = scope.unscoped if config.unscoped?
188
192
 
189
- scope.find(_fk)
193
+ scope.find_by!(config.find_by => value)
190
194
  end
191
195
 
192
196
  def maybe_raise_on_association_type_mismatch!(config, record)
193
197
  return if config.polymorphic?
194
198
  return if record.nil?
195
199
 
196
- klass = config.inferred_class_name.constantize
200
+ klass = config.inferred_foreign_type.constantize
197
201
 
198
202
  return if record.class <= klass || record.class >= klass
199
203
 
@@ -10,7 +10,7 @@ module Subroutine
10
10
  def validate!
11
11
  super
12
12
 
13
- if as && foreign_key
13
+ if config[:as] && foreign_key
14
14
  raise ArgumentError, ":as and :foreign_key options should not be provided together to an association invocation"
15
15
  end
16
16
  end
@@ -34,12 +34,13 @@ module Subroutine
34
34
  config[:as] || field_name
35
35
  end
36
36
 
37
- def class_name
38
- config[:class_name]&.to_s
37
+ def foreign_type
38
+ (config[:foreign_type] || config[:class_name])&.to_s
39
39
  end
40
+ alias class_name foreign_type
40
41
 
41
- def inferred_class_name
42
- class_name || as.to_s.camelize
42
+ def inferred_foreign_type
43
+ foreign_type || as.to_s.camelize
43
44
  end
44
45
 
45
46
  def foreign_key
@@ -54,8 +55,12 @@ module Subroutine
54
55
  foreign_key_method.to_s.gsub(/_id$/, "_type").to_sym
55
56
  end
56
57
 
58
+ def find_by
59
+ (config[:find_by] || :id).to_sym
60
+ end
61
+
57
62
  def build_foreign_key_field
58
- build_child_field(foreign_key_method, type: :integer)
63
+ build_child_field(foreign_key_method)
59
64
  end
60
65
 
61
66
  def build_foreign_type_field
@@ -7,7 +7,7 @@ module Subroutine
7
7
  class Configuration < ::SimpleDelegator
8
8
 
9
9
  PROTECTED_GROUP_IDENTIFIERS = %i[all original default].freeze
10
- INHERITABLE_OPTIONS = %i[mass_assignable field_reader field_writer groups].freeze
10
+ INHERITABLE_OPTIONS = %i[mass_assignable field_reader field_writer groups aka].freeze
11
11
  NO_GROUPS = [].freeze
12
12
 
13
13
  def self.from(field_name, options)
@@ -100,6 +100,7 @@ module Subroutine
100
100
  groups = nil if groups == false
101
101
  opts[:groups] = Array(groups).map(&:to_sym).presence
102
102
  opts.delete(:group)
103
+ opts[:aka] = opts[:aka].to_sym if opts[:aka]
103
104
  opts
104
105
  end
105
106
 
data/lib/subroutine/op.rb CHANGED
@@ -37,28 +37,11 @@ module Subroutine
37
37
  op
38
38
  end
39
39
 
40
- protected
41
-
42
- def field(field_name, options = {})
43
- result = super(field_name, options)
44
-
45
- if options[:aka]
46
- Array(options[:aka]).each do |as|
47
- self._error_map = _error_map.merge(as.to_sym => field_name.to_sym)
48
- end
49
- end
50
-
51
- result
52
- end
53
-
54
40
  end
55
41
 
56
42
  class_attribute :_failure_class
57
43
  self._failure_class = Subroutine::Failure
58
44
 
59
- class_attribute :_error_map
60
- self._error_map = {}
61
-
62
45
  def initialize(inputs = {})
63
46
  setup_fields(inputs)
64
47
  setup_outputs
@@ -82,7 +65,7 @@ module Subroutine
82
65
 
83
66
  if errors.empty?
84
67
  validate_outputs!
85
- true
68
+ self
86
69
  else
87
70
  raise _failure_class, self
88
71
  end
@@ -91,6 +74,7 @@ module Subroutine
91
74
  # the action which should be invoked upon form submission (from the controller)
92
75
  def submit
93
76
  submit!
77
+ true
94
78
  rescue Exception => e
95
79
  if e.respond_to?(:record)
96
80
  inherit_errors(e.record) unless e.record == self
@@ -127,18 +111,23 @@ module Subroutine
127
111
  raise NotImplementedError
128
112
  end
129
113
 
130
- # applies the errors in error_object to self
131
- # returns false so failure cases can end with this invocation
132
- def inherit_errors(error_object)
114
+ def inherit_errors(error_object, prefix: nil)
133
115
  error_object = error_object.errors if error_object.respond_to?(:errors)
134
116
 
135
- error_object.each do |k, v|
136
- if respond_to?(k)
137
- errors.add(k, v)
138
- elsif _error_map[k.to_sym]
139
- errors.add(_error_map[k.to_sym], v)
117
+ error_object.each do |field_name, error|
118
+ field_name = "#{prefix}#{field_name}" if prefix
119
+ field_name = field_name.to_sym
120
+
121
+ field_config = get_field_config(field_name)
122
+ field_config ||= begin
123
+ kv = field_configurations.find { |_k, config| config[:aka] == field_name }
124
+ kv ? kv.last : nil
125
+ end
126
+
127
+ if field_config
128
+ errors.add(field_config.field_name, error)
140
129
  else
141
- errors.add(:base, error_object.full_message(k, v))
130
+ errors.add(:base, error_object.full_message(field_name, error))
142
131
  end
143
132
  end
144
133
 
@@ -2,10 +2,10 @@
2
2
 
3
3
  module Subroutine
4
4
 
5
- MAJOR = 1
5
+ MAJOR = 2
6
6
  MINOR = 0
7
- PATCH = 1
8
- PRE = nil
7
+ PATCH = 0
8
+ PRE = "beta"
9
9
 
10
10
  VERSION = [MAJOR, MINOR, PATCH, PRE].compact.join(".")
11
11
 
@@ -23,11 +23,21 @@ module Subroutine
23
23
  assert_equal doug.id, op.user_id
24
24
  end
25
25
 
26
+ def test_it_can_be_nil
27
+ op = SimpleAssociationOp.new user: nil
28
+ assert_nil op.user
29
+ assert_nil op.user_id
30
+
31
+ op = SimpleAssociationOp.new
32
+ assert_nil op.user
33
+ assert_nil op.user_id
34
+ end
35
+
26
36
  def test_it_looks_up_an_association
27
37
  all_mock = mock
28
38
 
29
39
  ::User.expects(:all).returns(all_mock)
30
- all_mock.expects(:find).with(1).returns(doug)
40
+ all_mock.expects(:find_by!).with(id: 1).returns(doug)
31
41
 
32
42
  op = SimpleAssociationOp.new user_type: "User", user_id: doug.id
33
43
  assert_equal doug, op.user
@@ -37,7 +47,7 @@ module Subroutine
37
47
  all_mock = mock
38
48
 
39
49
  ::User.expects(:all).returns(all_mock)
40
- all_mock.expects(:find).with(1).returns(doug)
50
+ all_mock.expects(:find_by!).with(id: 1).returns(doug)
41
51
 
42
52
  op = SimpleAssociationOp.new user_id: doug.id
43
53
  assert_equal doug, op.user
@@ -49,7 +59,7 @@ module Subroutine
49
59
 
50
60
  ::User.expects(:all).returns(all_mock)
51
61
  all_mock.expects(:unscoped).returns(unscoped_mock)
52
- unscoped_mock.expects(:find).with(1).returns(doug)
62
+ unscoped_mock.expects(:find_by!).with(id: 1).returns(doug)
53
63
 
54
64
  op = UnscopedSimpleAssociationOp.new user_id: doug.id
55
65
  assert_equal doug, op.user
@@ -59,7 +69,7 @@ module Subroutine
59
69
  all_mock = mock
60
70
  ::User.expects(:all).never
61
71
  ::AdminUser.expects(:all).returns(all_mock)
62
- all_mock.expects(:find).with(1).returns(doug)
72
+ all_mock.expects(:find_by!).with(id: 1).returns(doug)
63
73
 
64
74
  op = PolymorphicAssociationOp.new(admin_type: "AdminUser", admin_id: doug.id)
65
75
  assert_equal doug, op.admin
@@ -70,11 +80,60 @@ module Subroutine
70
80
  assert_equal "AdminUser", op.admin_type
71
81
  end
72
82
 
83
+ def test_it_allows_foreign_key_to_be_set
84
+ all_mock = mock
85
+ ::User.expects(:all).returns(all_mock)
86
+ all_mock.expects(:find_by!).with(id: "foobarbaz").returns(doug)
87
+
88
+ op = ::AssociationWithForeignKeyOp.new(user_identifier: "foobarbaz")
89
+ assert_equal doug, op.user
90
+ assert_equal "user_identifier", op.field_configurations[:user][:foreign_key]
91
+ end
92
+
93
+ def test_it_allows_a_foreign_key_and_find_by_to_be_set
94
+ all_mock = mock
95
+ ::User.expects(:all).returns(all_mock)
96
+ all_mock.expects(:find_by!).with(email_address: doug.email_address).returns(doug)
97
+
98
+ op = ::AssociationWithFindByAndForeignKeyOp.new(email_address: doug.email_address)
99
+ assert_equal doug, op.user
100
+ assert_equal "email_address", op.field_configurations[:user][:find_by]
101
+ end
102
+
103
+ def test_it_allows_a_find_by_to_be_set
104
+ all_mock = mock
105
+ ::User.expects(:all).returns(all_mock)
106
+ all_mock.expects(:find_by!).with(email_address: doug.email_address).returns(doug)
107
+
108
+ op = ::AssociationWithFindByKeyOp.new(user_id: doug.email_address)
109
+ assert_equal doug, op.user
110
+ assert_equal "email_address", op.field_configurations[:user][:find_by]
111
+ end
112
+
113
+ def test_values_are_correct_for_find_by_usage
114
+ op = ::AssociationWithFindByKeyOp.new(user: doug)
115
+ assert_equal doug, op.user
116
+ assert_equal doug.email_address, op.user_id
117
+ end
118
+
119
+ def test_values_are_correct_for_foreign_key_usage
120
+ op = ::AssociationWithForeignKeyOp.new(user: doug)
121
+ assert_equal doug, op.user
122
+ assert_equal doug.id, op.user_identifier
123
+ end
124
+
125
+ def test_values_are_correct_for_both_foreign_key_and_find_by_usage
126
+ op = ::AssociationWithFindByAndForeignKeyOp.new(user: doug)
127
+ assert_equal doug, op.user
128
+ assert_equal doug.email_address, op.email_address
129
+ assert_equal false, op.respond_to?(:user_id)
130
+ end
131
+
73
132
  def test_it_inherits_associations_via_fields_from
74
133
  all_mock = mock
75
134
 
76
135
  ::User.expects(:all).returns(all_mock)
77
- all_mock.expects(:find).with(1).returns(doug)
136
+ all_mock.expects(:find_by!).with(id: 1).returns(doug)
78
137
 
79
138
  op = ::InheritedSimpleAssociation.new(user_type: "User", user_id: doug.id)
80
139
  assert_equal doug, op.user
@@ -88,7 +147,7 @@ module Subroutine
88
147
 
89
148
  ::User.expects(:all).returns(all_mock)
90
149
  all_mock.expects(:unscoped).returns(unscoped_mock)
91
- unscoped_mock.expects(:find).with(1).returns(doug)
150
+ unscoped_mock.expects(:find_by!).with(id: 1).returns(doug)
92
151
 
93
152
  op = ::InheritedUnscopedAssociation.new(user_type: "User", user_id: doug.id)
94
153
  assert_equal doug, op.user
@@ -100,7 +159,7 @@ module Subroutine
100
159
  all_mock = mock
101
160
  ::User.expects(:all).never
102
161
  ::AdminUser.expects(:all).returns(all_mock)
103
- all_mock.expects(:find).with(1).returns(doug)
162
+ all_mock.expects(:find_by!).with(id: 1).returns(doug)
104
163
 
105
164
  op = ::InheritedPolymorphicAssociationOp.new(admin_type: "AdminUser", admin_id: doug.id)
106
165
  assert_equal doug, op.admin
@@ -127,6 +127,13 @@ module Subroutine
127
127
  assert_equal ["has gotta be @admin.com"], op.errors[:email]
128
128
  end
129
129
 
130
+ def test_validation_errors_can_be_inherited_and_prefixed
131
+ op = PrefixedInputsOp.new(user_email_address: "foo@bar.com")
132
+ refute op.submit
133
+
134
+ assert_equal ["has gotta be @admin.com"], op.errors[:user_email_address]
135
+ end
136
+
130
137
  def test_when_valid_perform_completes_it_returns_control
131
138
  op = ::SignupOp.new(email: "foo@bar.com", password: "password123")
132
139
  op.submit!
@@ -139,6 +146,11 @@ module Subroutine
139
146
  assert_equal "foo@bar.com", u.email_address
140
147
  end
141
148
 
149
+ def test_instance_and_class_submit_bang_return_instance
150
+ assert ::SignupOp.new(email: "foo@bar.com", password: "password123").submit!.is_a?(::SignupOp)
151
+ assert ::SignupOp.submit!(email: "foo@bar.com", password: "password123").is_a?(::SignupOp)
152
+ end
153
+
142
154
  def test_it_raises_an_error_when_used_with_a_bang_and_performing_or_validation_fails
143
155
  op = ::SignupOp.new(email: "foo@bar.com")
144
156
 
data/test/support/ops.rb CHANGED
@@ -23,6 +23,14 @@ class User
23
23
  new(id: id)
24
24
  end
25
25
 
26
+ def self.find_by(params)
27
+ new(params)
28
+ end
29
+
30
+ def self.find_by!(params)
31
+ find_by(params) || raise
32
+ end
33
+
26
34
  end
27
35
 
28
36
  class AdminUser < ::User
@@ -304,6 +312,24 @@ class AssociationWithClassOp < ::OpWithAssociation
304
312
 
305
313
  end
306
314
 
315
+ class AssociationWithForeignKeyOp < ::OpWithAssociation
316
+
317
+ association :user, foreign_key: "user_identifier"
318
+
319
+ end
320
+
321
+ class AssociationWithFindByKeyOp < ::OpWithAssociation
322
+
323
+ association :user, find_by: "email_address"
324
+
325
+ end
326
+
327
+ class AssociationWithFindByAndForeignKeyOp < ::OpWithAssociation
328
+
329
+ association :user, foreign_key: "email_address", find_by: "email_address"
330
+
331
+ end
332
+
307
333
  class ExceptAssociationOp < ::Subroutine::Op
308
334
 
309
335
  fields_from ::PolymorphicAssociationOp, except: %i[admin]
@@ -447,3 +473,15 @@ class CustomFailureClassOp < ::Subroutine::Op
447
473
  end
448
474
 
449
475
  end
476
+
477
+ class PrefixedInputsOp < ::Subroutine::Op
478
+
479
+ string :user_email_address
480
+
481
+ def perform
482
+ u = AdminUser.new(email_address: user_email_address)
483
+ u.valid?
484
+ inherit_errors(u, prefix: :user_)
485
+ end
486
+
487
+ end
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: 1.0.1
4
+ version: 2.0.0.beta
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Nelson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-05-21 00:00:00.000000000 Z
11
+ date: 2021-06-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -214,9 +214,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
214
214
  version: '0'
215
215
  required_rubygems_version: !ruby/object:Gem::Requirement
216
216
  requirements:
217
- - - ">="
217
+ - - ">"
218
218
  - !ruby/object:Gem::Version
219
- version: '0'
219
+ version: 1.3.1
220
220
  requirements: []
221
221
  rubygems_version: 3.0.6
222
222
  signing_key: