Skip to content

Commit

Permalink
Merge pull request #305 from ahx/referenced-content-schema
Browse files Browse the repository at this point in the history
Support discriminator for request bodies
  • Loading branch information
ahx authored Dec 3, 2024
2 parents 1640a6b + 063c78a commit 44aea14
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 46 deletions.
20 changes: 14 additions & 6 deletions lib/openapi_first/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ def initialize(resolved, filepath:, config:)
ref_resolver = JSONSchemer::CachedResolver.new do |uri|
Refs.load_file(File.join(File.dirname(filepath), uri.path))
end
@doc = JSONSchemer.openapi(resolved, ref_resolver:)
configuration = JSONSchemer::Configuration.new(
ref_resolver:,
insert_property_defaults: true,
after_property_validation: config.hooks[:after_request_body_property_validation]
)
@doc = JSONSchemer.openapi(resolved, configuration:)
@config = config
@openapi_version = (resolved['openapi'] || resolved['swagger'])[0..2]
end
Expand All @@ -32,15 +37,16 @@ def router # rubocop:disable Metrics/MethodLength
resolved['paths'].each do |path, path_item_object|
path_item_object.slice(*REQUEST_METHODS).keys.map do |request_method|
operation_object = path_item_object[request_method]
build_requests(path, request_method, operation_object, path_item_object).each do |request|
operation_pointer = JsonPointer.append('#', 'paths', URI::DEFAULT_PARSER.escape(path), request_method)
build_requests(path:, request_method:, operation_object:, operation_pointer:,
path_item_object:).each do |request|
router.add_request(
request,
request_method:,
path:,
content_type: request.content_type
)
end
operation_pointer = JsonPointer.append('#', 'paths', URI::DEFAULT_PARSER.escape(path), request_method)
build_responses(operation_pointer:, operation_object:).each do |response|
router.add_response(
response,
Expand All @@ -55,14 +61,16 @@ def router # rubocop:disable Metrics/MethodLength
router
end

def build_requests(path, request_method, operation_object, path_item_object)
def build_requests(path:, request_method:, operation_object:, operation_pointer:, path_item_object:)
hooks = config.hooks
path_item_parameters = path_item_object['parameters']
parameters = operation_object['parameters'].to_a.chain(path_item_parameters.to_a)
required_body = operation_object.dig('requestBody', 'required') == true
result = operation_object.dig('requestBody', 'content')&.map do |content_type, content|
result = operation_object.dig('requestBody', 'content')&.map do |content_type, _content|
content_schema = @doc.ref(JsonPointer.append(operation_pointer, 'requestBody', 'content', content_type,
'schema'))
Request.new(path:, request_method:, operation_object:, parameters:, content_type:,
content_schema: content['schema'], required_body:, hooks:, openapi_version:)
content_schema:, required_body:, hooks:, openapi_version:)
end || []
return result if required_body

Expand Down
11 changes: 3 additions & 8 deletions lib/openapi_first/request_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,10 @@
module OpenapiFirst
# Validates a Request against a request definition.
class RequestValidator
VALIDATORS = [
Validators::RequestParameters,
Validators::RequestBody
].freeze

def initialize(request_definition, openapi_version:, hooks: {})
@validators = VALIDATORS.filter_map do |klass|
klass.for(request_definition, hooks:, openapi_version:)
end
@validators = []
@validators << Validators::RequestBody.new(request_definition) if request_definition.content_schema
@validators.concat Validators::RequestParameters.for(request_definition, openapi_version:, hooks:)
end

def call(parsed_request)
Expand Down
20 changes: 6 additions & 14 deletions lib/openapi_first/validators/request_body.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,9 @@
module OpenapiFirst
module Validators
class RequestBody
def self.for(request_definition, openapi_version:, hooks: {})
schema = request_definition.content_schema
return unless schema

after_property_validation = hooks[:after_request_body_property_validation]

new(Schema.new(schema, after_property_validation:, openapi_version:),
required: request_definition.required_request_body?)
end

def initialize(schema, required:)
@schema = schema
@required = required
def initialize(request_definition)
@schema = request_definition.content_schema
@required = request_definition.required_request_body?
end

def call(request)
Expand All @@ -25,7 +15,9 @@ def call(request)
return
end

validation = @schema.validate(request_body)
validation = Schema::ValidationResult.new(
@schema.validate(request_body, access_mode: 'write')
)
Failure.fail!(:invalid_body, errors: validation.errors) if validation.error?
end

Expand Down
19 changes: 4 additions & 15 deletions lib/openapi_first/validators/request_parameters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module OpenapiFirst
module Validators
class RequestParameters
module RequestParameters
RequestHeaders = Data.define(:schema) do
def call(parsed_values)
validation = schema.validate(parsed_values[:headers])
Expand Down Expand Up @@ -38,23 +38,12 @@ def call(parsed_values)
cookie_schema: RequestCookies
}.freeze

def self.for(operation, openapi_version:, hooks: {})
def self.for(request_definition, openapi_version:, hooks: {})
after_property_validation = hooks[:after_request_parameter_property_validation]
validators = VALIDATORS.filter_map do |key, klass|
schema = operation.send(key)
VALIDATORS.filter_map do |key, klass|
schema = request_definition.send(key)
klass.new(Schema.new(schema, after_property_validation:, openapi_version:)) if schema
end
return if validators.empty?

new(validators)
end

def initialize(validators)
@validators = validators
end

def call(parsed_values)
@validators.each { |validator| validator.call(parsed_values) }
end
end
end
Expand Down
19 changes: 19 additions & 0 deletions spec/data/discriminator-refs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,25 @@ paths:
propertyName: petType
type: array
"/pets-file":
post:
requestBody:
required: true
content:
application/json:
schema:
items:
oneOf:
- "$ref": "./components/schemas/cat.yaml"
- "$ref": "./components/schemas/dog.yaml"
discriminator:
propertyName: petType
mapping:
cat: "./components/schemas/cat.yaml"
dog: "./components/schemas/dog.yaml"
type: array
responses:
"201":
description: successful
get:
summary: Pets
responses:
Expand Down
20 changes: 17 additions & 3 deletions spec/hooks_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def build_request(path, method: 'GET', body: nil)
}
}
],
'get' => {
'post' => {
'parameters' => [
{
'name' => 'page',
Expand All @@ -102,6 +102,20 @@ def build_request(path, method: 'GET', body: nil)
}
}
],
'requestBody' => {
'content' => {
'application/json' => {
'schema' => {
'type' => 'object',
'properties' => {
'name' => {
'type' => 'string'
}
}
}
}
}
},
'responses' => {
'200' => {
'description' => 'ok'
Expand All @@ -120,7 +134,7 @@ def build_request(path, method: 'GET', body: nil)
end
end

definition.validate_request(build_request('/blue?page=2'))
definition.validate_request(build_request('/blue?page=2', method: 'POST', body: '{"name": "Quentin"}'))

expect(called).to eq([
[{ 'color' => 'blue' }, 'color', {
Expand All @@ -138,7 +152,7 @@ def build_request(path, method: 'GET', body: nil)
data[property] = 'two' if property == 'page'
end
end
validated = definition.validate_request(build_request('/blue?page=2'))
validated = definition.validate_request(build_request('/blue?page=2', method: 'POST', body: '{"name": "Quentin"}'))
expect(validated.parsed_query['page']).to eq('two')
expect(validated).to be_valid
end
Expand Down
39 changes: 39 additions & 0 deletions spec/middlewares/request_validation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,45 @@ def status = 409
end
end

context 'with discriminator' do
let(:app) do
Rack::Builder.app do
use(OpenapiFirst::Middlewares::RequestValidation,
spec: File.expand_path('../data/discriminator-refs.yaml', __dir__))
run lambda { |_env|
Rack::Response.new('hello', 200).finish
}
end
end

context 'with an invalid request' do
let(:request_body) { json_dump([{ id: 1, petType: 'unknown', meow: 'Huh' }]) }

it 'fails' do
header 'Content-Type', 'application/json'
post '/pets-file', request_body

expect(last_response.status).to eq 400
end
end

context 'with a valid request' do
let(:request_body) do
json_dump([
{ id: 1, petType: 'cat', meow: 'Prrr' },
{ id: 2, petType: 'dog', bark: 'Woof' }
])
end

it 'succeeds' do
header 'Content-Type', 'application/json'
post '/pets-file', request_body

expect(last_response.status).to eq 200
end
end
end

context 'with error_response: false' do
let(:called) { [] }

Expand Down

0 comments on commit 44aea14

Please sign in to comment.