# url

> Parse, build, join and escape URLs and query strings without string surgery.

This is the Markdown version of https://osl-lang.org/packages/url. Every page of the OSL docs has one: add .md to its path. https://osl-lang.org/llms.txt lists them all.

```osl
import "std:url"
```

`std:url` takes URLs apart and puts them back together. Reach for it whenever a URL is more than a string you pass along: reading a query parameter, adding one, resolving a link against a base, or escaping user input before it goes into an address. Building URLs with `++` works until a value contains a space or an ampersand.

## Parse a URL

`url.parse(text)` returns a result holding the parts. A URL that can't be parsed is an error, not a half-filled record:

```osl
import "std:url"

auto u = url.parse("https://ada:pw@example.com:8443/a/b%20c?x=1&y=two+words&x=2#top").unwrap()
log u.scheme u.hostname u.port
log u.host
log u.user u.password
log u.path
log u.rawPath
log u.queryString
log u.fragment

log url.parse("http://[bad").isErr()
```

Output:

```text
https example.com 8443
example.com:8443
ada pw
/a/b c
/a/b%20c
x=1&y=two+words&x=2
top
true
```

Try changing the port, or removing the `#top`, and run it again: the parts that are missing come back as empty strings.

`host` includes the port and `hostname` doesn't. `path` is decoded and `rawPath` is as it was written. The parts that are absent come back as empty strings.

## Query parameters

`u.query` is the query string already decoded into an object. A parameter that appears once is a string, and one that repeats is an array of strings. To read one parameter from a URL string directly, use `url.param`, which returns `null` for a parameter that isn't there:

```osl
import "std:url"

auto u = url.parse("https://example.com/s?x=1&y=two+words&x=2").unwrap()
log u.query

string link = "https://example.com/s?q=hello&n=2"
log url.param(link, "q").unwrap()
log url.param(link, "missing").unwrap()
```

Output:

```text
{"x":["1","2"],"y":"two words"}
hello
null
```

`url.parseQuery("a=1&b=2")` does the same for a bare query string, with or without the leading `?`. `url.stringifyQuery` goes the other way, with keys sorted so the result is stable, and an array becomes a repeated key:

```osl
import "std:url"

log url.parseQuery("?a=1&b=two+words&a=3").unwrap()
log url.stringifyQuery({b: 2, a: "x y", list: [1, 2]})
```

Output:

```text
{"a":["1","3"],"b":"two words"}
a=x+y&b=2&list=1&list=2
```

## Set parameters on a URL

`url.withParams(url, params)` returns the URL with the parameters set. Existing ones with the same name are replaced and the rest are kept:

```osl
import "std:url"

string next = url.withParams("https://example.com/s?q=1&keep=yes", {q: "new", page: 2}).unwrap()
log next
```

Output:

```text
https://example.com/s?keep=yes&page=2&q=new
```

This is the safe way to add a user's search text to a link: every value is escaped for you.

## Escaping

`url.escape` makes text safe to use as one query key or value, and `url.unescape` reverses it (a `+` becomes a space). Unescaping can fail on a broken `%` sequence, so it returns a result:

```osl
import "std:url"

log url.escape("a b&c=d/é")
log url.unescape("a%20b+c").unwrap()
log url.unescape("100%").isErr()
```

Output:

```text
a+b%26c%3Dd%2F%C3%A9
a b c
true
```

## Join and build

`url.join(base, reference)` resolves a link the way a browser does, so a page can turn `../api` into a full address. `url.build` assembles a URL from the same parts `parse` returns:

```osl
import "std:url"

log url.join("https://example.com/docs/guide/", "../api?v=2").unwrap()
log url.join("https://example.com/docs/guide", "/root").unwrap()
log url.build({scheme: "https", host: "example.com", path: "/a b", query: {k: "v"}, fragment: "f"})
```

Output:

```text
https://example.com/docs/api?v=2
https://example.com/root
https://example.com/a%20b?k=v#f
```

## Checking input

`url.isValid(text)` is true only for an absolute URL with a scheme and a host, so a relative path or a bare domain is rejected:

```osl
import "std:url"

for text in ["https://example.com", "example.com", "/relative", "ftp://files.example.com/a"] (
  log text url.isValid(text)
)
```

Output:

```text
https://example.com true
example.com false
/relative false
ftp://files.example.com/a true
```

A valid URL can still use a scheme you don't want. If a form accepts links, also check `scheme` is `http` or `https` before following one.

**Quiz.** Why use `url.withParams` instead of joining `"?q=" ++ text` onto a URL?

- [ ] It is faster.
- [x] It escapes every value, so spaces and `&` in the text can't break the URL.
- [ ] It checks that the site exists.

Answer: String joining works until a value contains a character that means something in a URL.

### Exercise: Next page link

Given a search URL, print the same URL with `page` set to 2. Keep the existing `q`.

Starting code:

```osl
import "std:url"

string current = "https://example.com/search?q=osl"
log current
```

It should print:

```text
https://example.com/search?page=2&q=osl
```

Hint 1: `url.withParams(url, object)` returns a result.

Hint 2: `url.withParams(current, {page: 2}).unwrap()`.

A solution:

```osl
import "std:url"

string current = "https://example.com/search?q=osl"
log url.withParams(current, {page: 2}).unwrap()
```

## API reference

## API reference

Every member of `std:url`, generated from the source of OSL.

### Functions

#### `url.build(object parts) string`

build assembles a URL from the parts url.parse returns. Accepted keys:
scheme, host, or hostname with port, path, rawPath, query (an object whose
array values repeat the key) or queryString, fragment, and user with
password. Every key is optional; an unknown key throws ValueError. The
valid key of url.parse's result is ignored, so a parsed URL builds back.

#### `url.decode(any query) object`

**Deprecated.** Deprecated: use url.parseQuery(text) instead, which fails with ParseError for malformed input.

decode reads a query string into an object, or {} when it is malformed.

#### `url.encode(object m) string`

**Deprecated.** Deprecated: use url.stringifyQuery(params) instead.

encode encodes an object as a query string.

#### `url.escape(string text) string`

escape percent-encodes text for use as a query key or value, writing
spaces as "+".

#### `url.isValid(string text) boolean`

isValid reports whether text is an absolute URL with a scheme and a host,
such as "https://example.com".

#### `url.join(string base, string ref) result<string, error>`

join resolves a relative reference against a base URL, as a browser
resolves a link: join("https://x.com/a/", "b") is "https://x.com/a/b".
The result always stays on base's origin, so a ref that names its own
scheme or host (such as "https://other.example" or "//other.example")
fails with ValueError. Fails with ParseError when either URL is malformed.

#### `url.param(string url, string key) result<string?, error>`

param returns the first value of a query parameter in url, or null when
the parameter is absent. Fails with ParseError when url is malformed.

#### `url.parse(string text) result<{fragment: string, host: string, hostname: string, password: string, path: string, port: string, query: object, queryString: string, rawPath: string, scheme: string, user: string, valid: boolean (deprecated)}, error>`

parse splits a URL into its components. It returns {scheme, host,
hostname, port, path, rawPath, query, queryString, fragment, user,
password}: every key is a string ("" when the URL has no such part) except
query, an object mapping each query key to its value, or to an array of
values when the key repeats. path is decoded and rawPath keeps its escapes.
The deprecated key valid is always true.
Relative references such as "../a" parse too. Fails with ParseError for
malformed input such as a bad percent-escape or port.

#### `url.parseQuery(string text) result<object, error>`

parseQuery reads a query string such as "a=1&b=two+words" (a leading "?"
is allowed) into an object mapping each key to its decoded value, or to an
array of values when the key repeats. Fails with ParseError for a bad
percent-escape or a ";" separator.

#### `url.stringifyQuery(object params) string`

stringifyQuery encodes an object as a query string with sorted keys:
{a: "1", b: "two words"} becomes "a=1&b=two+words". An array value
repeats the key once per element; other values are converted to strings.

#### `url.unescape(string text) result<string, error>`

unescape decodes a percent-encoded query component, turning "+" into a
space. Fails with ParseError for a malformed escape such as "%zz".

#### `url.withParams(string url, object params) result<string, error>`

withParams returns url with the given query parameters set, replacing
existing values for those keys and keeping the others. An array value
repeats the key. Fails with ParseError when url is malformed.
