diff --git a/Project.toml b/Project.toml index 6b41141..c753c79 100644 --- a/Project.toml +++ b/Project.toml @@ -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" diff --git a/src/client.jl b/src/client.jl index 906192b..20b88c2 100644 --- a/src/client.jl +++ b/src/client.jl @@ -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 @@ -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) diff --git a/test/param_deserialize.jl b/test/param_deserialize.jl index 2a9b7a6..d30ba3e 100644 --- a/test/param_deserialize.jl +++ b/test/param_deserialize.jl @@ -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