diff --git a/guides/fields/validation.md b/guides/fields/validation.md index 62dc523e4a1..3eb860c661e 100644 --- a/guides/fields/validation.md +++ b/guides/fields/validation.md @@ -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: diff --git a/lib/graphql/schema/validator.rb b/lib/graphql/schema/validator.rb index fac5c34511b..dd3b606cff1 100644 --- a/lib/graphql/schema/validator.rb +++ b/lib/graphql/schema/validator.rb @@ -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) || diff --git a/lib/graphql/schema/validator/allow_blank_validator.rb b/lib/graphql/schema/validator/allow_blank_validator.rb index 99cbc934bf0..c2df2b5f672 100644 --- a/lib/graphql/schema/validator/allow_blank_validator.rb +++ b/lib/graphql/schema/validator/allow_blank_validator.rb @@ -8,7 +8,7 @@ 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 @@ -16,10 +16,10 @@ def initialize(allow_blank_positional, allow_blank: nil, message: "%{validated} 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 diff --git a/lib/graphql/schema/validator/allow_null_validator.rb b/lib/graphql/schema/validator/allow_null_validator.rb index 33da57d0768..9089f00f945 100644 --- a/lib/graphql/schema/validator/allow_null_validator.rb +++ b/lib/graphql/schema/validator/allow_null_validator.rb @@ -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 diff --git a/lib/graphql/schema/validator/exclusion_validator.rb b/lib/graphql/schema/validator/exclusion_validator.rb index 0a7a9b1cd99..1e915eea8e1 100644 --- a/lib/graphql/schema/validator/exclusion_validator.rb +++ b/lib/graphql/schema/validator/exclusion_validator.rb @@ -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 diff --git a/lib/graphql/schema/validator/format_validator.rb b/lib/graphql/schema/validator/format_validator.rb index bdbd3d6a02d..e790e3ecd17 100644 --- a/lib/graphql/schema/validator/format_validator.rb +++ b/lib/graphql/schema/validator/format_validator.rb @@ -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 diff --git a/lib/graphql/schema/validator/inclusion_validator.rb b/lib/graphql/schema/validator/inclusion_validator.rb index 3735ab1ba6a..2cf4f3e0a57 100644 --- a/lib/graphql/schema/validator/inclusion_validator.rb +++ b/lib/graphql/schema/validator/inclusion_validator.rb @@ -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 diff --git a/lib/graphql/schema/validator/length_validator.rb b/lib/graphql/schema/validator/length_validator.rb index cddba1582d3..69a0b2037ab 100644 --- a/lib/graphql/schema/validator/length_validator.rb +++ b/lib/graphql/schema/validator/length_validator.rb @@ -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 diff --git a/lib/graphql/schema/validator/numericality_validator.rb b/lib/graphql/schema/validator/numericality_validator.rb index 96e8c92e7c7..f59b08d8685 100644 --- a/lib/graphql/schema/validator/numericality_validator.rb +++ b/lib/graphql/schema/validator/numericality_validator.rb @@ -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 diff --git a/lib/graphql/schema/validator/required_validator.rb b/lib/graphql/schema/validator/required_validator.rb index b26505e0f94..dfeabe80bea 100644 --- a/lib/graphql/schema/validator/required_validator.rb +++ b/lib/graphql/schema/validator/required_validator.rb @@ -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) @@ -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 @@ -122,7 +123,7 @@ 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 @@ -130,8 +131,9 @@ 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 diff --git a/spec/graphql/schema/validator/allow_blank_validator_spec.rb b/spec/graphql/schema/validator/allow_blank_validator_spec.rb index edb082f20d7..22840a0561c 100644 --- a/spec/graphql/schema/validator/allow_blank_validator_spec.rb +++ b/spec/graphql/schema/validator/allow_blank_validator_spec.rb @@ -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") @@ -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 }) diff --git a/spec/graphql/schema/validator/allow_null_validator_spec.rb b/spec/graphql/schema/validator/allow_null_validator_spec.rb index 6f80ae97820..605f3967a14 100644 --- a/spec/graphql/schema/validator/allow_null_validator_spec.rb +++ b/spec/graphql/schema/validator/allow_null_validator_spec.rb @@ -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 diff --git a/spec/graphql/schema/validator_spec.rb b/spec/graphql/schema/validator_spec.rb index d5768c76a81..9d5385401ad 100644 --- a/spec/graphql/schema/validator_spec.rb +++ b/spec/graphql/schema/validator_spec.rb @@ -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" @@ -41,7 +41,7 @@ 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 }, @@ -49,10 +49,12 @@ def validate(obj, ctx, value) } ]) + 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 },