subroutine 1.0.0.rc2 → 2.0.0.beta

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: 83e75f941dcb1020be24cefa722acc6a2fa5689479eac294e50cdfab09a8ea97
4
- data.tar.gz: c7bbe661699afcbfd8ffe6b3da0ca06dd21ea6dd9c29e80265ad507361754483
3
+ metadata.gz: d0ade77ec5abb9601e1dce7bc53e34baac35c978a41aef3268b57ee5b9def6e5
4
+ data.tar.gz: c450947886efc34b26b202b52202909f44735a2d5f26a3c1af51fdf93be509c6
5
5
  SHA512:
6
- metadata.gz: 26f93744cee2306723ca1d4949b0cc1d85570cd761ac6254edc50b58ed26d884b81e3a53e478818072077177861d74b5f806527d03ca6716c9e6eba3575b1d8f
7
- data.tar.gz: 157b2503fba1c1866a4bda9e13b04311acf2dd87caf163010025f6495f08305bf63859bbb3d810c3a789bfbb753c0f9bc03f21518805158d79c5490bc66a8ad6
6
+ metadata.gz: c171a9bb82076ed4f5e407375d19e423a5e6bba0211192109d300e3b7372d537a0bac3d67ef5c480ef9af34ddd2a849a07fef79459f5597ea97e195cad239fe7
7
+ data.tar.gz: 52502e8dd504de2d59faa4665ab7c755c9ff6ba8b951e79acaa264c4d257f09a20da58977f1565e8d121023d6a2f5291cd4528eca143d697252b2f6a27d4197b
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 as 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/Gemfile CHANGED
@@ -5,4 +5,4 @@ source "https://rubygems.org"
5
5
  # Specify your gem's dependencies in subroutine.gemspec
6
6
  gemspec
7
7
 
8
- gem "activemodel", "~> 4.2.11"
8
+ eval_gemfile "gemfiles/am60.gemfile"
data/README.md CHANGED
@@ -13,26 +13,37 @@ class SignupOp < ::Subroutine::Op
13
13
  string :email
14
14
  string :password
15
15
 
16
+ string :company_name
17
+
16
18
  validates :name, presence: true
17
19
  validates :email, presence: true
18
20
  validates :password, presence: true
21
+ validates :company_name, presence: true
19
22
 
20
- outputs :signed_up_user
23
+ outputs :user
24
+ outputs :business
21
25
 
22
26
  protected
23
27
 
24
28
  def perform
25
29
  u = create_user!
26
- deliver_welcome_email!(u)
30
+ b = create_business!(u)
31
+
32
+ deliver_welcome_email(u)
27
33
 
28
- output :signed_up_user, u
34
+ output :user, u
35
+ output :business, b
29
36
  end
30
37
 
31
38
  def create_user!
32
- User.create!(params)
39
+ User.create!(name: name, email: email, password: password)
33
40
  end
34
41
 
35
- def deliver_welcome_email!(u)
42
+ def create_business!(owner)
43
+ Business.create!(company_name: company_name, owner: owner)
44
+ end
45
+
46
+ def deliver_welcome_email(u)
36
47
  UserMailer.welcome(u.id).deliver_later
37
48
  end
38
49
  end
@@ -45,7 +56,9 @@ end
45
56
  - Clear and concise intention in a single file
46
57
  - Multi-model operations become simple
47
58
 
48
- [Implementing an Op](https://github.com/guideline-tech/subroutine/wiki/Implementing-an-Op)
49
- [Using an Op](https://github.com/guideline-tech/subroutine/wiki/Using-an-Op)
50
- [Errors](https://github.com/guideline-tech/subroutine/wiki/Errors)
51
- [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)
@@ -2,4 +2,6 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec :path => '../'
4
4
 
5
- gem 'activemodel', '~> 4.1.9'
5
+ gem 'rack'
6
+ gem 'activemodel', '~> 4.1.0'
7
+ gem 'actionpack', '~> 4.1.0'
@@ -2,4 +2,6 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec :path => '../'
4
4
 
5
+ gem 'rack'
5
6
  gem 'activemodel', '~> 4.2.0'
7
+ gem 'actionpack', '~> 4.2.0'
@@ -2,4 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec :path => '../'
4
4
 
5
- gem 'activemodel', '~> 5.0.7.2'
5
+ gem 'activemodel', '~> 5.0.0'
6
+ gem 'actionpack', '~> 5.0.0'
@@ -2,4 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec :path => '../'
4
4
 
5
- gem 'activemodel', '~> 5.1.7'
5
+ gem 'activemodel', '~> 5.1.0'
6
+ gem 'actionpack', '~> 5.1.0'
@@ -2,4 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec :path => '../'
4
4
 
5
- gem 'activemodel', '~> 5.2.3'
5
+ gem 'activemodel', '~> 5.2.0'
6
+ gem 'actionpack', '~> 5.2.0'
@@ -2,4 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec :path => '../'
4
4
 
5
- gem 'activemodel', '~> 6.0.0.rc1'
5
+ gem 'activemodel', '~> 6.0.0'
6
+ gem 'actionpack', '~> 6.0.0'
@@ -69,16 +69,17 @@ module Subroutine
69
69
  config = ::Subroutine::AssociationFields::Configuration.new(field_name, options)
70
70
 
71
71
  if config.polymorphic?
72
- string config.foreign_type_method, config.build_foreign_type_field
72
+ field config.foreign_type_method, config.build_foreign_type_field
73
73
  else
74
74
  class_eval <<-EV, __FILE__, __LINE__ + 1
75
+ try(:silence_redefinition_of_method, :#{config.foreign_type_method})
75
76
  def #{config.foreign_type_method}
76
- #{config.inferred_class_name.inspect}
77
+ #{config.inferred_foreign_type.inspect}
77
78
  end
78
79
  EV
79
80
  end
80
81
 
81
- integer config.foreign_key_method, config.build_foreign_key_field
82
+ field config.foreign_key_method, config.build_foreign_key_field
82
83
 
83
84
  field_without_association(config.as, config)
84
85
  else
@@ -99,13 +100,14 @@ module Subroutine
99
100
 
100
101
  excepts = []
101
102
  association_fields.each_pair do |_name, config|
102
- excepts << config.foreign_key_method
103
- excepts << config.foreign_type_method if config.polymorphic?
103
+ excepts |= config.related_field_names
104
104
  end
105
105
 
106
106
  out = params.except(*excepts)
107
- association_fields.each_pair do |field_name, _config|
108
- out[field_name] = get_field(field_name) if field_provided?(field_name)
107
+ association_fields.each_pair do |field_name, config|
108
+ next unless field_provided?(field_name)
109
+
110
+ out[field_name] = config.field_reader? ? send(field_name) : get_field(field_name)
109
111
  end
110
112
 
111
113
  out
@@ -117,7 +119,7 @@ module Subroutine
117
119
  if config&.behavior == :association
118
120
  maybe_raise_on_association_type_mismatch!(config, value)
119
121
  set_field(config.foreign_type_method, value&.class&.name, opts) if config.polymorphic?
120
- set_field(config.foreign_key_method, value&.id, opts)
122
+ set_field(config.foreign_key_method, value&.send(config.find_by), opts)
121
123
  association_cache[config.field_name] = value
122
124
  else
123
125
  if config&.behavior == :association_component
@@ -135,10 +137,7 @@ module Subroutine
135
137
  stored_result = association_cache[config.field_name]
136
138
  return stored_result unless stored_result.nil?
137
139
 
138
- fk = get_field(config.foreign_key_method)
139
- type = config.polymorphic? ? get_field(config.foreign_type_method) : send(config.foreign_type_method)
140
-
141
- result = fetch_association_instance(type, fk, config.unscoped?)
140
+ result = fetch_association_instance(config)
142
141
  association_cache[config.field_name] = result
143
142
  else
144
143
  get_field_without_association(field_name)
@@ -173,25 +172,32 @@ module Subroutine
173
172
  end
174
173
  end
175
174
 
176
- def fetch_association_instance(_type, _fk, _unscoped = false)
177
- 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
178
182
 
179
- klass = _type
180
183
  klass = klass.classify.constantize if klass.is_a?(String)
181
-
182
184
  return nil unless klass
183
185
 
186
+ foreign_key = config.foreign_key_method
187
+ value = send(foreign_key)
188
+ return nil unless value
189
+
184
190
  scope = klass.all
185
- scope = scope.unscoped if _unscoped
191
+ scope = scope.unscoped if config.unscoped?
186
192
 
187
- scope.find(_fk)
193
+ scope.find_by!(config.find_by => value)
188
194
  end
189
195
 
190
196
  def maybe_raise_on_association_type_mismatch!(config, record)
191
197
  return if config.polymorphic?
192
198
  return if record.nil?
193
199
 
194
- klass = config.inferred_class_name.constantize
200
+ klass = config.inferred_foreign_type.constantize
195
201
 
196
202
  return if record.class <= klass || record.class >= klass
197
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
@@ -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,11 +48,15 @@ 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
@@ -52,7 +64,7 @@ module Subroutine
52
64
  end
53
65
 
54
66
  def build_foreign_type_field
55
- build_child_field(foreign_type_method)
67
+ build_child_field(foreign_type_method, type: :string)
56
68
  end
57
69
 
58
70
  def unscoped?
@@ -65,8 +77,8 @@ module Subroutine
65
77
 
66
78
  protected
67
79
 
68
- def build_child_field(name)
69
- ComponentConfiguration.new(name, inheritable_options.merge(association_name: as))
80
+ def build_child_field(name, opts = {})
81
+ ComponentConfiguration.new(name, inheritable_options.merge(opts).merge(association_name: as))
70
82
  end
71
83
 
72
84
  end
@@ -85,17 +85,24 @@ module Subroutine
85
85
  def initialize(*args, &block)
86
86
  raise Subroutine::Auth::AuthorizationNotDeclaredError unless self.class.authorization_declared
87
87
 
88
- super(args.extract_options!, &block)
89
-
90
88
  @skip_auth_checks = false
91
89
 
90
+ inputs = case args.last
91
+ when *::Subroutine::Fields.allowed_input_classes
92
+ args.pop
93
+ else
94
+ {}
95
+ end
96
+
97
+ super(inputs, &block)
98
+
92
99
  user = args.shift
93
100
 
94
- if self.class.supported_user_class_names.include?(user.class.name)
95
- @current_user = user
96
- else
101
+ unless self.class.supported_user_class_names.include?(user.class.name)
97
102
  raise ArgumentError, "current_user must be one of the following types {#{self.class.supported_user_class_names.join(",")}} but was #{user.class.name}"
98
103
  end
104
+
105
+ @current_user = user
99
106
  end
100
107
 
101
108
  def skip_auth_checks!
@@ -14,6 +14,18 @@ module Subroutine
14
14
 
15
15
  extend ActiveSupport::Concern
16
16
 
17
+ def self.allowed_input_classes
18
+ @allowed_input_classes ||= begin
19
+ out = [Hash]
20
+ out << ActionController::Parameters if action_controller_params_loaded?
21
+ out
22
+ end
23
+ end
24
+
25
+ def self.action_controller_params_loaded?
26
+ defined?(::ActionController::Parameters)
27
+ end
28
+
17
29
  included do
18
30
  class_attribute :field_configurations
19
31
  self.field_configurations = {}
@@ -46,9 +58,12 @@ module Subroutine
46
58
  onlys = options.key?(:only) ? Array(options.delete(:only)) : nil
47
59
 
48
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
+
49
64
  thing.field_configurations.each_pair do |field_name, config|
50
- next if excepts&.include?(field_name)
51
- 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)
52
67
 
53
68
  config.required_modules.each do |mod|
54
69
  include mod unless included_modules.include?(mod)
@@ -119,16 +134,18 @@ module Subroutine
119
134
  end
120
135
 
121
136
  def ensure_field_accessors(config)
122
- if config.field_writer? && !instance_methods.include?(:"#{config.field_name}=")
137
+ if config.field_writer?
123
138
  class_eval <<-EV, __FILE__, __LINE__ + 1
139
+ try(:silence_redefinition_of_method, :#{config.field_name}=)
124
140
  def #{config.field_name}=(v)
125
141
  set_field(:#{config.field_name}, v)
126
142
  end
127
143
  EV
128
144
  end
129
145
 
130
- if config.field_reader? && !instance_methods.include?(:"#{config.field_name}")
146
+ if config.field_reader?
131
147
  class_eval <<-EV, __FILE__, __LINE__ + 1
148
+ try(:silence_redefinition_of_method, :#{config.field_name})
132
149
  def #{config.field_name}
133
150
  get_field(:#{config.field_name})
134
151
  end
@@ -139,6 +156,9 @@ module Subroutine
139
156
  end
140
157
 
141
158
  def setup_fields(inputs = {})
159
+ if ::Subroutine::Fields.action_controller_params_loaded? && inputs.is_a?(::ActionController::Parameters)
160
+ inputs = inputs.to_unsafe_h if inputs.respond_to?(:to_unsafe_h)
161
+ end
142
162
  @provided_fields = {}.with_indifferent_access
143
163
  param_groups[:original] = inputs.with_indifferent_access
144
164
  mass_assign_initial_params
@@ -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,7 @@ 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]
99
104
  opts
100
105
  end
101
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
7
  PATCH = 0
8
- PRE = "rc2"
8
+ PRE = "beta"
9
9
 
10
10
  VERSION = [MAJOR, MINOR, PATCH, PRE].compact.join(".")
11
11
 
data/subroutine.gemspec CHANGED
@@ -1,15 +1,16 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("lib", __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'subroutine/version'
5
+ require "subroutine/version"
5
6
 
6
7
  Gem::Specification.new do |spec|
7
8
  spec.name = "subroutine"
8
9
  spec.version = Subroutine::VERSION
9
10
  spec.authors = ["Mike Nelson"]
10
11
  spec.email = ["mike@mnelson.io"]
11
- spec.summary = %q{Feature-driven operation objects.}
12
- spec.description = %q{An interface for creating feature-driven operations.}
12
+ spec.summary = "Feature-driven operation objects."
13
+ spec.description = "An interface for creating feature-driven operations."
13
14
  spec.homepage = "https://github.com/mnelson/subroutine"
14
15
  spec.license = "MIT"
15
16
 
@@ -19,12 +20,14 @@ Gem::Specification.new do |spec|
19
20
  spec.require_paths = ["lib"]
20
21
 
21
22
  spec.add_dependency "activemodel", ">= 4.0.0"
23
+ spec.add_dependency "activesupport", ">= 4.0.0"
22
24
 
25
+ spec.add_development_dependency "actionpack", ">= 4.0"
23
26
  spec.add_development_dependency "bundler"
24
- spec.add_development_dependency "rake"
27
+ spec.add_development_dependency "byebug"
25
28
  spec.add_development_dependency "m"
26
29
  spec.add_development_dependency "minitest"
27
30
  spec.add_development_dependency "minitest-reporters"
28
31
  spec.add_development_dependency "mocha"
29
- spec.add_development_dependency "byebug"
32
+ spec.add_development_dependency "rake"
30
33
  end
@@ -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
@@ -118,5 +118,17 @@ module Subroutine
118
118
  RequireUserOp.new(user.id)
119
119
  end
120
120
 
121
+ def test_actioncontroller_parameters_can_be_provided
122
+ raw_params = { some_input: "foobarbaz" }.with_indifferent_access
123
+ params = ::ActionController::Parameters.new(raw_params)
124
+ op = RequireUserOp.new(user, params)
125
+ op.submit!
126
+
127
+ assert_equal "foobarbaz", op.some_input
128
+
129
+ assert_equal raw_params, op.params
130
+ assert_equal user, op.current_user
131
+ end
132
+
121
133
  end
122
134
  end
@@ -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
 
@@ -304,5 +330,17 @@ module Subroutine
304
330
  end
305
331
  end
306
332
 
333
+ def test_actioncontroller_parameters_can_be_provided
334
+ raw_params = { email: "foo@bar.com", password: "password123!" }.with_indifferent_access
335
+ params = ::ActionController::Parameters.new(raw_params)
336
+ op = SignupOp.new(params)
337
+ op.submit!
338
+
339
+ assert_equal "foo@bar.com", op.email
340
+ assert_equal "password123!", op.password
341
+
342
+ assert_equal raw_params, op.params
343
+ end
344
+
307
345
  end
308
346
  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
@@ -177,6 +185,8 @@ class RequireUserOp < OpWithAuth
177
185
 
178
186
  require_user!
179
187
 
188
+ string :some_input
189
+
180
190
  end
181
191
 
182
192
  class RequireNoUserOp < OpWithAuth
@@ -206,7 +216,7 @@ class CustomAuthorizeOp < OpWithAuth
206
216
  protected
207
217
 
208
218
  def authorize_user_is_correct
209
- unauthorized! unless current_user.email_address.to_s =~ /example\.com$/
219
+ unauthorized! unless current_user.email_address.to_s =~ /example\.com$/ # rubocop:disable Performance/RegexpMatch
210
220
  end
211
221
 
212
222
  end
@@ -302,6 +312,36 @@ class AssociationWithClassOp < ::OpWithAssociation
302
312
 
303
313
  end
304
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
+
333
+ class ExceptAssociationOp < ::Subroutine::Op
334
+
335
+ fields_from ::PolymorphicAssociationOp, except: %i[admin]
336
+
337
+ end
338
+
339
+ class OnlyAssociationOp < ::Subroutine::Op
340
+
341
+ fields_from ::PolymorphicAssociationOp, only: %i[admin]
342
+
343
+ end
344
+
305
345
  class InheritedSimpleAssociation < ::Subroutine::Op
306
346
 
307
347
  fields_from SimpleAssociationOp
@@ -433,3 +473,15 @@ class CustomFailureClassOp < ::Subroutine::Op
433
473
  end
434
474
 
435
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
data/test/test_helper.rb CHANGED
@@ -1,16 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'subroutine'
4
- require 'minitest/autorun'
5
- require 'minitest/unit'
3
+ require "subroutine"
4
+ require "minitest/autorun"
5
+ require "minitest/unit"
6
6
 
7
- require 'minitest/reporters'
8
- require 'mocha/minitest'
7
+ require "minitest/reporters"
8
+ require "mocha/minitest"
9
9
 
10
10
  require "byebug"
11
+ require "action_controller"
11
12
 
12
13
  Minitest::Reporters.use!([Minitest::Reporters::DefaultReporter.new])
13
14
 
14
15
  class TestCase < ::Minitest::Test; end
15
16
 
16
- require_relative 'support/ops'
17
+ require_relative "support/ops"
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.rc2
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-01-30 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
@@ -24,6 +24,34 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: 4.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 4.0.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 4.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: actionpack
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '4.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '4.0'
27
55
  - !ruby/object:Gem::Dependency
28
56
  name: bundler
29
57
  requirement: !ruby/object:Gem::Requirement
@@ -39,7 +67,7 @@ dependencies:
39
67
  - !ruby/object:Gem::Version
40
68
  version: '0'
41
69
  - !ruby/object:Gem::Dependency
42
- name: rake
70
+ name: byebug
43
71
  requirement: !ruby/object:Gem::Requirement
44
72
  requirements:
45
73
  - - ">="
@@ -109,7 +137,7 @@ dependencies:
109
137
  - !ruby/object:Gem::Version
110
138
  version: '0'
111
139
  - !ruby/object:Gem::Dependency
112
- name: byebug
140
+ name: rake
113
141
  requirement: !ruby/object:Gem::Requirement
114
142
  requirements:
115
143
  - - ">="
@@ -133,6 +161,7 @@ files:
133
161
  - ".ruby-gemset"
134
162
  - ".ruby-version"
135
163
  - ".travis.yml"
164
+ - CHANGELOG.MD
136
165
  - Gemfile
137
166
  - LICENSE.txt
138
167
  - README.md