// message.mbt — HTTP message model (RequestContext / ResponseContext).
//
// These are *canonicalization* models, not a full HTTP parser. They hold
// exactly the fields RFC 9421's derived components need, in raw form, so that
// `@target-uri`, `@authority`, `@path`, `@query`, and `@request-target` can be
// emitted byte-for-byte without a URL library re-encoding anything.
///|
/// An HTTP request message captured for signature canonicalization.
pub(all) struct RequestContext {
method : String
scheme : String
authority : String
path : String
query : String?
headers : OrderedHeaders
body : Bytes?
}
///|
/// An HTTP response message captured for signature canonicalization.
///
/// `related_request` is optional and enables the `req` component parameter on
/// response signatures (RFC 9421 §3.2).
pub(all) struct ResponseContext {
status : Int
headers : OrderedHeaders
body : Bytes?
related_request : RequestContext?
}
///|
/// Validates and constructs a `RequestContext`.
///
/// The constructor performs the full validation suite: method token, scheme
/// grammar, CR/LF hygiene on authority/path/query, header safety, and body
/// size (when a body is present and `limits` is provided).
pub fn RequestContext::new(
method : String,
scheme : String,
authority : String,
path : String,
query : String?,
headers : OrderedHeaders,
body : Bytes?,
limits : Limits,
) -> Result[RequestContext, HsError] {
Ok(
request_context_build(
method, scheme, authority, path, query, headers, body, limits,
),
) catch {
e => Err(e)
}
}
///|
/// Internal: validates and builds a request context, raising on failure.
fn request_context_build(
method : String,
scheme : String,
authority : String,
path : String,
query : String?,
headers : OrderedHeaders,
body : Bytes?,
limits : Limits,
) -> RequestContext raise HsError {
check_method(method)
check_scheme(scheme)
check_authority(authority)
check_path(path)
if query is Some(_) {
check_query(query.unwrap())
}
match body {
Some(b) => limits.check_body_size_for_digest(b.length())
None => ()
}
{ method, scheme, authority, path, query, headers, body }
}
///|
/// Constructs a `RequestContext` using the default limits.
pub fn RequestContext::new_default(
method : String,
scheme : String,
authority : String,
path : String,
query : String?,
headers : OrderedHeaders,
body : Bytes?,
) -> Result[RequestContext, HsError] {
RequestContext::new(
method,
scheme,
authority,
path,
query,
headers,
body,
Limits::default(),
)
}
///|
/// Validates and constructs a `ResponseContext`.
///
/// Status codes must be in 100..=999 and header/body rules match requests.
pub fn ResponseContext::new(
status : Int,
headers : OrderedHeaders,
body : Bytes?,
related_request : RequestContext?,
limits : Limits,
) -> Result[ResponseContext, HsError] {
try {
check_status(status)
match body {
Some(b) => limits.check_body_size_for_digest(b.length())
None => ()
}
Ok({ status, headers, body, related_request })
} catch {
e => Err(e)
}
}
///|
/// Constructs a `ResponseContext` using the default limits.
pub fn ResponseContext::new_default(
status : Int,
headers : OrderedHeaders,
body : Bytes?,
related_request : RequestContext?,
) -> Result[ResponseContext, HsError] {
ResponseContext::new(
status,
headers,
body,
related_request,
Limits::default(),
)
}
///|
/// Builds the `@target-uri` value: `scheme://authority/path?query`.
///
/// The components are concatenated verbatim. In particular the path is used
/// as-is (including an empty path when the request-target was `*` or
/// `OPTIONS *`); no percent-encoding or normalization is applied.
pub fn RequestContext::target_uri(self : RequestContext) -> String {
let mut s = self.scheme + "://" + self.authority + self.path
match self.query {
Some(q) => if q.is_empty() { s = s + "?" } else { s = s + "?" + q }
None => ()
}
s
}
///|
/// Builds the `@request-target` value: `method SP request-target`.
///
/// The request-target is the origin-form `path?query` (with an empty path
/// represented as empty), preserved verbatim.
pub fn RequestContext::request_target(self : RequestContext) -> String {
let mut s = self.method + " "
s = s + self.path
match self.query {
Some(q) => if q.is_empty() { s = s + "?" } else { s = s + "?" + q }
None => ()
}
s
}
///|
/// The `@authority` component value (verbatim).
pub fn RequestContext::authority_component(self : RequestContext) -> String {
self.authority
}
///|
/// The `@path` component value (verbatim).
pub fn RequestContext::path_component(self : RequestContext) -> String {
self.path
}
///|
/// The raw query string without the leading `?`, or the empty string when the
/// request has no query.
pub fn RequestContext::raw_query(self : RequestContext) -> String {
match self.query {
Some(q) => q
None => ""
}
}
///|
/// The `@query` component value: the entire query string including the
/// leading `?` (RFC 9421 §2.2.7). When the query is absent, the value is a
/// lone `?`.
pub fn RequestContext::query_component(self : RequestContext) -> String {
"?" + self.raw_query()
}
///|
/// Returns `true` if the request has a query (including an empty one), which
/// is distinct from "no query" for RFC 9421 `@query` canonicalization.
pub fn RequestContext::has_query(self : RequestContext) -> Bool {
self.query is Some(_)
}