subroutine 1.0.0 → 2.0.1

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: 25c11b61b2c6af19a7be888c2ba66284cb799cf72131e9f7a7893ccf188babd6
4
- data.tar.gz: b22a8fec39573b99429fb27ad07340c1e0fa3133562a5190b8d6d088eeb9dbab
3
+ metadata.gz: 9fbd9c97951c8e07f3f84fe1f9826d4fff45a7f0fe99045d375e34d530fb0934
4
+ data.tar.gz: db76eef88d0afd766a7a906d0cf5a202286d90da3f6ec113e798b6709f3c96e4
5
5
  SHA512:
6
- metadata.gz: 559745dc62dde51e43bde01b4b5fd2598a40edf6b02945abc2f7d71a0b948e5c76d259ac8239ba91fe8aadd4f23db4569feeaf01320287744b25dd9b168b4c29
7
- data.tar.gz: b8bb15b169ddd8fe7a8e64bb4dcd8f6815b458fda0dc6f035310c2e2ea483a92196357f4893945f982ed661e1dd01ca4d51a43a9250e4b96fd5eeda1459ff22f
6
+ metadata.gz: 2310334da4ebf40292d6868c1ab59b028d790fd01db3ef55825d4f680810e157ffd35c9d6a19e6b1e58816aad8eb87f01b1ae9e9395914350d23b80a84b4874b
7
+ data.tar.gz: 4b426d21e7e91f27405daf3a7438f4a049f4a5a7ee13002488259e5599989a867d2fbf9c31f56cff7c8be452708175ba786700729ab88b2f186c4ed4159fcecb
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.5.1
1
+ 2.5.7
data/CHANGELOG.MD ADDED
@@ -0,0 +1,111 @@
1
+ ## Subroutine 1.0
2
+
3
+ A massive refactor took place between 0.9 and 1.0, including breaking changes. The goal was to reduce complexity, simplify backtraces, and increase the overall safety and reliability of the library.
4
+
5
+ ### Subroutine::Fields
6
+
7
+ `Subroutine::Fields` was completely refactored to manage field declaration, configuration, and access in a more systematic and safer way.
8
+
9
+ `Op._fields` was removed in favor of `Op.field_configurations`. `field_configurations` is a hash with keys of the field name and values of `FieldConfiguration` objects. FieldConfiguration objects are SimpleDelegates to the underlying option hashes. They answer questions and provide a mechanism for validating the configuration of a field.
10
+
11
+ Fields can be accessed via helpers and accessors can be managed by the field declration. Helpers include `get_field`, `set_field`, and `clear_field`.
12
+
13
+ ```ruby
14
+ class SomeOp < ::Subroutine::Op
15
+ string :foo, read_accessor: field_reader: false, field_writer: true
16
+
17
+ def perform
18
+ self.foo = "bar"
19
+ self.foo # NoMethodError
20
+ self.get_field(:foo) # => "bar"
21
+ end
22
+ end
23
+ ```
24
+
25
+ Fields can be omitted from mass assignment, meaning they would not be respected via constructor signatures.
26
+
27
+ ```ruby
28
+ class SomeOp < ::Subroutine::Op
29
+ string :foo, mass_assignable: false
30
+ def perform
31
+ puts foo
32
+ end
33
+ end
34
+
35
+ SomeOp.submit!(foo: "Hello World!") # raises ::Subroutine::Fields::MassAssignmentError
36
+ SomeOp.new{|op| op.foo = "Hello World!" }.submit! # prints "Hello World!"
37
+ ```
38
+
39
+ This is especially useful when dealing with user input and potentially unsafe attributes.
40
+
41
+ ```ruby
42
+ class UserUpdateOp < ::Op
43
+ association :user
44
+ string :first_name
45
+ string :last_name
46
+ integer :credit_balance_cents, mass_assignable: false
47
+
48
+ def perform
49
+ user.update(params)
50
+ end
51
+ end
52
+
53
+ # some_controller.rb
54
+ def update
55
+ UserUpdateOp.submit!(params.merge(user: current_user))
56
+ end
57
+ ```
58
+
59
+ Field groups were added as well, allowing you to access subsets of the fields easily.
60
+
61
+ ```ruby
62
+ class AccountUpdateOp < ::Op
63
+ association :account
64
+
65
+ with_options group: :user do
66
+ string :first_name
67
+ string :last_name
68
+ date :dob
69
+ end
70
+
71
+ with_options group: :business do
72
+ string :company_name
73
+ string :ein
74
+ end
75
+
76
+ def perform
77
+ account.user.update(user_params)
78
+ account.business.update(business_params)
79
+ end
80
+
81
+ end
82
+ ```
83
+
84
+ ActionController::Parameters from Rails 5+ are now transformed to a hash in `Subroutine::Fields` by default. This means strong parameters are essentially unused when passing `Subroutine::Fields`.
85
+
86
+ Read more about field management and access in https://github.com/guideline-tech/subroutine/wiki/Param-Usage
87
+
88
+ ### Subroutine::Association
89
+
90
+ The `Subroutine::Association` module has been moved to `Subroutine::AssociationFields`.
91
+
92
+ Only native types are stored in params now. The objects loaded from associations are stored in an `association_cache`. This ensures access to fields are consistent regardless of the inputs.
93
+
94
+ ```ruby
95
+ class SomeOp < ::Subroutine::Op
96
+ association :user
97
+ association :resource, polymorphic: true
98
+ end
99
+
100
+ user = User.find(4)
101
+
102
+ op = SomeOp.new(user: user, resource: user)
103
+ op.params #=> { user_id: 4, resource_type: "User", resource_id: 4 }
104
+ op.params_with_association #=> { user: <User:103204 @id=4>, resource: <User:103204 @id=4> }
105
+
106
+ op = SomeOp.new(user_id: user.id, resource_type: "User", resource_id: user.id)
107
+ op.params #=> { user_id: 4, resource_type: "User", resource_id: 4 }
108
+ op.params_with_association #=> { user: <User:290053 @id=4>, resource: <User:29042 @id=4> }
109
+ ```
110
+
111
+ Assignment of associations now validates the type. If an association is not polymorphic, the type will be validated against the expected type.
data/README.md CHANGED
@@ -12,7 +12,7 @@ class SignupOp < ::Subroutine::Op
12
12
  string :name
13
13
  string :email
14
14
  string :password
15
-
15
+
16
16
  string :company_name
17
17
 
18
18
  validates :name, presence: true
@@ -28,7 +28,7 @@ class SignupOp < ::Subroutine::Op
28
28
  def perform
29
29
  u = create_user!
30
30
  b = create_business!(u)
31
-
31
+
32
32
  deliver_welcome_email(u)
33
33
 
34
34
  output :user, u
@@ -38,7 +38,7 @@ class SignupOp < ::Subroutine::Op
38
38
  def create_user!
39
39
  User.create!(name: name, email: email, password: password)
40
40
  end
41
-
41
+
42
42
  def create_business!(owner)
43
43
  Business.create!(company_name: company_name, owner: owner)
44
44
  end
@@ -56,7 +56,9 @@ end
56
56
  - Clear and concise intention in a single file
57
57
  - Multi-model operations become simple
58
58
 
59
- [Implementing an Op](https://github.com/guideline-tech/subroutine/wiki/Implementing-an-Op)
60
- [Using an Op](https://github.com/guideline-tech/subroutine/wiki/Using-an-Op)
61
- [Errors](https://github.com/guideline-tech/subroutine/wiki/Errors)
62
- [Basic Usage in Rails](https://github.com/guideline-tech/subroutine/wiki/Rails-Usage)
59
+ ## Continue Reading
60
+
61
+ - [Implementing an Op](https://github.com/guideline-tech/subroutine/wiki/Implementing-an-Op)
62
+ - [Using an Op](https://github.com/guideline-tech/subroutine/wiki/Using-an-Op)
63
+ - [Errors](https://github.com/guideline-tech/subroutine/wiki/Errors)
64
+ - [Basic Usage in Rails](https://github.com/guideline-tech/subroutine/wiki/Rails-Usage)
@@ -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
@@ -19,6 +19,13 @@ module Subroutine
19
19
  super + [::Subroutine::AssociationFields]
20
20
  end
21
21
 
22
+ def related_field_names
23
+ out = super
24
+ out << foreign_key_method
25
+ out << foreign_type_method if polymorphic?
26
+ out
27
+ end
28
+
22
29
  def polymorphic?
23
30
  !!config[:polymorphic]
24
31
  end
@@ -27,12 +34,13 @@ module Subroutine
27
34
  config[:as] || field_name
28
35
  end
29
36
 
30
- def class_name
31
- config[:class_name]&.to_s
37
+ def foreign_type
38
+ (config[:foreign_type] || config[:class_name])&.to_s
32
39
  end
40
+ alias class_name foreign_type
33
41
 
34
- def inferred_class_name
35
- class_name || as.to_s.camelize
42
+ def inferred_foreign_type
43
+ foreign_type || as.to_s.camelize
36
44
  end
37
45
 
38
46
  def foreign_key
@@ -40,15 +48,19 @@ module Subroutine
40
48
  end
41
49
 
42
50
  def foreign_key_method
43
- foreign_key || "#{field_name}_id"
51
+ (foreign_key || "#{field_name}_id").to_sym
44
52
  end
45
53
 
46
54
  def foreign_type_method
47
- foreign_key_method.gsub(/_id$/, "_type")
55
+ foreign_key_method.to_s.gsub(/_id$/, "_type").to_sym
56
+ end
57
+
58
+ def find_by
59
+ (config[:find_by] || :id).to_sym
48
60
  end
49
61
 
50
62
  def build_foreign_key_field
51
- build_child_field(foreign_key_method, type: :integer)
63
+ build_child_field(foreign_key_method, type: :foreign_key, foreign_key_type: config[:foreign_key_type])
52
64
  end
53
65
 
54
66
  def build_foreign_type_field
@@ -66,7 +78,10 @@ module Subroutine
66
78
  protected
67
79
 
68
80
  def build_child_field(name, opts = {})
69
- ComponentConfiguration.new(name, inheritable_options.merge(opts).merge(association_name: as))
81
+ child_opts = inheritable_options
82
+ child_opts.merge!(opts)
83
+ child_opts[:association_name] = as
84
+ ComponentConfiguration.new(name, child_opts)
70
85
  end
71
86
 
72
87
  end
@@ -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
@@ -100,8 +100,7 @@ module Subroutine
100
100
 
101
101
  excepts = []
102
102
  association_fields.each_pair do |_name, config|
103
- excepts << config.foreign_key_method
104
- excepts << config.foreign_type_method if config.polymorphic?
103
+ excepts |= config.related_field_names
105
104
  end
106
105
 
107
106
  out = params.except(*excepts)
@@ -120,7 +119,7 @@ module Subroutine
120
119
  if config&.behavior == :association
121
120
  maybe_raise_on_association_type_mismatch!(config, value)
122
121
  set_field(config.foreign_type_method, value&.class&.name, opts) if config.polymorphic?
123
- set_field(config.foreign_key_method, value&.id, opts)
122
+ set_field(config.foreign_key_method, value&.send(config.find_by), opts)
124
123
  association_cache[config.field_name] = value
125
124
  else
126
125
  if config&.behavior == :association_component
@@ -138,10 +137,7 @@ module Subroutine
138
137
  stored_result = association_cache[config.field_name]
139
138
  return stored_result unless stored_result.nil?
140
139
 
141
- fk = config.field_reader? ? send(config.foreign_key_method) : get_field(config.foreign_id_method)
142
- type = config.field_reader? || !config.polymorphic? ? send(config.foreign_type_method) : get_field(config.foreign_type_method)
143
-
144
- result = fetch_association_instance(type, fk, config.unscoped?)
140
+ result = fetch_association_instance(config)
145
141
  association_cache[config.field_name] = result
146
142
  else
147
143
  get_field_without_association(field_name)
@@ -176,25 +172,32 @@ module Subroutine
176
172
  end
177
173
  end
178
174
 
179
- def fetch_association_instance(_type, _fk, _unscoped = false)
180
- 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
181
182
 
182
- klass = _type
183
183
  klass = klass.classify.constantize if klass.is_a?(String)
184
-
185
184
  return nil unless klass
186
185
 
186
+ foreign_key = config.foreign_key_method
187
+ value = send(foreign_key)
188
+ return nil unless value
189
+
187
190
  scope = klass.all
188
- scope = scope.unscoped if _unscoped
191
+ scope = scope.unscoped if config.unscoped?
189
192
 
190
- scope.find(_fk)
193
+ scope.find_by!(config.find_by => value)
191
194
  end
192
195
 
193
196
  def maybe_raise_on_association_type_mismatch!(config, record)
194
197
  return if config.polymorphic?
195
198
  return if record.nil?
196
199
 
197
- klass = config.inferred_class_name.constantize
200
+ klass = config.inferred_foreign_type.constantize
198
201
 
199
202
  return if record.class <= klass || record.class >= klass
200
203
 
@@ -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)
@@ -22,7 +22,7 @@ module Subroutine
22
22
  attr_reader :field_name
23
23
 
24
24
  def initialize(field_name, options)
25
- @field_name = field_name
25
+ @field_name = field_name.to_sym
26
26
  config = sanitize_options(options)
27
27
  super(config)
28
28
  validate!
@@ -38,6 +38,10 @@ module Subroutine
38
38
  []
39
39
  end
40
40
 
41
+ def related_field_names
42
+ [field_name]
43
+ end
44
+
41
45
  def behavior
42
46
  nil
43
47
  end
@@ -96,6 +100,8 @@ module Subroutine
96
100
  groups = nil if groups == false
97
101
  opts[:groups] = Array(groups).map(&:to_sym).presence
98
102
  opts.delete(:group)
103
+ opts[:aka] = opts[:aka].to_sym if opts[:aka]
104
+ opts[:name] = field_name
99
105
  opts
100
106
  end
101
107
 
@@ -58,9 +58,12 @@ module Subroutine
58
58
  onlys = options.key?(:only) ? Array(options.delete(:only)) : nil
59
59
 
60
60
  things.each do |thing|
61
+ local_excepts = excepts.map { |field| thing.get_field_config(field)&.related_field_names }.flatten.compact.uniq if excepts
62
+ local_onlys = onlys.map { |field| thing.get_field_config(field)&.related_field_names }.flatten.compact.uniq if onlys
63
+
61
64
  thing.field_configurations.each_pair do |field_name, config|
62
- next if excepts&.include?(field_name)
63
- next if onlys && !onlys.include?(field_name)
65
+ next if local_excepts&.include?(field_name)
66
+ next if local_onlys && !local_onlys.include?(field_name)
64
67
 
65
68
  config.required_modules.each do |mod|
66
69
  include mod unless included_modules.include?(mod)
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
 
@@ -63,6 +63,15 @@ end
63
63
  String(value)
64
64
  end
65
65
 
66
+ ::Subroutine::TypeCaster.register :foreign_key do |value, options = {}|
67
+ next nil if value.blank?
68
+
69
+ next ::Subroutine::TypeCaster.cast(value, type: options[:foreign_key_type]) if options[:foreign_key_type]
70
+ next ::Subroutine::TypeCaster.cast(value, type: :integer) if options[:name] && options[:name].to_s.end_with?("_id")
71
+
72
+ value
73
+ end
74
+
66
75
  ::Subroutine::TypeCaster.register :boolean, :bool do |value, _options = {}|
67
76
  !!(String(value) =~ /^(yes|true|1|ok)$/)
68
77
  end
@@ -2,9 +2,9 @@
2
2
 
3
3
  module Subroutine
4
4
 
5
- MAJOR = 1
5
+ MAJOR = 2
6
6
  MINOR = 0
7
- PATCH = 0
7
+ PATCH = 1
8
8
  PRE = nil
9
9
 
10
10
  VERSION = [MAJOR, MINOR, PATCH, PRE].compact.join(".")
@@ -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,72 @@ 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: 10).returns(doug)
87
+
88
+ op = ::AssociationWithForeignKeyOp.new(owner_id: 10)
89
+ assert_equal doug, op.user
90
+ assert_equal "owner_id", op.field_configurations[:user][:foreign_key]
91
+ end
92
+
93
+ def test_the_foreign_key_is_cast
94
+ all_mock = mock
95
+ ::User.expects(:all).returns(all_mock)
96
+ all_mock.expects(:find_by!).with(id: 10).returns(doug)
97
+
98
+ op = ::AssociationWithForeignKeyOp.new(owner_id: "10")
99
+ assert_equal doug, op.user
100
+ assert_equal 10, op.owner_id
101
+ assert_equal "owner_id", op.field_configurations[:user][:foreign_key]
102
+ end
103
+
104
+ def test_it_allows_a_foreign_key_and_find_by_to_be_set
105
+ all_mock = mock
106
+ ::User.expects(:all).returns(all_mock)
107
+ all_mock.expects(:find_by!).with(email_address: "foo@bar.com").returns(doug)
108
+
109
+ op = ::AssociationWithFindByAndForeignKeyOp.new(email_address: "foo@bar.com")
110
+ assert_equal doug, op.user
111
+ assert_equal "foo@bar.com", op.email_address
112
+ assert_equal "email_address", op.field_configurations[:user][:find_by]
113
+ end
114
+
115
+ def test_it_allows_a_find_by_to_be_set
116
+ all_mock = mock
117
+ ::User.expects(:all).returns(all_mock)
118
+ all_mock.expects(:find_by!).with(email_address: doug.email_address).returns(doug)
119
+
120
+ op = ::AssociationWithFindByKeyOp.new(user_id: doug.email_address)
121
+ assert_equal doug, op.user
122
+ assert_equal "email_address", op.field_configurations[:user][:find_by]
123
+ end
124
+
125
+ def test_values_are_correct_for_find_by_usage
126
+ op = ::AssociationWithFindByKeyOp.new(user: doug)
127
+ assert_equal doug, op.user
128
+ assert_equal doug.email_address, op.user_id
129
+ end
130
+
131
+ def test_values_are_correct_for_foreign_key_usage
132
+ op = ::AssociationWithForeignKeyOp.new(user: doug)
133
+ assert_equal doug, op.user
134
+ assert_equal doug.id, op.owner_id
135
+ end
136
+
137
+ def test_values_are_correct_for_both_foreign_key_and_find_by_usage
138
+ op = ::AssociationWithFindByAndForeignKeyOp.new(user: doug)
139
+ assert_equal doug, op.user
140
+ assert_equal doug.email_address, op.email_address
141
+ assert_equal false, op.respond_to?(:user_id)
142
+ end
143
+
73
144
  def test_it_inherits_associations_via_fields_from
74
145
  all_mock = mock
75
146
 
76
147
  ::User.expects(:all).returns(all_mock)
77
- all_mock.expects(:find).with(1).returns(doug)
148
+ all_mock.expects(:find_by!).with(id: 1).returns(doug)
78
149
 
79
150
  op = ::InheritedSimpleAssociation.new(user_type: "User", user_id: doug.id)
80
151
  assert_equal doug, op.user
@@ -88,7 +159,7 @@ module Subroutine
88
159
 
89
160
  ::User.expects(:all).returns(all_mock)
90
161
  all_mock.expects(:unscoped).returns(unscoped_mock)
91
- unscoped_mock.expects(:find).with(1).returns(doug)
162
+ unscoped_mock.expects(:find_by!).with(id: 1).returns(doug)
92
163
 
93
164
  op = ::InheritedUnscopedAssociation.new(user_type: "User", user_id: doug.id)
94
165
  assert_equal doug, op.user
@@ -100,7 +171,7 @@ module Subroutine
100
171
  all_mock = mock
101
172
  ::User.expects(:all).never
102
173
  ::AdminUser.expects(:all).returns(all_mock)
103
- all_mock.expects(:find).with(1).returns(doug)
174
+ all_mock.expects(:find_by!).with(id: 1).returns(doug)
104
175
 
105
176
  op = ::InheritedPolymorphicAssociationOp.new(admin_type: "AdminUser", admin_id: doug.id)
106
177
  assert_equal doug, op.admin
@@ -42,6 +42,13 @@ module Subroutine
42
42
  assert_equal [:baz], op.field_configurations.keys.sort
43
43
  end
44
44
 
45
+ def test_fields_from_ignores_except_association_fields
46
+ op = ::ExceptAssociationOp.new
47
+ refute op.field_configurations.key?(:admin)
48
+ refute op.field_configurations.key?(:admin_id)
49
+ refute op.field_configurations.key?(:admin_type)
50
+ end
51
+
45
52
  def test_fields_from_only_fields
46
53
  op = ::OnlyFooBarOp.new
47
54
  assert op.field_configurations.key?(:foo)
@@ -49,6 +56,13 @@ module Subroutine
49
56
  refute_equal [:baz], op.field_configurations.keys.sort
50
57
  end
51
58
 
59
+ def test_fields_from_only_association_fields
60
+ op = ::OnlyAssociationOp.new
61
+ assert op.field_configurations.key?(:admin)
62
+ assert op.field_configurations.key?(:admin_type)
63
+ assert op.field_configurations.key?(:admin_id)
64
+ end
65
+
52
66
  def test_defaults_declaration_options
53
67
  op = ::DefaultsOp.new
54
68
  assert_equal "foo", op.foo
@@ -113,6 +127,13 @@ module Subroutine
113
127
  assert_equal ["has gotta be @admin.com"], op.errors[:email]
114
128
  end
115
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
+
116
137
  def test_when_valid_perform_completes_it_returns_control
117
138
  op = ::SignupOp.new(email: "foo@bar.com", password: "password123")
118
139
  op.submit!
@@ -125,6 +146,11 @@ module Subroutine
125
146
  assert_equal "foo@bar.com", u.email_address
126
147
  end
127
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
+
128
154
  def test_it_raises_an_error_when_used_with_a_bang_and_performing_or_validation_fails
129
155
  op = ::SignupOp.new(email: "foo@bar.com")
130
156
 
@@ -261,5 +261,28 @@ module Subroutine
261
261
  op.date_input = "2015-13-01"
262
262
  end
263
263
  end
264
+
265
+ def test_foreign_key_inputs
266
+ op.fk_input_owner_id = nil
267
+ assert_nil op.fk_input_owner_id
268
+
269
+ op.fk_input_owner_id = ""
270
+ assert_nil op.fk_input_owner_id
271
+
272
+ op.fk_input_owner_id = "19402"
273
+ assert_equal 19402, op.fk_input_owner_id
274
+
275
+ op.fk_input_owner_id = "19402.0"
276
+ assert_equal 19402, op.fk_input_owner_id
277
+
278
+ op.fk_input_email_address = nil
279
+ assert_nil op.fk_input_email_address
280
+
281
+ op.fk_input_email_address = ""
282
+ assert_nil op.fk_input_email_address
283
+
284
+ op.fk_input_email_address = "foo@bar.com"
285
+ assert_equal "foo@bar.com", op.fk_input_email_address
286
+ end
264
287
  end
265
288
  end
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
@@ -158,6 +166,8 @@ class TypeCastOp < ::Subroutine::Op
158
166
  array :array_input, default: "foo"
159
167
  array :type_array_input, of: :integer
160
168
  file :file_input
169
+ foreign_key :fk_input_owner_id
170
+ foreign_key :fk_input_email_address, foreign_key_type: :string
161
171
 
162
172
  end
163
173
 
@@ -208,7 +218,7 @@ class CustomAuthorizeOp < OpWithAuth
208
218
  protected
209
219
 
210
220
  def authorize_user_is_correct
211
- unauthorized! unless current_user.email_address.to_s =~ /example\.com$/
221
+ unauthorized! unless current_user.email_address.to_s =~ /example\.com$/ # rubocop:disable Performance/RegexpMatch
212
222
  end
213
223
 
214
224
  end
@@ -304,6 +314,36 @@ class AssociationWithClassOp < ::OpWithAssociation
304
314
 
305
315
  end
306
316
 
317
+ class AssociationWithForeignKeyOp < ::OpWithAssociation
318
+
319
+ association :user, foreign_key: "owner_id"
320
+
321
+ end
322
+
323
+ class AssociationWithFindByKeyOp < ::OpWithAssociation
324
+
325
+ association :user, find_by: "email_address", foreign_key_type: :string
326
+
327
+ end
328
+
329
+ class AssociationWithFindByAndForeignKeyOp < ::OpWithAssociation
330
+
331
+ association :user, foreign_key: "email_address", find_by: "email_address"
332
+
333
+ end
334
+
335
+ class ExceptAssociationOp < ::Subroutine::Op
336
+
337
+ fields_from ::PolymorphicAssociationOp, except: %i[admin]
338
+
339
+ end
340
+
341
+ class OnlyAssociationOp < ::Subroutine::Op
342
+
343
+ fields_from ::PolymorphicAssociationOp, only: %i[admin]
344
+
345
+ end
346
+
307
347
  class InheritedSimpleAssociation < ::Subroutine::Op
308
348
 
309
349
  fields_from SimpleAssociationOp
@@ -435,3 +475,15 @@ class CustomFailureClassOp < ::Subroutine::Op
435
475
  end
436
476
 
437
477
  end
478
+
479
+ class PrefixedInputsOp < ::Subroutine::Op
480
+
481
+ string :user_email_address
482
+
483
+ def perform
484
+ u = AdminUser.new(email_address: user_email_address)
485
+ u.valid?
486
+ inherit_errors(u, prefix: :user_)
487
+ end
488
+
489
+ 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.0
4
+ version: 2.0.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: 2020-02-19 00:00:00.000000000 Z
11
+ date: 2022-02-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -161,6 +161,7 @@ files:
161
161
  - ".ruby-gemset"
162
162
  - ".ruby-version"
163
163
  - ".travis.yml"
164
+ - CHANGELOG.MD
164
165
  - Gemfile
165
166
  - LICENSE.txt
166
167
  - README.md
@@ -217,7 +218,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
217
218
  - !ruby/object:Gem::Version
218
219
  version: '0'
219
220
  requirements: []
220
- rubygems_version: 3.0.6
221
+ rubygems_version: 3.3.7
221
222
  signing_key:
222
223
  specification_version: 4
223
224
  summary: Feature-driven operation objects.