subroutine 3.0.5 → 4.0.1

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