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

ModuleDescription
nupp.io.uri.spiImplementation interfaces consumed during ordinary module initialization.

Module contents

Types

TypeKindDescription
ComponentsrecordThe parts a URI is built from when it is not written out as text.
URIrecordA parsed URI.

Functions

FunctionKindDescription
isURIfunctionReports whether a value is a URI.
newURIfunctionParses absolute URI text, or assembles one from components.
validatefunctionReports 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

NameKindDescription
schemefieldThe scheme, which is required and may not be empty.
userInfofieldThe user information before @, when there is any.
hostfieldThe host, when there is an authority.
portfieldThe port, 0 through 65535.
pathfieldThe path, which may be empty.
queryfieldThe query after ?, without the ?.
fragmentfieldThe fragment after , without the .

schemefield#

scheme: string

The scheme, which is required and may not be empty.

userInfofield#

userInfo: string?

The user information before @, when there is any.

hostfield#

host: string?

The host, when there is an authority.

portfield#

port: integer?

The port, 0 through 65535.

pathfield#

path: string?

The path, which may be empty.

queryfield#

query: string?

The query after ?, without the ?.

fragmentfield#

fragment: string?

The fragment after #, without the #.

URIrecord#

record URI ...

A parsed URI.

Members

NameKindDescription
toStringmethodThe whole URI as text.
schememethodThe scheme, without the colon.
authoritymethodThe authority between // and the path.
usernamemethodThe user name before any password.
passwordmethodThe password after the user name.
hostmethodThe host.
portmethodThe port, when one was written.
pathmethodThe path, which is empty rather than nil when absent.
querymethodThe query, without the ?.
fragmentmethodThe fragment, without the .
userInfomethodThe user information as written, user name and password together.
withSchememethodThe URI with its scheme replaced.
withUserInfomethodThe URI with its user information replaced.
withHostmethodThe URI with its host replaced.
withPortmethodThe URI with its port replaced.
withPathmethodThe URI with its path replaced.
withQuerymethodThe URI with its query replaced.
withFragmentmethodThe URI with its fragment replaced.
concatPathmethodThe URI with more appended to its path.
resolvemethodResolves a reference against this URI, as a browser resolves a link.
withEndpointmethodThe URI with another's scheme and authority.
__tostringmetamethod
__eqmetamethod

toStringmethod#

toString: function toString(self): string

The whole URI as text.

Arguments
NameTypeDescription
selfany

this URI

Returns
TypeDescription
string

the URI text

schememethod#

scheme: function scheme(self): string

The scheme, without the colon.

Arguments
NameTypeDescription
selfany

this URI

Returns
TypeDescription
string

the scheme

authoritymethod#

authority: function authority(self): string?

The authority between // and the path.

Arguments
NameTypeDescription
selfany

this URI

Returns
TypeDescription
string?

the authority, or nil when there is none

usernamemethod#

username: function username(self): string

The user name before any password.

Arguments
NameTypeDescription
selfany

this URI

Returns
TypeDescription
string

the user name, empty when there is none

passwordmethod#

password: function password(self): string?

The password after the user name.

Arguments
NameTypeDescription
selfany

this URI

Returns
TypeDescription
string?

the password, or nil when there is none

hostmethod#

host: function host(self): string?

The host.

Arguments
NameTypeDescription
selfany

this URI

Returns
TypeDescription
string?

the host, or nil when there is no authority

portmethod#

port: function port(self): integer?

The port, when one was written.

Arguments
NameTypeDescription
selfany

this URI

Returns
TypeDescription
integer?

the port, or nil when the URI names none

pathmethod#

path: function path(self): string

The path, which is empty rather than nil when absent.

Arguments
NameTypeDescription
selfany

this URI

Returns
TypeDescription
string

the path

querymethod#

query: function query(self): string?

The query, without the ?.

Arguments
NameTypeDescription
selfany

this URI

Returns
TypeDescription
string?

the query, or nil when there is none

fragmentmethod#

fragment: function fragment(self): string?

The fragment, without the #.

Arguments
NameTypeDescription
selfany

this URI

Returns
TypeDescription
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
NameTypeDescription
selfany

this URI

Returns
TypeDescription
string?

the user information, or nil when there is none

withSchememethod#

withScheme: function withScheme(self, value: string): URI

The URI with its scheme replaced.

Arguments
NameTypeDescription
selfany

this URI

valuestring

the scheme to use, without the colon

Returns
TypeDescription
URI

the new URI, or this one when nothing changes

Raises
TypeCondition
string

when value is not a scheme, or when the rest of this URI would not keep its authority under the new scheme

withUserInfomethod#

withUserInfo: function withUserInfo(self, value: string?): URI

The URI with its user information replaced.

Arguments
NameTypeDescription
selfany

this URI

valuestring?

the user information, or nil to remove it

Returns
TypeDescription
URI

the new URI, or this one when nothing changes

Raises
TypeCondition
string

when the user information cannot be used here

withHostmethod#

withHost: function withHost(self, value: string?): URI

The URI with its host replaced.

Arguments
NameTypeDescription
selfany

this URI

valuestring?

the host, or nil to remove the authority

Returns
TypeDescription
URI

the new URI, or this one when nothing changes

Raises
TypeCondition
string

when the host cannot be used here

withPortmethod#

withPort: function withPort(self, value: integer?): URI

The URI with its port replaced.

Arguments
NameTypeDescription
selfany

this URI

valueinteger?

the port, 0 through 65535, or nil to remove it

Returns
TypeDescription
URI

the new URI, or this one when nothing changes

Raises
TypeCondition
string

when port is not a port number

withPathmethod#

withPath: function withPath(self, value: string): URI

The URI with its path replaced.

Arguments
NameTypeDescription
selfany

this URI

valuestring

the path

Returns
TypeDescription
URI

the new URI, or this one when nothing changes

Raises
TypeCondition
string

when the path cannot be used here

withQuerymethod#

withQuery: function withQuery(self, value: string?): URI

The URI with its query replaced.

Arguments
NameTypeDescription
selfany

this URI

valuestring?

the query, without the ?, or nil to remove it

Returns
TypeDescription
URI

the new URI, or this one when nothing changes

Raises
TypeCondition
string

when the query cannot be used here

withFragmentmethod#

withFragment: function withFragment(self, value: string?): URI

The URI with its fragment replaced.

Arguments
NameTypeDescription
selfany

this URI

valuestring?

the fragment, without the #, or nil to remove it

Returns
TypeDescription
URI

the new URI, or this one when nothing changes

Raises
TypeCondition
string

when the fragment cannot be used here

concatPathmethod#

concatPath: function concatPath(self, value: string): URI

The URI with more appended to its path.

Arguments
NameTypeDescription
selfany

this URI

valuestring

the path to append

Returns
TypeDescription
URI

the new URI, or this one when path is empty

Raises
TypeCondition
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
NameTypeDescription
selfany

this URI

referencestring

the reference to resolve

Returns
TypeDescription
URI?

the resolved URI, or nil when the reference cannot be resolved

string?

a failure reason, when unsuccessful

withEndpointmethod#

withEndpoint: function withEndpoint(self, endpoint: URI): URI

The URI with another's scheme and authority.

Arguments
NameTypeDescription
selfany

this URI

endpointURI

the URI to take the endpoint from

Returns
TypeDescription
URI

the new URI

Raises
TypeCondition
string

when endpoint is not a URI

__tostringmetamethod#

__tostring: function(self): string
Arguments
NameTypeDescription
?self
Returns
TypeDescription
string

__eqmetamethod#

__eq: function(self, any): boolean
Arguments
NameTypeDescription
?self
?any
Returns
TypeDescription
boolean

Functions#

isURIfunction#

function isURI(value: any): boolean

Reports whether a value is a URI.

Arguments

NameTypeDescription
valueany

the value to test

Returns

TypeDescription
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

NameTypeDescription
valuestring | Components

the URI text, or the components to assemble

Returns

TypeDescription
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

NameTypeDescription
valuestring

the text to test

Returns

TypeDescription
boolean

whether it parses

string?

why it did not, when it did not