///|
/// Fluent builders for JRDs and links.
///
/// The builders collect members step by step and validate everything on
/// `build()`, which always returns a `Result` — no `unwrap`, no panic.
/// Validation applies the RFC constraints plus `Limits::default()`
/// bounds:
///
/// * subject and aliases must be absolute URIs (RFC 7033 Sections 4.4.1
/// and 4.4.2);
/// * property identifiers must be URIs (Sections 4.4.3 and 4.4.4.5);
/// * rel must be exactly one URI or registered relation type (Section
/// 4.4.4.1);
/// * href must be an absolute URI (Section 4.4.4.3);
/// * title keys must be plausible language tags or `und` (Section
/// 4.4.4.4);
/// * extension member names must not collide with reserved members
/// (DuplicateReservedExtension).
///
/// Setters store values without validation so callers can build
/// incrementally; only `build()` reports problems.
///|
/// Builds a `JsonResourceDescriptor` fluently.
pub struct JrdBuilder {
mut subject : String?
aliases : Array[String]
properties : Map[String, PropertyValue]
links : Array[JrdLink]
extensions : Map[String, Json]
}
///|
/// Start an empty JRD builder.
pub fn JrdBuilder::new() -> JrdBuilder {
{
subject: None,
aliases: [],
properties: Map([]),
links: [],
extensions: Map([]),
}
}
///|
/// Set the subject (validated on build).
pub fn JrdBuilder::subject(self : JrdBuilder, s : String) -> JrdBuilder {
self.subject = Some(s)
self
}
///|
/// Add an alias (validated on build).
pub fn JrdBuilder::alias(self : JrdBuilder, a : String) -> JrdBuilder {
self.aliases.push(a)
self
}
///|
/// Add a subject-level property with an explicit value (name validated
/// on build).
pub fn JrdBuilder::property(
self : JrdBuilder,
uri : String,
value : PropertyValue,
) -> JrdBuilder {
self.properties.set(uri, value)
self
}
///|
/// Add a subject-level string property (name validated on build).
pub fn JrdBuilder::property_string(
self : JrdBuilder,
uri : String,
s : String,
) -> JrdBuilder {
self.properties.set(uri, StringValue(s))
self
}
///|
/// Add a subject-level null property (name validated on build).
pub fn JrdBuilder::property_null(self : JrdBuilder, uri : String) -> JrdBuilder {
self.properties.set(uri, NullValue)
self
}
///|
/// Add a link (the link itself is validated on build).
pub fn JrdBuilder::link(self : JrdBuilder, link : JrdLink) -> JrdBuilder {
self.links.push(link)
self
}
///|
/// Add an unknown (extension) top-level member, preserved for
/// round-tripping. The name must not collide with a reserved member
/// (checked on build).
pub fn JrdBuilder::extension(
self : JrdBuilder,
name : String,
value : Json,
) -> JrdBuilder {
self.extensions.set(name, value)
self
}
///|
/// Build the JRD, validating subject, aliases, property names, links and
/// extension names, and enforcing `Limits::default()` bounds.
pub fn JrdBuilder::build(
self : JrdBuilder,
) -> Result[JsonResourceDescriptor, WebFingerError] {
let limits = Limits::default()
match self.subject {
None => ()
Some(s) =>
match check_absolute_uri(s) {
Ok(_) =>
if utf8_byte_length(s) > limits.max_subject_bytes {
return Err(
WebFingerError(
Subject,
LimitExceeded,
None,
"subject exceeds \{limits.max_subject_bytes} bytes",
),
)
}
Err(_) =>
return Err(
WebFingerError(
Subject,
InvalidUri,
None,
"subject must be an absolute URI",
),
)
}
}
if self.aliases.length() > limits.max_aliases {
return Err(
WebFingerError(
Alias,
LimitExceeded,
None,
"too many aliases (\{self.aliases.length()} > \{limits.max_aliases})",
),
)
}
for alias in self.aliases {
match check_absolute_uri(alias) {
Ok(_) =>
if utf8_byte_length(alias) > limits.max_alias_bytes {
return Err(
WebFingerError(
Alias,
LimitExceeded,
None,
"alias exceeds \{limits.max_alias_bytes} bytes",
),
)
}
Err(_) =>
return Err(
WebFingerError(
Alias,
InvalidUri,
None,
"alias must be an absolute URI: \{alias}",
),
)
}
}
if self.properties.length() > limits.max_properties {
return Err(
WebFingerError(
Property,
LimitExceeded,
None,
"too many properties (\{self.properties.length()} > \{limits.max_properties})",
),
)
}
for name, value in self.properties {
match check_absolute_uri(name) {
Ok(_) => ()
Err(_) =>
return Err(
WebFingerError(
Property,
InvalidProperty,
None,
"property name must be a URI: \{name}",
),
)
}
match value {
StringValue(s) =>
if utf8_byte_length(s) > limits.max_property_value_bytes {
return Err(
WebFingerError(
Property,
LimitExceeded,
None,
"property value exceeds \{limits.max_property_value_bytes} bytes",
),
)
}
NullValue => ()
}
}
if self.links.length() > limits.max_links {
return Err(
WebFingerError(
Link,
LimitExceeded,
None,
"too many links (\{self.links.length()} > \{limits.max_links})",
),
)
}
for link in self.links {
match check_link_model(link, limits) {
Ok(_) => ()
Err(e) => return Err(e)
}
}
if self.extensions.length() > limits.max_extension_members {
return Err(
WebFingerError(
Builder,
LimitExceeded,
None,
"too many extension members (\{self.extensions.length()} > \{limits.max_extension_members})",
),
)
}
for name in self.extensions.keys() {
if is_reserved(name, reserved_top_level()) {
return Err(
WebFingerError(
Builder,
DuplicateReservedExtension,
None,
"extension member name is reserved: \{name}",
),
)
}
}
Ok({
subject: self.subject,
aliases: self.aliases,
properties: self.properties,
links: self.links,
extensions: self.extensions,
})
}
///|
/// Internal: validate a link model (used by JrdBuilder and by the CLI's
/// validation paths). Checks rel, href, title keys, property names,
/// extension names and link-level limits.
fn check_link_model(
link : JrdLink,
limits : Limits,
) -> Result[Unit, WebFingerError] {
match check_rel_value(link.rel) {
Ok(_) => ()
Err(_) =>
return Err(
WebFingerError(
Link,
InvalidRelValue,
None,
"rel must be a URI or a registered relation type: \{link.rel}",
),
)
}
match link.href {
None => ()
Some(href) =>
match check_absolute_uri(href) {
Ok(_) =>
if utf8_byte_length(href) > limits.max_string_bytes {
return Err(
WebFingerError(
Link,
LimitExceeded,
None,
"href exceeds \{limits.max_string_bytes} bytes",
),
)
}
Err(_) =>
return Err(
WebFingerError(
Link,
InvalidUri,
None,
"href must be an absolute URI: \{href}",
),
)
}
}
if link.titles.length() > limits.max_titles_per_link {
return Err(
WebFingerError(
Link,
LimitExceeded,
None,
"too many titles (\{link.titles.length()} > \{limits.max_titles_per_link})",
),
)
}
for tag, text in link.titles {
if !is_plausible_language_tag(tag) {
return Err(
WebFingerError(
Link,
InvalidLanguageTag,
None,
"title key is not a plausible language tag or 'und': \{tag}",
),
)
}
if utf8_byte_length(text) > limits.max_string_bytes {
return Err(
WebFingerError(
Link,
LimitExceeded,
None,
"title text exceeds \{limits.max_string_bytes} bytes",
),
)
}
}
if link.properties.length() > limits.max_properties_per_link {
return Err(
WebFingerError(
Link,
LimitExceeded,
None,
"too many link properties (\{link.properties.length()} > \{limits.max_properties_per_link})",
),
)
}
for name, value in link.properties {
match check_absolute_uri(name) {
Ok(_) => ()
Err(_) =>
return Err(
WebFingerError(
Property,
InvalidProperty,
None,
"link property name must be a URI: \{name}",
),
)
}
match value {
StringValue(s) =>
if utf8_byte_length(s) > limits.max_property_value_bytes {
return Err(
WebFingerError(
Property,
LimitExceeded,
None,
"link property value exceeds \{limits.max_property_value_bytes} bytes",
),
)
}
NullValue => ()
}
}
for name in link.extensions.keys() {
if is_reserved(name, reserved_link_members()) {
return Err(
WebFingerError(
Builder,
DuplicateReservedExtension,
None,
"extension member name is reserved: \{name}",
),
)
}
}
Ok(())
}
///|
/// Builds a `JrdLink` fluently.
pub struct JrdLinkBuilder {
rel : String
mut media_type : String?
mut href : String?
titles : Map[String, String]
properties : Map[String, PropertyValue]
extensions : Map[String, Json]
}
///|
/// Start a link builder with its (required) relation type, validated on
/// build.
pub fn JrdLinkBuilder::new(rel : String) -> JrdLinkBuilder {
{
rel,
media_type: None,
href: None,
titles: Map([]),
properties: Map([]),
extensions: Map([]),
}
}
///|
/// Set the target media type (the `type` member).
pub fn JrdLinkBuilder::media_type(
self : JrdLinkBuilder,
t : String,
) -> JrdLinkBuilder {
self.media_type = Some(t)
self
}
///|
/// Set the target URI (validated on build).
pub fn JrdLinkBuilder::href(
self : JrdLinkBuilder,
h : String,
) -> JrdLinkBuilder {
self.href = Some(h)
self
}
///|
/// Add a title in a language (key validated on build; `und` allowed).
pub fn JrdLinkBuilder::title(
self : JrdLinkBuilder,
language : String,
text : String,
) -> JrdLinkBuilder {
self.titles.set(language, text)
self
}
///|
/// Add a link property with an explicit value (name validated on build).
pub fn JrdLinkBuilder::property(
self : JrdLinkBuilder,
uri : String,
value : PropertyValue,
) -> JrdLinkBuilder {
self.properties.set(uri, value)
self
}
///|
/// Add a string link property.
pub fn JrdLinkBuilder::property_string(
self : JrdLinkBuilder,
uri : String,
s : String,
) -> JrdLinkBuilder {
self.properties.set(uri, StringValue(s))
self
}
///|
/// Add a null link property.
pub fn JrdLinkBuilder::property_null(
self : JrdLinkBuilder,
uri : String,
) -> JrdLinkBuilder {
self.properties.set(uri, NullValue)
self
}
///|
/// Add an unknown link member, preserved for round-tripping (name
/// checked on build).
pub fn JrdLinkBuilder::extension(
self : JrdLinkBuilder,
name : String,
value : Json,
) -> JrdLinkBuilder {
self.extensions.set(name, value)
self
}
///|
/// Build the link, validating rel, href, title keys, property names and
/// extension names.
pub fn JrdLinkBuilder::build(
self : JrdLinkBuilder,
) -> Result[JrdLink, WebFingerError] {
let link : JrdLink = {
rel: self.rel,
media_type: self.media_type,
href: self.href,
titles: self.titles,
properties: self.properties,
extensions: self.extensions,
}
match check_link_model(link, Limits::default()) {
Ok(_) => Ok(link)
Err(e) => Err(e)
}
}