// link_serializer.mbt — Deterministic RFC 8288 `Link` header serialization.
//
// The serializer is the inverse of `link_parser.mbt`. Its output is fully
// deterministic so that `canonicalize_link_header` (in canonicalize.mbt) is
// a well-defined idempotent function:
//
// - the target is always the first element, in `<...>`,
// - `rel` is always present and always quoted (RFC 8288 examples use the
// quoted form), with relation types joined by a single SP,
// - the single-occurrence target attributes `anchor`, `media`, `title`
// and `type` are always quoted,
// - `hreflang` is emitted once per value, always quoted, in input order,
// - `title*` is emitted as an RFC 8187 extended value (never quoted,
// because the parser treats a `name*` value as an ext-value),
// - extension parameters are emitted in input order; their values use the
// token form when every byte is a `tchar` and the quoted-string form
// otherwise, and a `name*` extension parameter is emitted as an
// unquoted ext-value.
//
// Round-trip guarantee: `parse_link_header(serialize_link_header(links))`
// is semantically equivalent to `links` for any model that came from the
// parser (quoting differences on extension parameters are not semantic).
///|
/// Serializes one link as a single `link-value`.
pub fn serialize_link(link : WebLink) -> String {
let sb = StringBuilder()
emit_link_value(sb, link)
sb.to_string()
}
///|
/// Serializes a list of links as a `Link` header field value (a
/// comma-space separated list of link-values).
pub fn serialize_link_header(links : Array[WebLink]) -> String {
let sb = StringBuilder()
for i = 0; i < links.length(); i = i + 1 {
if i > 0 {
sb.write_string(", ")
}
emit_link_value(sb, links[i])
}
sb.to_string()
}
///|
/// Serializes a `LinkSet` as a `Link` header field value.
pub fn serialize_link_header_from_linkset(linkset : LinkSet) -> String {
serialize_link_header(linkset.links())
}
///|
fn emit_link_value(sb : StringBuilder, link : WebLink) -> Unit {
sb.write_char('<')
sb.write_string(link.target())
sb.write_char('>')
// rel: always quoted, relation types joined by a single SP.
sb.write_string("; rel=")
emit_quoted(sb, relation_list_text(link.relations()))
match link.anchor() {
Some(a) => emit_quoted_param(sb, "anchor", a)
None => ()
}
for hl in link.hreflang() {
emit_quoted_param(sb, "hreflang", hl)
}
match link.media() {
Some(m) => emit_quoted_param(sb, "media", m)
None => ()
}
match link.title() {
Some(t) => emit_quoted_param(sb, "title", t)
None => ()
}
match link.title_star() {
Some(ev) => {
sb.write_string("; title*=")
sb.write_string(serialize_extended_value(ev))
}
None => ()
}
match link.media_type() {
Some(t) => emit_quoted_param(sb, "type", t)
None => ()
}
for p in link.extensions() {
emit_extension_param(sb, p)
}
}
///|
fn relation_list_text(relations : Array[RelationType]) -> String {
let sb = StringBuilder()
for i = 0; i < relations.length(); i = i + 1 {
if i > 0 {
sb.write_char(' ')
}
match relations[i] {
Registered(name) => sb.write_string(name)
Extension(uri) => sb.write_string(uri)
}
}
sb.to_string()
}
///|
/// Emits `; name="value"` with the value always quoted.
fn emit_quoted_param(sb : StringBuilder, name : String, value : String) -> Unit {
sb.write_string("; ")
sb.write_string(name)
sb.write_char('=')
emit_quoted(sb, value)
}
///|
fn emit_quoted(sb : StringBuilder, value : String) -> Unit {
sb.write_string(serialize_quoted_string(value))
}
///|
/// Emits one extension parameter in a form the parser accepts. A `name*`
/// parameter is an RFC 8187 extended value and must be emitted unquoted
/// (its serialized value uses only ext-value bytes). Other values use the
/// token form when possible and the quoted-string form otherwise.
fn emit_extension_param(sb : StringBuilder, p : LinkParameter) -> Unit {
sb.write_string("; ")
sb.write_string(p.name())
if is_ext_value_name(p.name()) {
match p.value() {
Some(v) => {
sb.write_char('=')
sb.write_string(v)
}
None => ()
}
} else {
match p.value() {
Some(v) => {
sb.write_char('=')
if can_be_token(v) {
sb.write_string(v)
} else {
emit_quoted(sb, v)
}
}
None => ()
}
}
}