Skip to content

Commit

Permalink
Restructure passing data from validation to error response
Browse files Browse the repository at this point in the history
  • Loading branch information
ahx committed Nov 9, 2023
1 parent a94cd2c commit d3745ed
Show file tree
Hide file tree
Showing 13 changed files with 119 additions and 98 deletions.
36 changes: 24 additions & 12 deletions lib/openapi_first/error_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,32 @@
module OpenapiFirst
# This is the base class for error responses
class ErrorResponse
## @param status [Integer] The intended HTTP status code.
## @param message [String] A descriptive error message.
## @param location [Symbol] The location of the error (:body, :query, :header, :cookie, :path).
## @param validation_result [ValidationResult]
def initialize(status:, location:, message:, validation_result:)
@status = status
@message = message
@location = location
@validation_output = validation_result&.output
@schema = validation_result&.schema
@data = validation_result&.data
## @param request_validation_error [OpenapiFirst::RequestValidationError]
def initialize(request_validation_error)
@request_validation_error = request_validation_error
end

attr_reader :status, :location, :message, :schema, :data, :validation_output
extend Forwardable

attr_reader :request_validation_error

def_delegators :@request_validation_error, :status, :location, :schema_validation

def validation_output
schema_validation&.output
end

def schema
schema_validation&.schema
end

def data
schema_validation&.data
end

def message
request_validation_error.message
end

def render
Rack::Response.new(body, status, Rack::CONTENT_TYPE => content_type).finish
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
# frozen_string_literal: true

require 'json_schemer'
require_relative 'validation_result'
require_relative 'json_schema/result'

module OpenapiFirst
class SchemaValidation
attr_reader :raw_schema
class JsonSchema
attr_reader :schema

SCHEMAS = {
'3.1' => 'https://spec.openapis.org/oas/3.1/dialect/base',
'3.0' => 'json-schemer://openapi30/schema'
}.freeze

def initialize(schema, openapi_version:, write: true)
@raw_schema = schema
@schema = schema
@schemer = JSONSchemer.schema(
schema,
access_mode: write ? 'write' : 'read',
Expand All @@ -25,9 +25,9 @@ def initialize(schema, openapi_version:, write: true)
end

def validate(data)
ValidationResult.new(
Result.new(
output: @schemer.validate(data),
schema: raw_schema,
schema:,
data:
)
end
Expand Down
17 changes: 17 additions & 0 deletions lib/openapi_first/json_schema/result.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module OpenapiFirst
class JsonSchema
Result = Struct.new(:output, :schema, :data, keyword_init: true) do
def valid? = output['valid']
def error? = !output['valid']

# Returns a message that is used in exception messages.
def message
return if valid?

(output['errors']&.map { |e| e['error'] }&.join('. ') || output['error'])
end
end
end
end
8 changes: 4 additions & 4 deletions lib/openapi_first/operation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

require 'forwardable'
require 'set'
require_relative 'schema_validation'
require_relative 'json_schema'

module OpenapiFirst
class Operation # rubocop:disable Metrics/ClassLength
Expand Down Expand Up @@ -54,15 +54,15 @@ def response_body_schema(status, content_type)
schema = media_type['schema']
return unless schema

SchemaValidation.new(schema, write: false, openapi_version:)
JsonSchema.new(schema, write: false, openapi_version:)
end

def request_body_schema(request_content_type)
(@request_body_schema ||= {})[request_content_type] ||= begin
content = operation_object.dig('requestBody', 'content')
media_type = find_content_for_content_type(content, request_content_type)
schema = media_type&.fetch('schema', nil)
SchemaValidation.new(schema, write: write?, openapi_version:) if schema
JsonSchema.new(schema, write: write?, openapi_version:) if schema
end
end

Expand Down Expand Up @@ -146,7 +146,7 @@ def build_json_schema(parameter_defs)
result['properties'][parameter.name] = parameter.schema if parameter.schema
result['required'] << parameter.name if parameter.required?
end
SchemaValidation.new(schema, openapi_version:)
JsonSchema.new(schema, openapi_version:)
end

def response_by_code(status)
Expand Down
8 changes: 4 additions & 4 deletions lib/openapi_first/request_body_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def validate!
private

def validate_request_content_type!(operation, content_type)
operation.valid_request_content_type?(content_type) || RequestValidation.fail!(415)
operation.valid_request_content_type?(content_type) || RequestValidation.fail!(415, :header)
end

def validate_request_body!(operation, body, content_type)
Expand All @@ -27,15 +27,15 @@ def validate_request_body!(operation, body, content_type)
schema = operation&.request_body_schema(content_type)
return unless schema

validation_result = schema.validate(body)
RequestValidation.fail!(400, :body, validation_result:) if validation_result.error?
schema_validation = schema.validate(body)
RequestValidation.fail!(400, :body, schema_validation:) if schema_validation.error?
body
end

def validate_request_body_presence!(body, operation)
return unless operation.request_body['required'] && body.nil?

RequestValidation.fail!(400, :body, message: 'Request body is required')
RequestValidation.fail!(400, :body)
end
end
end
45 changes: 24 additions & 21 deletions lib/openapi_first/request_validation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
require_relative 'error_response'
require_relative 'request_body_validator'
require_relative 'string_keyed_hash'
require_relative 'request_validation_error_message'
require_relative 'request_validation_error'
require 'openapi_parameters'

module OpenapiFirst
Expand All @@ -16,16 +16,15 @@ class RequestValidation
FAIL = :request_validation_failed
private_constant :FAIL

# @param location [Symbol] one of :body, :header, :cookie, :query, :path
def self.fail!(status = 400, location = nil, message: nil, validation_result: nil)
throw FAIL, {
# @param status [Integer] The intended HTTP status code (usually 400)
# @param location [Symbol] One of :body, :header, :cookie, :query, :path
# @param schema_validation [OpenapiFirst::JsonSchema::Result]
def self.fail!(status, location, schema_validation: nil)
throw FAIL, RequestValidationError.new(
status:,
location:,
message: RequestValidationErrorMessage.build(
message || validation_result&.message || Rack::Utils::HTTP_STATUS_CODES[status], location
),
validation_result:
}
schema_validation:
)
end

def initialize(app, options = {})
Expand All @@ -41,7 +40,7 @@ def call(env)

error = validate_request(operation, env)
if error
raise RequestInvalidError, error[:message] if @raise
raise RequestInvalidError, error.error_message if @raise

return error_response(error).render
end
Expand All @@ -50,8 +49,8 @@ def call(env)

private

def error_response(error_object)
@error_response_class.new(**error_object)
def error_response(validation_error)
@error_response_class.new(validation_error)
end

def validate_request(operation, env)
Expand All @@ -61,7 +60,7 @@ def validate_request(operation, env)
validate_path_params!(operation, env)
validate_cookie_params!(operation, env)
validate_header_params!(operation, env)
RequestBodyValidator.new(operation, env).validate! if operation.request_body
validate_request_body!(operation, env)
nil
end
end
Expand All @@ -72,8 +71,8 @@ def validate_path_params!(operation, env)

hashy = StringKeyedHash.new(env[Router::RAW_PATH_PARAMS])
unpacked_path_params = OpenapiParameters::Path.new(path_parameters).unpack(hashy)
validation_result = operation.path_parameters_schema.validate(unpacked_path_params)
RequestValidation.fail!(400, :path, validation_result:) if validation_result.error?
schema_validation = operation.path_parameters_schema.validate(unpacked_path_params)
RequestValidation.fail!(400, :path, schema_validation:) if schema_validation.error?
env[PATH_PARAMS] = unpacked_path_params
env[PARAMS].merge!(unpacked_path_params)
end
Expand All @@ -83,8 +82,8 @@ def validate_query_params!(operation, env)
return if operation.query_parameters.empty?

unpacked_query_params = OpenapiParameters::Query.new(query_parameters).unpack(env['QUERY_STRING'])
validation_result = operation.query_parameters_schema.validate(unpacked_query_params)
RequestValidation.fail!(400, :query, validation_result:) if validation_result.error?
schema_validation = operation.query_parameters_schema.validate(unpacked_query_params)
RequestValidation.fail!(400, :query, schema_validation:) if schema_validation.error?
env[QUERY_PARAMS] = unpacked_query_params
env[PARAMS].merge!(unpacked_query_params)
end
Expand All @@ -94,8 +93,8 @@ def validate_cookie_params!(operation, env)
return unless cookie_parameters&.any?

unpacked_params = OpenapiParameters::Cookie.new(cookie_parameters).unpack(env['HTTP_COOKIE'])
validation_result = operation.cookie_parameters_schema.validate(unpacked_params)
RequestValidation.fail!(400, :cookie, validation_result:) if validation_result.error?
schema_validation = operation.cookie_parameters_schema.validate(unpacked_params)
RequestValidation.fail!(400, :cookie, schema_validation:) if schema_validation.error?
env[COOKIE_PARAMS] = unpacked_params
end

Expand All @@ -104,9 +103,13 @@ def validate_header_params!(operation, env)
return if header_parameters.empty?

unpacked_header_params = OpenapiParameters::Header.new(header_parameters).unpack_env(env)
validation_result = operation.header_parameters_schema.validate(unpacked_header_params)
RequestValidation.fail!(400, :header, validation_result:) if validation_result.error?
schema_validation = operation.header_parameters_schema.validate(unpacked_header_params)
RequestValidation.fail!(400, :header, schema_validation:) if schema_validation.error?
env[HEADER_PARAMS] = unpacked_header_params
end

def validate_request_body!(operation, env)
RequestBodyValidator.new(operation, env).validate! if operation.request_body
end
end
end
23 changes: 23 additions & 0 deletions lib/openapi_first/request_validation_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

module OpenapiFirst
RequestValidationError = Struct.new(:status, :location, :schema_validation, keyword_init: true)

class RequestValidationError
TOPICS = {
body: 'Request body invalid:',
query: 'Query parameter invalid:',
header: 'Header parameter invalid:',
path: 'Path segment invalid:',
cookie: 'Cookie value invalid:'
}.freeze

def message
schema_validation&.message || Rack::Utils::HTTP_STATUS_CODES[status]
end

def error_message
"#{TOPICS.fetch(location)} #{message}"
end
end
end
19 changes: 0 additions & 19 deletions lib/openapi_first/request_validation_error_message.rb

This file was deleted.

6 changes: 3 additions & 3 deletions lib/openapi_first/response_validation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ def validate_response_header(name, definition, unpacked_headers, openapi_version

return unless definition.key?('schema')

validation = SchemaValidation.new(definition['schema'], openapi_version:)
validation = JsonSchema.new(definition['schema'], openapi_version:)
value = unpacked_headers[name]
validation_result = validation.validate(value)
raise ResponseHeaderInvalidError, validation_result.message if validation_result.error?
schema_validation = validation.validate(value)
raise ResponseHeaderInvalidError, schema_validation.message if schema_validation.error?
end

def unpack_response_headers(response_header_definitions, response_headers)
Expand Down
15 changes: 0 additions & 15 deletions lib/openapi_first/validation_result.rb

This file was deleted.

4 changes: 2 additions & 2 deletions spec/schema_validation_spec.rb → spec/json_schema_spec.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# frozen_string_literal: true

require 'spec_helper'
require 'openapi_first/schema_validation'
require 'openapi_first/json_schema'

RSpec.describe OpenapiFirst::SchemaValidation do
RSpec.describe OpenapiFirst::JsonSchema do
let(:schema) do
{
'required' => ['count'],
Expand Down
Loading

0 comments on commit d3745ed

Please sign in to comment.