///|
/// An outgoing HTTP response containing the status code, headers, cookies, and body.
pub(all) struct HttpResponse {
/// The HTTP status code (e.g., `OK`, `NotFound`, `InternalServerError`).
mut status_code : StatusCode
/// Response headers as key-value pairs.
headers : Map[String, String]
/// Cookies to send via `Set-Cookie` headers.
cookies : Map[String, @cookie.CookieItem]
/// The raw response body bytes.
mut raw_body : Bytes
// TODO: better error message when using skip
} derive(Debug(ignore=Bytes))
///|
/// Deserializes the response body into a value of type `T` via the `BodyReader` trait.
pub fn[T : BodyReader] HttpResponse::read_body(self : HttpResponse) -> T raise {
T::from_request(
HttpRequest(Other(""), "", self.headers, raw_body=self.raw_body),
)
}
///|
/// Creates a new `HttpResponse` with the given status code and optional headers, cookies, and body.
pub fn HttpResponse::HttpResponse(
status_code~ : StatusCode,
headers? : Map[String, String],
cookies? : Map[String, @cookie.CookieItem],
raw_body? : Bytes,
) -> HttpResponse {
{
status_code,
headers: headers.unwrap_or({}),
cookies: cookies.unwrap_or({}),
raw_body: raw_body.unwrap_or(b""),
}
}
///|
/// Sets the response body from any `Responder` and returns the response for chaining.
///
/// Applies the responder's `options()` (e.g. sets Content-Type) and uses the
/// `output_bytes()` fast path when available to avoid an intermediate buffer copy.
pub fn HttpResponse::body(
self : HttpResponse,
body : &Responder,
) -> HttpResponse {
body.options(self)
match body.output_bytes() {
Some(bytes) => self.raw_body = bytes
None => {
let buf = Buffer()
body.output(buf)
self.raw_body = buf.to_bytes()
}
}
self
}
///|
/// Sets the response body to the JSON representation of the given value.
/// Also sets `Content-Type: application/json; charset=utf-8`.
pub fn HttpResponse::json(self : HttpResponse, obj : &ToJson) -> HttpResponse {
self.body(obj.to_json())
}
///|
/// Serializes a typed value as JSON, sets `Content-Type`, and returns the response.
///
/// Uses case-insensitive header matching: any existing `content-type` /
/// `CONTENT-TYPE` header is replaced, not duplicated.
pub fn[T : ToJson] HttpResponse::json_value(
self : HttpResponse,
value : T,
) -> HttpResponse {
@httputil.set_header_case_insensitive(
self.headers,
"Content-Type",
"application/json; charset=utf-8",
)
let json_str = value.to_json().stringify()
self.raw_body = @utf8.encode(json_str)
self
}
///|
/// Sets a response header and returns self for fluent chaining.
///
/// Uses case-insensitive matching: setting `Content-Type` will replace any
/// existing `content-type` or `CONTENT-TYPE` header.
pub fn HttpResponse::header(
self : HttpResponse,
name : String,
value : String,
) -> HttpResponse {
@httputil.set_header_case_insensitive(self.headers, name, value)
self
}
///|
/// Converts this response into a `Responder` trait object for use as a handler return value.
pub fn HttpResponse::to_responder(self : HttpResponse) -> &Responder {
self
}
///|
/// Sets a cookie on the response with the given name, value, and optional attributes.
pub fn HttpResponse::set_cookie(
self : HttpResponse,
name : String,
value : String,
max_age? : Int,
path? : String,
domain? : String,
secure? : Bool,
http_only? : Bool,
same_site? : @cookie.SameSiteOption,
) -> Unit {
let item = @cookie.CookieItem(
name~,
value~,
max_age?,
path?,
domain?,
secure?,
http_only?,
same_site?,
)
self.cookies.set(name, item)
}
///|
/// Deletes a cookie by setting its value to empty and max-age to 0.
pub fn HttpResponse::delete_cookie(self : HttpResponse, key : String) -> Unit {
self.set_cookie(key, "", max_age=0)
}
///|
test "read_response_body" {
let res = HttpResponse(status_code=OK, raw_body=b"{\"Hello\":\"Response\"}")
let text : String = res.read_body()
let json : Json = res.read_body()
debug_inspect(
text,
content=(
#|"{\"Hello\":\"Response\"}"
),
)
json_inspect(json, content={ "Hello": "Response" })
}