interfacer-http 0.2.2

A magic HTTP client, like retrofit in Java
Documentation
#![feature(custom_attribute, param_attrs)]
#![allow(unused_attributes)]

use interfacer_http::{
    http::{header::CONTENT_TYPE, header::COOKIE, Request, Response},
    http_service, mime,
    mock::{Client, Error},
    url::Url,
    ContentInto, ToContent,
};
use serde_derive::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
struct User {
    name: String,
    age: i32,
}

const MOCK_BASE_URL: &str = "https://mock.rs";
const DEFAULT_COOKIE: &str = "cookie=cookie";

#[rustfmt::skip]
#[http_service]
trait UserService {
    type Error;

    #[options]
    async fn ping(&self) -> Result<Response<()>, Self::Error>;

    #[get("/api/user/{id}")]
    #[expect(200, mime::APPLICATION_JSON)]
    async fn get_user(&self, id: u64) -> Result<Response<User>, Self::Error>;

    #[get("/api/user?age_max={age_max}")]
    #[expect(200, mime::APPLICATION_JSON)]
    async fn get_users(&self, age_max: u8) -> Result<Response<Vec<User>>, Self::Error>;

    #[put("/api/user/{id}", mime::APPLICATION_JSON)]
    #[expect(200, "application/json")]
    async fn put_user(
        &self,
        id: u64,
        #[body] user: &User,
        #[header(COOKIE)] cookie: &str,
    ) -> Result<Response<User>, Self::Error>;

    #[post("/api/user", mime::APPLICATION_WWW_FORM_URLENCODED)]
    #[expect(201, mime::APPLICATION_MSGPACK)]
    async fn post_user(
        &self,
        #[body] user: &User,
        #[header(COOKIE)] cookie: &str,
    ) -> Result<Response<User>, Self::Error>;
}

async fn ping_handler(req: Request<Vec<u8>>) -> Result<Response<Vec<u8>>, Error> {
    assert_eq!(Url::parse(MOCK_BASE_URL)?.join("/")?.as_str(), req.uri());
    assert_eq!("OPTIONS", req.method());
    Ok(Response::builder().status(200).body(Vec::new())?)
}

#[tokio::test]
async fn test_ping() -> Result<(), Error> {
    let service = Client::new(MOCK_BASE_URL.parse()?, ping_handler);
    let resp = service.ping().await?;
    assert_eq!(200, resp.status());
    Ok(())
}

async fn get_user_handler(req: Request<Vec<u8>>) -> Result<Response<Vec<u8>>, Error> {
    assert_eq!(
        Url::parse(MOCK_BASE_URL)?.join("/api/user/0")?.as_str(),
        req.uri()
    );
    assert_eq!("GET", req.method());
    Ok(Response::builder()
        .status(200)
        .header(CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
        .body(
            User {
                name: "hexi".to_string(),
                age: 20,
            }
            .to_content(&mime::APPLICATION_JSON)?,
        )?)
}

#[tokio::test]
async fn test_get_user() -> Result<(), Error> {
    let service = Client::new(MOCK_BASE_URL.parse()?, get_user_handler);
    let resp = service.get_user(0).await?;
    assert_eq!(200, resp.status());
    assert_eq!(
        &User {
            name: "hexi".to_string(),
            age: 20,
        },
        resp.body()
    );
    Ok(())
}

async fn get_users_handler(req: Request<Vec<u8>>) -> Result<Response<Vec<u8>>, Error> {
    assert_eq!(
        Url::parse(MOCK_BASE_URL)?
            .join("/api/user?age_max=40")?
            .as_str(),
        req.uri()
    );
    assert_eq!("GET", req.method());
    Ok(Response::builder()
        .status(200)
        .header(CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
        .body(
            vec![
                User {
                    name: "hexi".to_string(),
                    age: 20,
                },
                User {
                    name: "boss".to_string(),
                    age: 35,
                },
            ]
            .to_content(&mime::APPLICATION_JSON)?,
        )?)
}

#[tokio::test]
async fn test_get_users() -> Result<(), Error> {
    let service = Client::new(MOCK_BASE_URL.parse()?, get_users_handler);
    let resp = service.get_users(40).await?;
    assert_eq!(200, resp.status());
    assert_eq!(
        &vec![
            User {
                name: "hexi".to_string(),
                age: 20,
            },
            User {
                name: "boss".to_string(),
                age: 35,
            },
        ],
        resp.body()
    );
    Ok(())
}

async fn put_user_handler(req: Request<Vec<u8>>) -> Result<Response<Vec<u8>>, Error> {
    assert_eq!(
        Url::parse(MOCK_BASE_URL)?.join("/api/user/0")?.as_str(),
        req.uri()
    );
    assert_eq!("PUT", req.method());
    assert_eq!(
        mime::APPLICATION_JSON,
        req.headers().get(CONTENT_TYPE).unwrap().to_str().unwrap()
    );
    assert_eq!(
        DEFAULT_COOKIE,
        req.headers().get(COOKIE).unwrap().to_str().unwrap()
    );
    let user: User = req.into_body().content_into(&mime::APPLICATION_JSON)?;
    Ok(Response::builder()
        .status(200)
        .header(CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
        .body(user.to_content(&mime::APPLICATION_JSON)?)?)
}

#[tokio::test]
async fn test_put_user() -> Result<(), Error> {
    let service = Client::new(MOCK_BASE_URL.parse()?, put_user_handler);
    let user = User {
        name: "hexi".to_string(),
        age: 20,
    };
    let resp = service.put_user(0, &user, DEFAULT_COOKIE).await?;
    assert_eq!(200, resp.status());
    assert_eq!(&user, resp.body());
    Ok(())
}

async fn post_user_handler(req: Request<Vec<u8>>) -> Result<Response<Vec<u8>>, Error> {
    assert_eq!(
        Url::parse(MOCK_BASE_URL)?.join("/api/user")?.as_str(),
        req.uri()
    );
    assert_eq!("POST", req.method());
    assert_eq!(
        mime::APPLICATION_WWW_FORM_URLENCODED,
        req.headers().get(CONTENT_TYPE).unwrap().to_str().unwrap()
    );
    assert_eq!(
        DEFAULT_COOKIE,
        req.headers().get(COOKIE).unwrap().to_str().unwrap()
    );
    let user: User = req
        .into_body()
        .content_into(&mime::APPLICATION_WWW_FORM_URLENCODED)?;
    Ok(Response::builder()
        .status(201)
        .header(CONTENT_TYPE, mime::APPLICATION_MSGPACK.as_ref())
        .body(user.to_content(&mime::APPLICATION_MSGPACK)?)?)
}

#[tokio::test]
async fn test_post_user() -> Result<(), Error> {
    let service = Client::new(MOCK_BASE_URL.parse()?, post_user_handler);
    let user = User {
        name: "hexi".to_string(),
        age: 20,
    };
    let resp = service.post_user(&user, DEFAULT_COOKIE).await?;
    assert_eq!(201, resp.status());
    assert_eq!(&user, resp.body());
    Ok(())
}