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 +4 -4
- data/CHANGELOG.MD +14 -0
- data/lib/subroutine/association_fields/configuration.rb +4 -0
- data/lib/subroutine/association_fields.rb +11 -10
- data/lib/subroutine/fields/configuration.rb +0 -2
- data/lib/subroutine/fields.rb +89 -93
- data/lib/subroutine/op.rb +11 -2
- data/lib/subroutine/outputs/configuration.rb +0 -2
- data/lib/subroutine/outputs.rb +0 -1
- data/lib/subroutine/type_caster.rb +3 -19
- data/lib/subroutine/version.rb +2 -2
- data/lib/subroutine.rb +13 -4
- data/test/subroutine/association_test.rb +10 -0
- data/test/subroutine/base_test.rb +6 -0
- data/test/subroutine/fields_test.rb +77 -6
- data/test/subroutine/type_caster_test.rb +5 -142
- data/test/support/ops.rb +6 -5
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c838c235e50f88722d2f39f19560fa5208a1e18f7cd0ee8edeeeea568e47166c
|
4
|
+
data.tar.gz: 4bb808af75863ee0785ef8989b6ffc3423838dae84bb7a481d18aa759325c5be
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
@@ -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
|
-
|
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.
|
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
|
-
|
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)
|
data/lib/subroutine/fields.rb
CHANGED
@@ -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 :
|
31
|
-
|
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.
|
48
|
-
|
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
|
-
|
110
|
-
|
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
|
-
|
111
|
+
#{group_name}_provided_params
|
112
|
+
end
|
113
113
|
|
114
|
-
|
115
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
140
|
-
|
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
|
-
|
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
|
-
|
163
|
-
|
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
|
-
|
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
|
176
|
-
get_param_group(
|
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
|
180
|
-
get_param_group(:
|
192
|
+
def original_params
|
193
|
+
get_param_group(:original)
|
181
194
|
end
|
182
195
|
|
183
|
-
def
|
184
|
-
get_param_group(:
|
196
|
+
def all_provided_params
|
197
|
+
get_param_group(:provided)
|
185
198
|
end
|
186
|
-
alias
|
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
|
194
|
-
|
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
|
-
|
199
|
-
default_params.slice(*ungrouped_fields.keys)
|
210
|
+
all_provided_params
|
200
211
|
end
|
212
|
+
alias params all_params
|
201
213
|
|
202
|
-
def
|
203
|
-
|
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
|
-
|
226
|
+
all_provided_params.key?(key)
|
213
227
|
end
|
214
228
|
|
215
229
|
def get_field(name)
|
216
|
-
|
230
|
+
all_params_with_default_params[name]
|
217
231
|
end
|
218
232
|
|
219
|
-
def set_field(name, value,
|
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
|
-
|
226
|
-
|
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
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/subroutine/outputs.rb
CHANGED
@@ -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
|
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,
|
118
|
+
::Subroutine::TypeCaster.register :time, :timestamp, :datetime do |value, _options = {}|
|
126
119
|
next nil unless value.present?
|
127
120
|
|
128
|
-
|
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 = {}|
|
data/lib/subroutine/version.rb
CHANGED
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.
|
10
|
-
@
|
18
|
+
def self.include_defaults_in_params=(bool)
|
19
|
+
@include_defaults_in_params = !!bool
|
11
20
|
end
|
12
21
|
|
13
|
-
def self.
|
14
|
-
return !!@
|
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
|
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
|
-
|
118
|
-
assert_equal
|
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
|
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 =
|
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:
|
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:
|
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:
|
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:
|
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
|
+
date: 2024-05-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|