import {
"bobzhang/crescent/core"
...
}///|
test "construct and inspect a request" {
let req = @core.HttpRequest(
Get,
"/users?page=2",
{ "Authorization": "Bearer token" },
raw_body=b"",
)
debug_inspect(
req,
content=(
#|{
#| http_method: Get,
#| url: "/users?page=2",
#| headers: { "Authorization": "Bearer token" },
#| raw_body: ...,
#| cached_path: None,
#| cached_query_params: None,
#|}
),
)
assert_eq(req.path(), "/users")
assert_eq(req.query_string(), Some("page=2"))
assert_eq(req.get_query("page"), Some("2"))
assert_eq(req.get_header("authorization"), Some("Bearer token"))
}///|
test "query parameters are percent-decoded" {
let req = @core.HttpRequest(
Get,
"/search?q=hello%20world&lang=en",
{},
raw_body=b"",
)
|> x => { (x.get_query("q"), x.get_query("lang"), x.get_query("missing")) }
debug_inspect(
req,
content=(
#|(Some("hello world"), Some("en"), None)
),
)
}///|
test "read body as string" {
let req = @core.HttpRequest(Post, "/", {}, raw_body=b"hello")
let text : String = req.body()
assert_eq(text, "hello")
}///|
test "read body as JSON" {
let req = @core.HttpRequest(Post, "/", {}, raw_body=b"{\"name\":\"Alice\"}")
let body : Json = req.body()
debug_inspect(
body,
content=(
#|Object({ "name": String("Alice") })
),
)
}///|
#warnings("-unnecessary_annotation")
struct CoreDocUser {
name : String
age : Int
} derive(FromJson, Eq, Debug)
///|
test "json shorthand parses typed value" {
let req = @core.HttpRequest(
Post,
"/",
{},
raw_body=b"{\"name\":\"Bob\",\"age\":30}",
)
let user : CoreDocUser = req.json()
debug_inspect(
user,
content=(
#|{ name: "Bob", age: 30 }
),
)
}///|
test "read a cookie from the request" {
let req = @core.HttpRequest(
Get,
"/",
{ "Cookie": "session=abc123; theme=dark" },
raw_body=b"",
)
guard req.get_cookie("session") is Some(c) else {
fail("expected session cookie")
}
assert_eq(c.value, "abc123")
assert_eq(req.get_cookie("missing"), None)
}///|
test "factory methods set the right status code" {
assert_eq(@core.HttpResponse::ok().status_code, OK)
assert_eq(@core.HttpResponse::created().status_code, Created)
assert_eq(@core.HttpResponse::not_found().status_code, NotFound)
assert_eq(@core.HttpResponse::bad_request().status_code, BadRequest)
assert_eq(
@core.HttpResponse::internal_server_error().status_code,
InternalServerError,
)
}///|
test "fluent response building" {
let res = @core.HttpResponse::ok()
.header("X-Request-Id", "abc-123")
.body(@core.html("<h1>Hello</h1>"))
assert_eq(res.status_code, OK)
debug_inspect(
res.headers.get("Content-Type"),
content="Some(\"text/html; charset=utf-8\")",
)
debug_inspect(res.headers.get("X-Request-Id"), content="Some(\"abc-123\")")
}///|
test "json response sets content type and body" {
let res = @core.HttpResponse::ok().json(({ "status": "ok" } : Json))
debug_inspect(
res.headers.get("Content-Type"),
content="Some(\"application/json; charset=utf-8\")",
)
}///|
test "redirect helpers" {
let r301 = @core.HttpResponse::redirect("/new")
debug_inspect(
r301,
content=(
#|{
#| status_code: MovedPermanently,
#| headers: { "Location": "/new" },
#| cookies: {},
#| raw_body: ...,
#|}
),
)
let r302 = @core.HttpResponse::redirect_temporary("/temp")
assert_eq(r302.status_code, Found)
let r307 = @core.HttpResponse::redirect_307("/keep-method")
assert_eq(r307.status_code, TemporaryRedirect)
}///|
test "set and delete cookies" {
let res = @core.HttpResponse::ok()
res.set_cookie("session", "xyz", path="/", http_only=true, secure=true)
guard res.cookies.get("session") is Some(c) else {
fail("expected session cookie")
}
assert_eq(c.value, "xyz")
assert_eq(c.path, Some("/"))
assert_eq(c.http_only, Some(true))
// Deleting sets Max-Age=0
res.delete_cookie("session")
guard res.cookies.get("session") is Some(deleted) else {
fail("expected deleted cookie")
}
assert_eq(deleted.max_age, Some(0))
}///|
test "error helper creates structured JSON body" {
let res = @core.HttpResponse::error(BadRequest, "name is required")
assert_eq(res.status_code, BadRequest)
let body : String = res.read_body()
assert_true(body.contains("name is required"))
assert_true(body.contains("400"))
}///|
test "status code round-trips through integer" {
assert_eq(@core.StatusCode::from_int(200), OK)
assert_eq(@core.StatusCode::from_int(404), NotFound)
let ok : @core.StatusCode = OK
assert_eq(ok.to_int(), 200)
// Unknown codes become Custom
assert_eq(@core.StatusCode::from_int(599), Custom(599))
}///|
test "method round-trips through string" {
assert_eq(@core.HttpMethod::from_string("GET"), Get)
let get : @core.HttpMethod = Get
assert_eq(get.to_method_string(), "GET")
// Unknown methods use Other
let custom = @core.HttpMethod::from_string("PURGE")
assert_eq(custom.to_method_string(), "PURGE")
}///|
pub(open) trait Responder {
fn options(Self, HttpResponse) -> Unit
fn output(Self, Buffer) -> Unit
fn output_bytes(Self) -> Bytes?
}| Method | Purpose |
|---|---|
| options(res) | Set status code and headers (e.g. Content-Type) on the response |
| output(buf) | Write the body bytes into a buffer |
| output_bytes() | Return pre-encoded bytes directly (fast path, avoids a copy) |
| Type | Content-Type |
|---|---|
| String / StringView | text/plain; charset=utf-8 |
| Json / &ToJson | application/json; charset=utf-8 |
| Bytes | application/octet-stream |
| HttpResponse | Copies status, merges headers, forwards body |
| HttpRequest | Merges headers, forwards body (useful for proxying) |
| Html (via html()) | text/html; charset=utf-8 |
///|
test "string responder sets text/plain" {
let res = @core.HttpResponse(status_code=OK)
let responder : &@core.Responder = "hello"
responder.options(res)
debug_inspect(
res.headers.get("Content-Type"),
content="Some(\"text/plain; charset=utf-8\")",
)
}///|
test "html helper sets text/html content type" {
let res = @core.HttpResponse(status_code=OK)
let responder = @core.html("<h1>Hello</h1>")
responder.options(res)
debug_inspect(
res.headers.get("Content-Type"),
content="Some(\"text/html; charset=utf-8\")",
)
let buf = Buffer()
responder.output(buf)
assert_eq(buf.contents(), @utf8.encode("<h1>Hello</h1>"))
}///|
pub(open) trait BodyReader {
fn from_request(HttpRequest) -> Self raise
}| Type | Behavior |
|---|---|
| String | UTF-8 decode (raises on invalid bytes) |
| Json | Parse as JSON value |
| Bytes | Return raw bytes as-is |
| FixedArray[Byte] | Convert to fixed-size byte array |
| Array[Byte] | Convert to resizable byte array |
///|
test "read response body as typed value" {
let res = @core.HttpResponse(status_code=OK, raw_body=b"hello")
let text : String = res.read_body()
assert_eq(text, "hello")
}impl BodyReader for Stringimpl BodyReader for FixedArray[Byte]impl BodyReader for Bytesimpl BodyReader for Array[Byte]impl BodyReader for Jsonpub(open) trait Responder {
fn options(Self, res : HttpResponse) -> Unit
fn output(Self, buf : Buffer) -> Unit
fn output_bytes(Self) -> Bytes?
}fn output_bytes(_self : String) -> Bytes?fn output_bytes(self : Bytes) -> Bytes?impl Responder for StringViewfn output_bytes(_self : StringView) -> Bytes?type Htmlimpl Show for HttpMethodpub(all) struct HttpRequest {
http_method : HttpMethod
url : String
headers : Map[String, String]
raw_body : Bytes
// private fields
} derive(Debug)impl Responder for HttpRequestfn HttpRequest::HttpRequest(http_method : HttpMethod, url : String, headers : Map[String, String], raw_body? : Bytes) -> HttpRequestfn HttpRequest::from_method_string(http_method : String, url : String, headers : Map[String, String], raw_body : Bytes) -> HttpRequestpub(all) struct HttpResponse {
status_code : StatusCode
headers : Map[String, String]
cookies : Map[String, CookieItem]
raw_body : Bytes
} derive(Debug)impl Responder for HttpResponsefn HttpResponse::HttpResponse(status_code~ : StatusCode, headers? : Map[String, String], cookies? : Map[String, CookieItem], raw_body? : Bytes) -> HttpResponsefn HttpResponse::set_cookie(self : HttpResponse, name : String, value : String, max_age? : Int, path? : String, domain? : String, secure? : Bool, http_only? : Bool, same_site? : SameSiteOption) -> Unitpub(all) enum StatusCode {
Continue
SwitchingProtocols
Processing
EarlyHints
OK
Created
Accepted
NonAuthoritativeInfo
NoContent
ResetContent
PartialContent
MultiStatus
AlreadyReported
IMUsed
MultipleChoices
MovedPermanently
Found
SeeOther
NotModified
UseProxy
TemporaryRedirect
PermanentRedirect
BadRequest
Unauthorized
PaymentRequired
Forbidden
NotFound
MethodNotAllowed
NotAcceptable
ProxyAuthRequired
RequestTimeout
Conflict
Gone
LengthRequired
PreconditionFailed
RequestEntityTooLarge
RequestUriTooLong
UnsupportedMediaType
RequestedRangeNotSatisfiable
ExpectationFailed
Teapot
MisdirectedRequest
UnprocessableEntity
Locked
FailedDependency
TooEarly
UpgradeRequired
PreconditionRequired
TooManyRequests
RequestHeaderFieldsTooLarge
UnavailableForLegalReasons
InternalServerError
NotImplemented
BadGateway
ServiceUnavailable
GatewayTimeout
HttpVersionNotSupported
VariantAlsoNegotiates
InsufficientStorage
LoopDetected
NotExtended
NetworkAuthenticationRequired
Custom(Int)
} derive(Eq, ToJson, Debug)impl Show for StatusCodeCrescent: A web framework for MoonBit.
Dependencies