subroutine 3.0.5 → 4.0.1

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: e7441c613353cda046900b7165d787576d3cb757943496fb7b4ccc65a9f5faf1
4
- data.tar.gz: b154f7ab48dc32c391be450bc684ee34d59451d6e327d90f463654d53e8f0a55
3
+ metadata.gz: c838c235e50f88722d2f39f19560fa5208a1e18f7cd0ee8edeeeea568e47166c
4
+ data.tar.gz: 4bb808af75863ee0785ef8989b6ffc3423838dae84bb7a481d18aa759325c5be
5
5
  SHA512:
6
- metadata.gz: 9da5a1f35f9888ebc970e23c60e53871258d837767b6baf3597fb216c64eca039171d993f4abc8c02627b678623e47df35defe5a79799a6c166ff7aebc2cb712
7
- data.tar.gz: 5df7cc84df8ec5b19f60f9101fb66abeee95b578b474d956d378857132a60881a039c55ddecc4b8e6405b1ef9c95fb69f5b3d2d3ffc3fd5cb02bdce0aba8da4d
6
+ metadata.gz: a008be5744be667696675df24ba4edf19a39716f27074ec192500ed30ba750d11974f1c3892144b13ff693631899cd0aa0931c5995b823ebe112073646fb5363
7
+ data.tar.gz: f45212abc2f417d85c73db00ee83b9359ddc80d46fd77f78a45c802efe51a7374dcc978c0d7ded73ddd67c6637f252516c86abdf0f6f0ab309ef763476c17113
data/CHANGELOG.MD CHANGED
@@ -1,7 +1,21 @@
1
+ ## Subroutine 4.0.1
2
+
3
+ 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.
4
+
5
+ ## Subroutine 4.0
6
+
7
+ 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.
8
+
9
+ 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`.
10
+
1
11
  ## Subroutine 3.0
2
12
 
3
13
  Add support for Rails 6.1. Drop support for Rails 6.0 and lower.
4
14
 
15
+ ## Subroutine 2.3
16
+
17
+ 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.
18
+
5
19
  ## Subroutine 2.2
6
20
 
7
21
  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
@@ -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,101 @@ 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
+ get_param_group(name)[key.to_sym] = value
177
190
  end
178
191
 
179
- def ungrouped_params
180
- get_param_group(:ungrouped)
192
+ def original_params
193
+ get_param_group(:original)
181
194
  end
182
195
 
183
- def all_params
184
- get_param_group(:all)
196
+ def all_provided_params
197
+ get_param_group(:provided)
185
198
  end
186
- alias params all_params
199
+ alias provided_params all_provided_params
187
200
 
188
201
  def all_default_params
189
202
  get_param_group(:default)
190
203
  end
191
204
  alias defaults all_default_params
205
+ alias default_params all_default_params
192
206
 
193
- def all_params_with_defaults
194
- all_default_params.merge(all_params)
195
- end
196
- alias params_with_defaults all_params_with_defaults
207
+ def all_params
208
+ return all_params_with_default_params if include_defaults_in_params?
197
209
 
198
- def ungrouped_defaults
199
- default_params.slice(*ungrouped_fields.keys)
210
+ all_provided_params
200
211
  end
212
+ alias params all_params
201
213
 
202
- def ungrouped_params_with_defaults
203
- ungrouped_defaults.merge(ungrouped_params)
214
+ def all_params_with_default_params
215
+ param_cache[:provided_and_default] ||= all_default_params.merge(all_provided_params)
204
216
  end
217
+ alias all_params_with_defaults all_params_with_default_params
218
+ alias params_with_defaults all_params_with_defaults
219
+
205
220
 
206
221
  def get_field_config(field_name)
207
222
  self.class.get_field_config(field_name)
208
223
  end
209
224
 
210
- # check if a specific field was provided
211
225
  def field_provided?(key)
212
- !!@provided_fields[key]
226
+ all_provided_params.key?(key)
213
227
  end
214
228
 
215
229
  def get_field(name)
216
- field_provided?(name) ? all_params[name] : all_default_params[name]
230
+ all_params_with_default_params[name]
217
231
  end
218
232
 
219
- def set_field(name, value, opts = {})
233
+ def set_field(name, value, group_type: :provided)
220
234
  config = get_field_config(name)
221
- @provided_fields[name] = true unless opts[:track_provided] == false
222
235
  value = attempt_cast(value, config) do |e|
223
236
  "Error during assignment of field `#{name}`: #{e}"
224
237
  end
225
- each_param_group_for_field(name) do |h|
226
- h[name] = value
238
+
239
+ set_param_group_value(group_type, config.field_name, value)
240
+
241
+ config.groups.each do |group_name|
242
+ set_param_group_value(:"#{group_name}_#{group_type}", config.field_name, value)
227
243
  end
244
+
245
+ param_cache.clear
246
+
228
247
  value
229
248
  end
230
249
 
231
250
  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
251
+ param_cache.clear
252
+ param_groups.each_pair do |key, group|
253
+ next if key == :original
254
+ next if key == :default
240
255
 
241
- def ungrouped_fields
242
- fields.select { |f| f.groups.empty? }.each_with_object({}) do |f, h|
243
- h[f.name] = f
256
+ group.delete(name)
244
257
  end
245
258
  end
246
259
 
@@ -253,16 +266,12 @@ module Subroutine
253
266
  end
254
267
 
255
268
  if original_params.key?(field_name)
256
- set_field(field_name, original_params[field_name])
269
+ set_field(field_name, original_params[field_name], group_type: :provided)
257
270
  end
258
271
 
259
272
  next unless config.has_default?
260
273
 
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
274
+ set_field(field_name, config.get_default, group_type: :default)
266
275
  end
267
276
  end
268
277
 
@@ -273,18 +282,5 @@ module Subroutine
273
282
  raise ::Subroutine::TypeCaster::TypeCastError, message, e.backtrace
274
283
  end
275
284
 
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
285
  end
290
286
  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"
@@ -4,16 +4,9 @@ require 'date'
4
4
  require 'time'
5
5
  require 'bigdecimal'
6
6
  require 'securerandom'
7
- require 'active_support'
8
- require 'active_support/json'
9
- require 'active_support/core_ext/date_time/acts_like'
10
- require 'active_support/core_ext/date_time/calculations'
11
- require 'active_support/core_ext/object/acts_like'
12
7
  require 'active_support/core_ext/object/blank'
13
8
  require 'active_support/core_ext/object/try'
14
9
  require 'active_support/core_ext/array/wrap'
15
- require 'active_support/core_ext/time/acts_like'
16
- require 'active_support/core_ext/time/calculations'
17
10
 
18
11
  module Subroutine
19
12
  module TypeCaster
@@ -113,7 +106,7 @@ end
113
106
  t ||= value if value.is_a?(::Time)
114
107
  t ||= value if value.try(:acts_like?, :time)
115
108
  t ||= ::Time.parse(String(value))
116
- t.utc.iso8601(::ActiveSupport::JSON::Encoding.time_precision)
109
+ t.utc.iso8601
117
110
  end
118
111
 
119
112
  ::Subroutine::TypeCaster.register :date do |value, _options = {}|
@@ -122,19 +115,10 @@ end
122
115
  ::Date.parse(String(value))
123
116
  end
124
117
 
125
- ::Subroutine::TypeCaster.register :time, :timestamp, :datetime do |value, options = {}|
118
+ ::Subroutine::TypeCaster.register :time, :timestamp, :datetime do |value, _options = {}|
126
119
  next nil unless value.present?
127
120
 
128
- value = if value.try(:acts_like?, :time)
129
- value.to_time
130
- else
131
- ::Time.parse(String(value))
132
- end
133
-
134
- # High precision must be opted into. The original implementation is to set usec:0
135
- next value if options[:precision] == :high || ::Subroutine.preserve_time_precision?
136
-
137
- value.change(usec: 0)
121
+ ::Time.parse(String(value))
138
122
  end
139
123
 
140
124
  ::Subroutine::TypeCaster.register :hash, :object, :hashmap, :dict do |value, _options = {}|
@@ -2,9 +2,9 @@
2
2
 
3
3
  module Subroutine
4
4
 
5
- MAJOR = 3
5
+ MAJOR = 4
6
6
  MINOR = 0
7
- PATCH = 5
7
+ PATCH = 1
8
8
  PRE = nil
9
9
 
10
10
  VERSION = [MAJOR, MINOR, PATCH, PRE].compact.join(".")
data/lib/subroutine.rb CHANGED
@@ -1,17 +1,26 @@
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"
6
15
 
7
16
  module Subroutine
8
17
 
9
- def self.preserve_time_precision=(bool)
10
- @preserve_time_precision = !!bool
18
+ def self.include_defaults_in_params=(bool)
19
+ @include_defaults_in_params = !!bool
11
20
  end
12
21
 
13
- def self.preserve_time_precision?
14
- return !!@preserve_time_precision if defined?(@preserve_time_precision)
22
+ def self.include_defaults_in_params?
23
+ return !!@include_defaults_in_params if defined?(@instance_defaults_in_params)
15
24
 
16
25
  false
17
26
  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
@@ -241,7 +241,7 @@ module Subroutine
241
241
  assert_nil op.date_input
242
242
  end
243
243
 
244
- def test_time_inputs__with_seconds_precision
244
+ def test_time_inputs
245
245
  op.time_input = nil
246
246
  assert_nil op.time_input
247
247
 
@@ -256,153 +256,16 @@ module Subroutine
256
256
  assert_equal 0, op.time_input.min
257
257
  assert_equal 0, op.time_input.sec
258
258
 
259
- op.time_input = ::DateTime.new(2022, 12, 22)
260
- assert_equal ::Time, op.time_input.class
261
- refute_equal ::DateTime, op.time_input.class
262
-
263
- assert_equal 0, op.time_input.utc_offset
264
- assert_equal 2022, op.time_input.year
265
- assert_equal 12, op.time_input.month
266
- assert_equal 22, op.time_input.day
267
- assert_equal 0, op.time_input.hour
268
- assert_equal 0, op.time_input.min
269
- assert_equal 0, op.time_input.sec
270
-
271
- op.time_input = '2023-05-05T10:00:30.123456Z'
259
+ op.time_input = '2023-05-05T10:00:30Z'
272
260
  assert_equal ::Time, op.time_input.class
273
261
  refute_equal ::DateTime, op.time_input.class
274
262
 
275
- assert_equal 0, op.time_input.utc_offset
276
263
  assert_equal 2023, op.time_input.year
277
264
  assert_equal 5, op.time_input.month
278
265
  assert_equal 5, op.time_input.day
279
266
  assert_equal 10, op.time_input.hour
280
267
  assert_equal 0, op.time_input.min
281
268
  assert_equal 30, op.time_input.sec
282
- assert_equal 0, op.time_input.usec
283
-
284
- op.time_input = '2023-05-05T10:00:30Z'
285
- assert_equal ::Time, op.time_input.class
286
- assert_equal 0, op.time_input.utc_offset
287
- assert_equal 2023, op.time_input.year
288
- assert_equal 5, op.time_input.month
289
- assert_equal 5, op.time_input.day
290
- assert_equal 10, op.time_input.hour
291
- assert_equal 0, op.time_input.min
292
- assert_equal 30, op.time_input.sec
293
- assert_equal 0, op.time_input.usec
294
-
295
- op.time_input = '2024-11-11T16:42:23.246+0100'
296
- assert_equal ::Time, op.time_input.class
297
- assert_equal 3600, op.time_input.utc_offset
298
- assert_equal 2024, op.time_input.year
299
- assert_equal 11, op.time_input.month
300
- assert_equal 11, op.time_input.day
301
- assert_equal 16, op.time_input.hour
302
- assert_equal 42, op.time_input.min
303
- assert_equal 23, op.time_input.sec
304
- assert_equal 0, op.time_input.usec
305
-
306
- time = Time.at(1678741605.123456).utc
307
- op.time_input = time
308
- refute_equal time, op.time_input
309
- refute_equal time.object_id, op.time_input.object_id
310
- assert_equal 2023, op.time_input.year
311
- assert_equal 3, op.time_input.month
312
- assert_equal 13, op.time_input.day
313
- assert_equal 21, op.time_input.hour
314
- assert_equal 6, op.time_input.min
315
- assert_equal 45, op.time_input.sec
316
- assert_equal 0, op.time_input.usec
317
- end
318
-
319
- def test_time_inputs__with_preserve_time_precision
320
- Subroutine.stubs(:preserve_time_precision?).returns(true)
321
-
322
- time = Time.at(1678741605.123456).utc
323
- op.time_input = time
324
- assert_equal 2023, op.time_input.year
325
- assert_equal 3, op.time_input.month
326
- assert_equal 13, op.time_input.day
327
- assert_equal 21, op.time_input.hour
328
- assert_equal 6, op.time_input.min
329
- assert_equal 45, op.time_input.sec
330
- assert_equal 123456, op.time_input.usec
331
- end
332
-
333
- def test_time_inputs__with_high_precision
334
- op.precise_time_input = nil
335
- assert_nil op.precise_time_input
336
-
337
- op.precise_time_input = '2022-12-22'
338
- assert_equal ::Time, op.precise_time_input.class
339
- refute_equal ::DateTime, op.precise_time_input.class
340
-
341
- assert_equal 2022, op.precise_time_input.year
342
- assert_equal 12, op.precise_time_input.month
343
- assert_equal 22, op.precise_time_input.day
344
- assert_equal 0, op.precise_time_input.hour
345
- assert_equal 0, op.precise_time_input.min
346
- assert_equal 0, op.precise_time_input.sec
347
-
348
- op.precise_time_input = ::DateTime.new(2022, 12, 22)
349
- assert_equal ::Time, op.precise_time_input.class
350
- refute_equal ::DateTime, op.precise_time_input.class
351
-
352
- assert_equal 0, op.precise_time_input.utc_offset
353
- assert_equal 2022, op.precise_time_input.year
354
- assert_equal 12, op.precise_time_input.month
355
- assert_equal 22, op.precise_time_input.day
356
- assert_equal 0, op.precise_time_input.hour
357
- assert_equal 0, op.precise_time_input.min
358
- assert_equal 0, op.precise_time_input.sec
359
-
360
- op.precise_time_input = '2023-05-05T10:00:30.123456Z'
361
- assert_equal ::Time, op.precise_time_input.class
362
- refute_equal ::DateTime, op.precise_time_input.class
363
-
364
- assert_equal 0, op.precise_time_input.utc_offset
365
- assert_equal 2023, op.precise_time_input.year
366
- assert_equal 5, op.precise_time_input.month
367
- assert_equal 5, op.precise_time_input.day
368
- assert_equal 10, op.precise_time_input.hour
369
- assert_equal 0, op.precise_time_input.min
370
- assert_equal 30, op.precise_time_input.sec
371
- assert_equal 123456, op.precise_time_input.usec
372
-
373
- op.precise_time_input = '2023-05-05T10:00:30Z'
374
- assert_equal ::Time, op.precise_time_input.class
375
- assert_equal 0, op.precise_time_input.utc_offset
376
- assert_equal 2023, op.precise_time_input.year
377
- assert_equal 5, op.precise_time_input.month
378
- assert_equal 5, op.precise_time_input.day
379
- assert_equal 10, op.precise_time_input.hour
380
- assert_equal 0, op.precise_time_input.min
381
- assert_equal 30, op.precise_time_input.sec
382
- assert_equal 0, op.precise_time_input.usec
383
-
384
- op.precise_time_input = '2024-11-11T16:42:23.246+0100'
385
- assert_equal ::Time, op.precise_time_input.class
386
- assert_equal 3600, op.precise_time_input.utc_offset
387
- assert_equal 2024, op.precise_time_input.year
388
- assert_equal 11, op.precise_time_input.month
389
- assert_equal 11, op.precise_time_input.day
390
- assert_equal 16, op.precise_time_input.hour
391
- assert_equal 42, op.precise_time_input.min
392
- assert_equal 23, op.precise_time_input.sec
393
- assert_equal 246000, op.precise_time_input.usec
394
-
395
- time = Time.at(1678741605.123456).utc
396
- op.precise_time_input = time
397
- assert_equal time, op.precise_time_input
398
- assert_equal time.object_id, op.precise_time_input.object_id
399
- assert_equal 2023, op.precise_time_input.year
400
- assert_equal 3, op.precise_time_input.month
401
- assert_equal 13, op.precise_time_input.day
402
- assert_equal 21, op.precise_time_input.hour
403
- assert_equal 6, op.precise_time_input.min
404
- assert_equal 45, op.precise_time_input.sec
405
- assert_equal 123456, op.precise_time_input.usec
406
269
  end
407
270
 
408
271
  def test_iso_date_inputs
@@ -424,11 +287,11 @@ module Subroutine
424
287
 
425
288
  op.iso_time_input = '2022-12-22T10:30:24Z'
426
289
  assert_equal ::String, op.iso_time_input.class
427
- assert_equal '2022-12-22T10:30:24.000Z', op.iso_time_input
290
+ assert_equal '2022-12-22T10:30:24Z', op.iso_time_input
428
291
 
429
- op.iso_time_input = Time.parse('2022-12-22T10:30:24.123456Z')
292
+ op.iso_time_input = Time.parse('2022-12-22T10:30:24Z')
430
293
  assert_equal ::String, op.iso_time_input.class
431
- assert_equal '2022-12-22T10:30:24.123Z', op.iso_time_input
294
+ assert_equal '2022-12-22T10:30:24Z', op.iso_time_input
432
295
  end
433
296
 
434
297
  def test_file_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
@@ -177,7 +173,6 @@ class TypeCastOp < ::Subroutine::Op
177
173
  boolean :boolean_input
178
174
  date :date_input
179
175
  time :time_input, default: -> { Time.now }
180
- time :precise_time_input, precision: :high
181
176
  iso_date :iso_date_input
182
177
  iso_time :iso_time_input
183
178
  object :object_input
@@ -343,6 +338,12 @@ class SimpleAssociationOp < ::OpWithAssociation
343
338
 
344
339
  end
345
340
 
341
+ class SafeAssociationOp < ::OpWithAssociation
342
+
343
+ association :user, raise_on_miss: false
344
+
345
+ end
346
+
346
347
  class SimpleAssociationWithStringIdOp < ::OpWithAssociation
347
348
 
348
349
  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.5
4
+ version: 4.0.1
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-11-22 00:00:00.000000000 Z
11
+ date: 2024-05-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel