Skip to content

Commit a12ba5a

Browse files
krynjutanmaykm
andauthored
Add support for multipart/form-data POST data parsing (#41)
* Add support for multipart/form-data POST data parsing * client and server handling of multipart/form-data - client and server handling of multipart/form-data - update test to add petstore upload_file - add tests for forms * also test required form param condition Updated test code to test required form params. Test code re-generated with fixed codegen. Bumped version in prep for tagging --------- Co-authored-by: tan <[email protected]>
1 parent ab990fc commit a12ba5a

File tree

35 files changed

+1222
-36
lines changed

35 files changed

+1222
-36
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ keywords = ["Swagger", "OpenAPI", "REST"]
44
license = "MIT"
55
desc = "OpenAPI server and client helper for Julia"
66
authors = ["JuliaHub Inc."]
7-
version = "0.1.12"
7+
version = "0.1.13"
88

99
[deps]
1010
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"

src/client.jl

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ using MbedTLS
77
using Dates
88
using TimeZones
99
using LibCURL
10+
using HTTP
1011

1112
import Base: convert, show, summary, getproperty, setproperty!, iterate
1213
import ..OpenAPI: APIModel, UnionAPIModel, OneOfAPIModel, AnyOfAPIModel, APIClientImpl, OpenAPIException, InvocationException, to_json, from_json, validate_property, property_type
@@ -227,12 +228,40 @@ function prep_args(ctx::Ctx)
227228
isempty(ctx.file) && (ctx.body === nothing) && isempty(ctx.form) && !("Content-Length" in keys(ctx.header)) && (ctx.header["Content-Length"] = "0")
228229
headers = ctx.header
229230
body = nothing
231+
232+
header_pairs = [convert(HTTP.Header, p) for p in headers]
233+
content_type_set = HTTP.header(header_pairs, "Content-Type", nothing)
234+
if !isnothing(content_type_set)
235+
content_type_set = lowercase(content_type_set)
236+
end
237+
230238
if !isempty(ctx.form)
231-
headers["Content-Type"] = "application/x-www-form-urlencoded"
232-
body = URIs.escapeuri(ctx.form)
239+
if !isnothing(content_type_set) && content_type_set !== "multipart/form-data" && content_type_set !== "application/x-www-form-urlencoded"
240+
throw(OpenAPIException("Content type already set to $content_type_set. To send form data, it must be multipart/form-data or application/x-www-form-urlencoded."))
241+
end
242+
if isnothing(content_type_set)
243+
if !isempty(ctx.file)
244+
headers["Content-Type"] = content_type_set = "multipart/form-data"
245+
else
246+
headers["Content-Type"] = content_type_set = "application/x-www-form-urlencoded"
247+
end
248+
end
249+
if content_type_set == "application/x-www-form-urlencoded"
250+
body = URIs.escapeuri(ctx.form)
251+
else
252+
# we shall process it along with file uploads where we send multipart/form-data
253+
end
233254
end
234255

235-
if !isempty(ctx.file)
256+
if !isempty(ctx.file) || (content_type_set == "multipart/form-data")
257+
if !isnothing(content_type_set) && content_type_set !== "multipart/form-data"
258+
throw(OpenAPIException("Content type already set to $content_type_set. To send file, it must be multipart/form-data."))
259+
end
260+
261+
if isnothing(content_type_set)
262+
headers["Content-Type"] = content_type_set = "multipart/form-data"
263+
end
264+
236265
# use a separate downloader for file uploads
237266
# until we have something like https://github.com/JuliaLang/Downloads.jl/pull/148
238267
downloader = Downloads.Downloader()
@@ -249,19 +278,25 @@ function prep_args(ctx::Ctx)
249278
LibCURL.curl_mime_filedata(part, _v)
250279
# TODO: make provision to call curl_mime_type in future?
251280
end
281+
for (_k,_v) in ctx.form
282+
# add multipart sections for form data as well
283+
part = LibCURL.curl_mime_addpart(mime)
284+
LibCURL.curl_mime_name(part, _k)
285+
LibCURL.curl_mime_data(part, _v, length(_v))
286+
end
252287
Downloads.Curl.setopt(easy, LibCURL.CURLOPT_MIMEPOST, mime)
253288
end
254289
kwargs[:downloader] = downloader
255290
end
256291

257292
if ctx.body !== nothing
258293
(isempty(ctx.form) && isempty(ctx.file)) || throw(OpenAPIException("Can not send both form-encoded data and a request body"))
259-
if is_json_mime(get(ctx.header, "Content-Type", "application/json"))
294+
if is_json_mime(something(content_type_set, "application/json"))
260295
body = to_json(ctx.body)
261-
elseif ("application/x-www-form-urlencoded" == ctx.header["Content-Type"]) && isa(ctx.body, Dict)
296+
elseif ("application/x-www-form-urlencoded" == content_type_set) && isa(ctx.body, Dict)
262297
body = URIs.escapeuri(ctx.body)
263-
elseif isa(ctx.body, APIModel) && isempty(get(ctx.header, "Content-Type", ""))
264-
headers["Content-Type"] = "application/json"
298+
elseif isa(ctx.body, APIModel) && isnothing(content_type_set)
299+
headers["Content-Type"] = content_type_set = "application/json"
265300
body = to_json(ctx.body)
266301
else
267302
body = ctx.body

src/server.jl

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,18 @@ function get_param(source::Dict, name::String, required::Bool)
3737
return val
3838
end
3939

40+
function get_param(source::Vector{HTTP.Forms.Multipart}, name::String, required::Bool)
41+
ind = findfirst(x -> x.name == name, source)
42+
if required && isnothing(ind)
43+
throw(ValidationException("required parameter \"$name\" missing"))
44+
elseif isnothing(ind)
45+
return nothing
46+
else
47+
return source[ind]
48+
end
49+
end
50+
51+
4052
function to_param_type(::Type{T}, strval::String) where {T <: Number}
4153
parse(T, strval)
4254
end
@@ -45,6 +57,7 @@ to_param_type(::Type{T}, val::T) where {T} = val
4557
to_param_type(::Type{T}, ::Nothing) where {T} = nothing
4658
to_param_type(::Type{String}, val::Vector{UInt8}) = String(copy(val))
4759
to_param_type(::Type{Vector{UInt8}}, val::String) = convert(Vector{UInt8}, copy(codeunits(val)))
60+
to_param_type(::Type{Vector{T}}, val::Vector{T}, _collection_format::Union{String,Nothing}) where {T} = val
4861

4962
function to_param_type(::Type{T}, strval::String) where {T <: APIModel}
5063
from_json(T, JSON.parse(strval))
@@ -80,9 +93,25 @@ function to_param(T, source::Dict, name::String; required::Bool=false, collectio
8093
end
8194
end
8295

96+
function to_param(T, source::Vector{HTTP.Forms.Multipart}, name::String; required::Bool=false, collection_format::Union{String,Nothing}=",", multipart::Bool=false, isfile::Bool=false)
97+
param = get_param(source, name, required)
98+
if param === nothing
99+
return nothing
100+
end
101+
if multipart
102+
# param is a Multipart
103+
param = isfile ? take!(param.data) : String(take!(param.data))
104+
end
105+
if T <: Vector
106+
return to_param_type(T, param, collection_format)
107+
else
108+
return to_param_type(T, param)
109+
end
110+
end
111+
83112
server_response(resp::HTTP.Response) = resp
84113
server_response(::Nothing) = server_response("")
85114
server_response(ret) = server_response(to_json(ret))
86115
server_response(resp::String) = HTTP.Response(200, resp)
87116

88-
end # module Servers
117+
end # module Servers

test/client/petstore_v2/petstore/src/apis/api_PetApi.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ function _oacinternal_upload_file(_api::PetApi, pet_id::Int64; additional_metada
239239
_ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_upload_file_PetApi, "/pet/{petId}/uploadImage", ["petstore_auth", ])
240240
OpenAPI.Clients.set_param(_ctx.path, "petId", pet_id) # type Int64
241241
OpenAPI.Clients.set_param(_ctx.form, "additionalMetadata", additional_metadata) # type String
242-
OpenAPI.Clients.set_param(_ctx.file, "file", file) # type String
242+
OpenAPI.Clients.set_param(_ctx.file, "file", file) # type Vector{UInt8}
243243
OpenAPI.Clients.set_header_accept(_ctx, ["application/json", ])
244244
OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["multipart/form-data", ] : [_mediaType])
245245
return _ctx

test/client/petstore_v3/petstore/src/apis/api_PetApi.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ function _oacinternal_upload_file(_api::PetApi, pet_id::Int64; additional_metada
238238
_ctx = OpenAPI.Clients.Ctx(_api.client, "POST", _returntypes_upload_file_PetApi, "/pet/{petId}/uploadImage", ["petstore_auth", ])
239239
OpenAPI.Clients.set_param(_ctx.path, "petId", pet_id) # type Int64
240240
OpenAPI.Clients.set_param(_ctx.form, "additionalMetadata", additional_metadata) # type String
241-
OpenAPI.Clients.set_param(_ctx.file, "file", file) # type String
241+
OpenAPI.Clients.set_param(_ctx.file, "file", file) # type Vector{UInt8}
242242
OpenAPI.Clients.set_header_accept(_ctx, ["application/json", ])
243243
OpenAPI.Clients.set_header_content_type(_ctx, (_mediaType === nothing) ? ["multipart/form-data", ] : [_mediaType])
244244
return _ctx

test/client/petstore_v3/petstore_test_petapi.jl

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ using OpenAPI
66
using OpenAPI.Clients
77
import OpenAPI.Clients: Client
88

9-
function test(uri)
9+
function test(uri; test_file_upload=false)
1010
@info("PetApi")
1111
client = Client(uri)
1212
api = PetApi(client)
@@ -53,6 +53,16 @@ function test(uri)
5353
@test api_return === nothing
5454
@test http_resp.status == 200
5555

56+
if test_file_upload
57+
@info("PetApi - upload_file")
58+
api_return, http_resp = upload_file(api, 1; additional_metadata="my metadata", file=@__FILE__)
59+
@test isa(api_return, ApiResponse)
60+
@test api_return.code == 1
61+
@test api_return.type == "pet"
62+
@test api_return.message == "file uploaded"
63+
@test http_resp.status == 200
64+
end
65+
5666
# does not work yet. issue: https://github.com/JuliaWeb/Requests.jl/issues/139
5767
#@info("PetApi - upload_file")
5868
#img = joinpath(dirname(@__FILE__), "cat.png")

test/client/petstore_v3/runtests.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,19 @@ function test_stress()
2020
TestUserApi.test_parallel(server)
2121
end
2222

23-
function petstore_tests()
23+
function petstore_tests(; test_file_upload=false)
2424
TestUserApi.test(server)
2525
TestStoreApi.test(server)
26-
TestPetApi.test(server)
26+
TestPetApi.test(server; test_file_upload=test_file_upload)
2727
end
2828

29-
function runtests()
29+
function runtests(; test_file_upload=false)
3030
@testset "petstore v3" begin
3131
@testset "miscellaneous" begin
3232
test_misc()
3333
end
3434
@testset "petstore apis" begin
35-
petstore_tests()
35+
petstore_tests(; test_file_upload=test_file_upload)
3636
end
3737
if get(ENV, "STRESS_PETSTORE", "false") == "true"
3838
@testset "stress" begin

test/client/runtests.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ include("utilstests.jl")
88
include("petstore_v3/runtests.jl")
99
include("petstore_v2/runtests.jl")
1010

11-
function runtests(; skip_petstore=false)
11+
function runtests(; skip_petstore=false, test_file_upload=false)
1212
@testset "Client" begin
1313
@testset "Utils" begin
1414
test_longpoll_exception_check()
@@ -25,7 +25,7 @@ function runtests(; skip_petstore=false)
2525
if get(ENV, "RUNNER_OS", "") == "Linux"
2626
@testset "V3" begin
2727
@info("Running petstore v3 tests")
28-
PetStoreV3Tests.runtests()
28+
PetStoreV3Tests.runtests(; test_file_upload=test_file_upload)
2929
end
3030
@testset "V2" begin
3131
@info("Running petstore v2 tests")
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# OpenAPI Generator Ignore
2+
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
3+
4+
# Use this file to prevent files from being overwritten by the generator.
5+
# The patterns follow closely to .gitignore or .dockerignore.
6+
7+
# As an example, the C# client generator defines ApiClient.cs.
8+
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
9+
#ApiClient.cs
10+
11+
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
12+
#foo/*/qux
13+
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
14+
15+
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
16+
#foo/**/qux
17+
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
18+
19+
# You can also negate patterns with an exclamation (!).
20+
# For example, you can ignore all files in a docs folder with the file extension .md:
21+
#docs/*.md
22+
# Then explicitly reverse the ignore rule for a single file:
23+
#!docs/README.md
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
README.md
2+
docs/DefaultApi.md
3+
docs/TestResponse.md
4+
src/FormsClient.jl
5+
src/apis/api_DefaultApi.jl
6+
src/modelincludes.jl
7+
src/models/model_TestResponse.jl

0 commit comments

Comments
 (0)