subroutine 1.0.0.rc4 → 2.0.0.beta2

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: 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.