subroutine 3.0.2 → 4.1.0

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: a69ecebbd8549a3067d41ac4d8981d51c175614f4e052b296226cdce3ad15b30
4
- data.tar.gz: e375bae8ee9f1e73ff3eb3128373d50e4cd20af0cf68eaeef16af28688ca2d36
3
+ metadata.gz: 5b099aef955fc06d44e4a9c628f261e15300fe1dca247c4f8ee042be538037a8
4
+ data.tar.gz: 3879046fc03f34b01c243f8a1cc1630c30075f6c186593509935b4ae1b62d6a9
5
5
  SHA512:
6
- metadata.gz: bc51e2794c9a41589aec6d47097cc13644f6e9683b5d5e99ce13310dcf07184d23896dd493ab00ddf54dfa648d7b6236a45968318194b8da53ac66663ae28fc7
7
- data.tar.gz: ef4e70f7bf48e180485082f1107b84a9ffbc02037dcc100432435d07d6a634d427ce58546f3010f24723f7e9fbdac52067944fba1294c71078ce11cf30e66488
6
+ metadata.gz: 60327bcf9acf93b74b5ff94b45d18c7765830f2c2456481585274b75348b316dca245597f332fe95c54b47d6a91f21de7876c96bdcfb3418d213a38d45f1fbdb
7
+ data.tar.gz: aa61977d8dec95499096f58d9590a059796d0cfe211f604237cb53ab69bc72ad634515c621ad76b128dea11bfba6acb1d6c82b607c22c8fe3fae74364353a3cc
data/CHANGELOG.MD CHANGED
@@ -1,7 +1,34 @@
1
+ ## Subroutine 4.1.0
2
+
3
+ A field can no opt out of the natural assignment behavior of ActiveSupport::HashWithIndifferentAccess. Top level param groups are still accessible via indifferent access but if a field sets the `bypass_indifferent_assignment` option to `true` the HashWithIndifferentAccess assignment behavior will be bypassed in favor of direct Hash-like assignment.
4
+
5
+ ```ruby
6
+ class MyOp < Subroutine::Op
7
+
8
+ object :some_hash
9
+ object :some_other_hash, bypass_indifferent_assignment: true
10
+
11
+ end
12
+ ```
13
+
14
+ ## Subroutine 4.0.1
15
+
16
+ Association fields can now use `find_by()` instead of `find_by!()` by passing a `raise_on_miss: false` option. This places the responsibility on the op to manage nil cases rather than handling RecordNotFound errors.
17
+
18
+ ## Subroutine 4.0
19
+
20
+ The `Subroutine::Fields` module now contains a class_attribute that allows the altering of param accessor behaviors. `include_defaults_in_params` is now available to opt into including the default values in usage of the `all_params (alias params)` method. Backwards compatibility is preserved by defaulting the value to `false`. If switched to true, when an input is omitted and the field is configured with a default value, it will be included in the `all_params` object.
21
+
22
+ Removed all usage of `ungrouped` params and refactored the storage of grouped params. Params are now stored in either the provided group or the defaults group and accessed via provided_params, params, default_params, and params_with_defaults. Grouped params are accessed the same way but with the group name prefixed eg. `my_private_default_params`.
23
+
1
24
  ## Subroutine 3.0
2
25
 
3
26
  Add support for Rails 6.1. Drop support for Rails 6.0 and lower.
4
27
 
28
+ ## Subroutine 2.3
29
+
30
+ Support dynamic types for foreign keys on association fields. The class type is used at runtime to determine the casting behavior of the foreign key field.
31
+
5
32
  ## Subroutine 2.2
6
33
 
7
34
  Add `type` validation for Output.
@@ -30,6 +30,10 @@ module Subroutine
30
30
  !!config[:polymorphic]
31
31
  end
32
32
 
33
+ def raise_on_miss?
34
+ config[:raise_on_miss] != false
35
+ end
36
+
33
37
  def as
34
38
  config[:as] || field_name
35
39
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "delegate"
4
- require "active_support/concern"
5
3
  require "subroutine/association_fields/configuration"
6
4
  require "subroutine/association_fields/association_type_mismatch_error"
7
5
 
@@ -72,8 +70,7 @@ module Subroutine
72
70
  field config.foreign_type_method, config.build_foreign_type_field
73
71
  else
74
72
  class_eval <<-EV, __FILE__, __LINE__ + 1
75
- try(:silence_redefinition_of_method, :#{config.foreign_type_method})
76
- def #{config.foreign_type_method}
73
+ silence_redefinition_of_method def #{config.foreign_type_method}
77
74
  #{config.inferred_foreign_type.inspect}
78
75
  end
79
76
  EV
@@ -113,20 +110,20 @@ module Subroutine
113
110
  out
114
111
  end
115
112
 
116
- def set_field_with_association(field_name, value, opts = {})
113
+ def set_field_with_association(field_name, value, **opts)
117
114
  config = get_field_config(field_name)
118
115
 
119
116
  if config&.behavior == :association
120
117
  maybe_raise_on_association_type_mismatch!(config, value)
121
- set_field(config.foreign_type_method, value&.class&.name, opts) if config.polymorphic?
122
- set_field(config.foreign_key_method, value&.send(config.find_by), opts)
118
+ set_field(config.foreign_type_method, value&.class&.name, **opts) if config.polymorphic?
119
+ set_field(config.foreign_key_method, value&.send(config.find_by), **opts)
123
120
  association_cache[config.field_name] = value
124
121
  else
125
122
  if config&.behavior == :association_component
126
123
  clear_field_without_association(config.association_name)
127
124
  end
128
125
 
129
- set_field_without_association(field_name, value, opts)
126
+ set_field_without_association(field_name, value, **opts)
130
127
  end
131
128
  end
132
129
 
@@ -180,7 +177,7 @@ module Subroutine
180
177
  get_field(config.foreign_type_method)
181
178
  end
182
179
 
183
- klass = klass.classify.constantize if klass.is_a?(String)
180
+ klass = klass.camelize.constantize if klass.is_a?(String)
184
181
  return nil unless klass
185
182
 
186
183
  foreign_key = config.foreign_key_method
@@ -190,7 +187,11 @@ module Subroutine
190
187
  scope = klass.all
191
188
  scope = scope.unscoped if config.unscoped?
192
189
 
193
- scope.find_by!(config.find_by => value)
190
+ if config.raise_on_miss?
191
+ scope.find_by!(config.find_by => value)
192
+ else
193
+ scope.find_by(config.find_by => value)
194
+ end
194
195
  end
195
196
 
196
197
  def maybe_raise_on_association_type_mismatch!(config, record)
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "delegate"
4
-
5
3
  module Subroutine
6
4
  module Fields
7
5
  class Configuration < ::SimpleDelegator
@@ -78,6 +76,10 @@ module Subroutine
78
76
  config[:field_reader] != false
79
77
  end
80
78
 
79
+ def bypass_indifferent_assignment?
80
+ config[:bypass_indifferent_assignment] == true
81
+ end
82
+
81
83
  def groups
82
84
  config[:groups] || NO_GROUPS
83
85
  end
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/concern"
4
- require "active_support/core_ext/object/duplicable"
5
- require "active_support/core_ext/hash/indifferent_access"
6
- require "active_support/core_ext/object/deep_dup"
7
-
8
3
  require "subroutine/type_caster"
9
4
  require "subroutine/fields/configuration"
10
5
  require "subroutine/fields/mass_assignment_error"
@@ -27,11 +22,9 @@ module Subroutine
27
22
  end
28
23
 
29
24
  included do
30
- class_attribute :field_configurations
31
- self.field_configurations = {}
32
-
33
- class_attribute :field_groups
34
- self.field_groups = Set.new
25
+ class_attribute :include_defaults_in_params, instance_accessor: false, instance_predicate: false
26
+ class_attribute :field_configurations, default: {}
27
+ class_attribute :fields_by_group, default: Hash.new { |h, k| h[k] = Set.new }
35
28
  end
36
29
 
37
30
  module ClassMethods
@@ -44,8 +37,15 @@ module Subroutine
44
37
 
45
38
  ensure_field_accessors(config)
46
39
 
47
- config.groups.each do |group_name|
48
- ensure_group_accessors(group_name)
40
+ if config.groups.any?
41
+ new_fields_by_group = self.fields_by_group.deep_dup
42
+
43
+ config.groups.each do |group_name|
44
+ new_fields_by_group[group_name] << config.field_name
45
+ ensure_group_accessors(group_name)
46
+ end
47
+
48
+ self.fields_by_group = new_fields_by_group
49
49
  end
50
50
 
51
51
  config
@@ -75,18 +75,16 @@ module Subroutine
75
75
  end
76
76
  alias inputs_from fields_from
77
77
 
78
- def fields_in_group(group_name)
79
- field_configurations.each_with_object({}) do |(field_name, config), h|
80
- next unless config.in_group?(group_name)
81
-
82
- h[field_name] = config
83
- end
84
- end
85
-
86
78
  def get_field_config(field_name)
87
79
  field_configurations[field_name.to_sym]
88
80
  end
89
81
 
82
+ def include_defaults_in_params?
83
+ return include_defaults_in_params unless include_defaults_in_params.nil?
84
+
85
+ Subroutine.include_defaults_in_params?
86
+ end
87
+
90
88
  def respond_to_missing?(method_name, *args, &block)
91
89
  ::Subroutine::TypeCaster.casters.key?(method_name.to_sym) || super
92
90
  end
@@ -106,29 +104,33 @@ module Subroutine
106
104
  protected
107
105
 
108
106
  def ensure_group_accessors(group_name)
109
- group_name = group_name.to_sym
110
- return if field_groups.include?(group_name)
107
+ class_eval <<-EV, __FILE__, __LINE__ + 1
108
+ silence_redefinition_of_method def #{group_name}_params
109
+ return #{group_name}_params_with_defaults if include_defaults_in_params?
111
110
 
112
- self.field_groups |= [group_name]
111
+ #{group_name}_provided_params
112
+ end
113
113
 
114
- class_eval <<-EV, __FILE__, __LINE__ + 1
115
- def #{group_name}_params
116
- param_groups[:#{group_name}]
114
+ silence_redefinition_of_method def #{group_name}_provided_params
115
+ get_param_group(:#{group_name}_provided)
117
116
  end
118
117
 
119
- def #{group_name}_default_params
120
- group_field_names = fields_in_group(:#{group_name}).keys
121
- all_default_params.slice(*group_field_names)
118
+ silence_redefinition_of_method def #{group_name}_default_params
119
+ get_param_group(:#{group_name}_default)
122
120
  end
123
121
  alias #{group_name}_defaults #{group_name}_default_params
124
122
 
125
- def #{group_name}_params_with_default_params
126
- #{group_name}_default_params.merge(param_groups[:#{group_name}])
123
+ silence_redefinition_of_method def #{group_name}_params_with_default_params
124
+ param_cache[:#{group_name}_provided_and_default] ||= begin
125
+ #{group_name}_default_params.merge(#{group_name}_provided_params)
126
+ end
127
127
  end
128
128
  alias #{group_name}_params_with_defaults #{group_name}_params_with_default_params
129
129
 
130
- def without_#{group_name}_params
131
- all_params.except(*#{group_name}_params.keys)
130
+ silence_redefinition_of_method def without_#{group_name}_params
131
+ param_cache[:without_#{group_name}] ||= begin
132
+ all_params.except(*#{group_name}_params.keys)
133
+ end
132
134
  end
133
135
  EV
134
136
  end
@@ -136,17 +138,15 @@ module Subroutine
136
138
  def ensure_field_accessors(config)
137
139
  if config.field_writer?
138
140
  class_eval <<-EV, __FILE__, __LINE__ + 1
139
- try(:silence_redefinition_of_method, :#{config.field_name}=)
140
- def #{config.field_name}=(v)
141
- set_field(:#{config.field_name}, v)
141
+ silence_redefinition_of_method def #{config.field_name}=(v)
142
+ set_field(:#{config.field_name}, v, group_type: :provided)
142
143
  end
143
144
  EV
144
145
  end
145
146
 
146
147
  if config.field_reader?
147
148
  class_eval <<-EV, __FILE__, __LINE__ + 1
148
- try(:silence_redefinition_of_method, :#{config.field_name})
149
- def #{config.field_name}
149
+ silence_redefinition_of_method def #{config.field_name}
150
150
  get_field(:#{config.field_name})
151
151
  end
152
152
  EV
@@ -159,88 +159,108 @@ module Subroutine
159
159
  if ::Subroutine::Fields.action_controller_params_loaded? && inputs.is_a?(::ActionController::Parameters)
160
160
  inputs = inputs.to_unsafe_h if inputs.respond_to?(:to_unsafe_h)
161
161
  end
162
- @provided_fields = {}.with_indifferent_access
163
- param_groups[:original] = inputs.with_indifferent_access
162
+ inputs.each_pair do |k, v|
163
+ set_param_group_value(:original, k, v)
164
+ end
164
165
  mass_assign_initial_params
165
166
  end
166
167
 
168
+ def include_defaults_in_params?
169
+ self.class.include_defaults_in_params?
170
+ end
171
+
172
+ def param_cache
173
+ @param_cache ||= {}
174
+ end
175
+
167
176
  def param_groups
168
177
  @param_groups ||= Hash.new { |h, k| h[k] = {}.with_indifferent_access }
169
178
  end
170
179
 
171
180
  def get_param_group(name)
172
- param_groups[name.to_sym]
181
+ name = name.to_sym
182
+ return param_groups[name] if param_groups.key?(name)
183
+
184
+ param_groups[name] = yield if block_given?
185
+ param_groups[name]
173
186
  end
174
187
 
175
- def original_params
176
- get_param_group(:original)
188
+ def set_param_group_value(name, key, value)
189
+ config = get_field_config(key)
190
+ group = get_param_group(name)
191
+
192
+ if config&.bypass_indifferent_assignment?
193
+ group.regular_writer(group.send(:convert_key, key), value)
194
+ else
195
+ group[key.to_sym] = value
196
+ end
177
197
  end
178
198
 
179
- def ungrouped_params
180
- get_param_group(:ungrouped)
199
+ def original_params
200
+ get_param_group(:original)
181
201
  end
182
202
 
183
- def all_params
184
- get_param_group(:all)
203
+ def all_provided_params
204
+ get_param_group(:provided)
185
205
  end
186
- alias params all_params
206
+ alias provided_params all_provided_params
187
207
 
188
208
  def all_default_params
189
209
  get_param_group(:default)
190
210
  end
191
211
  alias defaults all_default_params
212
+ alias default_params all_default_params
192
213
 
193
- def all_params_with_defaults
194
- all_default_params.merge(all_params)
195
- end
196
- alias params_with_defaults all_params_with_defaults
214
+ def all_params
215
+ return all_params_with_default_params if include_defaults_in_params?
197
216
 
198
- def ungrouped_defaults
199
- default_params.slice(*ungrouped_fields.keys)
217
+ all_provided_params
200
218
  end
219
+ alias params all_params
201
220
 
202
- def ungrouped_params_with_defaults
203
- ungrouped_defaults.merge(ungrouped_params)
221
+ def all_params_with_default_params
222
+ param_cache[:provided_and_default] ||= all_default_params.merge(all_provided_params)
204
223
  end
224
+ alias all_params_with_defaults all_params_with_default_params
225
+ alias params_with_defaults all_params_with_defaults
226
+
205
227
 
206
228
  def get_field_config(field_name)
207
229
  self.class.get_field_config(field_name)
208
230
  end
209
231
 
210
- # check if a specific field was provided
211
232
  def field_provided?(key)
212
- !!@provided_fields[key]
233
+ all_provided_params.key?(key)
213
234
  end
214
235
 
215
236
  def get_field(name)
216
- field_provided?(name) ? all_params[name] : all_default_params[name]
237
+ all_params_with_default_params[name]
217
238
  end
218
239
 
219
- def set_field(name, value, opts = {})
240
+ def set_field(name, value, group_type: :provided)
220
241
  config = get_field_config(name)
221
- @provided_fields[name] = true unless opts[:track_provided] == false
222
242
  value = attempt_cast(value, config) do |e|
223
243
  "Error during assignment of field `#{name}`: #{e}"
224
244
  end
225
- each_param_group_for_field(name) do |h|
226
- h[name] = value
245
+
246
+ set_param_group_value(group_type, config.field_name, value)
247
+
248
+ config.groups.each do |group_name|
249
+ set_param_group_value(:"#{group_name}_#{group_type}", config.field_name, value)
227
250
  end
251
+
252
+ param_cache.clear
253
+
228
254
  value
229
255
  end
230
256
 
231
257
  def clear_field(name)
232
- each_param_group_for_field(name) do |h|
233
- h.delete(name)
234
- end
235
- end
236
-
237
- def fields_in_group(group_name)
238
- self.class.fields_in_group(group_name)
239
- end
258
+ param_cache.clear
259
+ param_groups.each_pair do |key, group|
260
+ next if key == :original
261
+ next if key == :default
240
262
 
241
- def ungrouped_fields
242
- fields.select { |f| f.groups.empty? }.each_with_object({}) do |f, h|
243
- h[f.name] = f
263
+ group.delete(name)
244
264
  end
245
265
  end
246
266
 
@@ -253,16 +273,12 @@ module Subroutine
253
273
  end
254
274
 
255
275
  if original_params.key?(field_name)
256
- set_field(field_name, original_params[field_name])
276
+ set_field(field_name, original_params[field_name], group_type: :provided)
257
277
  end
258
278
 
259
279
  next unless config.has_default?
260
280
 
261
- value = attempt_cast(config.get_default, config) do |e|
262
- "Error for default `#{field}`: #{e}"
263
- end
264
-
265
- param_groups[:default][field_name] = value
281
+ set_field(field_name, config.get_default, group_type: :default)
266
282
  end
267
283
  end
268
284
 
@@ -273,18 +289,5 @@ module Subroutine
273
289
  raise ::Subroutine::TypeCaster::TypeCastError, message, e.backtrace
274
290
  end
275
291
 
276
- def each_param_group_for_field(name)
277
- config = get_field_config(name)
278
- yield all_params
279
-
280
- if config.groups.empty?
281
- yield ungrouped_params
282
- else
283
- config.groups.each do |group_name|
284
- yield param_groups[group_name]
285
- end
286
- end
287
- end
288
-
289
292
  end
290
293
  end
data/lib/subroutine/op.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_model"
4
-
5
3
  require "subroutine/failure"
6
4
  require "subroutine/fields"
7
5
  require "subroutine/outputs"
@@ -48,6 +46,17 @@ module Subroutine
48
46
  yield self if block_given?
49
47
  end
50
48
 
49
+ def inspect
50
+ values = provided_params.map do |(key, value)|
51
+ "#{key}: #{value.inspect}"
52
+ end
53
+ values.sort!
54
+ values = values.join(", ")
55
+
56
+ oid = format('%x', (object_id << 1))
57
+ "#<#{self.class}:0x#{oid} #{values}>"
58
+ end
59
+
51
60
  def submit!
52
61
  begin
53
62
  observe_submission do
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "delegate"
4
-
5
3
  module Subroutine
6
4
  module Outputs
7
5
  class Configuration < ::SimpleDelegator
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/concern"
4
3
  require "subroutine/outputs/configuration"
5
4
  require "subroutine/outputs/output_not_set_error"
6
5
  require "subroutine/outputs/unknown_output_error"
@@ -2,9 +2,9 @@
2
2
 
3
3
  module Subroutine
4
4
 
5
- MAJOR = 3
6
- MINOR = 0
7
- PATCH = 2
5
+ MAJOR = 4
6
+ MINOR = 1
7
+ PATCH = 0
8
8
  PRE = nil
9
9
 
10
10
  VERSION = [MAJOR, MINOR, PATCH, PRE].compact.join(".")
data/lib/subroutine.rb CHANGED
@@ -1,5 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_model"
4
+ require "active_support/concern"
5
+ require "active_support/core_ext/hash/indifferent_access"
6
+ require "active_support/core_ext/module/redefine_method"
7
+ require "active_support/core_ext/object/deep_dup"
8
+ require "active_support/core_ext/object/duplicable"
9
+ require "active_support/core_ext/string/inflections"
10
+ require "delegate"
11
+
3
12
  require "subroutine/version"
4
13
  require "subroutine/fields"
5
14
  require "subroutine/op"
15
+
16
+ module Subroutine
17
+
18
+ def self.include_defaults_in_params=(bool)
19
+ @include_defaults_in_params = !!bool
20
+ end
21
+
22
+ def self.include_defaults_in_params?
23
+ return !!@include_defaults_in_params if defined?(@instance_defaults_in_params)
24
+
25
+ false
26
+ end
27
+
28
+ end
@@ -335,5 +335,15 @@ module Subroutine
335
335
  assert_equal({}, op.without_info_params)
336
336
  end
337
337
 
338
+ def test_find_by_is_used_if_raise_on_miss_is_false
339
+ all_mock = mock
340
+
341
+ ::User.expects(:all).returns(all_mock)
342
+ all_mock.expects(:find_by).with(id: 1).returns(nil)
343
+
344
+ op = SafeAssociationOp.new user_type: "User", user_id: 1
345
+ assert_nil op.user
346
+ end
347
+
338
348
  end
339
349
  end
@@ -318,5 +318,11 @@ module Subroutine
318
318
  assert_equal raw_params, op.params
319
319
  end
320
320
 
321
+ def test_inspect_is_pretty
322
+ op = SignupOp.new({ email: "foo@bar.com", password: "password123!" })
323
+ oid = format('%x', (op.object_id << 1))
324
+ assert_equal "#<SignupOp:0x#{oid} email: \"foo@bar.com\", password: \"password123!\">", op.inspect
325
+ end
326
+
321
327
  end
322
328
  end
@@ -25,6 +25,25 @@ module Subroutine
25
25
 
26
26
  end
27
27
 
28
+ class WhateverWithDefaultsIncluded < Whatever
29
+ self.include_defaults_in_params = true
30
+ end
31
+
32
+ class MutationBase
33
+ include Subroutine::Fields
34
+
35
+ field :foo, group: :three_letter
36
+ field :bar, group: :three_letter
37
+
38
+ end
39
+
40
+ class MutationChild < MutationBase
41
+
42
+ field :qux, group: :three_letter
43
+ field :food, group: :four_letter
44
+
45
+ end
46
+
28
47
  def test_fields_are_configured
29
48
  assert_equal 6, Whatever.field_configurations.size
30
49
  assert_equal :string, Whatever.field_configurations[:foo][:type]
@@ -75,17 +94,45 @@ module Subroutine
75
94
 
76
95
  def test_params_does_not_include_defaults
77
96
  instance = Whatever.new(foo: "abc")
97
+ assert_equal({ "foo" => "abc" }, instance.provided_params)
78
98
  assert_equal({ "foo" => "foo", "bar" => 3, "qux" => "qux" }, instance.defaults)
79
99
  assert_equal({ "foo" => "abc" }, instance.params)
80
100
  assert_equal({ "foo" => "abc", "bar" => 3, "qux" => "qux" }, instance.params_with_defaults)
81
101
  end
82
102
 
83
- def test_named_params_do_not_include_defaults_unlesss_asked_for
103
+ def test_params_include_defaults_if_globally_configured
104
+ Subroutine.stubs(:include_defaults_in_params?).returns(true)
84
105
  instance = Whatever.new(foo: "abc")
106
+ assert Whatever.include_defaults_in_params?
107
+ assert_equal({ "foo" => "abc" }, instance.provided_params)
108
+ assert_equal({ "foo" => "foo", "bar" => 3, "qux" => "qux" }, instance.defaults)
109
+ assert_equal({ "foo" => "abc", "bar" => 3, "qux" => "qux" }, instance.params)
110
+ assert_equal({ "foo" => "abc", "bar" => 3, "qux" => "qux" }, instance.params_with_defaults)
111
+ end
112
+
113
+ def test_params_includes_defaults_if_opted_into
114
+ refute Subroutine.include_defaults_in_params?
115
+ instance = WhateverWithDefaultsIncluded.new(foo: "abc")
116
+ assert_equal({ "foo" => "abc" }, instance.provided_params)
117
+ assert_equal({ "foo" => "foo", "bar" => 3, "qux" => "qux" }, instance.defaults)
118
+ assert_equal({ "foo" => "abc", "bar" => 3, "qux" => "qux" }, instance.params)
119
+ assert_equal({ "foo" => "abc", "bar" => 3, "qux" => "qux" }, instance.params_with_defaults)
120
+ end
121
+
122
+ def test_named_params_do_not_include_defaults_unless_asked_for
123
+ instance = Whatever.new(foo: "abc")
124
+ assert_equal({}, instance.sekret_provided_params)
85
125
  assert_equal({}, instance.sekret_params)
86
126
  assert_equal({ "bar" => 3 }, instance.sekret_params_with_defaults)
87
127
  end
88
128
 
129
+ def test_named_params_include_defaults_if_configured
130
+ instance = WhateverWithDefaultsIncluded.new(foo: "abc")
131
+ assert_equal({}, instance.sekret_provided_params)
132
+ assert_equal({ "bar" => 3 }, instance.sekret_params)
133
+ assert_equal({ "bar" => 3 }, instance.sekret_params_with_defaults)
134
+ end
135
+
89
136
  def test_fields_can_opt_out_of_mass_assignment
90
137
  assert_raises Subroutine::Fields::MassAssignmentError do
91
138
  Whatever.new(foo: "abc", protekted: "foo")
@@ -113,18 +160,28 @@ module Subroutine
113
160
  assert_equal "bar", instance.foo
114
161
  end
115
162
 
163
+ def test_set_field_adds_to_provided_params
164
+ instance = Whatever.new
165
+ instance.set_field(:foo, "bar")
166
+ assert_equal true, instance.provided_params.key?(:foo)
167
+ end
168
+
169
+ def test_set_field_can_add_to_the_default_params
170
+ instance = Whatever.new
171
+ instance.set_field(:foo, "bar", group_type: :default)
172
+ assert_equal false, instance.provided_params.key?(:foo)
173
+ assert_equal "bar", instance.default_params[:foo]
174
+ end
175
+
116
176
  def test_group_fields_are_accessible_at_the_class
117
- results = Whatever.fields_in_group(:sekret)
118
- assert_equal true, results.key?(:protekted_group_input)
119
- assert_equal true, results.key?(:bar)
120
- assert_equal false, results.key?(:protekted)
177
+ fields = Whatever.fields_by_group[:sekret].sort
178
+ assert_equal %i[bar protekted_group_input], fields
121
179
  end
122
180
 
123
181
  def test_groups_fields_are_accessible
124
182
  op = Whatever.new(foo: "bar", protekted_group_input: "pgi", bar: 8)
125
183
  assert_equal({ protekted_group_input: "pgi", bar: 8 }.with_indifferent_access, op.sekret_params)
126
184
  assert_equal({ protekted_group_input: "pgi", foo: "bar", bar: 8 }.with_indifferent_access, op.params)
127
- assert_equal({ foo: "bar" }.with_indifferent_access, op.ungrouped_params)
128
185
  end
129
186
 
130
187
  def test_fields_from_allows_merging_of_config
@@ -143,5 +200,19 @@ module Subroutine
143
200
  assert_equal(%i[amount_cents credit_cents], OuterInheritanceOp.field_configurations.keys.sort)
144
201
  end
145
202
 
203
+ def test_field_definitions_are_not_mutated_by_subclasses
204
+ assert_equal(%i[bar foo], MutationBase.field_configurations.keys.sort)
205
+ assert_equal(%i[bar foo food qux], MutationChild.field_configurations.keys.sort)
206
+ end
207
+
208
+ def test_group_fields_are_not_mutated_by_subclasses
209
+ assert_equal(%i[three_letter], MutationBase.fields_by_group.keys.sort)
210
+ assert_equal(%i[four_letter three_letter], MutationChild.fields_by_group.keys.sort)
211
+
212
+ assert_equal(%i[bar foo], MutationBase.fields_by_group[:three_letter].sort)
213
+ assert_equal(%i[bar foo qux], MutationChild.fields_by_group[:three_letter].sort)
214
+ assert_equal(%i[food], MutationChild.fields_by_group[:four_letter].sort)
215
+ end
216
+
146
217
  end
147
218
  end
@@ -175,6 +175,34 @@ module Subroutine
175
175
 
176
176
  op.object_input = { 'foo' => { 'bar' => :baz } }
177
177
  assert_equal({ 'foo' => { 'bar' => :baz } }, op.object_input)
178
+
179
+ op.object_input = { foo: "bar" }
180
+ assert op.object_input.is_a?(ActiveSupport::HashWithIndifferentAccess)
181
+ end
182
+
183
+ def test_hash_inputs_can_opt_out_of_indifferent_access_behavior
184
+ op.no_indifferent_object_input = nil
185
+ assert_nil op.no_indifferent_object_input
186
+
187
+ op.no_indifferent_object_input = ''
188
+ assert_equal({}, op.no_indifferent_object_input)
189
+
190
+ op.no_indifferent_object_input = [[:a, :b]]
191
+ assert_equal({ a: :b }, op.no_indifferent_object_input)
192
+
193
+ op.no_indifferent_object_input = { foo: "bar" }
194
+ assert_equal({ foo: "bar" }, op.no_indifferent_object_input)
195
+ assert op.no_indifferent_object_input.key?(:foo)
196
+ refute op.no_indifferent_object_input.key?("foo")
197
+ refute op.no_indifferent_object_input.is_a?(ActiveSupport::HashWithIndifferentAccess)
198
+ assert op.no_indifferent_object_input.is_a?(Hash)
199
+
200
+ op.no_indifferent_object_input = { "foo" => "bar" }
201
+ assert_equal({ "foo" => "bar" }, op.no_indifferent_object_input)
202
+ assert op.no_indifferent_object_input.key?("foo")
203
+ refute op.no_indifferent_object_input.key?(:foo)
204
+ refute op.no_indifferent_object_input.is_a?(ActiveSupport::HashWithIndifferentAccess)
205
+ assert op.no_indifferent_object_input.is_a?(Hash)
178
206
  end
179
207
 
180
208
  def test_array_inputs
data/test/support/ops.rb CHANGED
@@ -27,10 +27,6 @@ class User
27
27
  new(params)
28
28
  end
29
29
 
30
- def self.find_by!(params)
31
- find_by(params) || raise
32
- end
33
-
34
30
  def self.type_for_attribute(attribute)
35
31
  case attribute
36
32
  when :id
@@ -180,6 +176,7 @@ class TypeCastOp < ::Subroutine::Op
180
176
  iso_date :iso_date_input
181
177
  iso_time :iso_time_input
182
178
  object :object_input
179
+ object :no_indifferent_object_input, bypass_indifferent_assignment: true
183
180
  array :array_input, default: "foo"
184
181
  array :type_array_input, of: :integer
185
182
  file :file_input
@@ -342,6 +339,12 @@ class SimpleAssociationOp < ::OpWithAssociation
342
339
 
343
340
  end
344
341
 
342
+ class SafeAssociationOp < ::OpWithAssociation
343
+
344
+ association :user, raise_on_miss: false
345
+
346
+ end
347
+
345
348
  class SimpleAssociationWithStringIdOp < ::OpWithAssociation
346
349
 
347
350
  association :string_id_user
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: 3.0.2
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Nelson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-04-03 00:00:00.000000000 Z
11
+ date: 2024-05-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel