nupp.io.uri

nupp.io.uri parses one absolute URI into an immutable value and answers 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.

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 answer 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 answers 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 answers the normalized complete URI, and tostring(uri) is the same call.

Validating untrusted text#

uri.newURI answers 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 answers 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 answer a reason.

Deriving a new URI#

Every with operation answers a new URI and leaves the original unchanged. Supplying a component equal to its current normalized value answers 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.

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 applies RFC reference resolution, the way a browser resolves a link against the page it is on. It answers 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. 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({
    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.provider

Module contents

Constructors

ConstructorDescription
newURIParses absolute URI text, or assembles one from components.

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.
validatefunctionReports whether text parses as an absolute URI, without building one.

Constructors#

newURIconstructor#

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({
    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

Types#

Componentsrecord#

record Components
    scheme: string
    userInfo: string?
    host: string?
    port: integer?
    path: string?
    query: string?
    fragment: string?
end

The parts a URI is built from when it is not written out as text.

Fields

scheme#
scheme: string

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

userInfo#
userInfo: string?

The user information before @, when there is any.

host#
host: string?

The host, when there is an authority.

port#
port: integer?

The port, 0 through 65535.

path#
path: string?

The path, which may be empty.

query#
query: string?

The query after ?, without the ?.

fragment#
fragment: string?

The fragment after #, without the #.

URIrecord#

record URI
    function toString(self): string end

    function scheme(self): string end

    function authority(self): string? end

    function username(self): string end

    function password(self): string? end

    function host(self): string? end

    function port(self): integer? end

    function path(self): string end

    function query(self): string? end

    function fragment(self): string? end

    function userInfo(self): string? end

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

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

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

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

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

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

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

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

    function resolve(self, reference: string): (URI?, string?) end

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

    metamethod __tostring: function(self): string
    metamethod __eq: function(self, any): boolean
end

A parsed URI.

Methods

toString#
toString: function toString(self): string

The whole URI as text.

Arguments
NameTypeDescription
selfany

this URI

Returns
TypeDescription
string

the URI text

scheme#
scheme: function scheme(self): string

The scheme, without the colon.

Arguments
NameTypeDescription
selfany

this URI

Returns
TypeDescription
string

the scheme

authority#
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

username#
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

password#
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

host#
host: function host(self): string?

The host.

Arguments
NameTypeDescription
selfany

this URI

Returns
TypeDescription
string?

the host, or nil when there is no authority

port#
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

path#
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

query#
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

fragment#
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

userInfo#
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

withScheme#
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
  • when the scheme cannot be used here

withUserInfo#
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
  • when the user information cannot be used here

withHost#
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
  • when the host cannot be used here

withPort#
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
  • when port is not a port number

withPath#
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
  • when the path cannot be used here

withQuery#
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
  • when the query cannot be used here

withFragment#
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
  • when the fragment cannot be used here

concatPath#
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
  • when the path cannot be appended

resolve#
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

withEndpoint#
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
  • when endpoint is not a URI

__tostring#
__tostring: function(self): string
Arguments
NameTypeDescription
?self
Returns
TypeDescription
string
__eq#
__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

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