Skip to content

Commit b11986e

Browse files
committed
Add the new modules
1 parent 7c5ff47 commit b11986e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+3502
-1
lines changed

lib/openapi_parser.rb

+11-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
require 'openapi_parser/errors'
1111
require 'openapi_parser/concern'
1212
require 'openapi_parser/schemas'
13+
require 'openapi_parser/schemas_3_1'
1314
require 'openapi_parser/path_item_finder'
1415
require 'openapi_parser/request_operation'
1516
require 'openapi_parser/schema_validator'
@@ -93,7 +94,16 @@ def parse_json(content)
9394
end
9495

9596
def load_hash(hash, config:, uri:, schema_registry:)
96-
root = Schemas::OpenAPI.new(hash, config, uri: uri, schema_registry: schema_registry)
97+
case hash["openapi"]&.split('.')
98+
in ['3', '0', _]
99+
root = Schemas::OpenAPI.new(hash, config, uri: uri, schema_registry: schema_registry)
100+
in ['3', '1', _]
101+
root = Schemas31::OpenAPI.new(hash, config, uri: uri, schema_registry: schema_registry)
102+
else
103+
# Too avoid breaking changes, we don't raise error here.
104+
# raise NotSupportedSpecificationVersionError.new("Unsupported specification version: #{hash['openapi']}")
105+
root = Schemas::OpenAPI.new(hash, config, uri: uri, schema_registry: schema_registry)
106+
end
97107

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

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# validate AllOf schema
2+
class OpenAPIParser::Schema31Validator
3+
class AllOfValidator < Base
4+
# coerce and validate value
5+
# @param [Object] value
6+
# @param [OpenAPIParser::Schemas31Schema] schema
7+
def coerce_and_validate(value, schema, **keyword_args)
8+
if value.nil? && schema.nullable
9+
return [value, nil]
10+
end
11+
12+
# if any schema return error, it's not valida all of value
13+
remaining_keys = value.kind_of?(Hash) ? value.keys : []
14+
nested_additional_properties = false
15+
schema.all_of.each do |s|
16+
# We need to store the reference to all of, so we can perform strict check on allowed properties
17+
_coerced, err = validatable.validate_schema(
18+
value,
19+
s,
20+
:parent_all_of => true,
21+
parent_discriminator_schemas: keyword_args[:parent_discriminator_schemas]
22+
)
23+
24+
if s.type == "object"
25+
remaining_keys -= (s.properties || {}).keys
26+
nested_additional_properties = true if s.additional_properties
27+
else
28+
# If this is not allOf having array of objects inside, but e.g. having another anyOf/oneOf nested
29+
remaining_keys.clear
30+
end
31+
32+
return [nil, err] if err
33+
end
34+
35+
# If there are nested additionalProperites, we allow not defined extra properties and lean on the specific
36+
# additionalProperties validation
37+
if !nested_additional_properties && !remaining_keys.empty?
38+
return [nil, OpenAPIParser::NotExistPropertyDefinition.new(remaining_keys, schema.object_reference)]
39+
end
40+
41+
[value, nil]
42+
end
43+
end
44+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
class OpenAPIParser::Schema31Validator
2+
class AnyOfValidator < Base
3+
# @param [Object] value
4+
# @param [OpenAPIParser::Schemas31Schema] schema
5+
def coerce_and_validate(value, schema, **_keyword_args)
6+
if value.nil? && schema.nullable
7+
return [value, nil]
8+
end
9+
if schema.discriminator
10+
return validate_discriminator_schema(schema.discriminator, value)
11+
end
12+
13+
# in all schema return error (=true) not any of data
14+
schema.any_of.each do |s|
15+
coerced, err = validatable.validate_schema(value, s)
16+
return [coerced, nil] if err.nil?
17+
end
18+
[nil, OpenAPIParser::NotAnyOf.new(value, schema.object_reference)]
19+
end
20+
end
21+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
class OpenAPIParser::Schema31Validator
2+
class ArrayValidator < Base
3+
# @param [Array] value
4+
# @param [OpenAPIParser::Schemas31Schema] schema
5+
def coerce_and_validate(value, schema, **_keyword_args)
6+
return OpenAPIParser::ValidateError.build_error_result(value, schema) unless value.kind_of?(Array)
7+
8+
value, err = validate_max_min_items(value, schema)
9+
return [nil, err] if err
10+
11+
value, err = validate_unique_items(value, schema)
12+
return [nil, err] if err
13+
14+
# array type have an schema in items property
15+
items_schema = schema.items
16+
coerced_values = value.map do |v|
17+
coerced, err = validatable.validate_schema(v, items_schema)
18+
return [nil, err] if err
19+
20+
coerced
21+
end
22+
23+
value.each_index { |idx| value[idx] = coerced_values[idx] } if @coerce_value
24+
25+
[value, nil]
26+
end
27+
28+
def validate_max_min_items(value, schema)
29+
return [nil, OpenAPIParser::MoreThanMaxItems.new(value, schema.object_reference)] if schema.maxItems && value.length > schema.maxItems
30+
return [nil, OpenAPIParser::LessThanMinItems.new(value, schema.object_reference)] if schema.minItems && value.length < schema.minItems
31+
32+
[value, nil]
33+
end
34+
35+
def validate_unique_items(value, schema)
36+
return [nil, OpenAPIParser::NotUniqueItems.new(value, schema.object_reference)] if schema.uniqueItems && value.length != value.uniq.length
37+
38+
[value, nil]
39+
end
40+
end
41+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
class OpenAPIParser::Schema31Validator
2+
class Base
3+
def initialize(validatable, coerce_value)
4+
@validatable = validatable
5+
@coerce_value = coerce_value
6+
end
7+
8+
attr_reader :validatable
9+
10+
# need override
11+
def coerce_and_validate(_value, _schema, **_keyword_args)
12+
raise 'need implement'
13+
end
14+
15+
def validate_discriminator_schema(discriminator, value, parent_discriminator_schemas: [])
16+
property_name = discriminator.property_name
17+
if property_name.nil? || !value.key?(property_name)
18+
return [nil, OpenAPIParser::NotExistDiscriminatorPropertyName.new(discriminator.property_name, value, discriminator.object_reference)]
19+
end
20+
mapping_key = value[property_name]
21+
22+
# it's allowed to have discriminator without mapping, then we need to lookup discriminator.property_name
23+
# but the format is not the full path, just model name in the components
24+
mapping_target = discriminator.mapping&.[](mapping_key) || "#/components/schemas/#{mapping_key}"
25+
26+
# Find object does O(n) search at worst, then caches the result, so this is ok for repeated search
27+
resolved_schema = discriminator.root.find_object(mapping_target)
28+
29+
unless resolved_schema
30+
return [nil, OpenAPIParser::NotExistDiscriminatorMappedSchema.new(mapping_target, discriminator.object_reference)]
31+
end
32+
validatable.validate_schema(
33+
value,
34+
resolved_schema,
35+
**{discriminator_property_name: discriminator.property_name, parent_discriminator_schemas: parent_discriminator_schemas}
36+
)
37+
end
38+
end
39+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
class OpenAPIParser::Schema31Validator
2+
class BooleanValidator < Base
3+
include ::OpenAPIParser::Schema31Validator::Enumable
4+
5+
TRUE_VALUES = ['true', '1'].freeze
6+
FALSE_VALUES = ['false', '0'].freeze
7+
8+
def coerce_and_validate(value, schema, **_keyword_args)
9+
value = coerce(value) if @coerce_value
10+
11+
return OpenAPIParser::ValidateError.build_error_result(value, schema) unless value.kind_of?(TrueClass) || value.kind_of?(FalseClass)
12+
13+
value, err = check_enum_include(value, schema)
14+
return [nil, err] if err
15+
16+
[value, nil]
17+
end
18+
19+
private
20+
21+
def coerce(value)
22+
return true if TRUE_VALUES.include?(value)
23+
24+
return false if FALSE_VALUES.include?(value)
25+
26+
value
27+
end
28+
end
29+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
class OpenAPIParser::Schema31Validator
2+
module Enumable
3+
# check enum value by schema
4+
# @param [Object] value
5+
# @param [OpenAPIParser::Schemas31Schema] schema
6+
def check_enum_include(value, schema)
7+
return [value, nil] unless schema.enum
8+
return [value, nil] if schema.enum.include?(value)
9+
10+
[nil, OpenAPIParser::NotEnumInclude.new(value, schema.object_reference)]
11+
end
12+
end
13+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
class OpenAPIParser::Schema31Validator
2+
class FloatValidator < Base
3+
include ::OpenAPIParser::Schema31Validator::Enumable
4+
include ::OpenAPIParser::Schema31Validator::MinimumMaximum
5+
6+
# validate float value by schema
7+
# @param [Object] value
8+
# @param [OpenAPIParser::Schemas31Schema] schema
9+
def coerce_and_validate(value, schema, **_keyword_args)
10+
value = coerce(value) if @coerce_value
11+
12+
return validatable.validate_integer(value, schema) if value.kind_of?(Integer)
13+
14+
coercer_and_validate_numeric(value, schema)
15+
end
16+
17+
private
18+
19+
def coercer_and_validate_numeric(value, schema)
20+
return OpenAPIParser::ValidateError.build_error_result(value, schema) unless value.kind_of?(Numeric)
21+
22+
value, err = check_enum_include(value, schema)
23+
return [nil, err] if err
24+
25+
check_minimum_maximum(value, schema)
26+
end
27+
28+
def coerce(value)
29+
Float(value)
30+
rescue ArgumentError, TypeError
31+
value
32+
end
33+
end
34+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
class OpenAPIParser::Schema31Validator
2+
class IntegerValidator < Base
3+
include ::OpenAPIParser::Schema31Validator::Enumable
4+
include ::OpenAPIParser::Schema31Validator::MinimumMaximum
5+
6+
# validate integer value by schema
7+
# @param [Object] value
8+
# @param [OpenAPIParser::Schemas31Schema] schema
9+
def coerce_and_validate(value, schema, **_keyword_args)
10+
value = coerce(value) if @coerce_value
11+
12+
return OpenAPIParser::ValidateError.build_error_result(value, schema) unless value.kind_of?(Integer)
13+
14+
value, err = check_enum_include(value, schema)
15+
return [nil, err] if err
16+
17+
check_minimum_maximum(value, schema)
18+
end
19+
20+
private
21+
22+
def coerce(value)
23+
return value if value.kind_of?(Integer)
24+
25+
begin
26+
Integer(value)
27+
rescue ArgumentError, TypeError
28+
value
29+
end
30+
end
31+
end
32+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
class OpenAPIParser::Schema31Validator
2+
module MinimumMaximum
3+
# check minimum and maximum value by schema
4+
# @param [Object] value
5+
# @param [OpenAPIParser::Schemas31Schema] schema
6+
def check_minimum_maximum(value, schema)
7+
include_min_max = schema.minimum || schema.maximum
8+
return [value, nil] unless include_min_max
9+
10+
validate(value, schema)
11+
[value, nil]
12+
rescue OpenAPIParser::OpenAPIError => e
13+
return [nil, e]
14+
end
15+
16+
private
17+
18+
def validate(value, schema)
19+
reference = schema.object_reference
20+
21+
if schema.minimum
22+
if schema.exclusiveMinimum.present? && value <= schema.exclusiveMinimum
23+
raise OpenAPIParser::LessThanExclusiveMinimum.new(value, reference)
24+
elsif value < schema.minimum
25+
raise OpenAPIParser::LessThanMinimum.new(value, reference)
26+
end
27+
end
28+
29+
if schema.maximum
30+
if schema.exclusiveMaximum.present? && value >= schema.exclusiveMinimum
31+
raise OpenAPIParser::MoreThanExclusiveMaximum.new(value, reference)
32+
elsif value > schema.maximum
33+
raise OpenAPIParser::MoreThanMaximum.new(value, reference)
34+
end
35+
end
36+
end
37+
end
38+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
class OpenAPIParser::Schema31Validator
2+
class NilValidator < Base
3+
# @param [Object] value
4+
# @param [OpenAPIParser::Schemas31Schema] schema
5+
def coerce_and_validate(value, schema, **_keyword_args)
6+
return [value, nil] if schema.nullable
7+
8+
[nil, OpenAPIParser::NotNullError.new(schema.object_reference)]
9+
end
10+
end
11+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
class OpenAPIParser::Schema31Validator
2+
class ObjectValidator < Base
3+
# @param [Hash] value
4+
# @param [OpenAPIParser::Schemas31Schema] schema
5+
# @param [Boolean] parent_all_of true if component is nested under allOf
6+
# @param [String, nil] discriminator_property_name discriminator.property_name to ignore checking additional_properties
7+
def coerce_and_validate(value, schema, parent_all_of: false, parent_discriminator_schemas: [], discriminator_property_name: nil)
8+
return OpenAPIParser::ValidateError.build_error_result(value, schema) unless value.kind_of?(Hash)
9+
10+
properties = schema.properties || {}
11+
12+
required_set = schema.required ? schema.required.to_set : Set.new
13+
remaining_keys = value.keys
14+
15+
if schema.discriminator && !parent_discriminator_schemas.include?(schema)
16+
return validate_discriminator_schema(
17+
schema.discriminator,
18+
value,
19+
parent_discriminator_schemas: parent_discriminator_schemas + [schema]
20+
)
21+
else
22+
remaining_keys.delete('discriminator')
23+
end
24+
25+
coerced_values = value.map do |name, v|
26+
s = properties[name]
27+
coerced, err = if s
28+
remaining_keys.delete(name)
29+
validatable.validate_schema(v, s)
30+
else
31+
# TODO: we need to perform a validation based on schema.additional_properties here, if
32+
# additionalProperties are defined
33+
[v, nil]
34+
end
35+
36+
return [nil, err] if err
37+
38+
required_set.delete(name)
39+
[name, coerced]
40+
end
41+
42+
remaining_keys.delete(discriminator_property_name) if discriminator_property_name
43+
44+
if !remaining_keys.empty? && !parent_all_of && !schema.additional_properties
45+
# If object is nested in all of, the validation is already done in allOf validator. Or if
46+
# additionalProperties are defined, we will validate using that
47+
return [nil, OpenAPIParser::NotExistPropertyDefinition.new(remaining_keys, schema.object_reference)]
48+
end
49+
return [nil, OpenAPIParser::NotExistRequiredKey.new(required_set.to_a, schema.object_reference)] unless required_set.empty?
50+
51+
value.merge!(coerced_values.to_h) if @coerce_value
52+
53+
[value, nil]
54+
end
55+
end
56+
end

0 commit comments

Comments
 (0)