subroutine 3.0.2 → 4.1.0

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