Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/openapi_first/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,13 @@ def build_requests(path:, request_method:, operation_object:, parameters:)
configuration: schemer_configuration,
after_property_validation: config.hooks[:after_request_body_property_validation]
)
encoding = content_object['encoding']&.resolved || {}
Request.new(path:, request_method:, parameters:,
operation_object: operation_object.resolved,
content_type:,
content_schema:,
required_body:,
encoding:,
key: [path, request_method, content_type].join(':'))
end
end
Expand All @@ -156,6 +158,7 @@ def request_without_body(path:, request_method:, parameters:, operation_object:)
content_type: nil,
content_schema: nil,
required_body: false,
encoding: nil,
key: [path, request_method, nil].join(':'))
end

Expand Down
8 changes: 5 additions & 3 deletions lib/openapi_first/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,22 @@ module OpenapiFirst
class Request
# rubocop:disable Metrics/MethodLength
def initialize(path:, request_method:, operation_object:,
parameters:, content_type:, content_schema:, required_body:, key:)
parameters:, content_type:, content_schema:, required_body:, key:, encoding: nil)
@path = path
@request_method = request_method
@content_type = content_type
@content_schema = content_schema
@operation = operation_object
@allow_empty_content = content_type.nil? || required_body == false
@key = key
@encoding = encoding
@request_parser = RequestParser.new(
query_parameters: parameters.query,
path_parameters: parameters.path,
header_parameters: parameters.header,
cookie_parameters: parameters.cookie,
content_type:
content_type:,
encoding:
)
@validator = RequestValidator.new(
content_schema:,
Expand All @@ -38,7 +40,7 @@ def initialize(path:, request_method:, operation_object:,
end
# rubocop:enable Metrics/MethodLength

attr_reader :content_type, :content_schema, :operation, :request_method, :path, :key
attr_reader :content_type, :content_schema, :operation, :request_method, :path, :key, :encoding

def allow_empty_content?
@allow_empty_content
Expand Down
36 changes: 34 additions & 2 deletions lib/openapi_first/request_body_parsers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,40 @@ def self.read_body(request)
# The uploaded file should not be read during request validation.
module MultipartBodyParser
def self.call(request)
request.POST.transform_values do |value|
unpack_value(value)
call_with_encoding(request, {})
end

def self.call_with_encoding(request, encoding)
request.POST.each_with_object({}) do |(key, value), result|
parsed_value = unpack_value(value)

# Apply encoding-specific parsing if specified
if encoding.key?(key)
part_encoding = encoding[key]
content_type = part_encoding['contentType']

if content_type && parsed_value.is_a?(String)
parsed_value = parse_content_by_type(parsed_value, content_type, key)
end
end

result[key] = parsed_value
end
end

def self.parse_content_by_type(content, content_type, field_name)
case content_type.downcase
when 'application/json'
begin
JSON.parse(content)
rescue JSON::ParserError => e
# Log the parsing failure for debugging, but return original content to maintain compatibility
warn "Warning: Failed to parse JSON for field '#{field_name}': #{e.message}. Returning original content."
content
end
else
# For other content types (like text/csv), return as-is for now
content
end
end

Expand Down
17 changes: 15 additions & 2 deletions lib/openapi_first/request_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ def initialize(
path_parameters:,
header_parameters:,
cookie_parameters:,
content_type:
content_type:,
encoding: nil
)
@query_parser = OpenapiParameters::Query.new(query_parameters) if query_parameters
@path_parser = OpenapiParameters::Path.new(path_parameters) if path_parameters
@headers_parser = OpenapiParameters::Header.new(header_parameters) if header_parameters
@cookies_parser = OpenapiParameters::Cookie.new(cookie_parameters) if cookie_parameters
@body_parsers = RequestBodyParsers[content_type] if content_type
@encoding = encoding
end

attr_reader :query, :path, :headers, :cookies
Expand All @@ -30,12 +32,23 @@ def parse(request, route_params:)
query: parse_query(request.env[Rack::QUERY_STRING]),
headers: @headers_parser&.unpack_env(request.env),
cookies: @cookies_parser&.unpack(request.env[Rack::HTTP_COOKIE]),
body: @body_parsers&.call(request)
body: parse_body(request)
)
end

private

def parse_body(request)
return unless @body_parsers

# Use encoding-aware parsing for multipart requests when encoding is specified
if @encoding && @body_parsers == RequestBodyParsers::MultipartBodyParser
@body_parsers.call_with_encoding(request, @encoding)
else
@body_parsers.call(request)
end
end

def parse_query(query_string)
@query_parser&.unpack(query_string)
rescue OpenapiParameters::InvalidParameterError
Expand Down
35 changes: 35 additions & 0 deletions spec/data/request-body-validation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,41 @@ paths:
responses:
"200":
description: ok
/multipart-with-encoding:
post:
requestBody:
description: Contains a file and data attributes with different encodings
required: true
content:
multipart/form-data:
schema:
type: object
required:
- data
- file
properties:
data:
type: object
required:
- name
- description
properties:
name:
type: string
description:
type: string
file:
type: string
format: binary
description: Sample file
encoding:
file:
contentType: text/csv
data:
contentType: application/json
responses:
"200":
description: ok
components:
schemas:
Pet:
Expand Down
3 changes: 3 additions & 0 deletions spec/data/test.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name,age,city
John,30,New York
Jane,25,Los Angeles
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,21 @@ def fixture_path(name)
expect(names).to eq(['Quentin'])
end

it 'succeeds with multipart form data with encoding specification' do
csv_file = Rack::Test::UploadedFile.new(fixture_path('test.csv'), 'text/csv')
json_data = { name: 'Test Product', description: 'A sample product' }

post '/multipart-with-encoding',
'file' => csv_file,
'data' => json_data.to_json

expect(last_response.status).to eq(200), last_response.body

parsed_body = last_request.env[OpenapiFirst::REQUEST].parsed_body
expect(parsed_body['file']).to eq File.read(fixture_path('test.csv'))
expect(parsed_body['data']).to eq(json_data.transform_keys(&:to_s))
end

it 'succeeds without optional file upload' do
header Rack::CONTENT_TYPE, 'multipart/form-data'
post '/multipart-with-file', 'petId' => '12'
Expand Down
Loading