Skip to content

Add the new modules #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
12 changes: 11 additions & 1 deletion lib/openapi_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
require 'openapi_parser/errors'
require 'openapi_parser/concern'
require 'openapi_parser/schemas'
require 'openapi_parser/schemas_3_1'
require 'openapi_parser/path_item_finder'
require 'openapi_parser/request_operation'
require 'openapi_parser/schema_validator'
Expand Down Expand Up @@ -93,7 +94,16 @@ def parse_json(content)
end

def load_hash(hash, config:, uri:, schema_registry:)
root = Schemas::OpenAPI.new(hash, config, uri: uri, schema_registry: schema_registry)
case hash["openapi"]&.split('.')
in ['3', '0', _]
root = Schemas::OpenAPI.new(hash, config, uri: uri, schema_registry: schema_registry)
in ['3', '1', _]
root = Schemas31::OpenAPI.new(hash, config, uri: uri, schema_registry: schema_registry)
else
# Too avoid breaking changes, we don't raise error here.
# raise NotSupportedSpecificationVersionError.new("Unsupported specification version: #{hash['openapi']}")
root = Schemas::OpenAPI.new(hash, config, uri: uri, schema_registry: schema_registry)
end

OpenAPIParser::ReferenceExpander.expand(root, config.strict_reference_validation) if config.expand_reference

Expand Down
44 changes: 44 additions & 0 deletions lib/openapi_parser/schema_3_1_validator/all_of_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# validate AllOf schema
class OpenAPIParser::Schema31Validator
class AllOfValidator < Base
# coerce and validate value
# @param [Object] value
# @param [OpenAPIParser::Schemas31Schema] schema
def coerce_and_validate(value, schema, **keyword_args)
if value.nil? && schema.nullable
return [value, nil]
end

# if any schema return error, it's not valida all of value
remaining_keys = value.kind_of?(Hash) ? value.keys : []
nested_additional_properties = false
schema.all_of.each do |s|
# We need to store the reference to all of, so we can perform strict check on allowed properties
_coerced, err = validatable.validate_schema(
value,
s,
:parent_all_of => true,
parent_discriminator_schemas: keyword_args[:parent_discriminator_schemas]
)

if s.type == "object"
remaining_keys -= (s.properties || {}).keys
nested_additional_properties = true if s.additional_properties
else
# If this is not allOf having array of objects inside, but e.g. having another anyOf/oneOf nested
remaining_keys.clear
end

return [nil, err] if err
end

# If there are nested additionalProperites, we allow not defined extra properties and lean on the specific
# additionalProperties validation
if !nested_additional_properties && !remaining_keys.empty?
return [nil, OpenAPIParser::NotExistPropertyDefinition.new(remaining_keys, schema.object_reference)]
end

[value, nil]
end
end
end
21 changes: 21 additions & 0 deletions lib/openapi_parser/schema_3_1_validator/any_of_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class OpenAPIParser::Schema31Validator
class AnyOfValidator < Base
# @param [Object] value
# @param [OpenAPIParser::Schemas31Schema] schema
def coerce_and_validate(value, schema, **_keyword_args)
if value.nil? && schema.nullable
return [value, nil]
end
if schema.discriminator
return validate_discriminator_schema(schema.discriminator, value)
end

# in all schema return error (=true) not any of data
schema.any_of.each do |s|
coerced, err = validatable.validate_schema(value, s)
return [coerced, nil] if err.nil?
end
[nil, OpenAPIParser::NotAnyOf.new(value, schema.object_reference)]
end
end
end
41 changes: 41 additions & 0 deletions lib/openapi_parser/schema_3_1_validator/array_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
class OpenAPIParser::Schema31Validator
class ArrayValidator < Base
# @param [Array] value
# @param [OpenAPIParser::Schemas31Schema] schema
def coerce_and_validate(value, schema, **_keyword_args)
return OpenAPIParser::ValidateError.build_error_result(value, schema) unless value.kind_of?(Array)

value, err = validate_max_min_items(value, schema)
return [nil, err] if err

value, err = validate_unique_items(value, schema)
return [nil, err] if err

# array type have an schema in items property
items_schema = schema.items
coerced_values = value.map do |v|
coerced, err = validatable.validate_schema(v, items_schema)
return [nil, err] if err

coerced
end

value.each_index { |idx| value[idx] = coerced_values[idx] } if @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

[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

[value, nil]
end
end
end
39 changes: 39 additions & 0 deletions lib/openapi_parser/schema_3_1_validator/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
class OpenAPIParser::Schema31Validator
class Base
def initialize(validatable, coerce_value)
@validatable = validatable
@coerce_value = coerce_value
end

attr_reader :validatable

# need override
def coerce_and_validate(_value, _schema, **_keyword_args)
raise 'need implement'
end

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)]
end
mapping_key = value[property_name]

# it's allowed to have discriminator without mapping, then we need to lookup discriminator.property_name
# but the format is not the full path, just model name in the components
mapping_target = discriminator.mapping&.[](mapping_key) || "#/components/schemas/#{mapping_key}"

# Find object does O(n) search at worst, then caches the result, so this is ok for repeated search
resolved_schema = discriminator.root.find_object(mapping_target)

unless resolved_schema
return [nil, OpenAPIParser::NotExistDiscriminatorMappedSchema.new(mapping_target, discriminator.object_reference)]
end
validatable.validate_schema(
value,
resolved_schema,
**{discriminator_property_name: discriminator.property_name, parent_discriminator_schemas: parent_discriminator_schemas}
)
end
end
end
29 changes: 29 additions & 0 deletions lib/openapi_parser/schema_3_1_validator/boolean_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
class OpenAPIParser::Schema31Validator
class BooleanValidator < Base
include ::OpenAPIParser::Schema31Validator::Enumable

TRUE_VALUES = ['true', '1'].freeze
FALSE_VALUES = ['false', '0'].freeze

def coerce_and_validate(value, schema, **_keyword_args)
value = coerce(value) if @coerce_value

return OpenAPIParser::ValidateError.build_error_result(value, schema) unless value.kind_of?(TrueClass) || value.kind_of?(FalseClass)

value, err = check_enum_include(value, schema)
return [nil, err] if err

[value, nil]
end

private

def coerce(value)
return true if TRUE_VALUES.include?(value)

return false if FALSE_VALUES.include?(value)

value
end
end
end
13 changes: 13 additions & 0 deletions lib/openapi_parser/schema_3_1_validator/enumable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class OpenAPIParser::Schema31Validator
module Enumable
# check enum value by schema
# @param [Object] value
# @param [OpenAPIParser::Schemas31Schema] schema
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)]
end
end
end
34 changes: 34 additions & 0 deletions lib/openapi_parser/schema_3_1_validator/float_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
class OpenAPIParser::Schema31Validator
class FloatValidator < Base
include ::OpenAPIParser::Schema31Validator::Enumable
include ::OpenAPIParser::Schema31Validator::MinimumMaximum

# validate float value by schema
# @param [Object] value
# @param [OpenAPIParser::Schemas31Schema] schema
def coerce_and_validate(value, schema, **_keyword_args)
value = coerce(value) if @coerce_value

return validatable.validate_integer(value, schema) if value.kind_of?(Integer)

coercer_and_validate_numeric(value, schema)
end

private

def coercer_and_validate_numeric(value, schema)
return OpenAPIParser::ValidateError.build_error_result(value, schema) unless value.kind_of?(Numeric)

value, err = check_enum_include(value, schema)
return [nil, err] if err

check_minimum_maximum(value, schema)
end

def coerce(value)
Float(value)
rescue ArgumentError, TypeError
value
end
end
end
32 changes: 32 additions & 0 deletions lib/openapi_parser/schema_3_1_validator/integer_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
class OpenAPIParser::Schema31Validator
class IntegerValidator < Base
include ::OpenAPIParser::Schema31Validator::Enumable
include ::OpenAPIParser::Schema31Validator::MinimumMaximum

# validate integer value by schema
# @param [Object] value
# @param [OpenAPIParser::Schemas31Schema] schema
def coerce_and_validate(value, schema, **_keyword_args)
value = coerce(value) if @coerce_value

return OpenAPIParser::ValidateError.build_error_result(value, schema) unless value.kind_of?(Integer)

value, err = check_enum_include(value, schema)
return [nil, err] if err

check_minimum_maximum(value, schema)
end

private

def coerce(value)
return value if value.kind_of?(Integer)

begin
Integer(value)
rescue ArgumentError, TypeError
value
end
end
end
end
38 changes: 38 additions & 0 deletions lib/openapi_parser/schema_3_1_validator/minimum_maximum.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
class OpenAPIParser::Schema31Validator
module MinimumMaximum
# check minimum and maximum value by schema
# @param [Object] value
# @param [OpenAPIParser::Schemas31Schema] schema
def check_minimum_maximum(value, schema)
include_min_max = schema.minimum || schema.maximum
return [value, nil] unless include_min_max

validate(value, schema)
[value, nil]
rescue OpenAPIParser::OpenAPIError => e
return [nil, e]
end

private

def validate(value, schema)
reference = schema.object_reference

if schema.minimum
if schema.exclusiveMinimum.present? && value <= schema.exclusiveMinimum
raise OpenAPIParser::LessThanExclusiveMinimum.new(value, reference)
elsif value < schema.minimum
raise OpenAPIParser::LessThanMinimum.new(value, reference)
end
end

if schema.maximum
if schema.exclusiveMaximum.present? && value >= schema.exclusiveMinimum
raise OpenAPIParser::MoreThanExclusiveMaximum.new(value, reference)
elsif value > schema.maximum
raise OpenAPIParser::MoreThanMaximum.new(value, reference)
end
end
end
end
end
11 changes: 11 additions & 0 deletions lib/openapi_parser/schema_3_1_validator/nil_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class OpenAPIParser::Schema31Validator
class NilValidator < Base
# @param [Object] value
# @param [OpenAPIParser::Schemas31Schema] schema
def coerce_and_validate(value, schema, **_keyword_args)
return [value, nil] if schema.nullable

[nil, OpenAPIParser::NotNullError.new(schema.object_reference)]
end
end
end
56 changes: 56 additions & 0 deletions lib/openapi_parser/schema_3_1_validator/object_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
class OpenAPIParser::Schema31Validator
class ObjectValidator < Base
# @param [Hash] value
# @param [OpenAPIParser::Schemas31Schema] schema
# @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)

properties = schema.properties || {}

required_set = schema.required ? schema.required.to_set : Set.new
remaining_keys = value.keys

if schema.discriminator && !parent_discriminator_schemas.include?(schema)
return validate_discriminator_schema(
schema.discriminator,
value,
parent_discriminator_schemas: parent_discriminator_schemas + [schema]
)
else
remaining_keys.delete('discriminator')
end

coerced_values = value.map do |name, v|
s = properties[name]
coerced, err = if s
remaining_keys.delete(name)
validatable.validate_schema(v, s)
else
# TODO: we need to perform a validation based on schema.additional_properties here, if
# additionalProperties are defined
[v, nil]
end

return [nil, err] if err

required_set.delete(name)
[name, coerced]
end

remaining_keys.delete(discriminator_property_name) if discriminator_property_name

if !remaining_keys.empty? && !parent_all_of && !schema.additional_properties
# If object is nested in all of, the validation is already done in allOf validator. Or if
# additionalProperties are defined, we will validate using that
return [nil, OpenAPIParser::NotExistPropertyDefinition.new(remaining_keys, schema.object_reference)]
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, nil]
end
end
end
Loading