nupp.io.uri
nupp.io.uri parses one absolute URI into an immutable value and returns its components from the parse rather than from a substring. Reach for it when a program routes, rewrites or inspects a resource identifier.
const uri = nupp.io.uri
local endpoint, reason = uri.newURI("https://user@example.com:8443/api?q=1#top")
assert(endpoint, reason)
assert(endpoint:scheme() == "https")
assert(endpoint:host() == "example.com")
assert(endpoint:port() == 8443)Two URIs are equal when their normalized text is, so a host or scheme written in another case does not make one URI unequal to an otherwise identical one. Nothing has to be closed.
One specification#
Every provider parses as the WHATWG URL Standard does, which is what browsers and both HTTP transports do. What path() returns is therefore the path the request sends: %2e%2e is a dot segment and is resolved, a special scheme's host is lowercased and an IPv4 address written as 0x7f.1 is 127.0.0.1, an empty password is dropped, and spaces, controls and non-ASCII text are percent-encoded as UTF-8:
const uri = nupp.io.uri
local route = assert(uri.newURI("https://example.com/public/%2e%2e/secret"))
assert(route:path() == "/secret")
assert(assert(uri.newURI("http://0x7f.1/")):host() == "127.0.0.1")A special-scheme host that is not plain ASCII needs the standard's IDNA mapping. The native provider and a browser page's own URL carry it. A target with neither, such as a hostless portable one, parses without it and returns nil and "internationalized host needs a host URL parser" for such a host, including one already written with an xn-- label, rather than guessing.
URI has no public constructor. Creation goes through uri.newURI, which interns the 1024 most recently used normalized URIs. Repeated parsing and URI-producing operations therefore reuse the same immutable value while retention stays bounded.
Reading components#
scheme and path return a string, and path is empty rather than nil when the URI names none. username is empty when there is no user information. Every other component is optional and returns nil when the URI does not write it:
const uri = nupp.io.uri
local plain = assert(uri.newURI("mailto:someone@example.com"))
assert(plain:host() == nil)
assert(plain:port() == nil)
assert(plain:path() == "someone@example.com")The remaining accessors are authority, password, userInfo, query and fragment. toString returns the normalized complete URI, and tostring(uri) is the same call.
Validating untrusted text#
uri.newURI returns nil and a reason for malformed input, because bad text is an ordinary answer. uri.validate asks the same question without retaining an object, and uri.isURI separates a URI from a string or a record:
const uri = nupp.io.uri
local ok, why = uri.validate("http://[")
assert(not ok and why)
assert(not uri.isURI("https://example.com"))Parsing returns a reason, modification raises
A parse takes text from somewhere else, so failure is data and the caller is handed it. A with operation starts from a URI that already parsed, so a failure means the caller asked for something the grammar cannot express, which is a mistake at the call site rather than a bad input. Text that came from outside the program therefore goes through uri.newURI, uri.validate or resolve, all of which return a reason.
Deriving a new URI#
Every with operation returns a new URI and leaves the original unchanged. Supplying a component equal to its current normalized value returns the receiver itself rather than reparsing:
const uri = nupp.io.uri
local endpoint = assert(uri.newURI("https://user:pass@example.com:8443/api?q=1"))
local production = endpoint:withUserInfo(nil):withHost("api.example.com")
assert(production:userInfo() == nil)
assert(production:host() == "api.example.com")
assert(endpoint:host() == "example.com")The operations are withScheme, withUserInfo, withHost, withPort, withPath, withQuery and withFragment. Passing nil removes an optional component. A port must be an integer from 0 through 65535, and anything else raises. So does text that is more than the component: a scheme holding anything but a scheme, a host holding a : outside IPv6 brackets, or a scheme change that would make the rest of the URI parse with a different host.
concatPath appends path text without interpreting it as a reference:
const uri = nupp.io.uri
local api = assert(uri.newURI("https://api.example.com/v1"))
assert(api:concatPath("users"):path() == "/v1/users")withEndpoint(endpoint) takes another URI's scheme and authority and keeps the receiver's path, query and fragment, which is what reroutes a request URI through a configured service address.
Resolving a reference#
resolve parses a reference against the URI the way a browser resolves a link against the page it is on, by the same standard. A reference that repeats a special scheme without an authority, such as https:other.html, is relative, and \ separates segments where / does in a special scheme. It returns nil and a reason when the reference cannot be resolved:
const uri = nupp.io.uri
local page = assert(uri.newURI("https://example.com/docs/guide/index.html"))
local image, reason = page:resolve("../images/avatar.png")
assert(image, reason)
assert(image:path() == "/docs/images/avatar.png")Building from components#
uri.newURI also accepts a nupp.io.uri.Components record, built with new like any record. The components are assembled into text and then parsed, so one grammar decides what is valid:
const uri = nupp.io.uri
local status = assert(uri.newURI(new uri.Components(
scheme = "https",
userInfo = "reader:secret",
host = "example.com",
path = "/status",
query = "full=1"
)))
assert(status:userInfo() == "reader:secret")scheme is required and may not be empty. userInfo, host, path, query and fragment are optional strings, and port is an optional integer from 0 through 65535. Every field is URI text rather than a filesystem name.
Submodules
| Module | Description |
|---|---|
nupp.io.uri.spi | Implementation interfaces consumed during ordinary module initialization. |
Module contents
Types
| Type | Kind | Description |
|---|---|---|
Components | record | The parts a URI is built from when it is not written out as text. |
URI | record | A parsed URI. |
Functions
| Function | Kind | Description |
|---|---|---|
isURI | function | Reports whether a value is a URI. |
newURI | function | Parses absolute URI text, or assembles one from components. |
validate | function | Reports whether text parses as an absolute URI, without building one. |
Types#
Componentsrecord#
record Components ...The parts a URI is built from when it is not written out as text.
Members
| Name | Kind | Description |
|---|---|---|
scheme | field | The scheme, which is required and may not be empty. |
userInfo | field | The user information before @, when there is any. |
host | field | The host, when there is an authority. |
port | field | The port, 0 through 65535. |
path | field | The path, which may be empty. |
query | field | The query after ?, without the ?. |
fragment | field | The fragment after , without the . |
URIrecord#
record URI ...A parsed URI.
Members
| Name | Kind | Description |
|---|---|---|
toString | method | The whole URI as text. |
scheme | method | The scheme, without the colon. |
authority | method | The authority between // and the path. |
username | method | The user name before any password. |
password | method | The password after the user name. |
host | method | The host. |
port | method | The port, when one was written. |
path | method | The path, which is empty rather than nil when absent. |
query | method | The query, without the ?. |
fragment | method | The fragment, without the . |
userInfo | method | The user information as written, user name and password together. |
withScheme | method | The URI with its scheme replaced. |
withUserInfo | method | The URI with its user information replaced. |
withHost | method | The URI with its host replaced. |
withPort | method | The URI with its port replaced. |
withPath | method | The URI with its path replaced. |
withQuery | method | The URI with its query replaced. |
withFragment | method | The URI with its fragment replaced. |
concatPath | method | The URI with more appended to its path. |
resolve | method | Resolves a reference against this URI, as a browser resolves a link. |
withEndpoint | method | The URI with another's scheme and authority. |
__tostring | ||
__eq |
toStringmethod#
toString: function toString(self): stringThe whole URI as text.
Arguments
| Name | Type | Description |
|---|---|---|
self | any | this URI |
Returns
| Type | Description |
|---|---|
string | the URI text |
schememethod#
scheme: function scheme(self): stringThe scheme, without the colon.
Arguments
| Name | Type | Description |
|---|---|---|
self | any | this URI |
Returns
| Type | Description |
|---|---|
string | the scheme |
authoritymethod#
authority: function authority(self): string?The authority between // and the path.
Arguments
| Name | Type | Description |
|---|---|---|
self | any | this URI |
Returns
| Type | Description |
|---|---|
string? | the authority, or nil when there is none |
usernamemethod#
username: function username(self): stringThe user name before any password.
Arguments
| Name | Type | Description |
|---|---|---|
self | any | this URI |
Returns
| Type | Description |
|---|---|
string | the user name, empty when there is none |
passwordmethod#
password: function password(self): string?The password after the user name.
Arguments
| Name | Type | Description |
|---|---|---|
self | any | this URI |
Returns
| Type | Description |
|---|---|
string? | the password, or nil when there is none |
hostmethod#
host: function host(self): string?The host.
Arguments
| Name | Type | Description |
|---|---|---|
self | any | this URI |
Returns
| Type | Description |
|---|---|
string? | the host, or nil when there is no authority |
portmethod#
port: function port(self): integer?The port, when one was written.
Arguments
| Name | Type | Description |
|---|---|---|
self | any | this URI |
Returns
| Type | Description |
|---|---|
integer? | the port, or nil when the URI names none |
pathmethod#
path: function path(self): stringThe path, which is empty rather than nil when absent.
Arguments
| Name | Type | Description |
|---|---|---|
self | any | this URI |
Returns
| Type | Description |
|---|---|
string | the path |
querymethod#
query: function query(self): string?The query, without the ?.
Arguments
| Name | Type | Description |
|---|---|---|
self | any | this URI |
Returns
| Type | Description |
|---|---|
string? | the query, or nil when there is none |
fragmentmethod#
fragment: function fragment(self): string?The fragment, without the #.
Arguments
| Name | Type | Description |
|---|---|---|
self | any | this URI |
Returns
| Type | Description |
|---|---|
string? | the fragment, or nil when there is none |
userInfomethod#
userInfo: function userInfo(self): string?The user information as written, user name and password together.
Arguments
| Name | Type | Description |
|---|---|---|
self | any | this URI |
Returns
| Type | Description |
|---|---|
string? | the user information, or nil when there is none |
withSchememethod#
The URI with its scheme replaced.
Arguments
| Name | Type | Description |
|---|---|---|
self | any | this URI |
value | string | the scheme to use, without the colon |
Returns
| Type | Description |
|---|---|
URI | the new URI, or this one when nothing changes |
Raises
| Type | Condition |
|---|---|
string | when value is not a scheme, or when the rest of this URI would not keep its authority under the new scheme |
withUserInfomethod#
The URI with its user information replaced.
Arguments
| Name | Type | Description |
|---|---|---|
self | any | this URI |
value | string? | the user information, or nil to remove it |
Returns
| Type | Description |
|---|---|
URI | the new URI, or this one when nothing changes |
Raises
| Type | Condition |
|---|---|
string | when the user information cannot be used here |
withHostmethod#
The URI with its host replaced.
Arguments
| Name | Type | Description |
|---|---|---|
self | any | this URI |
value | string? | the host, or nil to remove the authority |
Returns
| Type | Description |
|---|---|
URI | the new URI, or this one when nothing changes |
Raises
| Type | Condition |
|---|---|
string | when the host cannot be used here |
withPortmethod#
The URI with its port replaced.
Arguments
| Name | Type | Description |
|---|---|---|
self | any | this URI |
value | integer? | the port, 0 through 65535, or nil to remove it |
Returns
| Type | Description |
|---|---|
URI | the new URI, or this one when nothing changes |
Raises
| Type | Condition |
|---|---|
string | when port is not a port number |
withPathmethod#
The URI with its path replaced.
Arguments
| Name | Type | Description |
|---|---|---|
self | any | this URI |
value | string | the path |
Returns
| Type | Description |
|---|---|
URI | the new URI, or this one when nothing changes |
Raises
| Type | Condition |
|---|---|
string | when the path cannot be used here |
withQuerymethod#
The URI with its query replaced.
Arguments
| Name | Type | Description |
|---|---|---|
self | any | this URI |
value | string? | the query, without the |
Returns
| Type | Description |
|---|---|
URI | the new URI, or this one when nothing changes |
Raises
| Type | Condition |
|---|---|
string | when the query cannot be used here |
withFragmentmethod#
The URI with its fragment replaced.
Arguments
| Name | Type | Description |
|---|---|---|
self | any | this URI |
value | string? | the fragment, without the |
Returns
| Type | Description |
|---|---|
URI | the new URI, or this one when nothing changes |
Raises
| Type | Condition |
|---|---|
string | when the fragment cannot be used here |
concatPathmethod#
The URI with more appended to its path.
Arguments
| Name | Type | Description |
|---|---|---|
self | any | this URI |
value | string | the path to append |
Returns
| Type | Description |
|---|---|
URI | the new URI, or this one when path is empty |
Raises
| Type | Condition |
|---|---|
string | when the path cannot be appended |
resolvemethod#
resolve: function resolve(self, reference: string): URI?, string?Resolves a reference against this URI, as a browser resolves a link.
The reference decides how much of this URI survives: an absolute one replaces all of it, a rooted path keeps the scheme and authority, and a relative path is taken from this one's directory.
Examples#
Resolve a relative reference:
local page = assert(newURI("https://example.com/docs/a.html"))
assert(assert(page:resolve("b.html")):toString()
== "https://example.com/docs/b.html")Arguments
| Name | Type | Description |
|---|---|---|
self | any | this URI |
reference | string | the reference to resolve |
Returns
| Type | Description |
|---|---|
URI? | the resolved URI, or nil when the reference cannot be resolved |
string? | a failure reason, when unsuccessful |
withEndpointmethod#
The URI with another's scheme and authority.
Arguments
| Name | Type | Description |
|---|---|---|
self | any | this URI |
endpoint | URI | the URI to take the endpoint from |
Returns
| Type | Description |
|---|---|
URI | the new URI |
Raises
| Type | Condition |
|---|---|
string | when endpoint is not a URI |
__tostring#
__tostring: function(self): stringArguments
| Name | Type | Description |
|---|---|---|
? | self |
Returns
| Type | Description |
|---|---|
string |
__eq#
__eq: function(self, any): booleanArguments
| Name | Type | Description |
|---|---|---|
? | self | |
? | any |
Returns
| Type | Description |
|---|---|
boolean |
Functions#
isURIfunction#
function isURI(value: any): booleanReports whether a value is a URI.
Arguments
| Name | Type | Description |
|---|---|---|
value | any | the value to test |
Returns
| Type | Description |
|---|---|
boolean | whether it is a URI |
newURIfunction#
function newURI(value: string | Components): URI?, string?Parses absolute URI text, or assembles one from components.
Text has to be absolute: a relative reference is resolved against a URI that already exists, through URI:resolve.
Examples#
Read a URI's parts:
const uri = nupp.io.uri
local address = assert(uri.newURI("https://example.com/a?b=1"))
assert(address:host() == "example.com" and address:query() == "b=1")Assemble one from components rather than writing the text out:
local endpoint = assert(uri.newURI(new uri.Components(
scheme = "https",
host = "example.com",
path = "/a",
query = "b=1"
)))Arguments
| Name | Type | Description |
|---|---|---|
value | string | Components | the URI text, or the components to assemble |
Returns
| Type | Description |
|---|---|
URI? | the URI, or nil when it could not be parsed |
string? | a failure reason, when unsuccessful |
validatefunction#
function validate(value: string): boolean, string?Reports whether text parses as an absolute URI, without building one.
Arguments
| Name | Type | Description |
|---|---|---|
value | string | the text to test |
Returns
| Type | Description |
|---|---|
boolean | whether it parses |
string? | why it did not, when it did not |