// error.mbt — Structured error model for moon-weblink.
//
// RFC 8288 Web Linking / RFC 9264 Linkset processing has many distinct
// failure modes across several processing stages. Public APIs never return
// `Result[T, String]`; they return `Result[T, LinkError]` so callers can
// dispatch on a stable (stage, kind) pair, an optional UTF-8 byte offset,
// and a short context string.
//
// `LinkError` is declared as a `suberror` (MoonBit's raise-based error
// type). Internal processing functions use `raise LinkError` and let errors
// propagate through the call stack without plumbing; every public boundary
// function wraps those internals in `try`/`catch` and converts to
// `Result[T, LinkError]`.
//
// Offsets are UTF-8 byte offsets into the exact input string that was
// passed to the public entry point. The offset is `0` when it is not
// meaningful for the kind of error (for example registry lookups). The
// `context` string is a short, truncated excerpt of the input around the
// failure point; it never echoes the whole (possibly megabytes-long) input.
///|
/// The processing stage in which an error was detected.
pub(all) enum LinkErrorStage {
Input
Header
LinkValue
Target
Parameter
Relation
QuotedString
ExtendedValue
UriReference
LinksetText
LinksetJson
Registry
Limit
} derive(Eq)
///|
/// The concrete error category. Stable across versions so callers can
/// switch on it without string matching.
pub(all) enum LinkErrorKind {
EmptyInput
UnexpectedCharacter
ExpectedAngleBracket
UnterminatedTarget
InvalidTarget
InvalidToken
MissingParameterName
InvalidParameter
UnterminatedQuotedString
InvalidQuotedPair
InvalidRelation
InvalidExtensionRelation
InvalidPercentEncoding
UnsupportedCharset
InvalidUtf8
InvalidLanguageTag
InvalidJson
InvalidJsonShape
InvalidLinkset
DuplicateParameter
LimitExceeded
TrailingInput
InvalidContextValue
InvalidMediaType
} derive(Eq)
///|
/// A structured error returned by every public API.
///
/// - `stage()` — where the failure happened.
/// - `kind()` — what failed.
/// - `offset()` — UTF-8 byte offset into the input, `0` when not meaningful.
/// - `context()` — short excerpt of the input at the failure point
/// (bounded, never the full input).
pub(all) suberror LinkError {
LinkError(LinkErrorStage, LinkErrorKind, Int, String)
}
///|
/// Constructs a `LinkError` with the given stage, kind and context and a
/// zero offset.
pub fn link_error(
stage : LinkErrorStage,
kind : LinkErrorKind,
context : String,
) -> LinkError {
LinkError(stage, kind, 0, context)
}
///|
/// Constructs a `LinkError` carrying an explicit UTF-8 byte offset.
pub fn link_error_at(
stage : LinkErrorStage,
kind : LinkErrorKind,
offset : Int,
context : String,
) -> LinkError {
LinkError(stage, kind, offset, context)
}
///|
/// The stage in which this error was detected.
pub fn LinkError::stage(self : LinkError) -> LinkErrorStage {
match self {
LinkError(stage, _, _, _) => stage
}
}
///|
/// The concrete kind of this error.
pub fn LinkError::kind(self : LinkError) -> LinkErrorKind {
match self {
LinkError(_, kind, _, _) => kind
}
}
///|
/// The UTF-8 byte offset into the input where the error was detected,
/// or `0` when the offset is not meaningful for this error kind.
pub fn LinkError::offset(self : LinkError) -> Int {
match self {
LinkError(_, _, offset, _) => offset
}
}
///|
/// A short, bounded excerpt of the input around the failure point.
pub fn LinkError::context(self : LinkError) -> String {
match self {
LinkError(_, _, _, context) => context
}
}
///|
/// A single-line human readable rendering of the error, intended for
/// terminal output and CLI use.
pub fn LinkError::to_display(self : LinkError) -> String {
let LinkError(stage, kind, offset, context) = self
"\{stage.to_string()}::\{kind.to_string()} at byte \{offset}: \{context}"
}
///|
/// Stable programmatic name for a stage, used by the CLI JSON output.
pub fn LinkErrorStage::to_string(self : LinkErrorStage) -> String {
match self {
Input => "Input"
Header => "Header"
LinkValue => "LinkValue"
Target => "Target"
Parameter => "Parameter"
Relation => "Relation"
QuotedString => "QuotedString"
ExtendedValue => "ExtendedValue"
UriReference => "UriReference"
LinksetText => "LinksetText"
LinksetJson => "LinksetJson"
Registry => "Registry"
Limit => "Limit"
}
}
///|
/// Stable programmatic name for a kind, used by the CLI JSON output.
pub fn LinkErrorKind::to_string(self : LinkErrorKind) -> String {
match self {
EmptyInput => "EmptyInput"
UnexpectedCharacter => "UnexpectedCharacter"
ExpectedAngleBracket => "ExpectedAngleBracket"
UnterminatedTarget => "UnterminatedTarget"
InvalidTarget => "InvalidTarget"
InvalidToken => "InvalidToken"
MissingParameterName => "MissingParameterName"
InvalidParameter => "InvalidParameter"
UnterminatedQuotedString => "UnterminatedQuotedString"
InvalidQuotedPair => "InvalidQuotedPair"
InvalidRelation => "InvalidRelation"
InvalidExtensionRelation => "InvalidExtensionRelation"
InvalidPercentEncoding => "InvalidPercentEncoding"
UnsupportedCharset => "UnsupportedCharset"
InvalidUtf8 => "InvalidUtf8"
InvalidLanguageTag => "InvalidLanguageTag"
InvalidJson => "InvalidJson"
InvalidJsonShape => "InvalidJsonShape"
InvalidLinkset => "InvalidLinkset"
DuplicateParameter => "DuplicateParameter"
LimitExceeded => "LimitExceeded"
TrailingInput => "TrailingInput"
InvalidContextValue => "InvalidContextValue"
InvalidMediaType => "InvalidMediaType"
}
}
///|
/// Maximum length of the context excerpt stored inside an error. Longer
/// inputs are truncated so that errors never carry megabytes of input.
pub fn max_context_bytes() -> Int {
80
}
///|
/// Downcasts the abstract `Error` caught in a `catch` clause back to the
/// `LinkError` suberror. Every function in this package raises `LinkError`
/// exclusively, so any other suberror reaching this boundary is a bug and is
/// reported loudly rather than silently lost.
fn unwrap_link_error(e : Error) -> LinkError {
match e {
LinkError(stage, kind, offset, context) =>
LinkError(stage, kind, offset, context)
_ =>
abort(
"internal error: unexpected non-LinkError propagated to a Result boundary",
)
}
}