diff --git a/README.md b/README.md index 11e2f57..66cfedf 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,25 @@ options = { OpenAPIParser.parse(yaml_file, options) ``` +### Response error redaction +Errors generated during response validation often contain sensitive customer information which you may not +want to appear in logs and exception reporting. You can configure the parser to redact data values from exceptions by +passing the `redact_response_errors: true` option. + +```ruby +normal_schema = YAML.safe_load_file('spec/data/normal.yml', permitted_classes: [Date, Time]) +root = OpenAPIParser.parse(normal_schema, redact_response_errors: true) +op = root.request_operation(:get, '/characters') +op.validate_response_body( + OpenAPIParser::RequestOperation::ValidatableResponseBody.new( + 200, + {'string_1' => 12}, + { 'Content-Type' => 'application/json' }) +) + +# Will raise with OpenAPIParser::ValidateError: #/paths/~1characters/get/responses/200/content/application~1json/schema/properties/string_1 expected string, but received Integer: +``` + ## ToDo - correct schema checker - more detailed validator diff --git a/lib/openapi_parser/config.rb b/lib/openapi_parser/config.rb index faf6008..72cecff 100644 --- a/lib/openapi_parser/config.rb +++ b/lib/openapi_parser/config.rb @@ -37,6 +37,10 @@ def validate_header @config.fetch(:validate_header, true) end + def redact_response_errors + @config.fetch(:redact_response_errors, false) + end + # @return [OpenAPIParser::SchemaValidator::Options] def request_validator_options @request_validator_options ||= OpenAPIParser::SchemaValidator::Options.new(coerce_value: coerce_value, @@ -49,7 +53,10 @@ def request_validator_options # @return [OpenAPIParser::SchemaValidator::ResponseValidateOptions] def response_validate_options - @response_validate_options ||= OpenAPIParser::SchemaValidator::ResponseValidateOptions. - new(strict: strict_response_validation, validate_header: validate_header) + @response_validate_options ||= OpenAPIParser::SchemaValidator::ResponseValidateOptions.new( + strict: strict_response_validation, + validate_header: validate_header, + redact_errors: redact_response_errors + ) end end diff --git a/lib/openapi_parser/errors.rb b/lib/openapi_parser/errors.rb index 18fc93f..5a407f0 100644 --- a/lib/openapi_parser/errors.rb +++ b/lib/openapi_parser/errors.rb @@ -5,29 +5,40 @@ def initialize(reference) end end + class ValueError < OpenAPIError + def initialize(value, reference, options:) + super(reference) + @options = options + @value = value + end + + def redacted_value + @options.redact_errors ? '' : @value.inspect + end + end + class MissingReferenceError < OpenAPIError def message "'#{@reference}' was referenced but could not be found" end end - class ValidateError < OpenAPIError - def initialize(data, type, reference) - super(reference) - @data = data + class ValidateError < ValueError + def initialize(value, type, reference, options:) + super(value, reference, options: options) @type = type end def message - "#{@reference} expected #{@type}, but received #{@data.class}: #{@data.inspect}" + "#{@reference} expected #{@type}, but received #{@value.class}: #{redacted_value}" end class << self # create ValidateError for SchemaValidator return data # @param [Object] value # @param [OpenAPIParser::Schemas::Base] schema - def build_error_result(value, schema) - [nil, OpenAPIParser::ValidateError.new(value, schema.type, schema.object_reference)] + def build_error_result(value, schema, options:) + [nil, OpenAPIParser::ValidateError.new(value, schema.type, schema.object_reference, options: options)] end end end @@ -71,149 +82,136 @@ def message end end - class NotExistDiscriminatorPropertyName < OpenAPIError - def initialize(key, value, reference) - super(reference) + class NotExistDiscriminatorPropertyName < ValueError + def initialize(key, value, reference, options:) + super(value, reference, options: options) @key = key - @value = value end def message - "discriminator propertyName #{@key} does not exist in value #{@value.inspect} in #{@reference}" + "discriminator propertyName #{@key} does not exist in value #{redacted_value} in #{@reference}" end end - class NotOneOf < OpenAPIError - def initialize(value, reference) - super(reference) - @value = value + class NotOneOf < ValueError + def initialize(value, reference, options:) + super(value, reference, options: options) end def message - "#{@value.inspect} isn't one of in #{@reference}" + "#{redacted_value} isn't one of in #{@reference}" end end - class NotAnyOf < OpenAPIError - def initialize(value, reference) - super(reference) - @value = value + class NotAnyOf < ValueError + def initialize(value, reference, options:) + super(value, reference, options: options) end def message - "#{@value.inspect} isn't any of in #{@reference}" + "#{redacted_value} isn't any of in #{@reference}" end end - class NotEnumInclude < OpenAPIError - def initialize(value, reference) - super(reference) - @value = value + class NotEnumInclude < ValueError + def initialize(value, reference, options:) + super(value, reference, options: options) end def message - "#{@value.inspect} isn't part of the enum in #{@reference}" + "#{redacted_value} isn't part of the enum in #{@reference}" end end - class LessThanMinimum < OpenAPIError - def initialize(value, reference) - super(reference) - @value = value + class LessThanMinimum < ValueError + def initialize(value, reference, options:) + super(value, reference, options: options) end def message - "#{@reference} #{@value.inspect} is less than minimum value" + "#{@reference} #{redacted_value} is less than minimum value" end end - class LessThanExclusiveMinimum < OpenAPIError - def initialize(value, reference) - super(reference) - @value = value + class LessThanExclusiveMinimum < ValueError + def initialize(value, reference, options:) + super(value, reference, options: options) end def message - "#{@reference} #{@value.inspect} cannot be less than or equal to exclusive minimum value" + "#{@reference} #{redacted_value} cannot be less than or equal to exclusive minimum value" end end - class MoreThanMaximum < OpenAPIError - def initialize(value, reference) - super(reference) - @value = value + class MoreThanMaximum < ValueError + def initialize(value, reference, options:) + super(value, reference, options: options) end def message - "#{@reference} #{@value.inspect} is more than maximum value" + "#{@reference} #{redacted_value} is more than maximum value" end end - class MoreThanExclusiveMaximum < OpenAPIError - def initialize(value, reference) - super(reference) - @value = value + class MoreThanExclusiveMaximum < ValueError + def initialize(value, reference, options:) + super(value, reference, options: options) end def message - "#{@reference} #{@value.inspect} cannot be more than or equal to exclusive maximum value" + "#{@reference} #{redacted_value} cannot be more than or equal to exclusive maximum value" end end - class InvalidPattern < OpenAPIError - def initialize(value, pattern, reference, example) - super(reference) - @value = value + class InvalidPattern < ValueError + def initialize(value, pattern, reference, example, options:) + super(value, reference, options: options) @pattern = pattern @example = example end def message - "#{@reference} pattern #{@pattern} does not match value: #{@value.inspect}#{@example ? ", example: #{@example}" : ""}" + "#{@reference} pattern #{@pattern} does not match value: #{redacted_value}#{@example ? ", example: #{@example}" : ""}" end end - class InvalidEmailFormat < OpenAPIError - def initialize(value, reference) - super(reference) - @value = value + class InvalidEmailFormat < ValueError + def initialize(value, reference, options:) + super(value, reference, options: options) end def message - "#{@reference} email address format does not match value: #{@value.inspect}" + "#{@reference} email address format does not match value: #{redacted_value}" end end - class InvalidUUIDFormat < OpenAPIError - def initialize(value, reference) - super(reference) - @value = value + class InvalidUUIDFormat < ValueError + def initialize(value, reference, options:) + super(value, reference, options: options) end def message - "#{@reference} Value: #{@value.inspect} is not conformant with UUID format" + "#{@reference} Value: #{redacted_value} is not conformant with UUID format" end end - class InvalidDateFormat < OpenAPIError - def initialize(value, reference) - super(reference) - @value = value + class InvalidDateFormat < ValueError + def initialize(value, reference, options:) + super(value, reference, options: options) end def message - "#{@reference} Value: #{@value.inspect} is not conformant with date format" + "#{@reference} Value: #{redacted_value} is not conformant with date format" end end - class InvalidDateTimeFormat < OpenAPIError - def initialize(value, reference) - super(reference) - @value = value + class InvalidDateTimeFormat < ValueError + def initialize(value, reference, options:) + super(value, reference, options: options) end def message - "#{@reference} Value: #{@value.inspect} is not conformant with date-time format" + "#{@reference} Value: #{redacted_value} is not conformant with date-time format" end end @@ -229,58 +227,53 @@ def message end end - class MoreThanMaxLength < OpenAPIError - def initialize(value, reference) - super(reference) - @value = value + class MoreThanMaxLength < ValueError + def initialize(value, reference, options:) + super(value, reference, options: options) end def message - "#{@reference} #{@value.inspect} is longer than max length" + "#{@reference} #{redacted_value} is longer than max length" end end - class LessThanMinLength < OpenAPIError - def initialize(value, reference) - super(reference) - @value = value + class LessThanMinLength < ValueError + def initialize(value, reference, options:) + super(value, reference, options: options) end def message - "#{@reference} #{@value.inspect} is shorter than min length" + "#{@reference} #{redacted_value} is shorter than min length" end end - class MoreThanMaxItems < OpenAPIError - def initialize(value, reference) - super(reference) - @value = value + class MoreThanMaxItems < ValueError + def initialize(value, reference, options:) + super(value, reference, options: options) end def message - "#{@reference} #{@value.inspect} contains more than max items" + "#{@reference} #{redacted_value} contains more than max items" end end - class LessThanMinItems < OpenAPIError - def initialize(value, reference) - super(reference) - @value = value + class LessThanMinItems < ValueError + def initialize(value, reference, options:) + super(value, reference, options: options) end def message - "#{@reference} #{@value.inspect} contains fewer than min items" + "#{@reference} #{redacted_value} contains fewer than min items" end end - class NotUniqueItems < OpenAPIError - def initialize(value, reference) - super(reference) - @value = value + class NotUniqueItems < ValueError + def initialize(value, reference, options:) + super(value, reference, options: options) end def message - "#{@reference} #{@value.inspect} contains duplicate items" + "#{@reference} #{redacted_value} contains duplicate items" end end end diff --git a/lib/openapi_parser/schema_validator.rb b/lib/openapi_parser/schema_validator.rb index fd9d2af..8e5a052 100644 --- a/lib/openapi_parser/schema_validator.rb +++ b/lib/openapi_parser/schema_validator.rb @@ -52,8 +52,7 @@ def validate(value, schema, options) def initialize(value, schema, options) @value = value @schema = schema - @coerce_value = options.coerce_value - @datetime_coerce_class = options.datetime_coerce_class + @options = options end # execute validate data @@ -80,7 +79,7 @@ def validate_schema(value, schema, **keyword_args) end # unknown return error - OpenAPIParser::ValidateError.build_error_result(value, schema) + OpenAPIParser::ValidateError.build_error_result(value, schema, options: @options) end # validate integer value by schema @@ -119,46 +118,46 @@ def validator(value, schema) end def string_validator - @string_validator ||= OpenAPIParser::SchemaValidator::StringValidator.new(self, @coerce_value, @datetime_coerce_class) + @string_validator ||= OpenAPIParser::SchemaValidator::StringValidator.new(self, options: @options) end def integer_validator - @integer_validator ||= OpenAPIParser::SchemaValidator::IntegerValidator.new(self, @coerce_value) + @integer_validator ||= OpenAPIParser::SchemaValidator::IntegerValidator.new(self, options: @options) end def float_validator - @float_validator ||= OpenAPIParser::SchemaValidator::FloatValidator.new(self, @coerce_value) + @float_validator ||= OpenAPIParser::SchemaValidator::FloatValidator.new(self, options: @options) end def boolean_validator - @boolean_validator ||= OpenAPIParser::SchemaValidator::BooleanValidator.new(self, @coerce_value) + @boolean_validator ||= OpenAPIParser::SchemaValidator::BooleanValidator.new(self, options: @options) end def object_validator - @object_validator ||= OpenAPIParser::SchemaValidator::ObjectValidator.new(self, @coerce_value) + @object_validator ||= OpenAPIParser::SchemaValidator::ObjectValidator.new(self, options: @options) end def array_validator - @array_validator ||= OpenAPIParser::SchemaValidator::ArrayValidator.new(self, @coerce_value) + @array_validator ||= OpenAPIParser::SchemaValidator::ArrayValidator.new(self, options: @options) end def any_of_validator - @any_of_validator ||= OpenAPIParser::SchemaValidator::AnyOfValidator.new(self, @coerce_value) + @any_of_validator ||= OpenAPIParser::SchemaValidator::AnyOfValidator.new(self, options: @options) end def all_of_validator - @all_of_validator ||= OpenAPIParser::SchemaValidator::AllOfValidator.new(self, @coerce_value) + @all_of_validator ||= OpenAPIParser::SchemaValidator::AllOfValidator.new(self, options: @options) end def one_of_validator - @one_of_validator ||= OpenAPIParser::SchemaValidator::OneOfValidator.new(self, @coerce_value) + @one_of_validator ||= OpenAPIParser::SchemaValidator::OneOfValidator.new(self, options: @options) end def nil_validator - @nil_validator ||= OpenAPIParser::SchemaValidator::NilValidator.new(self, @coerce_value) + @nil_validator ||= OpenAPIParser::SchemaValidator::NilValidator.new(self, options: @options) end def unspecified_type_validator - @unspecified_type_validator ||= OpenAPIParser::SchemaValidator::UnspecifiedTypeValidator.new(self, @coerce_value) + @unspecified_type_validator ||= OpenAPIParser::SchemaValidator::UnspecifiedTypeValidator.new(self, options: @options) end end diff --git a/lib/openapi_parser/schema_validator/any_of_validator.rb b/lib/openapi_parser/schema_validator/any_of_validator.rb index bbdf74d..a009c46 100644 --- a/lib/openapi_parser/schema_validator/any_of_validator.rb +++ b/lib/openapi_parser/schema_validator/any_of_validator.rb @@ -15,7 +15,7 @@ def coerce_and_validate(value, schema, **_keyword_args) coerced, err = validatable.validate_schema(value, s) return [coerced, nil] if err.nil? end - [nil, OpenAPIParser::NotAnyOf.new(value, schema.object_reference)] + [nil, OpenAPIParser::NotAnyOf.new(value, schema.object_reference, options: @options)] end end end diff --git a/lib/openapi_parser/schema_validator/array_validator.rb b/lib/openapi_parser/schema_validator/array_validator.rb index 71df390..d9f350b 100644 --- a/lib/openapi_parser/schema_validator/array_validator.rb +++ b/lib/openapi_parser/schema_validator/array_validator.rb @@ -3,7 +3,7 @@ class ArrayValidator < Base # @param [Array] value # @param [OpenAPIParser::Schemas::Schema] schema def coerce_and_validate(value, schema, **_keyword_args) - return OpenAPIParser::ValidateError.build_error_result(value, schema) unless value.kind_of?(Array) + return OpenAPIParser::ValidateError.build_error_result(value, schema, options: @options) unless value.kind_of?(Array) value, err = validate_max_min_items(value, schema) return [nil, err] if err @@ -20,20 +20,20 @@ def coerce_and_validate(value, schema, **_keyword_args) coerced end - value.each_index { |idx| value[idx] = coerced_values[idx] } if @coerce_value + value.each_index { |idx| value[idx] = coerced_values[idx] } if @options.coerce_value [value, nil] end def validate_max_min_items(value, schema) - return [nil, OpenAPIParser::MoreThanMaxItems.new(value, schema.object_reference)] if schema.maxItems && value.length > schema.maxItems - return [nil, OpenAPIParser::LessThanMinItems.new(value, schema.object_reference)] if schema.minItems && value.length < schema.minItems + return [nil, OpenAPIParser::MoreThanMaxItems.new(value, schema.object_reference, options: @options)] if schema.maxItems && value.length > schema.maxItems + return [nil, OpenAPIParser::LessThanMinItems.new(value, schema.object_reference, options: @options)] if schema.minItems && value.length < schema.minItems [value, nil] end def validate_unique_items(value, schema) - return [nil, OpenAPIParser::NotUniqueItems.new(value, schema.object_reference)] if schema.uniqueItems && value.length != value.uniq.length + return [nil, OpenAPIParser::NotUniqueItems.new(value, schema.object_reference, options: @options)] if schema.uniqueItems && value.length != value.uniq.length [value, nil] end diff --git a/lib/openapi_parser/schema_validator/base.rb b/lib/openapi_parser/schema_validator/base.rb index 3bc91de..fae8c9d 100644 --- a/lib/openapi_parser/schema_validator/base.rb +++ b/lib/openapi_parser/schema_validator/base.rb @@ -1,8 +1,8 @@ class OpenAPIParser::SchemaValidator class Base - def initialize(validatable, coerce_value) + def initialize(validatable, options:) @validatable = validatable - @coerce_value = coerce_value + @options = options end attr_reader :validatable @@ -15,7 +15,7 @@ def coerce_and_validate(_value, _schema, **_keyword_args) def validate_discriminator_schema(discriminator, value, parent_discriminator_schemas: []) property_name = discriminator.property_name if property_name.nil? || !value.key?(property_name) - return [nil, OpenAPIParser::NotExistDiscriminatorPropertyName.new(discriminator.property_name, value, discriminator.object_reference)] + return [nil, OpenAPIParser::NotExistDiscriminatorPropertyName.new(discriminator.property_name, value, discriminator.object_reference, options: @options)] end mapping_key = value[property_name] diff --git a/lib/openapi_parser/schema_validator/boolean_validator.rb b/lib/openapi_parser/schema_validator/boolean_validator.rb index b489f02..7f7faad 100644 --- a/lib/openapi_parser/schema_validator/boolean_validator.rb +++ b/lib/openapi_parser/schema_validator/boolean_validator.rb @@ -6,9 +6,9 @@ class BooleanValidator < Base FALSE_VALUES = ['false', '0'].freeze def coerce_and_validate(value, schema, **_keyword_args) - value = coerce(value) if @coerce_value + value = coerce(value) if @options.coerce_value - return OpenAPIParser::ValidateError.build_error_result(value, schema) unless value.kind_of?(TrueClass) || value.kind_of?(FalseClass) + return OpenAPIParser::ValidateError.build_error_result(value, schema, options: @options) unless value.kind_of?(TrueClass) || value.kind_of?(FalseClass) value, err = check_enum_include(value, schema) return [nil, err] if err diff --git a/lib/openapi_parser/schema_validator/enumable.rb b/lib/openapi_parser/schema_validator/enumable.rb index c251010..0c42792 100644 --- a/lib/openapi_parser/schema_validator/enumable.rb +++ b/lib/openapi_parser/schema_validator/enumable.rb @@ -7,7 +7,7 @@ def check_enum_include(value, schema) return [value, nil] unless schema.enum return [value, nil] if schema.enum.include?(value) - [nil, OpenAPIParser::NotEnumInclude.new(value, schema.object_reference)] + [nil, OpenAPIParser::NotEnumInclude.new(value, schema.object_reference, options: @options)] end end end diff --git a/lib/openapi_parser/schema_validator/float_validator.rb b/lib/openapi_parser/schema_validator/float_validator.rb index 92ef452..2665273 100644 --- a/lib/openapi_parser/schema_validator/float_validator.rb +++ b/lib/openapi_parser/schema_validator/float_validator.rb @@ -7,7 +7,7 @@ class FloatValidator < Base # @param [Object] value # @param [OpenAPIParser::Schemas::Schema] schema def coerce_and_validate(value, schema, **_keyword_args) - value = coerce(value) if @coerce_value + value = coerce(value) if @options.coerce_value return validatable.validate_integer(value, schema) if value.kind_of?(Integer) @@ -17,7 +17,7 @@ def coerce_and_validate(value, schema, **_keyword_args) private def coercer_and_validate_numeric(value, schema) - return OpenAPIParser::ValidateError.build_error_result(value, schema) unless value.kind_of?(Numeric) + return OpenAPIParser::ValidateError.build_error_result(value, schema, options: @options) unless value.kind_of?(Numeric) value, err = check_enum_include(value, schema) return [nil, err] if err diff --git a/lib/openapi_parser/schema_validator/integer_validator.rb b/lib/openapi_parser/schema_validator/integer_validator.rb index 4cff3c1..39b9359 100644 --- a/lib/openapi_parser/schema_validator/integer_validator.rb +++ b/lib/openapi_parser/schema_validator/integer_validator.rb @@ -7,9 +7,9 @@ class IntegerValidator < Base # @param [Object] value # @param [OpenAPIParser::Schemas::Schema] schema def coerce_and_validate(value, schema, **_keyword_args) - value = coerce(value) if @coerce_value + value = coerce(value) if @options.coerce_value - return OpenAPIParser::ValidateError.build_error_result(value, schema) unless value.kind_of?(Integer) + return OpenAPIParser::ValidateError.build_error_result(value, schema, options: @options) unless value.kind_of?(Integer) value, err = check_enum_include(value, schema) return [nil, err] if err diff --git a/lib/openapi_parser/schema_validator/minimum_maximum.rb b/lib/openapi_parser/schema_validator/minimum_maximum.rb index 3e3e98f..7353ca9 100644 --- a/lib/openapi_parser/schema_validator/minimum_maximum.rb +++ b/lib/openapi_parser/schema_validator/minimum_maximum.rb @@ -20,17 +20,17 @@ def validate(value, schema) if schema.minimum if schema.exclusiveMinimum && value <= schema.minimum - raise OpenAPIParser::LessThanExclusiveMinimum.new(value, reference) + raise OpenAPIParser::LessThanExclusiveMinimum.new(value, reference, options: @options) elsif value < schema.minimum - raise OpenAPIParser::LessThanMinimum.new(value, reference) + raise OpenAPIParser::LessThanMinimum.new(value, reference, options: @options) end end if schema.maximum if schema.exclusiveMaximum && value >= schema.maximum - raise OpenAPIParser::MoreThanExclusiveMaximum.new(value, reference) + raise OpenAPIParser::MoreThanExclusiveMaximum.new(value, reference, options: @options) elsif value > schema.maximum - raise OpenAPIParser::MoreThanMaximum.new(value, reference) + raise OpenAPIParser::MoreThanMaximum.new(value, reference, options: @options) end end end diff --git a/lib/openapi_parser/schema_validator/object_validator.rb b/lib/openapi_parser/schema_validator/object_validator.rb index 2850d03..682e300 100644 --- a/lib/openapi_parser/schema_validator/object_validator.rb +++ b/lib/openapi_parser/schema_validator/object_validator.rb @@ -5,7 +5,7 @@ class ObjectValidator < Base # @param [Boolean] parent_all_of true if component is nested under allOf # @param [String, nil] discriminator_property_name discriminator.property_name to ignore checking additional_properties def coerce_and_validate(value, schema, parent_all_of: false, parent_discriminator_schemas: [], discriminator_property_name: nil) - return OpenAPIParser::ValidateError.build_error_result(value, schema) unless value.kind_of?(Hash) + return OpenAPIParser::ValidateError.build_error_result(value, schema, options: @options) unless value.kind_of?(Hash) properties = schema.properties || {} @@ -48,7 +48,7 @@ def coerce_and_validate(value, schema, parent_all_of: false, parent_discriminato end return [nil, OpenAPIParser::NotExistRequiredKey.new(required_set.to_a, schema.object_reference)] unless required_set.empty? - value.merge!(coerced_values.to_h) if @coerce_value + value.merge!(coerced_values.to_h) if @options.coerce_value [value, nil] end diff --git a/lib/openapi_parser/schema_validator/one_of_validator.rb b/lib/openapi_parser/schema_validator/one_of_validator.rb index 8540dbe..87754d0 100644 --- a/lib/openapi_parser/schema_validator/one_of_validator.rb +++ b/lib/openapi_parser/schema_validator/one_of_validator.rb @@ -18,7 +18,7 @@ def coerce_and_validate(value, schema, **_keyword_args) if result [value, nil] else - [nil, OpenAPIParser::NotOneOf.new(value, schema.object_reference)] + [nil, OpenAPIParser::NotOneOf.new(value, schema.object_reference, options: @options)] end end end diff --git a/lib/openapi_parser/schema_validator/options.rb b/lib/openapi_parser/schema_validator/options.rb index 6824f03..77adb17 100644 --- a/lib/openapi_parser/schema_validator/options.rb +++ b/lib/openapi_parser/schema_validator/options.rb @@ -6,12 +6,13 @@ class Options # @return [Object, nil] coerce datetime string by this Object class # @!attribute [r] validate_header # @return [Boolean] validate header or not - attr_reader :coerce_value, :datetime_coerce_class, :validate_header + attr_reader :coerce_value, :datetime_coerce_class, :validate_header, :redact_errors - def initialize(coerce_value: nil, datetime_coerce_class: nil, validate_header: true) + def initialize(coerce_value: nil, datetime_coerce_class: nil, validate_header: true, redact_errors: false) @coerce_value = coerce_value @datetime_coerce_class = datetime_coerce_class @validate_header = validate_header + @redact_errors = redact_errors end end @@ -19,11 +20,12 @@ def initialize(coerce_value: nil, datetime_coerce_class: nil, validate_header: t class ResponseValidateOptions # @!attribute [r] strict # @return [Boolean] validate by strict (when not exist definition, raise error) - attr_reader :strict, :validate_header + attr_reader :strict, :validate_header, :redact_errors - def initialize(strict: false, validate_header: true) + def initialize(strict: false, validate_header: true, redact_errors: false) @strict = strict @validate_header = validate_header + @redact_errors = redact_errors end end end diff --git a/lib/openapi_parser/schema_validator/string_validator.rb b/lib/openapi_parser/schema_validator/string_validator.rb index e82f218..bf7ac44 100644 --- a/lib/openapi_parser/schema_validator/string_validator.rb +++ b/lib/openapi_parser/schema_validator/string_validator.rb @@ -2,13 +2,8 @@ class OpenAPIParser::SchemaValidator class StringValidator < Base include ::OpenAPIParser::SchemaValidator::Enumable - def initialize(validator, coerce_value, datetime_coerce_class) - super(validator, coerce_value) - @datetime_coerce_class = datetime_coerce_class - end - def coerce_and_validate(value, schema, **_keyword_args) - return OpenAPIParser::ValidateError.build_error_result(value, schema) unless value.kind_of?(String) + return OpenAPIParser::ValidateError.build_error_result(value, schema, options: @options) unless value.kind_of?(String) value, err = check_enum_include(value, schema) return [nil, err] if err @@ -42,12 +37,12 @@ def pattern_validate(value, schema) return [value, nil] unless schema.pattern return [value, nil] if value =~ /#{schema.pattern}/ - [nil, OpenAPIParser::InvalidPattern.new(value, schema.pattern, schema.object_reference, schema.example)] + [nil, OpenAPIParser::InvalidPattern.new(value, schema.pattern, schema.object_reference, schema.example, options: @options)] end def validate_max_min_length(value, schema) - return [nil, OpenAPIParser::MoreThanMaxLength.new(value, schema.object_reference)] if schema.maxLength && value.size > schema.maxLength - return [nil, OpenAPIParser::LessThanMinLength.new(value, schema.object_reference)] if schema.minLength && value.size < schema.minLength + return [nil, OpenAPIParser::MoreThanMaxLength.new(value, schema.object_reference, options: @options)] if schema.maxLength && value.size > schema.maxLength + return [nil, OpenAPIParser::LessThanMinLength.new(value, schema.object_reference, options: @options)] if schema.minLength && value.size < schema.minLength [value, nil] end @@ -60,7 +55,7 @@ def validate_email_format(value, schema) #return [value, nil] if value.match?(URI::MailTo::EMAIL_REGEXP) return [value, nil] if value.match(URI::MailTo::EMAIL_REGEXP) - return [nil, OpenAPIParser::InvalidEmailFormat.new(value, schema.object_reference)] + return [nil, OpenAPIParser::InvalidEmailFormat.new(value, schema.object_reference, options: @options)] end def validate_uuid_format(value, schema) @@ -68,7 +63,7 @@ def validate_uuid_format(value, schema) return [value, nil] if value.match(/[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}/) - return [nil, OpenAPIParser::InvalidUUIDFormat.new(value, schema.object_reference)] + return [nil, OpenAPIParser::InvalidUUIDFormat.new(value, schema.object_reference, options: @options)] end def validate_date_format(value, schema) @@ -77,11 +72,11 @@ def validate_date_format(value, schema) begin parsed_date = Date.iso8601(value) rescue ArgumentError - return [nil, OpenAPIParser::InvalidDateFormat.new(value, schema.object_reference)] + return [nil, OpenAPIParser::InvalidDateFormat.new(value, schema.object_reference, options: @options)] end unless parsed_date.strftime('%Y-%m-%d') == value - return [nil, OpenAPIParser::InvalidDateFormat.new(value, schema.object_reference)] + return [nil, OpenAPIParser::InvalidDateFormat.new(value, schema.object_reference, options: @options)] end return [value, nil] @@ -91,21 +86,21 @@ def validate_datetime_format(value, schema) return [value, nil] unless schema.format == 'date-time' begin - if @datetime_coerce_class.nil? + if @options.datetime_coerce_class.nil? # validate only DateTime.rfc3339(value) [value, nil] else # validate and coerce - if @datetime_coerce_class == Time + if @options.datetime_coerce_class == Time [DateTime.rfc3339(value).to_time, nil] else - [@datetime_coerce_class.rfc3339(value), nil] + [@options.datetime_coerce_class.rfc3339(value), nil] end end rescue ArgumentError # when rfc3339(value) failed - [nil, OpenAPIParser::InvalidDateTimeFormat.new(value, schema.object_reference)] + [nil, OpenAPIParser::InvalidDateTimeFormat.new(value, schema.object_reference, options: @options)] end end end diff --git a/lib/openapi_parser/schemas/response.rb b/lib/openapi_parser/schemas/response.rb index 2a10d04..f34b0e5 100644 --- a/lib/openapi_parser/schemas/response.rb +++ b/lib/openapi_parser/schemas/response.rb @@ -26,7 +26,8 @@ def validate(response_body, response_validate_options) return nil end - options = ::OpenAPIParser::SchemaValidator::Options.new # response validator not support any options + # response validator only supports redacted errors + options = ::OpenAPIParser::SchemaValidator::Options.new(redact_errors: response_validate_options.redact_errors) media_type.validate_parameter(response_body.response_data, options) end diff --git a/sig/openapi_parser/errors.rbs b/sig/openapi_parser/errors.rbs index b3187da..2e5591e 100644 --- a/sig/openapi_parser/errors.rbs +++ b/sig/openapi_parser/errors.rbs @@ -3,11 +3,20 @@ module OpenAPIParser def initialize: (untyped reference) -> untyped end - class ValidateError < OpenAPIError - def initialize: (untyped data, (String | nil) type, untyped reference) -> untyped + class ValueError < OpenAPIError + @redact: bool + + @options: SchemaValidator::ResponseValidateOptions + + def initialize: (untyped value, untyped reference, options: SchemaValidator::Options) -> untyped + def redacted_value: -> string + end + + class ValidateError < ValueError + def initialize: (untyped value, (String | nil) type, untyped reference, options: SchemaValidator::Options) -> untyped def message: -> String - def self.build_error_result: (Object value, OpenAPIParser::Schemas::Schema schema) -> [nil, OpenAPIParser::ValidateError] + def self.build_error_result: (Object value, OpenAPIParser::Schemas::Schema schema, options: SchemaValidator::Options) -> [nil, OpenAPIParser::ValidateError] end class NotExistDiscriminatorMappedSchema < OpenAPIError @@ -15,8 +24,8 @@ module OpenAPIParser def message: -> String end - class NotExistDiscriminatorPropertyName < OpenAPIError - def initialize: (untyped mapped_schema_reference, untyped value, untyped reference) -> untyped + class NotExistDiscriminatorPropertyName < ValueError + def initialize: (untyped mapped_schema_reference, untyped value, untyped reference, options: SchemaValidator::Options) -> untyped def message: -> String end end diff --git a/sig/openapi_parser/schema_validators/base.rbs b/sig/openapi_parser/schema_validators/base.rbs index 9851437..a683819 100644 --- a/sig/openapi_parser/schema_validators/base.rbs +++ b/sig/openapi_parser/schema_validators/base.rbs @@ -6,7 +6,7 @@ module OpenAPIParser attr_reader validatable: OpenAPIParser::SchemaValidator::Validatable - def initialize: (OpenAPIParser::SchemaValidator::Validatable validatable, (bool | nil) coerce_value) -> untyped + def initialize: (OpenAPIParser::SchemaValidator::Validatable validatable, options: Options) -> untyped def coerce_and_validate: (Object _value, OpenAPIParser::Schemas::Schema _schema, **untyped) -> [untyped, (ValidateError | NotExistDiscriminatorMappedSchema | nil)] def validate_discriminator_schema: ( OpenAPIParser::Schemas::Discriminator discriminator, diff --git a/sig/openapi_parser/schema_validators/options.rbs b/sig/openapi_parser/schema_validators/options.rbs index 1248c0f..c1f43e9 100644 --- a/sig/openapi_parser/schema_validators/options.rbs +++ b/sig/openapi_parser/schema_validators/options.rbs @@ -5,13 +5,15 @@ module OpenAPIParser attr_reader coerce_value: bool | nil attr_reader datetime_coerce_class: singleton(Object) | nil attr_reader validate_header: bool - def initialize: (?coerce_value: bool | nil, ?datetime_coerce_class: singleton(Object) | nil, ?validate_header: bool) -> untyped + attr_reader redact_errors: bool + def initialize: (?coerce_value: bool | nil, ?datetime_coerce_class: singleton(Object) | nil, ?validate_header: bool, ?redact_errors: bool) -> untyped end class ResponseValidateOptions attr_reader strict: bool attr_reader validate_header: bool - def initialize: (?strict: bool, ?validate_header: bool) -> untyped + attr_reader redact_errors: bool + def initialize: (?strict: bool, ?validate_header: bool, ?redact_errors: bool) -> untyped end end end diff --git a/spec/openapi_parser/schema_validator/base_spec.rb b/spec/openapi_parser/schema_validator/base_spec.rb index 0b9899f..70117a8 100644 --- a/spec/openapi_parser/schema_validator/base_spec.rb +++ b/spec/openapi_parser/schema_validator/base_spec.rb @@ -2,7 +2,7 @@ RSpec.describe OpenAPIParser::SchemaValidator::Base do describe '#coerce_and_validate(_value, _schema)' do - subject { OpenAPIParser::SchemaValidator::Base.new(nil, nil).coerce_and_validate(nil, nil) } + subject { OpenAPIParser::SchemaValidator::Base.new(nil, options: OpenAPIParser::SchemaValidator::Options.new).coerce_and_validate(nil, nil) } it { expect { subject }.to raise_error(StandardError).with_message('need implement') } end diff --git a/spec/openapi_parser/schema_validator/object_validator_spec.rb b/spec/openapi_parser/schema_validator/object_validator_spec.rb index 996e716..6f9db84 100644 --- a/spec/openapi_parser/schema_validator/object_validator_spec.rb +++ b/spec/openapi_parser/schema_validator/object_validator_spec.rb @@ -2,7 +2,7 @@ RSpec.describe OpenAPIParser::SchemaValidator::ObjectValidator do before do - @validator = OpenAPIParser::SchemaValidator::ObjectValidator.new(nil, nil) + @validator = OpenAPIParser::SchemaValidator::ObjectValidator.new(nil, options: OpenAPIParser::SchemaValidator::Options.new) @root = OpenAPIParser.parse( 'openapi' => '3.0.0', 'components' => { diff --git a/spec/openapi_parser/schemas/responses_spec.rb b/spec/openapi_parser/schemas/responses_spec.rb index daf642f..b7b859e 100644 --- a/spec/openapi_parser/schemas/responses_spec.rb +++ b/spec/openapi_parser/schemas/responses_spec.rb @@ -22,7 +22,7 @@ end end - describe '#validate_response_body(status_code, content_type, params)' do + describe '#validate_response_body' do subject { responses.validate(response_body, response_validate_options) } let(:root) { OpenAPIParser.parse(petstore_schema, {}) } @@ -94,6 +94,28 @@ it { expect { subject }.to raise_error(OpenAPIParser::NotExistRequiredKey) } end end + + context 'invalid body' do + let(:params) { [{ 'id' => 'not-an-integer', 'name' => 'name' }] } + let(:status_code) { 200 } + + it do + expect { subject }.to raise_error(OpenAPIParser::ValidateError).with_message( + '#/components/schemas/Pet/allOf/1/properties/id expected integer, but received String: "not-an-integer"' + ) + end + + context 'when error redaction is enabled' do + let(:response_validate_options) { } + let(:response_validate_options) { OpenAPIParser::SchemaValidator::ResponseValidateOptions.new(redact_errors: true) } + + it do + expect { subject }.to raise_error(OpenAPIParser::ValidateError).with_message( + '#/components/schemas/Pet/allOf/1/properties/id expected integer, but received String: ' + ) + end + end + end end describe 'resolve reference init' do