// relation.mbt — Link relation types (RFC 8288 Section 3.3).
//
// A relation type is either:
// - registered: the token form (`reg-rel-type`), stored case-preserved
// but compared case-insensitively, and
// - extension: an absolute URI (`ext-rel-type`).
//
// The distinction is purely syntactic. Whether a registered-form name is
// actually present in the IANA registry is a separate question answered by
// the offline registry in `relation_registry.mbt`; an extension relation
// type is never rejected because it is missing from the registry.
///|
/// Parses a single relation type from a non-empty token string.
///
/// If the string is a valid absolute URI it is classified as an extension
/// relation type; otherwise it must be a valid token (RFC 7230 `tchar`) and
/// is classified as a registered relation type.
///
/// Errors: `Relation::InvalidRelation` and `Relation::InvalidExtensionRelation`.
pub fn parse_relation_type(token : String) -> Result[RelationType, LinkError] {
if token.length() == 0 {
return Err(link_error(Relation, InvalidRelation, "empty relation type"))
}
match parse_uri_reference(token) {
Ok(uri) if uri.scheme() is Some(_) => Ok(Extension(token))
Ok(_) =>
if relation_token_valid(token) {
Ok(Registered(token))
} else {
Err(
link_error(
Relation,
InvalidRelation,
"invalid registered relation type: \{token}",
),
)
}
Err(_) =>
if relation_token_valid(token) {
Ok(Registered(token))
} else {
Err(
link_error(
Relation,
InvalidRelation,
"invalid relation type: \{token}",
),
)
}
}
}
///|
/// Parses a `rel` value (a space separated list of relation types) into an
/// ordered array. Runs of SP are treated as a single separator, matching
/// the `1*SP` rule.
///
/// Errors: `Relation::InvalidRelation` and `Limit::LimitExceeded`.
pub fn parse_relation_list(
value : String,
limits : Limits,
) -> Result[Array[RelationType], LinkError] {
let relations = Array::new()
for part in split_on_spaces(value) {
match parse_relation_type(part) {
Ok(rt) => relations.push(rt)
Err(e) => return Err(e)
}
if relations.length() > limits.max_relations_per_link() {
return Err(
link_error(
Limit,
LimitExceeded,
"rel value exceeds max_relations_per_link",
),
)
}
}
if relations.is_empty() {
return Err(
link_error(
Relation,
InvalidRelation,
"rel value contains no relation types",
),
)
}
Ok(relations)
}
///|
/// Splits a string on runs of SP (0x20). Consecutive SP bytes collapse into
/// one separator; empty input yields an empty list.
pub fn split_on_spaces(value : String) -> Array[String] {
let bytes = @utf8.encode(value)
let out = Array::new()
let mut i = 0
while i < bytes.length() {
if bytes[i] == 32 {
i = i + 1
continue
}
let start = i
while i < bytes.length() && bytes[i] != 32 {
i = i + 1
}
out.push(decode_utf8(bytes.view(start~, end=i)))
}
out
}
///|
/// Whether a string is a non-empty RFC 7230 token (all `tchar` bytes).
fn relation_token_valid(token : String) -> Bool {
if token.length() == 0 {
return false
}
let bytes = @utf8.encode(token)
for i = 0; i < bytes.length(); i = i + 1 {
if !token_char(bytes[i]) {
return false
}
}
true
}
///|
/// Whether a relation type is in the registered (token) form.
pub fn relation_type_is_registered(rt : RelationType) -> Bool {
rt.is_registered()
}
///|
/// Whether a relation type is in the extension (absolute URI) form.
pub fn relation_type_is_extension(rt : RelationType) -> Bool {
rt.is_extension()
}