# Web and software

The web types are the values a web app and its network pass around: URLs
and paths, domain names, HTTP statuses and headers, cookies, IP ranges,
and the problem documents an API reports failures with. Each follows its
standard, and, as every standard type is, each is read only in its
canonical text.

Here a shop builds its search links, follows the links on its pages, lets
its office's network in, reads the domain a seller types, retries a call
the server failed, and reports a sold-out item as RFC 9457 has it:

```jadpo
type ShopLink = Url {}

type SearchTerm = Text {
    min_length: 1
    max_length: 100
}

type CallerAddress = IpAddress {}

type FormText = Text {}

type YesNo = Bool {}

type NotADomain = Failure {
    kind: FailureKind.invalid_value
}

/// A search page's link for a term, on its first page.
function search_link(catalogue_page: ShopLink, tea_term: SearchTerm) -> ShopLink {
    var none_yet: Map<Text, Text> = []
    var tea_query: Map<Text, Text> = map.with(map.with(none_yet, "q", tea_term), "page", "1")
    return ShopLink(web.with_query(catalogue_page, tea_query))
}

/// Where a link on `page` leads.
function link_target(page: ShopLink, page_link: RelativeUrl) -> ShopLink {
    return ShopLink(web.resolve(page, page_link))
}

/// The basket's address from any page: a path is a relative URL too.
function basket_from(page: ShopLink, basket: UrlPath) -> ShopLink {
    return ShopLink(web.resolve(page, basket))
}

/// Whether a caller is on the office's network.
function from_office(office_network: Cidr, caller: CallerAddress) -> YesNo {
    return YesNo(web.cidr_contains(office_network, caller))
}

/// The domain a seller typed for their shop.
function shop_domain(raw: FormText) fails NotADomain -> DomainName {
    var typed_domain: DomainName = attempt text.parse(raw, as: DomainName) {
        malformed: NotADomain
    }
    return typed_domain
}

/// Whether a failed call is worth trying again: the server's fault, not
/// the request's.
function worth_retrying(status: HttpStatus) -> YesNo {
    return YesNo(status >= 500)
}

/// The problem a sold-out item is reported with.
function sold_out(item_page: ShopLink) -> ProblemDetails {
    return ProblemDetails {
        type_: "https://tea.example/problems/sold-out"
        status: 409
        title: "Sold out"
        detail: "Nothing is left of this item."
        instance: item_page
    }
}

/// A problem as a person reads it.
function problem_shown(problem: ProblemDetails, customer_locale: Locale) -> PresentationText {
    return text.format(problem, locale: customer_locale)
}

/// A problem as it is sent.
function problem_sent(problem: ProblemDetails) -> FormText {
    return FormText(json.write(problem))
}
```

```jadpo
test "links follow RFC 3986" {
    var search_page: ShopLink = "https://tea.example/search"
    var searched: ShopLink = "https://tea.example/search?page=1&q=tea%20%26%20cake"
    assert search_link(search_page, "tea & cake") == searched
    var sencha: ShopLink = "https://tea.example/teas/green/sencha"
    assert link_target(sencha, "../black/assam") == "https://tea.example/teas/black/assam"
    assert link_target(sencha, "#reviews") == "https://tea.example/teas/green/sencha#reviews"
    assert basket_from(sencha, "/basket") == "https://tea.example/basket"
}

test "an address is inside a range of its own family only" {
    assert from_office("192.0.2.0/24", "192.0.2.77")
    assert not from_office("192.0.2.0/24", "198.51.100.1")
    assert from_office("2001:db8::/32", "2001:db8:1::5")
    assert not from_office("::/0", "192.0.2.77")
}

test "a domain name is read in its ASCII form only" {
    var typed_domain: DomainName = attempt shop_domain("xn--mnchen-3ya.de")
    assert typed_domain == "xn--mnchen-3ya.de"
    assert shop_domain("München.de") fails NotADomain
    assert shop_domain("example.com.") fails NotADomain
}

test "a status's class is a comparison, and a problem is sent as RFC 9457 has it" {
    assert worth_retrying(503)
    assert not worth_retrying(404)
    var problem: ProblemDetails = sold_out("https://tea.example/items/assam")
    assert problem_shown(problem, Locale.en_gb) == "Sold out\nNothing is left of this item."
    var sent: FormText = "{\"type\":\"https://tea.example/problems/sold-out\",\"status\":409,\"title\":\"Sold out\",\"detail\":\"Nothing is left of this item.\",\"instance\":\"https://tea.example/items/assam\"}"
    assert problem_sent(problem) == sent
}
```

## Web operations

| Operation | Gives | Fails | SQL form |
|---|---|---|---|
| `web.with_query(url, entries)` | `Url` with each of `entries` (a `Map<Text, Text>`) added to its query, percent-encoded, in key order | | none |
| `web.resolve(base, relative)` | `Url`: a `RelativeUrl` or `UrlPath` resolved against `base` by RFC 3986 | | none |
| `web.cidr_contains(range, address)` | `Bool`: the `IpAddress` is inside the `Cidr` | | `x >>= a` |
| `semver.satisfies(version, range)` | `Bool`: a `SemanticVersion` is inside a `VersionRange` | | none |

`web`'s operations never fail, because each takes values that were
checked when they were built.

- `web.with_query`'s map is called `entries`, because `query` is a
  reserved word. Its entries are added in key order, by code point, after
  any query the URL already has and before its fragment. An empty map
  leaves the URL as it was.
- `web.resolve` is RFC 3986 §5.2's strict resolution. It normalises
  nothing but dot segments (`.` and `..`).
- `web.cidr_contains` never matches across address families, just as
  PostgreSQL's `>>=` doesn't. So an IPv4-mapped IPv6 address isn't inside
  an IPv4 range.

`semver.satisfies` is under [Software types](software-types.md).

## Which standard each type follows

| Type | Standard |
|---|---|
| `RelativeUrl`, `UrlPath` | RFC 3986's relative reference and absolute path |
| `DomainName` | UTS 46's ASCII form, `xn--mnchen-3ya.de` |
| `HttpStatus` | RFC 9110's three digits |
| `ProblemDetails` | RFC 9457's object |
| `JsonPointer` | RFC 6901 |
| `Cookie` | RFC 6265's `Set-Cookie` value |
| `Cidr` | RFC 4632's range, with its host bits zero |
| `MacAddress` | IEEE 802's form, `AC-DE-48-00-00-80` |
| `Imei` | 3GPP's 15 digits, with their Luhn check digit |
| `Emoji` | one of Unicode's fully-qualified emoji |

Every `UrlPath` is a `RelativeUrl`, and every `DomainName` is a
`Hostname`. So the first of each pair is built on the second (`public
type UrlPath = RelativeUrl {}`), and a value of the first passes where the
second is taken: `web.resolve`'s `relative` takes a path. It never works
the other way round.

## Types held as text

`UrlPath`, `RelativeUrl`, `Slug`, `DomainName`, `Hostname`, `WebOrigin`,
`HttpHeaderName`, `UserAgent`, `JsonPointer`, `Cookie`, `Cidr`,
`MacAddress`, `Imei`, `Emoji` and `PushToken` are held as text, as the
payment numbers are. Each is a pattern and, where a pattern can't say
everything, a named validator. For each of them:

- only the canonical text is read;
- none is ordered;
- `text.from` is `type.mismatch`, because each is text already;
- `text.parse` fails only as `malformed`.

`Hostname`, `WebOrigin` and `Cidr` are text, not an enum and objects,
because no operation reads their parts and each value has one text. In
detail:

- A **`RelativeUrl`** is RFC 3986's relative reference (§4.2), and is
  never empty. It is a path of any of the RFC's three kinds, a query and
  a fragment, or an authority after `//` followed by those. The
  authority's host is a `DomainName` or an IP address in its canonical
  text, IPv6 in brackets, and its port is from 1 to 65535. User
  information before the host is refused, because §3.2.1 deprecates it. A
  percent-encoding is in upper case (§2.1).
- A `RelativeUrl` is ASCII. An IRI, a URL with characters beyond ASCII
  (RFC 3987), is percent-encoded first, as `encoding.to_url_component`
  does.
- A **`UrlPath`** is RFC 3986's `path-absolute`, a path starting with
  `/`, with no query or fragment.
- A **`DomainName`** is what UTS 46's strict processing gives: IDNA 2008
  with STD3's rules, `CheckHyphens` and the DNS lengths, as Servo's `idna`
  1.1.0 does it on the pinned ICU4X. That means lower case; letters,
  digits and hyphens; each label beyond ASCII as its A-label (`xn--...`);
  no dot at the end; and a last label that isn't all digits.
- It refuses what IDNA 2008 refuses: `--` in the third and fourth places
  of a label other than an A-label's `xn--`, as some CDNs' host names
  have, and `_`, as DNS records' names (`_dmarc`) have. `localhost`, a
  single label, is a domain name.
- A domain name is shown as its A-label. Showing the Unicode form safely
  needs UTS 39's rules for characters that look alike, which aren't
  decided.
- A **`WebOrigin`** is `http` or `https` only. The HTML standard's other
  tuple origins, `ws`, `wss` and `ftp`, aren't what an `Origin` header
  carries from a page. The default port is left out, as the standard
  writes an origin.
- An **`HttpHeaderName`** is a header name in lower case, as HTTP/2 and
  HTTP/3 send field names, so `==` ignores case without doing anything
  special.
- A **`UserAgent`** is RFC 9110's grammar in ASCII, with one space between
  products and comments.
- A **`JsonPointer`** is RFC 6901's string form, not its URI fragment
  form. The empty pointer, which points at the whole document, is one.
- A **`Cookie`** is a server's `Set-Cookie` value in RFC 6265's strict
  syntax (§4.1.1), with RFC 6265bis's `SameSite` and name prefixes. Its
  attributes come after `; `, in the RFC's order and spelling, each at
  most once. An attribute the RFCs don't define, such as `Partitioned` or
  `Priority`, is refused.
- A cookie's `Max-Age` is never 0, as the strict syntax has it. To remove
  a cookie, give it an `Expires` in the past, whose weekday must be its
  date's. `SameSite=None` needs `Secure`, and the `__Secure-` and
  `__Host-` prefixes need their rules, compared ignoring case.
- A **`Cidr`**, a range of IP addresses, writes its address as
  `IpAddress` would write it canonically, RFC 5952's form for IPv6, with
  every host bit zero.
- A **`MacAddress`** is IEEE 802's own form, upper-case pairs joined by
  `-`, for EUI-48 only.
- An **`Imei`**, a phone's identity, is 15 digits with 3GPP TS 23.003's
  Luhn check digit. An IMEISV, the form with a software version, isn't
  one.
- An **`Emoji`** is one of Emoji 18.0's 3,963 fully-qualified emoji, from
  Unicode 18.0.0's final `emoji-test.txt` under the Unicode License v3,
  generated once into `jadpo_std::standard::emoji_data`. An unqualified or
  minimally-qualified form is refused, and so is a skin tone on its own.
- A **`PushToken`** is the token a phone gives an app for push
  notifications: opaque text of the characters Apple's and Google's tokens
  use, up to 4096 of them. It has no part saying which platform it is for,
  which would add an enum to the language. A Web Push subscription isn't
  a token.

## Types held as numbers, enums and objects

- An **`HttpStatus`** is a whole number from 100 to 599, RFC 9110 §15's
  three digits, with its registry open. A **`NetworkPort`** is one from 1
  to 65535; IANA reserves 0, so it is refused.
- Each is a type built on `Int` with its range. So `<` orders it, and
  `status >= 500` asks for a class of statuses. JSON sends it as a number,
  an `integer` column stores it, and `text.format` shows its digits, never
  grouped: `8080`, not `8,080`.
- `text.parse` reads only digits within the range, with no sign and no
  leading zero. Anything else is `malformed`. A type of your own with a
  narrower range, such as `type HighPort = NetworkPort { min: 1024 }`,
  breaks `min` or `max` by name.
- **`HttpMethod`** is the enum that [HTTP](http.md),
  [External services](external-services.md) and [Testing](testing.md)
  use. Its five variants are the methods routes are written with: `get`,
  `post`, `put`, `patch` and `delete`. Its codes are RFC 9110's tokens,
  such as `GET`, read ignoring case at a boundary as every standard enum's
  are, and it is stored by its code.
- RFC 9110's `HEAD`, `OPTIONS`, `CONNECT` and `TRACE` aren't variants.
  The runtime answers `HEAD` and `OPTIONS` itself, and no route is written
  with any of them. Adding them later breaks nothing. A proxy that must
  name the others holds the method as text.
- A **`ProblemDetails`** is RFC 9457's object for reporting an API
  failure: its five members, each of which may be `none`, as the RFC
  allows. `type` is a reserved word, so code writes `type_`, and the wire
  has `type`. That is the trailing `_` a closed list's code takes when it
  is a reserved word, used here for a standard object's part
  (`jadpo_std::standard::wire_name`).
- Its `type` and `instance` are absolute `Url`s. The RFC allows relative
  references; resolve one with `web.resolve` first. Its `title` is a line
  and its `detail` a paragraph.
- A member the RFC doesn't define is refused, as any object refuses an
  unknown field. An application with extension members declares its own
  type.
- A problem is sent with its `none` members left out, and read with
  missing members as `none`. It is stored as a column per member. In a
  field that may be `none`, a row whose columns are all `NULL` reads back
  as `none`; in a field that can't be, as a problem that says nothing.
- It has no single text form. `text.format` shows its title and detail
  on their own lines; with neither, its status; with no status, its type,
  or `about:blank`.
- A **`UtmParameters`** is the campaign tags a marketing link carries: an
  object of `source`, `medium` and `campaign`, each a line, and `term` and
  `content`, which may be `none`, as Google Analytics' campaign builder
  has them. GA4's `utm_id` and the other later tags aren't parts.
- Its canonical text is the query that carries them,
  `utm_source=mailing&utm_medium=email&utm_campaign=autumn_sale`, in that
  order, each value percent-encoded as `encoding.to_url_component`
  encodes it. Its JSON is its parts, and it is stored as a column per
  part.

## Personal data and shapes

A user agent, a cookie, a MAC address, an IMEI and a push token are
`online` [personal data](marking-personal-fields.md).
The other web types aren't personal data.

`type.standard_exists` matches none of these types. An application's
object can't have a field called `type`, as `ProblemDetails` does, and
`UtmParameters`' part names are too common to be a sign that a type is
one.
