subroutine 1.0.0.rc4 → 2.0.0.beta2

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: 6bd9cb93ba6c09a520b8db4d79b6aa2e5eefee2046b5b8d768976864dd4a7c89
4
- data.tar.gz: d5d0be02650acb97a25b3663f2722ace66f38ad2cb5457208da7ad1e3a2a9e0d
3
+ metadata.gz: bc8eb530552fea8e2bacd033372826e11c57734d9695973ebca956c76aa2ed93
4
+ data.tar.gz: 908764a3fc127881b4c719c2c5523ae280771683e765ea8f47ddda52bed76a33
5
5
  SHA512:
6
- metadata.gz: f4eaa685e8fff642d134ba9f97b0050c1eee01639b4429ebe2d4a522ff88de544f58a4080372af3f91f210f656efbf9c37c8a6fbdea1f36468067408e5615913
7
- data.tar.gz: e6b4c7fbadbaf6a1b8b8d765d962aed0acb7ba8c8f661b5b5c70da6b20d49458013d0f31ab34e0c4a7519452a568f8f9e51ee85f18be0a119ba62af1e24b1d94
6
+ metadata.gz: 4de2ad64fede40a510afe5b0af073eb6fae1800a61e2f9387337c1d46475ec439c1c35cb393ecececb3a5954b7c9e1b32be10e6be963dc7ce3fb840d40a3b368
7
+ data.tar.gz: 9ec24f1a6ce47edb502bded197096dec4d1ff444c4d3b0fda64291111aa7a29e67e7bd4f8e01a018c72795117c40e16fa3c68131b64db346e3c1073611c8134e
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/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'
@@ -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
 
@@ -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!
@@ -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
 
@@ -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)
@@ -141,6 +156,9 @@ module Subroutine
141
156
  end
142
157
 
143
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
144
162
  @provided_fields = {}.with_indifferent_access
145
163
  param_groups[:original] = inputs.with_indifferent_access
146
164
  mass_assign_initial_params
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,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 = "rc4"
8
+ PRE = "beta2"
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,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
@@ -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
@@ -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
 
@@ -177,6 +187,8 @@ class RequireUserOp < OpWithAuth
177
187
 
178
188
  require_user!
179
189
 
190
+ string :some_input
191
+
180
192
  end
181
193
 
182
194
  class RequireNoUserOp < OpWithAuth
@@ -206,7 +218,7 @@ class CustomAuthorizeOp < OpWithAuth
206
218
  protected
207
219
 
208
220
  def authorize_user_is_correct
209
- 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
210
222
  end
211
223
 
212
224
  end
@@ -302,6 +314,36 @@ class AssociationWithClassOp < ::OpWithAssociation
302
314
 
303
315
  end
304
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
+
305
347
  class InheritedSimpleAssociation < ::Subroutine::Op
306
348
 
307
349
  fields_from SimpleAssociationOp
@@ -433,3 +475,15 @@ class CustomFailureClassOp < ::Subroutine::Op
433
475
  end
434
476
 
435
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
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.rc4
4
+ version: 2.0.0.beta2
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-31 00:00:00.000000000 Z
11
+ date: 2022-02-17 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
@@ -189,7 +218,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
189
218
  - !ruby/object:Gem::Version
190
219
  version: 1.3.1
191
220
  requirements: []
192
- rubygems_version: 3.0.6
221
+ rubygems_version: 3.3.7
193
222
  signing_key:
194
223
  specification_version: 4
195
224
  summary: Feature-driven operation objects.