Skip to content

Commit

Permalink
Remove Schema class. Moving more OAD parsing into "Builder".
Browse files Browse the repository at this point in the history
  • Loading branch information
ahx committed Jan 11, 2025
1 parent abbbe3b commit eb82481
Show file tree
Hide file tree
Showing 15 changed files with 148 additions and 338 deletions.
1 change: 0 additions & 1 deletion lib/openapi_first.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
require_relative 'openapi_first/configuration'
require_relative 'openapi_first/definition'
require_relative 'openapi_first/version'
require_relative 'openapi_first/schema'
require_relative 'openapi_first/middlewares/response_validation'
require_relative 'openapi_first/middlewares/request_validation'

Expand Down
99 changes: 89 additions & 10 deletions lib/openapi_first/builder.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# frozen_string_literal: true

require 'json_schemer'
require_relative 'json_pointer'
require_relative 'ref_resolver'

module OpenapiFirst
# Builds parts of a Definition
# This knows how to read a resolved OpenAPI document and build {Request} and {Response} objects.
class Builder
class Builder # rubocop:disable Metrics/ClassLength
REQUEST_METHODS = %w[get head post put patch delete trace options].freeze

# Builds a router from a resolved OpenAPI document.
Expand All @@ -22,12 +23,11 @@ def initialize(contents, filepath:, config:)
insert_property_defaults: true
)
@config = config
@openapi_version = (contents['openapi'] || contents['swagger'])[0..2]
@contents = RefResolver.for(contents, dir: filepath && File.dirname(filepath))
end

attr_reader :openapi_version, :config
private attr_reader :schemer_configuration, :schemer_
attr_reader :config
private attr_reader :schemer_configuration

def detect_meta_schema(document, filepath)
# Copied from JSONSchemer 🙇🏻‍♂️
Expand All @@ -50,6 +50,8 @@ def router # rubocop:disable Metrics/MethodLength
path_item_object.resolved.keys.intersection(REQUEST_METHODS).map do |request_method|
operation_object = path_item_object[request_method]
parameters = operation_object['parameters']&.resolved.to_a.chain(path_item_object['parameters']&.resolved.to_a)
parameters = parse_parameters(parameters)

build_requests(path:, request_method:, operation_object:,
parameters:).each do |request|
router.add_request(
Expand All @@ -73,6 +75,28 @@ def router # rubocop:disable Metrics/MethodLength
router
end

def parse_parameters(parameters)
grouped_parameters = group_parameters(parameters)
ParsedParameters.new(
query: grouped_parameters[:query],
path: grouped_parameters[:path],
cookie: grouped_parameters[:cookie],
header: grouped_parameters[:header],
query_schema: build_parameter_schema(grouped_parameters[:query]),
path_schema: build_parameter_schema(grouped_parameters[:path]),
cookie_schema: build_parameter_schema(grouped_parameters[:cookie]),
header_schema: build_parameter_schema(grouped_parameters[:header])
)
end

def build_parameter_schema(parameters)
schema = build_parameters_schema(parameters)

JSONSchemer.schema(schema,
configuration: schemer_configuration,
after_property_validation: config.hooks[:after_request_parameter_property_validation])
end

def build_requests(path:, request_method:, operation_object:, parameters:)
required_body = operation_object['requestBody']&.resolved&.fetch('required', false) == true
result = operation_object.dig('requestBody', 'content')&.map do |content_type, content_object|
Expand All @@ -84,14 +108,14 @@ def build_requests(path:, request_method:, operation_object:, parameters:)
operation_object: operation_object.resolved,
parameters:, content_type:,
content_schema:,
required_body:, hooks: config.hooks, openapi_version:)
required_body:)
end || []
return result if required_body

result << Request.new(
path:, request_method:, operation_object: operation_object.resolved,
parameters:, content_type: nil, content_schema: nil,
required_body:, hooks: config.hooks, openapi_version:
required_body:
)
end

Expand All @@ -100,16 +124,71 @@ def build_responses(responses:)

responses.flat_map do |status, response_object|
headers = response_object['headers']&.resolved
headers_schema = JSONSchemer::Schema.new(
build_headers_schema(headers),
configuration: schemer_configuration
)
response_object['content']&.map do |content_type, content_object|
content_schema = content_object['schema'].schema(configuration: schemer_configuration)
Response.new(status:,
headers:,
headers_schema:,
content_type:,
content_schema:,
openapi_version:)
end || Response.new(status:, headers:, content_type: nil,
content_schema: nil, openapi_version:)
content_schema:)
end || Response.new(status:, headers:, headers_schema:, content_type: nil,
content_schema: nil)
end
end

IGNORED_HEADER_PARAMETERS = Set['Content-Type', 'Accept', 'Authorization'].freeze
private_constant :IGNORED_HEADER_PARAMETERS

def group_parameters(parameter_definitions)
result = {}
parameter_definitions&.each do |parameter|
(result[parameter['in'].to_sym] ||= []) << parameter
end
result[:header]&.reject! { IGNORED_HEADER_PARAMETERS.include?(_1['name']) }
result
end

def build_headers_schema(headers_object)
return unless headers_object&.any?

properties = {}
required = []
headers_object.each do |name, header|
schema = header['schema']
next if name.casecmp('content-type').zero?

properties[name] = schema if schema
required << name if header['required']
end
{
'properties' => properties,
'required' => required
}
end

def build_parameters_schema(parameters)
return unless parameters

properties = {}
required = []
parameters.each do |parameter|
schema = parameter['schema']
name = parameter['name']
properties[name] = schema if schema
required << name if parameter['required']
end

{
'properties' => properties,
'required' => required
}
end

ParsedParameters = Data.define(:path, :query, :header, :cookie, :path_schema, :query_schema, :header_schema,
:cookie_schema)
end
end
65 changes: 13 additions & 52 deletions lib/openapi_first/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,27 @@ module OpenapiFirst
# This class represents one of those requests.
class Request
def initialize(path:, request_method:, operation_object:,
parameters:, content_type:, content_schema:, required_body:,
hooks:, openapi_version:)
parameters:, content_type:, content_schema:, required_body:)
@path = path
@request_method = request_method
@content_type = content_type
@content_schema = content_schema
@required_request_body = required_body == true
@operation = operation_object
@parameters = build_parameters(parameters)
@request_parser = RequestParser.new(
query_parameters: @parameters[:query],
path_parameters: @parameters[:path],
header_parameters: @parameters[:header],
cookie_parameters: @parameters[:cookie],
query_parameters: parameters.query,
path_parameters: parameters.path,
header_parameters: parameters.header,
cookie_parameters: parameters.cookie,
content_type:
)
@validator = RequestValidator.new(self, hooks:, openapi_version:)
@validator = RequestValidator.new(
content_schema:,
required_request_body: required_body == true,
path_schema: parameters.path_schema,
query_schema: parameters.query_schema,
header_schema: parameters.header_schema,
cookie_schema: parameters.cookie_schema
)
end

attr_reader :content_type, :content_schema, :operation, :request_method, :path
Expand All @@ -42,51 +46,8 @@ def validate(request, route_params:)
ValidatedRequest.new(request, parsed_request:, error:, request_definition: self)
end

# These return a Schema instance for each type of parameters
%i[path query header cookie].each do |location|
define_method(:"#{location}_schema") do
build_parameters_schema(@parameters[location])
end
end

def required_request_body?
@required_request_body
end

def operation_id
@operation['operationId']
end

private

IGNORED_HEADERS = Set['Content-Type', 'Accept', 'Authorization'].freeze
private_constant :IGNORED_HEADERS

def build_parameters(parameter_definitions)
result = {}
parameter_definitions&.each do |parameter|
(result[parameter['in'].to_sym] ||= []) << parameter
end
result[:header]&.reject! { IGNORED_HEADERS.include?(_1['name']) }
result
end

def build_parameters_schema(parameters)
return unless parameters

properties = {}
required = []
parameters.each do |parameter|
schema = parameter['schema']
name = parameter['name']
properties[name] = schema if schema
required << name if parameter['required']
end

{
'properties' => properties,
'required' => required
}
end
end
end
18 changes: 15 additions & 3 deletions lib/openapi_first/request_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,22 @@
module OpenapiFirst
# Validates a Request against a request definition.
class RequestValidator
def initialize(request_definition, openapi_version:, hooks: {})
def initialize(
content_schema:,
required_request_body:,
path_schema:,
query_schema:,
header_schema:,
cookie_schema:
)
@validators = []
@validators << Validators::RequestBody.new(request_definition) if request_definition.content_schema
@validators.concat Validators::RequestParameters.for(request_definition, openapi_version:, hooks:)
@validators << Validators::RequestBody.new(content_schema:, required_request_body:) if content_schema
@validators.concat Validators::RequestParameters.for(
path_schema:,
query_schema:,
header_schema:,
cookie_schema:
)
end

def call(parsed_request)
Expand Down
24 changes: 3 additions & 21 deletions lib/openapi_first/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ module OpenapiFirst
# This is not a direct reflecton of the OpenAPI 3.X response definition, but a combination of
# status, content type and content schema.
class Response
def initialize(status:, headers:, content_type:, content_schema:, openapi_version:)
def initialize(status:, headers:, headers_schema:, content_type:, content_schema:)
@status = status
@content_type = content_type
@content_schema = content_schema
@headers = headers
@headers_schema = build_headers_schema(headers)
@headers_schema = headers_schema
@parser = ResponseParser.new(headers:, content_type:)
@validator = ResponseValidator.new(self, openapi_version:)
@validator = ResponseValidator.new(self)
end

# @attr_reader [Integer] status The HTTP status code of the response definition.
Expand All @@ -35,23 +35,5 @@ def validate(response)
def parse(request)
@parser.parse(request)
end

def build_headers_schema(headers_object)
return unless headers_object&.any?

properties = {}
required = []
headers_object.each do |name, header|
schema = header['schema']
next if name.casecmp('content-type').zero?

properties[name] = schema if schema
required << name if header['required']
end
{
'properties' => properties,
'required' => required
}
end
end
end
4 changes: 2 additions & 2 deletions lib/openapi_first/response_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ class ResponseValidator
Validators::ResponseBody
].freeze

def initialize(response_definition, openapi_version:)
def initialize(response_definition)
@validators = VALIDATORS.filter_map do |klass|
klass.for(response_definition, openapi_version:)
klass.for(response_definition)
end
end

Expand Down
31 changes: 0 additions & 31 deletions lib/openapi_first/schema.rb

This file was deleted.

6 changes: 3 additions & 3 deletions lib/openapi_first/validators/request_body.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
module OpenapiFirst
module Validators
class RequestBody
def initialize(request_definition)
@schema = request_definition.content_schema
@required = request_definition.required_request_body?
def initialize(content_schema:, required_request_body:)
@schema = content_schema
@required = required_request_body
end

def call(parsed_request)
Expand Down
Loading

0 comments on commit eb82481

Please sign in to comment.