Url
struct Param
One name=value pair of a query string, decoded.
Both halves are the real text: a b is a space, not a%20b. build_query re-encodes them on the way out.
struct Url
port is always RESOLVED: the scheme's default when the URL did not say. 0 means the scheme has no default this module knows, which is not an error. Parsing is not connecting, and a URL with a scheme we have never heard of is still a URL.
path is percent-encoded and never empty (/ when the URL had none). raw_query and fragment are stored without their ? and #, and are "" when absent, which does not distinguish http://h/p from http://h/p?, a difference nothing observable depends on.
let default_port (scheme: String): Int
Ports the schemes this module knows about default to. An unknown scheme gets 0; see Url's note.
struct Split
Splits s at the first sep, returning the part before and the part after. When sep is absent the whole string is before.
let parse (input: String): Result[Url, String]
Absolute URLs only. A scheme-relative (//host/x) or relative (/x) reference has no meaning without a base to resolve against, and resolution is a different function than parsing.
The scheme is lowercased, because schemes are case-insensitive, and normalising here is what lets u.scheme == "http" be written everywhere else without thinking about it. The host is left exactly as written, matching Go's net/url.
let request_target (u: Url): String
What goes after the method in an HTTP request line: the path, plus the query exactly as it arrived. Not the fragment; a fragment is never sent to a server.
let authority (u: Url): String
Host, re-bracketed if it is an IPv6 literal, plus the port when it is not the scheme's default.
This is what an HTTP Host: header wants (RFC 7230 section 5.4) and what a URL's authority component is, which is the same string for the same reasons, so it is written once here rather than twice, once in stringify and once in the HTTP client.
The brackets have to come back: they were stripped on the way in because getaddrinfo wants ::1, but any context that puts a host next to a :port needs them or the colons are ambiguous.
let stringify (u: Url): String
The port is omitted when it is the scheme's default, and an IPv6 host is re-bracketed, so this is CANONICAL output rather than a byte-for-byte reproduction of whatever was parsed. parse after stringify yields the same Url, which is the invariant that matters and the one the properties check.
let query_all (u: Url) (name: String): Array[String]
Every value for name, in the order they appeared. A query may legitimately repeat a key (?tag=a&tag=b), and a lookup that returns one value cannot say so.
let query_get (u: Url) (name: String): Option[String]
The FIRST value for name, matching Go's Values.Get. None is absent; a present-but-empty value (?q=) is Some("").
let query_params (u: Url): Array[Param]
All pairs, decoded, in order.
let build_query (params: Array[Param]): String
Builds a query string from pairs, percent-encoding both halves.
The leading ? is NOT included, so this can be assigned straight to a Url's raw_query. An empty array gives an empty string rather than a stray separator.
let with_query (u: Url) (params: Array[Param]): Url
u with its query replaced. A setter rather than a mutation: values are values here.