Skip to content

Commit 2f533ce

Browse files
Merge pull request #1040 from Shopify/seb-in-proc
Add support for procs and method calls in inclusion validation dropdown resolution
2 parents e498cf0 + 2cbb407 commit 2f533ce

File tree

6 files changed

+101
-38
lines changed

6 files changed

+101
-38
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,23 @@ to run. Since arguments are specified in the user interface via text area
485485
inputs, it’s important to check that they conform to the format your Task
486486
expects, and to sanitize any inputs if necessary.
487487

488+
#### Validating Task Parameters
489+
490+
Task attributes can be validated using Active Model Validations. Attributes are
491+
validated before a Task is enqueued.
492+
493+
If an attribute uses an inclusion validator with a supported `in:` option, the
494+
set of values will be used to populate a dropdown in the user interface. The
495+
following types are supported:
496+
497+
* Arrays
498+
* Procs and lambdas that optionally accept the Task instance, and return an Array.
499+
* Callable objects that receive one argument, the Task instance, and return an Array.
500+
* Methods that return an Array, called on the Task instance.
501+
502+
For enumerables that don't match the supported types, a text field will be
503+
rendered instead.
504+
488505
### Custom cursor columns to improve performance
489506

490507
The [job-iteration gem][job-iteration], on which this gem depends, adds an

app/helpers/maintenance_tasks/tasks_helper.rb

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,29 +102,53 @@ def csv_file_download_path(run)
102102
end
103103

104104
# Resolves values covered by the inclusion validator for a task attribute.
105-
# Only Arrays are supported, option types such as:
106-
# Procs, lambdas, symbols, and Range are not supported and return nil.
105+
# Supported option types:
106+
# - Arrays
107+
# - Procs and lambdas that optionally accept the Task instance, and return an Array.
108+
# - Callable objects that receive one argument, the Task instance, and return an Array.
109+
# - Methods that return an Array, called on the Task instance.
110+
#
111+
# Other types are not supported and will return nil.
107112
#
108113
# Returned values are used to populate a dropdown list of options.
109114
#
110115
# @param task_class [Class<Task>] The task class for which the value needs to be resolved.
111116
# @param parameter_name [String] The parameter name.
112117
#
113118
# @return [Array] value of the resolved inclusion option.
114-
def resolve_inclusion_value(task_class, parameter_name)
119+
def resolve_inclusion_value(task, parameter_name)
120+
task_class = task.class
115121
inclusion_validator = task_class.validators_on(parameter_name).find do |validator|
116122
validator.kind == :inclusion
117123
end
118124
return unless inclusion_validator
119125

120126
in_option = inclusion_validator.options[:in] || inclusion_validator.options[:within]
121-
in_option if in_option.is_a?(Array)
127+
resolved_in_option = case in_option
128+
when Proc
129+
if in_option.arity == 0
130+
in_option.call
131+
else
132+
in_option.call(task)
133+
end
134+
when Symbol
135+
method = task.method(in_option)
136+
method.call if method.arity.zero?
137+
else
138+
if in_option.respond_to?(:call)
139+
in_option.call(task)
140+
else
141+
in_option
142+
end
143+
end
144+
145+
resolved_in_option if resolved_in_option.is_a?(Array)
122146
end
123147

124148
# Return the appropriate field tag for the parameter, based on its type.
125149
# If the parameter has a `validates_inclusion_of` validator, return a dropdown list of options instead.
126150
def parameter_field(form_builder, parameter_name)
127-
inclusion_values = resolve_inclusion_value(form_builder.object.class, parameter_name)
151+
inclusion_values = resolve_inclusion_value(form_builder.object, parameter_name)
128152
return form_builder.select(parameter_name, inclusion_values, prompt: "Select a value") if inclusion_values
129153

130154
case form_builder.object.class.attribute_types[parameter_name]

test/dummy/app/tasks/maintenance/params_task.rb

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
# frozen_string_literal: true
22

33
module Maintenance
4+
class DropdownOptions
5+
class << self
6+
def call(_task)
7+
[100, 200, 300]
8+
end
9+
end
10+
end
11+
412
class ParamsTask < MaintenanceTasks::Task
513
attribute :post_ids, :string
614

@@ -20,24 +28,31 @@ class ParamsTask < MaintenanceTasks::Task
2028

2129
# Dropdown options with supported scenarios
2230
attribute :integer_dropdown_attr, :integer
31+
attribute :integer_dropdown_attr_proc_no_arg, :integer
32+
attribute :integer_dropdown_attr_proc_arg, :integer
33+
attribute :integer_dropdown_attr_from_method, :integer
34+
attribute :integer_dropdown_attr_callable, :integer
2335
attribute :boolean_dropdown_attr, :boolean
2436

2537
validates_inclusion_of :integer_dropdown_attr, in: [100, 200, 300], allow_nil: true
38+
validates_inclusion_of :integer_dropdown_attr_proc_no_arg, in: proc { [100, 200, 300] }, allow_nil: true
39+
validates_inclusion_of :integer_dropdown_attr_proc_arg, in: proc { |_task| [100, 200, 300] }, allow_nil: true
40+
validates_inclusion_of :integer_dropdown_attr_from_method, in: :dropdown_attr_options, allow_nil: true
41+
validates_inclusion_of :integer_dropdown_attr_callable, in: DropdownOptions, allow_nil: true
2642
validates_inclusion_of :boolean_dropdown_attr, within: [true, false], allow_nil: true
2743

2844
# Dropdown options with unsupported scenarios
29-
attribute :text_integer_attr, :integer
30-
attribute :text_integer_attr2, :integer
31-
attribute :text_integer_attr3, :integer
32-
33-
validates_inclusion_of :text_integer_attr, in: proc { [100, 200, 300] }, allow_nil: true
34-
validates_inclusion_of :text_integer_attr2, in: :undefined_symbol, allow_nil: true
35-
validates_inclusion_of :text_integer_attr3, in: (100..), allow_nil: true
45+
attribute :text_integer_attr_unbounded_range, :integer
46+
validates_inclusion_of :text_integer_attr_unbounded_range, in: (100..), allow_nil: true
3647

3748
class << self
3849
attr_accessor :fast_task
3950
end
4051

52+
def dropdown_attr_options
53+
[100, 200, 300]
54+
end
55+
4156
def collection
4257
Post.where(id: post_ids_array)
4358
end

test/helpers/maintenance_tasks/tasks_helper_test.rb

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,18 @@ class TasksHelperTest < ActionView::TestCase
9292
end
9393

9494
test "#resolve_inclusion_value resolves inclusion validator for task attributes" do
95-
assert_match "Select a value", markup("integer_dropdown_attr").squish
96-
97-
assert_match "Select a value", markup("boolean_dropdown_attr").squish
98-
99-
["text_integer_attr", "text_integer_attr2", "text_integer_attr3"].each do |text_integer_attr|
100-
refute_match "Select a value", markup(text_integer_attr).squish
95+
[
96+
"integer_dropdown_attr",
97+
"boolean_dropdown_attr",
98+
"integer_dropdown_attr_proc_no_arg",
99+
"integer_dropdown_attr_proc_arg",
100+
"integer_dropdown_attr_from_method",
101+
"integer_dropdown_attr_callable",
102+
].each do |attribute|
103+
assert_match "Select a value", markup(attribute).squish
101104
end
105+
106+
refute_match "Select a value", markup("text_integer_attr_unbounded_range").squish
102107
end
103108

104109
test "#parameter_field adds information about datetime fields when Time.zone_default is not set" do

test/models/maintenance_tasks/task_data_show_test.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,12 @@ class TaskDataShowTest < ActiveSupport::TestCase
8888
"time_attr",
8989
"boolean_attr",
9090
"integer_dropdown_attr",
91+
"integer_dropdown_attr_proc_no_arg",
92+
"integer_dropdown_attr_proc_arg",
93+
"integer_dropdown_attr_from_method",
94+
"integer_dropdown_attr_callable",
9195
"boolean_dropdown_attr",
92-
"text_integer_attr",
93-
"text_integer_attr2",
94-
"text_integer_attr3",
96+
"text_integer_attr_unbounded_range",
9597
],
9698
TaskDataShow.new("Maintenance::ParamsTask").parameter_names,
9799
)

test/system/maintenance_tasks/tasks_test.rb

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -117,24 +117,24 @@ class TasksTest < ApplicationSystemTestCase
117117
assert_equal("input", boolean_field.tag_name)
118118
assert_equal("checkbox", boolean_field[:type])
119119

120-
integer_dropdown_field = page.find_field("task[integer_dropdown_attr]")
121-
assert_equal("select", integer_dropdown_field.tag_name)
122-
assert_equal("select-one", integer_dropdown_field[:type])
123-
integer_dropdown_field_options = integer_dropdown_field.find_all("option").map { |option| option[:value] }
124-
assert_equal(["", "100", "200", "300"], integer_dropdown_field_options)
125-
126-
boolean_dropdown_field = page.find_field("task[boolean_dropdown_attr]")
127-
assert_equal("select", boolean_dropdown_field.tag_name)
128-
assert_equal("select-one", boolean_dropdown_field[:type])
129-
boolean_dropdown_field_options = boolean_dropdown_field.find_all("option").map { |option| option[:value] }
130-
assert_equal(["", "true", "false"], boolean_dropdown_field_options)
131-
132-
["text_integer_attr", "text_integer_attr2", "text_integer_attr3"].each do |text_integer_attr|
133-
text_integer_dropdown_field = page.find_field("task[#{text_integer_attr}]")
134-
assert_equal("input", text_integer_dropdown_field.tag_name)
135-
assert_equal("number", text_integer_dropdown_field[:type])
136-
assert_empty(text_integer_dropdown_field[:step])
120+
[
121+
"integer_dropdown_attr",
122+
"integer_dropdown_attr_proc_no_arg",
123+
"integer_dropdown_attr_proc_arg",
124+
"integer_dropdown_attr_from_method",
125+
"integer_dropdown_attr_callable",
126+
].each do |dropdown_integer_attr|
127+
integer_dropdown_field = page.find_field("task[#{dropdown_integer_attr}]")
128+
assert_equal("select", integer_dropdown_field.tag_name)
129+
assert_equal("select-one", integer_dropdown_field[:type])
130+
integer_dropdown_field_options = integer_dropdown_field.find_all("option").map { |option| option[:value] }
131+
assert_equal(["", "100", "200", "300"], integer_dropdown_field_options)
137132
end
133+
134+
text_integer_field = page.find_field("task[text_integer_attr_unbounded_range]")
135+
assert_equal("input", text_integer_field.tag_name)
136+
assert_equal("number", text_integer_field[:type])
137+
assert_empty(text_integer_field[:step])
138138
end
139139

140140
test "view a Task with multiple pages of Runs" do

0 commit comments

Comments
 (0)