Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ keywords = ["Swagger", "OpenAPI", "REST"]
license = "MIT"
desc = "OpenAPI server and client helper for Julia"
authors = ["JuliaHub Inc."]
version = "0.2.8"
version = "0.2.9"

[deps]
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
Expand Down
19 changes: 18 additions & 1 deletion src/client.jl
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,23 @@ function set_param(params::Dict{String,String}, name::String, value; collection_
end
end

# Escape one query parameter value for the URL. The OAS `form` style keeps the
# list separator comma literal (`?color=blue,black,brown` in the specification's
# Style Examples, per RFC6570 form-style expansion) while percent-encoding the
# items themselves. `set_param` joins collections before the item boundaries
# reach this point, so escaping each comma-separated segment individually
# preserves the separators. The spaceDelimited and pipeDelimited separators are
# percent-encoded to `%20`/`%7C`, which is what the specification requires for
# those styles. A comma inside a scalar value stays literal, which is valid in
# query strings (RFC3986 sub-delim) and percent-decodes identically.
escape_query_param_value(value::AbstractString) =
join((escapeuri(part) for part in split(value, ','; keepempty=true)), ",")

query_string(query::Dict{String,String}) = join(
(string(escapeuri(name), "=", escape_query_param_value(value)) for (name, value) in query),
"&",
)

prep_args(ctx::Ctx) = prep_args(Val(ctx.client.httplib), ctx)

response(::Type{Nothing}, resp::HTTPLibResponse, body) = nothing::Nothing
Expand Down Expand Up @@ -178,7 +195,7 @@ function do_request(ctx::Ctx, stream::Bool=false; stream_to::Union{Channel,Nothi
end
# append query params if needed
if !isempty(ctx.query)
resource_path = string(URIs.URI(URIs.URI(resource_path); query=escapeuri(ctx.query)))
resource_path = string(URIs.URI(URIs.URI(resource_path); query=query_string(ctx.query)))
end

body, kwargs = prep_args(ctx)
Expand Down
49 changes: 49 additions & 0 deletions test/param_deserialize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,52 @@ end
end

end

@testset "Query parameter serialization" begin
# The OAS `form` style (RFC6570) keeps the collection separator comma
# literal — `?color=blue,black,brown` in the specification's Style
# Examples — while percent-encoding the items. Previously the whole query
# value was escaped, turning separators into `%2C`, which spec-compliant
# servers read as a single item containing commas.
import OpenAPI.Clients: set_param, query_string

@testset "form-style csv keeps literal separators" begin
params = Dict{String,String}()
set_param(params, "status", ["pending", "sold"]; style="form", is_explode=false)
@test params["status"] == "pending,sold"
@test query_string(params) == "status=pending,sold"
end

@testset "items are percent-encoded individually" begin
params = Dict{String,String}()
set_param(params, "tags", ["a b", "c/d", "x&y"]; style="form", is_explode=false)
@test query_string(params) == "tags=a%20b,c%2Fd,x%26y"
end

@testset "space and pipe separators are percent-encoded" begin
# per the specification's Style Examples, spaceDelimited and
# pipeDelimited separators must be percent-encoded (%20 / %7C)
params = Dict{String,String}()
set_param(params, "s", ["blue", "black"]; collection_format="ssv")
set_param(params, "p", ["blue", "black"]; collection_format="pipes")
query = query_string(params)
@test occursin("s=blue%20black", query)
@test occursin("p=blue%7Cblack", query)
end

@testset "scalar values" begin
params = Dict{String,String}()
set_param(params, "q", "a b&c")
@test query_string(params) == "q=a%20b%26c"
# a comma in a scalar stays literal: valid per RFC3986 and
# percent-decodes identically on the server
params = Dict{String,String}()
set_param(params, "note", "a,b")
@test query_string(params) == "note=a,b"
end

@testset "keys are escaped" begin
params = Dict{String,String}("a key" => "v")
@test query_string(params) == "a%20key=v"
end
end
Loading