std/uri
Uri and UriReference after RFC 3986, normalized at construction, with IRIs, Urn, DataUri, UriTemplate, the file bridge to Path and the form codec of HTML - values that open nothing.
Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite
Die generierte Referenz von std/uri zeigt jede öffentliche Deklaration mit Signatur, Doc-Kommentar und Beispielen.
std/uri is Uniform Resource Identifiers as values. A Uri is read from text once, with Uri.tryFrom(text)?, and
normalized on the way in - the scheme and the host lower case, escapes upper case and decoded where they name an
unreserved character, dot segments gone, every character a URI cannot hold percent-encoded as UTF-8 - so ==,
hash() and compare() are over the canonical form without anybody asking for it. A text that is no URI is refused at
the door with a UriError that names it - and a literal where a Uri is expected, open("https://…"), is read by
the compiler, so a typo in one is an error at that line. Nothing in the package opens anything: an IP literal host is an address of
std/ip, and resolving a name or connecting is std/network's. Uri and UriError are in the
prelude; everything else is an import. The design, and why it reads RFC 3986 rather than the WHATWG standard, is
docs/design/URI.md.
Import#
use Uri, UriReference, UriError, Urn, UriTemplate, TemplateValue, TemplateValues, Authority, Host from "std/uri"
use TemplateVariable, TemplateCase, TemplateRoute, TemplateRoutes, route from "std/uri"
use String.formDecoded, List.formEncoded, String.percentDecoded from "std/uri"
use Schemes, repaired from "std/uri"use UriReference from "std/uri"
fn linkTarget(page: String, link: String): Result<Uri, UriError> {
const base = Uri.tryFrom(page)?
const reference = UriReference.tryFrom(link)?
reference.resolved against: base
}
print linkTarget("https://example.test/guide/start.html", "../reference/index.html#top")Declarations#
Uri#
type Uri with Show, Equals, Hash, Compare {
fn scheme(): String
fn authority(): Authority?
fn host(): Host?
fn port(): Int?
fn defaultPort(): Int?
fn socketAddress(): SocketAddress?
fn path(): String
fn segments(): List<String>
fn query(): String?
fn fragment(): String?
fn isUrl(): Bool
fn isUrn(): Bool
fn queryParameters(): Map<String, List<String>>
fn queryParameter(name: String): String?
fn withQueryParameters(parameters: Map<String, List<String>>): Self
fn withFragment(fragment: String?): Self
fn joined(relative: String): Self
fn normalized(): Self
fn relativeTo(base: Self): UriReference
fn text(): String
fn iriText(): String
fn show(): String
fn compare(other: Self): Ordering
}
extend Uri with From<DataUri>
extend Uri with TryFrom<String, UriError>
extend Uri with TryFrom<Path, UriError>
extend Uri with From<Urn>A URI per RFC 3986 section 3: it always has a scheme. What construction does not decide, because it is a guess about
a scheme rather than a fact about URIs, is normalized()'s: the default port of http, https, ws, wss and ftp
dropped and an empty path under an authority made / - so http://example.test:80 and http://example.test/ are two
values until both are normalized. port() is the port that is written; defaultPort() the scheme's, and
socketAddress() answers where to connect without a resolver when the host is an IP literal.
show() hides a password and text() does not. postgres://ada:hunter2@db/orders shows as
postgres://ada:***@db/orders, which is what interpolation, print and every failure message reach for; text() is the
whole text a request, a file or an encoder needs, and Encode writes it. iriText() is the IRI the URI stands for
(RFC 3987): escapes of UTF-8 characters outside ASCII decoded, so https://example.test/%C3%A4 reads as
https://example.test/ä, and Uri.tryFrom(uri.iriText()) is the same URI again. An IRI is read by tryFrom like any
URI. A host outside ASCII is stored as its A-labels - https://münchen.test/ is https://xn--mnchen-3ya.test/ - through
the IDNA of std/dns, and iriText() shows the U-labels again; a host IDNA refuses is InvalidHost, and
NonAsciiHost is a host of percent escapes that are not UTF-8.
segments() is the path split at its /, still percent-encoded, without the empty segment in front of an absolute
path (/a/b/ has a, b and ""); segment.percentDecoded() decodes one. joined(relative) builds a URI below this
one: each
segment of relative percent-encoded, and a .. never climbs above the receiver, so base.joined(input) stays
below base. query() is one opaque string, as RFC 3986 leaves it, and queryParameters() reads it as a form.
Uri is a capsule: tryFrom is the one way in and text() the way out, and
the two are the conversion pair Encode and Decode go through, so a URI in a JSON document is its text.
UriReference#
type UriReference with Show, Equals, Hash, Compare {
fn scheme(): String?
fn authority(): Authority?
fn host(): Host?
fn port(): Int?
fn path(): String
fn segments(): List<String>
fn query(): String?
fn fragment(): String?
fn isAbsolute(): Bool
fn isRelative(): Bool
fn uri(): Uri?
fn resolved(against: Uri): Uri
fn queryParameters(): Map<String, List<String>>
fn queryParameter(name: String): String?
fn withFragment(fragment: String?): Self
fn text(): String
fn iriText(): String
fn show(): String
fn compare(other: Self): Ordering
}
extend UriReference with TryFrom<String, UriError>
extend UriReference with From<Uri>
extend UriReference with TryFrom<Path, UriError>A URI reference per RFC 3986 section 4.1: a URI, or a relative reference - ../a, /b?c, //host/d, #top - that
means something only against a base. It has the reading members of Uri, with the scheme an Option, and
resolved(against:) is RFC 3986 section 5.2: a reference resolved against a Uri is a Uri, and cannot fail. The dot
segments of a relative path are kept until then, because they mean something only once a base is known.
uri.relativeTo(base) is the way back: the shortest reference that resolves into uri.
Authority and Host#
type Authority with Show, Equals, Hash {
userInfo: String?
host: Host
port: Int?
fn text(): String
fn show(): String
fn hostAndPort(): String
fn socketAddress(defaultPort: Int? = None): SocketAddress?
}
type Host with Show, Equals, Hash {
case Name(name: String)
case Address(address: IpAddress)
case Future(text: String)
fn domainName(): DomainName?
fn show(): String
}The [userInfo "@"] host [":" port] of a URI, as data. mailto:ada@example.test has no authority, and file:///x has
one whose host is Name(""). A host is an IP literal, an IPv4 address or a registered name, and the first of RFC 3986's
rules that fits decides: 127.0.0.1 and [::1] are Address, written back in RFC 5952's form, and 01.2.3.4 is a
Name. A zone identifier ([fe80::1%25eth0]) is refused, because RFC 9844 took it out of the URI grammar.
domainName() answers a registered name as the DomainName of std/dns a resolver is asked for, and None
for an address and for a name the DNS cannot carry - a percent escape, a sub-delimiter, a label longer than 63 bytes.
Urn#
type Urn with Show, Equals, Hash, Compare {
fn uri(): Uri
fn namespace(): String
fn specific(): String
fn resolution(): String?
fn query(): String?
fn fragment(): String?
fn assignedName(): Self
fn show(): String
fn compare(other: Self): Ordering
}
extend Urn with TryFrom<Uri, UriError>A urn: URI read after RFC 8141: the namespace identifier (lower case, two to thirty-two letters, digits and -), the
namespace-specific string, and the r-, q- and f-components. RFC 8141's equivalence ignores the last three, and it is
first.assignedName() == second.assignedName(); == compares the whole URN.
DataUri#
type DataUri with Show, Equals, Hash, Compare {
fn uri(): Uri
fn mediaType(): String
fn parameters(): List<DataParameter>
fn charset(): String
fn isBase64(): Bool
fn bytes(): Bytes
fn fragment(): String?
fn show(): String
fn compare(other: Self): Ordering
static fn encoded(bytes: Bytes, mediaType: String = "text/plain"): Result<Self, UriError>
}
extend DataUri with TryFrom<Uri, UriError>
type DataParameter with Show, Equals, Hash {
name: String
value: String
}A data: URI read after RFC 2397: the media type (lower case, text/plain when the header leaves it out), its
parameters (names lower case, values percent-decoded) and the bytes, percent-decoded or Base64-decoded when the header
ends in ;base64. charset() is the charset parameter and US-ASCII without one. Everything is checked when the
value is made, including that a Base64 body decodes, so bytes() cannot fail. A ? in the data belongs to the data and
a # starts the fragment. == is over the URI as written, and DataUri.encoded writes bytes as padded standard
Base64. It is not in the prelude.
const uri: Uri = "data:text/plain;charset=utf-8;base64,SGVsbG8="
const data = DataUri.tryFrom(uri)?
print "{data.mediaType()} {data.charset()} {data.bytes().length()}"UriTemplate#
type UriTemplate<Variables> with Show, Equals, Hash, TryFrom<String, UriError> {
static fn tryFrom(value: String): Result<Self, UriError>
fn templateVariables(): List<TemplateVariable>
fn expandedFrom(values: Variables): Result<UriReference, UriError> where Variables: Encode
fn decoded(reference: UriReference): Variables? where Variables: Decode
fn variables(): List<String>
fn level(): Int
fn expandedText(values: TemplateValues): Result<String, UriError>
fn expanded(values: TemplateValues): Result<UriReference, UriError>
fn isMatchable(): Bool
fn matched(reference: UriReference): TemplateValues?
fn show(): String
}
type TemplateValues = Map<String, TemplateValue>
type TemplateValue with Show, Equals, Hash {
case Text(value: String)
case Items(values: List<String>)
case Pairs(values: List<(String, String)>)
}
type TemplateVariable with Show, Equals, Hash {
name: String
optional: Bool
exploded: Bool
}An RFC 6570 template, all four levels: {var}, {+var} and {#var}, the operators ., /, ;, ? and & with
several variables, and the prefix and explode modifiers ({var:3}, {list*}). expanded answers the URI reference
the template makes with the values in it; a variable that is not in the map is undefined and expands to nothing, as the
RFC says. The tests are the RFC's examples and the whole uritemplate-test suite.
matched reads a template backwards: the values that expand it into a reference, decoded. It is defined for the
templates isMatchable() accepts - simple variables and path segments with a literal between them, an exploded path
segment last in the path, and query parameters written {?a,b} or {&c}, which match in any order and ignore the
parameters they do not name. That is the subset routes are written in (see
docs/design/WEB.md section 13).
Variables is the type whose fields the variables are. A literal where a UriTemplate<OrderPath> is expected is
read by the compiler (the literal rule), verbatim - {id} is the
template's variable and never an interpolation - and a variable that is no field of OrderPath, or a field without a
default that is no variable, is an error at the literal. expandedFrom and decoded go through the fields, with
Encode and Decode. A template read while the program runs is a UriTemplate<TemplateValues>, whose variables are
whatever the map holds.
use UriTemplate, TemplateValue, TemplateValues, UriReference from "std/uri"
fn orderOf(target: String): Result<String, UriError> {
const template: UriTemplate<TemplateValues> = "/orders/{id}{?fields}"
const values = template.matched(UriReference.tryFrom(target)?) ?? [:]
match values.get("id") {
Some(.Text(id)) => id
_ => "no order"
}
}
print orderOf("/orders/7?fields=total")Routes#
type TemplateCase<Value> with Show, Equals, Hash {
static fn named(name: String): Self
fn name(): String
fn show(): String
}
type TemplateRoute<Value> with Show {
template: UriTemplate<Value>
target: TemplateCase<Value>
fn show(): String
}
fn route<Value>(template: UriTemplate<Value>, to: TemplateCase<Value>): TemplateRoute<Value>
type TemplateRoutes<Value> with Show {
static fn of(routes: List<TemplateRoute<Value>>): Result<Self, UriError> where Value: Describe
fn matched(reference: UriReference): Value? where Value: Decode
fn link(value: Value): Result<UriReference, UriError> where Value: Encode
fn show(): String
}Templates typed against the cases of one type: what a router is built from (docs/design/WEB.md,
decision D5). Route.Order where a TemplateCase<Route> is expected is the case Order, named where it is written,
and route("/orders/{id}", to: Route.Order) is checked by the compiler: every variable is a field of the case, every
field without a default a variable, a field with a default at most a query variable, and the template one that can be
matched backwards. TemplateRoutes.of checks the same when the program starts, and it is what refuses a case without a
route; matched reads a reference into the case of the first route that matches it, and link expands the route of a
value's case, so the two directions come from one template.
use TemplateRoutes, route, UriReference from "std/uri"
type Page {
case Home
case Order(id: Int)
case Search(query: String = "")
}
const routes = TemplateRoutes.of([
route("/", to: Page.Home),
route("/orders/{id}", to: Page.Order),
route("/search{?query}", to: Page.Search),
])?
print routes.matched(UriReference.tryFrom("/orders/7")?)
print routes.matched(UriReference.tryFrom("/search?query=tea")?)
print routes.link(Page.Order(12))Path and file: URIs#
Uri.tryFrom(path) writes an absolute Path as a file: URI (C:/Users/ada as file:///C:/Users/ada, a UNC share as
file://server/share/x), and UriReference.tryFrom(path) writes a relative one as a relative reference.
Path.tryFrom(uri) reads every form RFC 8089 knows - file:///x, file:/x, file://localhost/x, file:///C|/x,
file:C:/x - and refuses another scheme, a query and an escape that names a separator. None of the conversions is a
From, so each type keeps its own conversion pair with String.
Repairing what a person typed#
fn repaired(text: String): StringUri.tryFrom reads RFC 3986 and refuses a text that is not a URI, so an address typed by a person - with spaces around
it, a tab pasted in, or \ for / - is refused or read differently from a browser. repaired(text) makes the repairs
the WHATWG URL parser makes before it parses, as a function a caller calls where a reader can see it, and not as a mode
of tryFrom: leading and trailing control characters and spaces (U+0000 to U+0020) are dropped, every tab, line feed and
carriage return is removed, and a \ before the first ? or # is read as / when the scheme is http, https,
ws, wss, ftp or file (in any case) or the text has none. A \ in the query or the fragment, after another
scheme (git+ssh:, mailto:, urn:), and any other control character inside the text stay: Uri.tryFrom
percent-encodes them, as a browser does. Nothing fails and nothing is checked; the function is idempotent, and
Uri.tryFrom decides whether the result is a URI.
use repaired from "std/uri"
const typed = " http:\\\\example.com\\a b\n"
print repaired(typed)
print(Uri.tryFrom(repaired(typed))?)Forms and escapes#
extend String {
fn percentDecoded(): Result<String, UriError>
}
extend String {
fn formDecoded(): List<(String, String)>
}
extend String with From<UriTemplate<TemplateValues>>
extend String with From<Uri>
extend String with From<UriReference>
extend List<(String, String)> {
fn formEncoded(): String
}text.formDecoded() and pairs.formEncoded() are application/x-www-form-urlencoded, byte for byte as the WHATWG URL
Standard's parser and serializer: + is a space,
bytes that are not UTF-8 read as U+FFFD, and only ASCII letters, digits and *-._ stay unescaped. It is the reading of
queryParameters() and the writing of withQueryParameters, and the format of an HTML form's body.
text.percentDecoded() decodes the escapes of a text and reads the bytes as UTF-8 (UriError.NotText where they are
not).
Schemes#
trait Schemes {
fn schemes(): List<String>
}The schemes a driver answers to, in lower case: the one member a registry of drivers dispatches on (URI.md section 11). It is here rather than in std/storage because it declares no scheme and opens nothing, so a package of a database driver can take it without taking a storage package.
UriError#
type UriError with Show, Error {
case InvalidScheme(text: String)
case InvalidEscape(text: String)
case InvalidHost(host: String, reason: String)
case NonAsciiHost(host: String)
case InvalidPort(text: String)
case Relative(text: String)
case NotText(reason: String)
case NotAFile(uri: String, reason: String)
case NotAUrn(uri: String, reason: String)
case NotAData(uri: String, reason: String)
case InvalidTemplate(template: String, reason: String)
case InvalidRoutes(reason: String)
fn show(): String
}Why a text is not what it was read as. A % that is not followed by two hexadecimal digits is refused rather than
encoded, because 100% in a path is a typo or a text somebody forgot to encode. Every case carries the display form,
so a password never reaches a message.
Related#
- std/ip - the addresses an IP literal host is.
- std/path -
Path, and why it is not a URI. - std/http - a client that takes a
Uri, and a server whose requests carry one. - std/storage - bytes at a
Uri, with a driver chosen by its scheme. - The standard library - the other packages.