Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion guides/fields/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,18 @@ argument :id, ID, required: false, validates: { allow_null: true }

Will permit any query that passes `id: null`.

Validation options may also be passed as procs that accept no parameters, for example:

```ruby
validates :title, String, validates: { format: { with: -> { Flipper.enabled?(:new_title_format) ? NEW_TITLE_FORMAT : /.*/ }}
```

## Custom Validators

You can write custom validators, too. A validator is a class that extends `GraphQL::Schema::Validator`. It should implement:

- `def initialize(..., **default_options)` to accept any validator-specific options and pass along the defaults to `super(**default_options)`
- `def validate(object, context, value)` which is called at runtime to validate `value`. It may return a String error message or an Array of Strings. GraphQL-Ruby will add those messages to the top-level `"errors"` array along with runtime context information.
- `def validate(_empty, context, value)` which is called at runtime to validate `value`. It may return a String error message or an Array of Strings. GraphQL-Ruby will add those messages to the top-level `"errors"` array along with runtime context information.

Then, custom validators can be attached either:

Expand Down
9 changes: 9 additions & 0 deletions lib/graphql/schema/validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ def partial_format(string, substitutions)
string
end

# @return [Object] The current value to use for validation, based on `config_value` from configuration time. If a Proc is given, this calls it and returns it.
def validation_parameter(config_value)
if config_value.is_a?(Proc)
config_value.call
else
config_value
end
end

# @return [Boolean] `true` if `value` is `nil` and this validator has `allow_null: true` or if value is `.blank?` and this validator has `allow_blank: true`
def permitted_empty_value?(value)
(value.nil? && @allow_null) ||
Expand Down
6 changes: 3 additions & 3 deletions lib/graphql/schema/validator/allow_blank_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@ class Validator
# @example Require a non-empty string for an argument
# argument :name, String, required: true, validate: { allow_blank: false }
class AllowBlankValidator < Validator
def initialize(allow_blank_positional, allow_blank: nil, message: "%{validated} can't be blank", **default_options)
def initialize(allow_blank_positional = nil, allow_blank: nil, message: "%{validated} can't be blank", **default_options)
@message = message
super(**default_options)
@allow_blank = allow_blank.nil? ? allow_blank_positional : allow_blank
end

def validate(_object, _context, value)
if value.respond_to?(:blank?) && value.blank?
if (value.nil? && @allow_null) || @allow_blank
if (value.nil? && validation_parameter(@allow_null)) || validation_parameter(@allow_blank)
# pass
else
@message
validation_parameter(@message)
end
end
end
Expand Down
6 changes: 3 additions & 3 deletions lib/graphql/schema/validator/allow_null_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ class Validator
# argument :name, String, required: false, validates: { allow_null: false }
class AllowNullValidator < Validator
MESSAGE = "%{validated} can't be null"
def initialize(allow_null_positional, allow_null: nil, message: MESSAGE, **default_options)
def initialize(allow_null_positional = nil, allow_null: nil, message: MESSAGE, **default_options)
@message = message
super(**default_options)
@allow_null = allow_null.nil? ? allow_null_positional : allow_null
end

def validate(_object, _context, value)
if value.nil? && !@allow_null
@message
if value.nil? && !validation_parameter(@allow_null)
validation_parameter(@message)
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions lib/graphql/schema/validator/exclusion_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ def initialize(message: "%{validated} is reserved", in:, **default_options)
def validate(_object, _context, value)
if permitted_empty_value?(value)
# pass
elsif @in_list.include?(value)
@message
elsif validation_parameter(@in_list).include?(value)
validation_parameter(@message)
end
end
end
Expand Down
6 changes: 3 additions & 3 deletions lib/graphql/schema/validator/format_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ def validate(_object, _context, value)
if permitted_empty_value?(value)
# Do nothing
elsif value.nil? ||
(@with_pattern && !value.match?(@with_pattern)) ||
(@without_pattern && value.match?(@without_pattern))
@message
(@with_pattern && !value.match?(validation_parameter(@with_pattern))) ||
(@without_pattern && value.match?(validation_parameter(@without_pattern)))
validation_parameter(@message)
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions lib/graphql/schema/validator/inclusion_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ def initialize(in:, message: "%{validated} is not included in the list", **defau
def validate(_object, _context, value)
if permitted_empty_value?(value)
# pass
elsif !@in_list.include?(value)
@message
elsif !validation_parameter(@in_list).include?(value)
validation_parameter(@message)
end
end
end
Expand Down
12 changes: 6 additions & 6 deletions lib/graphql/schema/validator/length_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ def initialize(
def validate(_object, _context, value)
return if permitted_empty_value?(value) # pass in this case
length = value.nil? ? 0 : value.length
if @maximum && length > @maximum
partial_format(@too_long, { count: @maximum })
elsif @minimum && length < @minimum
partial_format(@too_short, { count: @minimum })
elsif @is && length != @is
partial_format(@wrong_length, { count: @is })
if (current_max = validation_parameter(@maximum)) && length > current_max
partial_format(validation_parameter(@too_long), { count: current_max })
elsif (current_min = validation_parameter(@minimum)) && length < current_min
partial_format(validation_parameter(@too_short), { count: current_min })
elsif (current_is = validation_parameter(@is)) && length != current_is
partial_format(validation_parameter(@wrong_length), { count: current_is })
end
end
end
Expand Down
38 changes: 19 additions & 19 deletions lib/graphql/schema/validator/numericality_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,25 +55,25 @@ def validate(object, context, value)
if permitted_empty_value?(value)
# pass in this case
elsif value.nil? # @allow_null is handled in the parent class
@null_message
elsif @greater_than && value <= @greater_than
partial_format(@message, { comparison: "greater than", target: @greater_than })
elsif @greater_than_or_equal_to && value < @greater_than_or_equal_to
partial_format(@message, { comparison: "greater than or equal to", target: @greater_than_or_equal_to })
elsif @less_than && value >= @less_than
partial_format(@message, { comparison: "less than", target: @less_than })
elsif @less_than_or_equal_to && value > @less_than_or_equal_to
partial_format(@message, { comparison: "less than or equal to", target: @less_than_or_equal_to })
elsif @equal_to && value != @equal_to
partial_format(@message, { comparison: "equal to", target: @equal_to })
elsif @other_than && value == @other_than
partial_format(@message, { comparison: "something other than", target: @other_than })
elsif @even && !value.even?
(partial_format(@message, { comparison: "even", target: "" })).strip
elsif @odd && !value.odd?
(partial_format(@message, { comparison: "odd", target: "" })).strip
elsif @within && !@within.include?(value)
partial_format(@message, { comparison: "within", target: @within })
validation_parameter(@null_message)
elsif (current_greater_than = validation_parameter(@greater_than)) && value <= current_greater_than
partial_format(validation_parameter(@message), { comparison: "greater than", target: current_greater_than })
elsif (current_greater_than_or_equal_to = validation_parameter(@greater_than_or_equal_to)) && value < current_greater_than_or_equal_to
partial_format(validation_parameter(@message), { comparison: "greater than or equal to", target: current_greater_than_or_equal_to })
elsif (current_less_than = validation_parameter(@less_than)) && value >= current_less_than
partial_format(validation_parameter(@message), { comparison: "less than", target: current_less_than })
elsif (current_less_than_or_equal_to = validation_parameter(@less_than_or_equal_to)) && value > current_less_than_or_equal_to
partial_format(validation_parameter(@message), { comparison: "less than or equal to", target: current_less_than_or_equal_to })
elsif (current_equal_to = validation_parameter(@equal_to)) && value != current_equal_to
partial_format(validation_parameter(@message), { comparison: "equal to", target: current_equal_to })
elsif (current_other_than = validation_parameter(@other_than)) && value == current_other_than
partial_format(validation_parameter(@message), { comparison: "something other than", target: current_other_than })
elsif validation_parameter(@even) && !value.even?
(partial_format(validation_parameter(@message), { comparison: "even", target: "" })).strip
elsif validation_parameter(@odd) && !value.odd?
(partial_format(validation_parameter(@message), { comparison: "odd", target: "" })).strip
elsif (current_within = validation_parameter(@within)) && !current_within.include?(value)
partial_format(validation_parameter(@message), { comparison: "within", target: current_within })
end
end
end
Expand Down
10 changes: 6 additions & 4 deletions lib/graphql/schema/validator/required_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ def validate(_object, context, value)
no_visible_conditions = true

if !value.nil?
@one_of.each do |one_of_condition|
validation_parameter(@one_of).each do |one_of_condition|
one_of_condition = validation_parameter(one_of_condition)
case one_of_condition
when Symbol
if no_visible_conditions && visible_keywords.include?(one_of_condition)
Expand Down Expand Up @@ -108,7 +109,7 @@ def validate(_object, context, value)
end

if no_visible_conditions
if @allow_all_hidden
if validation_parameter(@allow_all_hidden)
return nil
else
raise GraphQL::Error, <<~ERR
Expand All @@ -122,16 +123,17 @@ def validate(_object, context, value)
if fully_matched_conditions == 1 && partially_matched_conditions == 0
nil # OK
else
@message || build_message(context)
validation_parameter(@message) || build_message(context)
end
end

def build_message(context)
argument_definitions = context.types.arguments(@validated)

required_names = @one_of.map do |arg_keyword|
arg_keyword = validation_parameter(arg_keyword)
if arg_keyword.is_a?(Array)
names = arg_keyword.map { |arg| arg_keyword_to_graphql_name(argument_definitions, arg) }
names = arg_keyword.map { |arg| arg_keyword_to_graphql_name(argument_definitions, validation_parameter(arg)) }
names.compact! # hidden arguments are `nil`
"(" + names.join(" and ") + ")"
else
Expand Down
13 changes: 12 additions & 1 deletion spec/graphql/schema/validator/allow_blank_validator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
include ValidatorHelpers

it "allows blank when configured" do
build_schema(String, {length: { minimum: 5 }, allow_blank: true})
build_schema(String, {length: { minimum: 5 }, allow_blank: -> { true } })
result = exec_query("query($str: String) { validated(value: $str) }", variables: { str: ValidatorHelpers::BlankString.new })
assert_equal "", result["data"]["validated"]
refute result.key?("errors")
Expand All @@ -19,6 +19,17 @@
assert_equal ["value is too short (minimum is 5)"], result["errors"].map { |e| e["message"] }
end

it "takes a custom message as a proc and calls it each time" do
i = 0
build_schema(String, {allow_blank: { allow_blank: false, message: -> { "Error ##{i += 1}"} } })
result = exec_query("query($str: String) { validated(value: $str) }", variables: { str: ValidatorHelpers::BlankString.new })
assert_nil result["data"]["validated"]
assert_equal ["Error #1"], result["errors"].map { |e| e["message"] }

result = exec_query("query($str: String) { validated(value: $str) }", variables: { str: ValidatorHelpers::BlankString.new })
assert_equal ["Error #2"], result["errors"].map { |e| e["message"] }
end

it "can be used standalone" do
build_schema(String, { allow_blank: false, allow_null: false })
result = exec_query("query($str: String) { validated(value: $str) }", variables: { str: ValidatorHelpers::BlankString.new })
Expand Down
10 changes: 8 additions & 2 deletions spec/graphql/schema/validator/allow_null_validator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,16 @@
end

it "can be used standalone" do
build_schema(String, { allow_null: false })
allow_null = false
msg = "can't be Null!!"
build_schema(String, { allow_null: { allow_null: -> { allow_null }, message: -> { msg } } })
result = exec_query("query($str: String) { validated(value: $str) }", variables: { str: nil })
assert_nil result["data"]["validated"]
assert_equal ["value can't be null"], result["errors"].map { |e| e["message"] }
assert_equal ["can't be Null!!"], result["errors"].map { |e| e["message"] }

allow_null = true
result = exec_query("query($str: String) { validated(value: $str) }", variables: { str: nil })
refute result.key?("errors")
end

it "allows nil when no validations are configured" do
Expand Down
8 changes: 5 additions & 3 deletions spec/graphql/schema/validator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def initialize(equal_to:, **rest)
end

def validate(object, context, value)
if value == @equal_to
if value == validation_parameter(@equal_to)
nil
else
"%{validated} doesn't have the right the right value"
Expand Down Expand Up @@ -41,18 +41,20 @@ def validate(obj, ctx, value)
build_tests(CustomValidator, Integer, [
{
name: "with a validator class as name",
config: { equal_to: 2 },
config: { equal_to: -> { 2 } },
cases: [
{ query: "{ validated(value: 2) }", error_messages: [], result: 2 },
{ query: "{ validated(value: 3) }", error_messages: ["value doesn't have the right the right value"], result: nil },
]
}
])

THE_NUMBER_FOUR = 4

build_tests(:custom, Integer, [
{
name: "with an installed symbol name",
config: { equal_to: 4 },
config: { equal_to: -> { THE_NUMBER_FOUR } },
cases: [
{ query: "{ validated(value: 4) }", error_messages: [], result: 4 },
{ query: "{ validated(value: 3) }", error_messages: ["value doesn't have the right the right value"], result: nil },
Expand Down