// error.mbt — Structured error model for moon-content-disposition.
//
// Content-Disposition processing has many distinct failure modes across
// several processing stages. Public APIs never return `Result[T, String]`;
// they return `Result[T, DispositionError]` so callers can dispatch on a
// stable (stage, kind) pair, an optional UTF-8 byte offset, and a short
// context string.
//
// `DispositionError` is declared as a `suberror` (MoonBit's raise-based
// error type). Internal processing functions use `raise DispositionError`
// 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, DispositionError]`.
//
// 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 limit checks that do not
// map to a single byte). The `context` string is a short, truncated excerpt
// of the input around the failure point; it never echoes the whole
// (possibly megabytes-long) header.
///|
/// The processing stage in which an error was detected.
pub enum DispositionErrorStage {
Input
DispositionType
ParameterName
ParameterValue
Token
QuotedString
ExtendedValue
PercentEncoding
Charset
FilenameResolution
FilenamePolicy
Serialization
Limit
}
///|
/// The concrete error category. Stable across versions so callers can
/// switch on it without string matching.
pub enum DispositionErrorKind {
EmptyInput
InvalidDispositionType
ExpectedToken
UnexpectedCharacter
MissingEquals
MissingParameterValue
InvalidParameterName
DuplicateParameter
UnterminatedQuotedString
InvalidQuotedPair
InvalidControlCharacter
InvalidExtendedValue
MissingCharset
InvalidCharset
UnsupportedCharset
InvalidLanguage
InvalidPercentEncoding
InvalidUtf8
InvalidFilename
UnsafeFilename
LimitExceeded
TrailingInput
}
///|
/// 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 suberror DispositionError {
DispositionError(DispositionErrorStage, DispositionErrorKind, Int, String)
}
///|
/// Constructs a `DispositionError` with the given stage, kind and context
/// and a zero offset.
pub fn disposition_error(
stage : DispositionErrorStage,
kind : DispositionErrorKind,
context : String
) -> DispositionError {
DispositionError(stage, kind, 0, context)
}
///|
/// Constructs a `DispositionError` carrying an explicit UTF-8 byte offset.
pub fn disposition_error_at(
stage : DispositionErrorStage,
kind : DispositionErrorKind,
offset : Int,
context : String
) -> DispositionError {
DispositionError(stage, kind, offset, context)
}
///|
/// The stage in which this error was detected.
pub fn DispositionError::stage(self : DispositionError) -> DispositionErrorStage {
match self {
DispositionError(stage, _, _, _) => stage
}
}
///|
/// The concrete kind of this error.
pub fn DispositionError::kind(self : DispositionError) -> DispositionErrorKind {
match self {
DispositionError(_, 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 DispositionError::offset(self : DispositionError) -> Int {
match self {
DispositionError(_, _, offset, _) => offset
}
}
///|
/// A short, bounded excerpt of the input around the failure point.
pub fn DispositionError::context(self : DispositionError) -> String {
match self {
DispositionError(_, _, _, context) => context
}
}
///|
/// A single-line human readable rendering of the error, intended for
/// terminal output and CLI use.
pub fn DispositionError::to_display(self : DispositionError) -> String {
let DispositionError(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 DispositionErrorStage::to_string(self : DispositionErrorStage) -> String {
match self {
Input => "Input"
DispositionType => "DispositionType"
ParameterName => "ParameterName"
ParameterValue => "ParameterValue"
Token => "Token"
QuotedString => "QuotedString"
ExtendedValue => "ExtendedValue"
PercentEncoding => "PercentEncoding"
Charset => "Charset"
FilenameResolution => "FilenameResolution"
FilenamePolicy => "FilenamePolicy"
Serialization => "Serialization"
Limit => "Limit"
}
}
///|
/// Stable programmatic name for a kind, used by the CLI JSON output.
pub fn DispositionErrorKind::to_string(self : DispositionErrorKind) -> String {
match self {
EmptyInput => "EmptyInput"
InvalidDispositionType => "InvalidDispositionType"
ExpectedToken => "ExpectedToken"
UnexpectedCharacter => "UnexpectedCharacter"
MissingEquals => "MissingEquals"
MissingParameterValue => "MissingParameterValue"
InvalidParameterName => "InvalidParameterName"
DuplicateParameter => "DuplicateParameter"
UnterminatedQuotedString => "UnterminatedQuotedString"
InvalidQuotedPair => "InvalidQuotedPair"
InvalidControlCharacter => "InvalidControlCharacter"
InvalidExtendedValue => "InvalidExtendedValue"
MissingCharset => "MissingCharset"
InvalidCharset => "InvalidCharset"
UnsupportedCharset => "UnsupportedCharset"
InvalidLanguage => "InvalidLanguage"
InvalidPercentEncoding => "InvalidPercentEncoding"
InvalidUtf8 => "InvalidUtf8"
InvalidFilename => "InvalidFilename"
UnsafeFilename => "UnsafeFilename"
LimitExceeded => "LimitExceeded"
TrailingInput => "TrailingInput"
}
}
///|
/// Unwraps a `Result` inside a `raise` function, re-raising the structured
/// error. This is the MoonBit 0.1.2026 equivalent of the `?` operator: the
/// public boundary functions below use it to convert the internal
/// raise-based error flow into `Result[T, DispositionError]`.
fn[T] unwrap_or_raise(r : Result[T, DispositionError]) -> T raise {
match r {
Ok(v) => v
Err(e) => raise e
}
}
///|
/// Downcasts the abstract `Error` caught in a `catch` clause back to the
/// concrete `DispositionError`. Every function in this package raises
/// `DispositionError` exclusively, so any other suberror reaching this
/// boundary is a bug and is reported loudly rather than silently lost.
fn unwrap_disposition_error(e : Error) -> DispositionError {
match e {
DispositionError(stage, kind, offset, context) =>
DispositionError(stage, kind, offset, context)
_ => abort("internal error: unexpected non-DispositionError propagated to a Result boundary")
}
}