Web and software
On this page
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:
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))}
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 calledentries, becausequeryis 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.resolveis RFC 3986 §5.2's strict resolution. It normalises nothing but dot segments (.and..).web.cidr_containsnever 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.
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.fromistype.mismatch, because each is text already;text.parsefails only asmalformed.
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
RelativeUrlis 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 aDomainNameor 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
RelativeUrlis ASCII. An IRI, a URL with characters beyond ASCII (RFC 3987), is percent-encoded first, asencoding.to_url_componentdoes. - A
UrlPathis RFC 3986'spath-absolute, a path starting with/, with no query or fragment. - A
DomainNameis what UTS 46's strict processing gives: IDNA 2008 with STD3's rules,CheckHyphensand the DNS lengths, as Servo'sidna1.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'sxn--, 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
WebOriginishttporhttpsonly. The HTML standard's other tuple origins,ws,wssandftp, aren't what anOriginheader carries from a page. The default port is left out, as the standard writes an origin. - An
HttpHeaderNameis a header name in lower case, as HTTP/2 and HTTP/3 send field names, so==ignores case without doing anything special. - A
UserAgentis RFC 9110's grammar in ASCII, with one space between products and comments. - A
JsonPointeris RFC 6901's string form, not its URI fragment form. The empty pointer, which points at the whole document, is one. - A
Cookieis a server'sSet-Cookievalue in RFC 6265's strict syntax (§4.1.1), with RFC 6265bis'sSameSiteand 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 asPartitionedorPriority, is refused. - A cookie's
Max-Ageis never 0, as the strict syntax has it. To remove a cookie, give it anExpiresin the past, whose weekday must be its date's.SameSite=NoneneedsSecure, and the__Secure-and__Host-prefixes need their rules, compared ignoring case. - A
Cidr, a range of IP addresses, writes its address asIpAddresswould write it canonically, RFC 5952's form for IPv6, with every host bit zero. - A
MacAddressis 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
Emojiis one of Emoji 18.0's 3,963 fully-qualified emoji, from Unicode 18.0.0's finalemoji-test.txtunder the Unicode License v3, generated once intojadpo_std::standard::emoji_data. An unqualified or minimally-qualified form is refused, and so is a skin tone on its own. - A
PushTokenis 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
HttpStatusis a whole number from 100 to 599, RFC 9110 §15's three digits, with its registry open. ANetworkPortis one from 1 to 65535; IANA reserves 0, so it is refused. - Each is a type built on
Intwith its range. So<orders it, andstatus >= 500asks for a class of statuses. JSON sends it as a number, anintegercolumn stores it, andtext.formatshows its digits, never grouped:8080, not8,080. text.parsereads only digits within the range, with no sign and no leading zero. Anything else ismalformed. A type of your own with a narrower range, such astype HighPort = NetworkPort { min: 1024 }, breaksminormaxby name.HttpMethodis the enum that HTTP, External services and Testing use. Its five variants are the methods routes are written with:get,post,put,patchanddelete. Its codes are RFC 9110's tokens, such asGET, read ignoring case at a boundary as every standard enum's are, and it is stored by its code.- RFC 9110's
HEAD,OPTIONS,CONNECTandTRACEaren't variants. The runtime answersHEADandOPTIONSitself, 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
ProblemDetailsis RFC 9457's object for reporting an API failure: its five members, each of which may benone, as the RFC allows.typeis a reserved word, so code writestype_, and the wire hastype. 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
typeandinstanceare absoluteUrls. The RFC allows relative references; resolve one withweb.resolvefirst. Itstitleis a line and itsdetaila 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
nonemembers left out, and read with missing members asnone. It is stored as a column per member. In a field that may benone, a row whose columns are allNULLreads back asnone; in a field that can't be, as a problem that says nothing. - It has no single text form.
text.formatshows its title and detail on their own lines; with neither, its status; with no status, its type, orabout:blank. - A
UtmParametersis the campaign tags a marketing link carries: an object ofsource,mediumandcampaign, each a line, andtermandcontent, which may benone, as Google Analytics' campaign builder has them. GA4'sutm_idand 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 asencoding.to_url_componentencodes 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.
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.
Markdown for agents: /docs/web-and-software.md.Built from Jadpo main at a707efb.