class OpenapiFirst::RequestValidation

Public Class Methods

new(app, raise_error: false) click to toggle source
# File lib/openapi_first/request_validation.rb, line 14
def initialize(app, raise_error: false)
  @app = app
  @raise = raise_error
end

Public Instance Methods

call(env) click to toggle source
# File lib/openapi_first/request_validation.rb, line 19
def call(env) # rubocop:disable Metrics/AbcSize
  operation = env[OpenapiFirst::OPERATION]
  return @app.call(env) unless operation

  env[INBOX] = Inbox.new(env)
  catch(:halt) do
    validate_query_parameters!(env, operation, env[PARAMETERS])
    req = Rack::Request.new(env)
    content_type = req.content_type
    return @app.call(env) unless operation.request_body

    validate_request_content_type!(content_type, operation)
    body = req.body.read
    req.body.rewind
    parse_and_validate_request_body!(env, content_type, body, operation)
    @app.call(env)
  end
end

Private Instance Methods

default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status]) click to toggle source
# File lib/openapi_first/request_validation.rb, line 77
def default_error(status, title = Rack::Utils::HTTP_STATUS_CODES[status])
  {
    status: status.to_s,
    title: title
  }
end
filtered_params(json_schema, params) click to toggle source
# File lib/openapi_first/request_validation.rb, line 115
def filtered_params(json_schema, params)
  json_schema['properties']
    .each_with_object({}) do |key_value, result|
      parameter_name = key_value[0].to_sym
      schema = key_value[1]
      next unless params.key?(parameter_name)

      value = params[parameter_name]
      result[parameter_name] = parse_parameter(value, schema)
    end
end
halt(response) click to toggle source
# File lib/openapi_first/request_validation.rb, line 40
def halt(response)
  throw :halt, response
end
halt_with_error(status, errors = [default_error(status)]) click to toggle source
# File lib/openapi_first/request_validation.rb, line 84
def halt_with_error(status, errors = [default_error(status)])
  raise RequestInvalidError, errors if @raise

  halt Rack::Response.new(
    MultiJson.dump(errors: errors),
    status,
    Rack::CONTENT_TYPE => 'application/vnd.api+json'
  ).finish
end
parse_and_validate_request_body!(env, content_type, body, operation) click to toggle source
# File lib/openapi_first/request_validation.rb, line 44
def parse_and_validate_request_body!(env, content_type, body, operation)
  validate_request_body_presence!(body, operation)
  return if body.empty?

  schema = operation&.request_body_schema(content_type)
  return unless schema

  parsed_request_body = parse_request_body!(body)
  errors = schema.validate(parsed_request_body)
  halt_with_error(400, serialize_request_body_errors(errors)) if errors.any?
  env[INBOX].merge! env[REQUEST_BODY] = Utils.deep_symbolize(parsed_request_body)
end
parse_array_parameter(value, schema) click to toggle source
# File lib/openapi_first/request_validation.rb, line 144
def parse_array_parameter(value, schema)
  array = value.is_a?(Array) ? value : value.split(',')
  return array unless schema['items']

  array.map! { |e| parse_simple_value(e, schema['items']) }
end
parse_parameter(value, schema) click to toggle source
# File lib/openapi_first/request_validation.rb, line 136
def parse_parameter(value, schema)
  return filtered_params(schema, value) if schema['properties']

  return parse_array_parameter(value, schema) if schema['type'] == 'array'

  parse_simple_value(value, schema)
end
parse_request_body!(body) click to toggle source
# File lib/openapi_first/request_validation.rb, line 57
def parse_request_body!(body)
  MultiJson.load(body)
rescue MultiJson::ParseError => e
  err = { title: 'Failed to parse body as JSON' }
  err[:detail] = e.cause unless ENV['RACK_ENV'] == 'production'
  halt_with_error(400, [err])
end
parse_simple_value(value, schema) click to toggle source
# File lib/openapi_first/request_validation.rb, line 151
def parse_simple_value(value, schema)
  return to_boolean(value) if schema['type'] == 'boolean'

  begin
    return Integer(value, 10) if schema['type'] == 'integer'
    return Float(value) if schema['type'] == 'number'
  rescue ArgumentError
    value
  end
  value
end
serialize_query_parameter_errors(validation_errors) click to toggle source
# File lib/openapi_first/request_validation.rb, line 127
def serialize_query_parameter_errors(validation_errors)
  validation_errors.map do |error|
    pointer = error['data_pointer'][1..].to_s
    {
      source: { parameter: pointer }
    }.update(ValidationFormat.error_details(error))
  end
end
serialize_request_body_errors(validation_errors) click to toggle source
# File lib/openapi_first/request_validation.rb, line 94
def serialize_request_body_errors(validation_errors)
  validation_errors.map do |error|
    {
      source: {
        pointer: error['data_pointer']
      }
    }.update(ValidationFormat.error_details(error))
  end
end
to_boolean(value) click to toggle source
# File lib/openapi_first/request_validation.rb, line 163
def to_boolean(value)
  return true if value == 'true'
  return false if value == 'false'

  value
end
validate_query_parameters!(env, operation, params) click to toggle source
# File lib/openapi_first/request_validation.rb, line 104
def validate_query_parameters!(env, operation, params)
  schema = operation.parameters_schema
  return unless schema

  params = filtered_params(schema.raw_schema, params)
  errors = schema.validate(Utils.deep_stringify(params))
  halt_with_error(400, serialize_query_parameter_errors(errors)) if errors.any?
  env[PARAMETERS] = params
  env[INBOX].merge! params
end
validate_request_body_presence!(body, operation) click to toggle source
# File lib/openapi_first/request_validation.rb, line 71
def validate_request_body_presence!(body, operation)
  return unless operation.request_body['required'] && body.empty?

  halt_with_error(415, 'Request body is required')
end
validate_request_content_type!(content_type, operation) click to toggle source
# File lib/openapi_first/request_validation.rb, line 65
def validate_request_content_type!(content_type, operation)
  return if operation.request_body.dig('content', content_type)

  halt_with_error(415)
end