use alux_ext::ext;
use alux_http::{
BytesOutAlg, EmptyOutAlg, HtmlOutAlg, HttpApiAlg, HttpMethod, HttpProgramExt, HttpStatus, JsonOutAlg,
RedirectOutAlg, ResultOutAlg, StatusOutAlg, TextOutAlg, http,
};
use alux_http_direct::{DirectHandlerImpl, DirectRequest, DirectResponse, DirectRoute};
use alux_http_text::TextHandlerImpl;
use core::future::Future;
use serde::Deserialize;
use std::io::{Error as IoError, ErrorKind};
#[derive(Debug, Deserialize)]
struct Amount {
value: u32,
}
trait ShopAlg {
fn item(&self, id: u32) -> impl Future<Output = Result<u32, IoError>> + Send;
fn items(&self) -> impl Future<Output = Vec<u32>> + Send;
fn add(&self, value: u32) -> impl Future<Output = u32> + Send;
fn note(&self, note: String) -> impl Future<Output = String> + Send;
fn clear(&self) -> impl Future<Output = ()> + Send;
fn home(&self) -> impl Future<Output = String> + Send;
fn page(&self) -> impl Future<Output = String> + Send;
fn stored(&self) -> impl Future<Output = Vec<u8>> + Send;
fn filed(&self, under: String) -> impl Future<Output = String> + Send;
}
#[ext(name = ShopOperationExt, defunc)]
impl<This> This
where
This: ShopAlg,
{
async fn shop_item(&self, id: u32) -> Result<u32, IoError> {
self.item(id).await
}
async fn shop_items(&self) -> Vec<u32> {
self.items().await
}
async fn shop_add(&self, value: u32) -> u32 {
self.add(value).await
}
async fn shop_fill(&self, amount: Amount) -> u32 {
self.add(amount.value).await
}
async fn shop_note(&self, note: String) -> String {
self.note(note).await
}
async fn shop_clear(&self) {
self.clear().await;
}
async fn shop_home(&self) -> String {
self.home().await
}
async fn shop_page(&self) -> String {
self.page().await
}
async fn shop_stored(&self) -> Vec<u8> {
self.stored().await
}
async fn shop_filed(&self, under: String) -> String {
self.filed(under).await
}
}
#[ext(name = ShopApiExt, defunc(via = http))]
impl<This> This
where
This: HttpApiAlg
+ JsonOutAlg
+ TextOutAlg
+ HtmlOutAlg
+ BytesOutAlg
+ EmptyOutAlg
+ RedirectOutAlg
+ StatusOutAlg
+ ResultOutAlg,
{
fn shop_api<Alg>(&self)
where
Alg: ShopAlg,
{
self.routes()
.get("/item/:id", self.op(Alg::shop_item).path::<u32>().json().result())
.get("/items", self.op(Alg::shop_items).json())
.post("/items", self.op(Alg::shop_add).body::<u32>().json().status::<201>())
.put("/items", self.op(Alg::shop_fill).form::<Amount>().json())
.patch("/items", self.op(Alg::shop_note).raw_body::<String>().text())
.delete("/items", self.op(Alg::shop_clear).empty())
.get("/home", self.op(Alg::shop_home).redirect())
.get("/page", self.op(Alg::shop_page).html())
.get("/stored", self.op(Alg::shop_stored).bytes())
.get("/filed/{*under}", self.op(Alg::shop_filed).path::<String>().text())
}
}
struct Shop;
impl ShopAlg for Shop {
async fn item(&self, id: u32) -> Result<u32, IoError> {
match id {
1 => Ok(7),
_ => Err(IoError::new(ErrorKind::NotFound, "no such reading")),
}
}
async fn items(&self) -> Vec<u32> {
vec![7]
}
async fn add(&self, value: u32) -> u32 {
value
}
async fn note(&self, note: String) -> String {
format!("noted {note}")
}
async fn clear(&self) {}
async fn home(&self) -> String {
"/items".to_owned()
}
async fn page(&self) -> String {
"<p>7</p>".to_owned()
}
async fn stored(&self) -> Vec<u8> {
b"seven".to_vec()
}
async fn filed(&self, under: String) -> String {
format!("filed under {under}")
}
}
fn shop() -> DirectRoute {
let api = DirectHandlerImpl::new(Shop);
api.compile_http(api.shop_api::<Shop>())
}
async fn asked(method: HttpMethod, path: &str) -> DirectResponse {
shop().answer(DirectRequest::new(method, path)).await
}
async fn sent(method: HttpMethod, path: &str, content_type: &str, body: &str) -> DirectResponse {
let request = DirectRequest::new(method, path).with_header("content-type", content_type).with_body(body);
shop().answer(request).await
}
#[tokio::test]
async fn answers_every_method_the_declaration_states() {
let items = asked(HttpMethod::Get, "/items").await;
assert_eq!(items.status(), HttpStatus::OK);
assert_eq!(items.text(), "[7]");
let created = sent(HttpMethod::Post, "/items", "application/json", "7").await;
assert_eq!(created.status(), HttpStatus::new(201));
assert_eq!(created.text(), "7");
let filled = sent(HttpMethod::Put, "/items", "application/x-www-form-urlencoded", "value=7").await;
assert_eq!(filled.status(), HttpStatus::OK);
assert_eq!(filled.text(), "7");
let noted = sent(HttpMethod::Patch, "/items", "text/plain", "seven").await;
assert_eq!(noted.text(), "noted seven");
let cleared = asked(HttpMethod::Delete, "/items").await;
assert_eq!(cleared.status(), HttpStatus::NO_CONTENT);
assert!(cleared.body().is_empty());
}
#[tokio::test]
async fn binds_what_a_path_states_it_binds() {
let found = asked(HttpMethod::Get, "/item/1").await;
assert_eq!(found.status(), HttpStatus::OK);
assert_eq!(found.text(), "7");
let missing = asked(HttpMethod::Get, "/item/9").await;
assert_eq!(missing.status(), HttpStatus::NOT_FOUND);
assert_eq!(missing.text(), "no such reading");
let filed = asked(HttpMethod::Get, "/filed/two/levels/down").await;
assert_eq!(filed.text(), "filed under two/levels/down");
}
#[tokio::test]
async fn answers_for_itself_where_no_endpoint_is_declared() {
let nowhere = asked(HttpMethod::Get, "/nothing").await;
assert_eq!(nowhere.status(), HttpStatus::NOT_FOUND);
let wrong_method = asked(HttpMethod::Head, "/items").await;
assert_eq!(wrong_method.status(), HttpStatus::METHOD_NOT_ALLOWED);
let deeper = asked(HttpMethod::Get, "/items/7").await;
assert_eq!(deeper.status(), HttpStatus::NOT_FOUND);
let unreadable = asked(HttpMethod::Get, "/item/seven").await;
assert_eq!(unreadable.status(), HttpStatus::BAD_REQUEST);
}
#[tokio::test]
async fn answers_with_what_each_output_kind_states() {
let home = asked(HttpMethod::Get, "/home").await;
assert_eq!(home.status(), HttpStatus::SEE_OTHER);
assert_eq!(home.header("location"), Some("/items"));
let page = asked(HttpMethod::Get, "/page").await;
assert_eq!(page.header("content-type"), Some("text/html; charset=utf-8"));
assert_eq!(page.text(), "<p>7</p>");
let stored = asked(HttpMethod::Get, "/stored").await;
assert_eq!(stored.header("content-type"), Some("application/octet-stream"));
assert_eq!(stored.body(), b"seven");
}
#[test]
fn agrees_with_the_text_interpretation_on_the_same_program() {
let text = TextHandlerImpl;
let described = text.compile_http(text.shop_api::<Shop>());
assert_eq!(shop().labels(), described.labels());
assert_eq!(
shop().labels(),
[
"GET /item/{id}",
"GET /items",
"POST /items",
"PUT /items",
"PATCH /items",
"DELETE /items",
"GET /home",
"GET /page",
"GET /stored",
"GET /filed/{*under}",
]
);
}