diff --git a/docs/src/index.md b/docs/src/index.md index d875f5b..2a5155e 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -44,6 +44,25 @@ URI("http://example.com/path?x=1&y=hi") julia> queryparams(u) == Dict("x" => "1", "y" => "hi") true + +julia> queryparampairs(URI("https://example.com/?x=1&x=2")) +2-element Vector{Pair{String, String}}: + "x" => "1" + "x" => "2" +``` + +A URI can be copied with selected components replaced. Use `nothing` to remove +a component: + +```jldoctest +julia> u = URI("https://example.com/page?x=1#section") +URI("https://example.com/page?x=1#section") + +julia> URI(u; fragment=nothing) +URI("https://example.com/page?x=1") + +julia> URI(u; query=[queryparampairs(u); "y" => "hello world"]) +URI("https://example.com/page?x=1&y=hello%20world#section") ``` ## Reference diff --git a/src/URIs.jl b/src/URIs.jl index 89aa6f6..00914da 100644 --- a/src/URIs.jl +++ b/src/URIs.jl @@ -27,18 +27,26 @@ end """ URI(; scheme="", host="", port="", etc...) URI(str) = parse(URI, str::String) - -A type representing a URI (e.g. a URL). Can be constructed from distinct parts using the various -supported keyword arguments, or from a string. The `URI` constructors will automatically escape any provided -`query` arguments, typically provided as `"key"=>"value"::Pair` or `Dict("key"=>"value")`. For all other components, you -need to manually percent encode them before passing them to the `URI` constructor. Note that multiple values for a -single query key can provided like `Dict("key"=>["value1", "value2"])`, in which case the constructor will -percent encode _only_ the values you pass in as the `query` part. + URI(uri::URI; component=value, ...) + +A type representing a URI (e.g. a URL). It can be constructed from a string or +from the component keyword arguments `scheme`, `userinfo`, `host`, `port`, +`path`, `query`, and `fragment`. Passing an existing `URI` as the first argument +copies it and replaces the components supplied as keywords. Set a component to +`nothing` to remove it. An empty string keeps the component present but empty, +including its delimiter where the URI syntax distinguishes those states (for +example, `query=""` adds `?`, while `query=nothing` removes it). + +The `URI` constructors automatically escape provided `query` collections, +typically given as `"key"=>"value"::Pair` or `Dict("key"=>"value")`. For all +other components, manually percent-encode them before passing them to the `URI` +constructor. Multiple values for one query key can be provided as +`Dict("key"=>["value1", "value2"])`; the constructor percent-encodes only the +values passed as the `query` part. When constructing a `URI` from a `String`, you need to ensure that the string is correctly percent encoded already. -The `URI` struct stores the complete URI in the `uri::String` field and the -component parts in the following `SubString` fields: +The `URI` struct stores the component parts in the following `SubString` fields: * `scheme`, e.g. `"http"` or `"https"` * `userinfo`, e.g. `"username:password"` * `host` e.g. `"julialang.org"` @@ -47,6 +55,10 @@ component parts in the following `SubString` fields: * `query` e.g. `"Foo=1&Bar=2"` * `fragment` +The `uri` field caches the original input string when one is available. It can +be empty for a URI built or updated from components. Use `string(uri)` to get +the complete URI string. + The `queryparams(::URI)` function returns a `Dict` containing the `query`. Note that you manually need to percent decode the content of the individual component fields before you further use @@ -72,24 +84,32 @@ end)() const nostring = "" -function URI(uri::URI; scheme::AbstractString=uri.scheme, - userinfo::AbstractString=uri.userinfo, - host::AbstractString=uri.host, - port::Union{Integer,AbstractString}=uri.port, - path::AbstractString=uri.path, +function URI(uri::URI; scheme::Union{Nothing,AbstractString}=uri.scheme, + userinfo::Union{Nothing,AbstractString}=uri.userinfo, + host::Union{Nothing,AbstractString}=uri.host, + port::Union{Nothing,Integer,AbstractString}=uri.port, + path::Union{Nothing,AbstractString}=uri.path, query=uri.query, - fragment::AbstractString=uri.fragment) + fragment::Union{Nothing,AbstractString}=uri.fragment) - @require isempty(host) || host[end] != '/' - @require scheme in uses_authority || isempty(host) - @require !isempty(host) || isempty(port) - @require !(scheme in ["http", "https"]) || isempty(path) || path[1] == '/' - @require !isempty(path) || !isempty(query) || isempty(fragment) + scheme = something(scheme, absent) + userinfo = something(userinfo, absent) + host = something(host, absent) + path = something(path, absent) + fragment = something(fragment, absent) - if port !== absent + if port === nothing + port = absent + elseif port !== absent port = string(port) end - querys = query isa AbstractString ? query : escapeuri(query) + querys = query === nothing ? absent : query isa AbstractString ? query : escapeuri(query) + + @require isempty(host) || host[end] != '/' "`host` must not end with '/'" + @require !isabsent(host) || isabsent(port) "`port` requires an authority component" + @require !isabsent(host) || isabsent(userinfo) "`userinfo` requires an authority component" + @require isabsent(host) || isempty(path) || startswith(path, "/") "`path` with an authority component must be empty or start with '/'" + @require !isabsent(host) || !startswith(path, "//") "`path` without an authority component must not start with '//'" # reject control characters in all components !isabsent(scheme) && _reject_ctl(scheme, :scheme) @@ -306,6 +326,7 @@ function formaturi(io::IO, isempty(scheme) || print(io, scheme, isabsent(host) ? ":" : "://") + isempty(scheme) && !isabsent(host) && print(io, "//") isabsent(userinfo) || print(io, userinfo, "@") isempty(host) || print(io, hoststring(host)) isabsent(port) || print(io, ":", port) @@ -493,6 +514,9 @@ Splits the path into component segments based on `/`, according to http://tools.ietf.org/html/rfc3986#section-3.3. Any fragment and query parts of the string are ignored if present. +An `AbstractString` argument is treated as a path, not as a complete URI. Wrap +a complete URI string in `URI(...)` to split only its path component. + A final empty path segment (trailing '/') is removed, if present. This is technically incompatible with the segment grammar of RFC3986, but it seems to be a common recommendation to make paths with and without a trailing slash @@ -657,6 +681,10 @@ end Join the path component of URI and other parts. +This follows filesystem-style path joining and appends a relative part to the +current path. Use [`resolvereference`](@ref) when resolving a URI reference +against a base URI, where the last path segment can be replaced. + If `uri` has no authority (host) component, the resulting path must not begin with `"//"`, since such a URI cannot be represented (RFC 3986 Section 3.3); an `ArgumentError` is thrown in that case. diff --git a/test/uri.jl b/test/uri.jl index 1a49abf..66ea850 100644 --- a/test/uri.jl +++ b/test/uri.jl @@ -410,8 +410,24 @@ urltests = URLTest[ @test URI("http://google.com/user"; query=["key" => "value"]) == URI("http://google.com/user?key=value") @test URI("http://google.com/user"; query="key" => "value") == URI("http://google.com/user?key=value") - # Precondition error messages refer to the function name (#32) - @test_throws ArgumentError("URI() requires `scheme in uses_authority || isempty(host)`") URI(; host="example.com") + # `nothing` removes a component, while `""` keeps an empty component. + source = URI("custom://user@example.com:8080/page?query#fragment") + @test URI(source; scheme=nothing) == URI("//user@example.com:8080/page?query#fragment") + @test URI(source; userinfo=nothing) == URI("custom://example.com:8080/page?query#fragment") + @test URI(source; userinfo=nothing, host=nothing, port=nothing) == URI("custom:/page?query#fragment") + @test URI(source; port=nothing) == URI("custom://user@example.com/page?query#fragment") + @test URI(source; path=nothing) == URI("custom://user@example.com:8080?query#fragment") + @test URI(source; query=nothing) == URI("custom://user@example.com:8080/page#fragment") + @test URI(source; fragment=nothing) == URI("custom://user@example.com:8080/page?query") + @test string(URI("https://example.com/page?query#fragment"; query="", fragment="")) == "https://example.com/page?#" + @test URI(URI("#fragment")) == URI("#fragment") + + # Generic URI syntax permits authorities for arbitrary schemes. + @test URI(URI("postgresql://localhost:5432/postgres"); userinfo="user:password") == + URI("postgresql://user:password@localhost:5432/postgres") + + # Precondition error messages refer to the function and component names (#26, #32). + @test_throws ArgumentError("URI() requires `path` with an authority component must be empty or start with '/'") URI(; scheme="https", host="example.com", path="relative") end @testset "URIs.splitpath" begin