caelix 0.0.21

Public Caelix framework crate.
Documentation
#![cfg(feature = "axum")]

use caelix::{
    Application, BoxFuture, Container, Guard, Injectable, Interceptor, Module, ModuleMetadata,
    Next, RequestContext, Response, Result, WebSocketGateway, WebSocketSession, controller,
    gateway,
};
use futures_util::{SinkExt, StreamExt};
use http_body_util::BodyExt;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio_tungstenite::{connect_async, tungstenite::Message};
use tower::ServiceExt;

struct AllowGuard;

impl Injectable for AllowGuard {
    fn dependencies() -> Vec<caelix::ProviderDependency> {
        caelix::provider_dependencies![]
    }

    fn create(_: &Container) -> BoxFuture<'_, Result<Self>> {
        Box::pin(async { Ok(Self) })
    }
}

impl Guard for AllowGuard {
    fn can_activate<'a>(&'a self, _: &'a RequestContext) -> BoxFuture<'a, Result<bool>> {
        Box::pin(async { Ok(true) })
    }
}

struct PassThroughInterceptor;

impl Injectable for PassThroughInterceptor {
    fn dependencies() -> Vec<caelix::ProviderDependency> {
        caelix::provider_dependencies![]
    }

    fn create(_: &Container) -> BoxFuture<'_, Result<Self>> {
        Box::pin(async { Ok(Self) })
    }
}

impl Interceptor for PassThroughInterceptor {
    fn intercept<'a>(
        &'a self,
        _: &'a RequestContext,
        next: Next<'a>,
    ) -> BoxFuture<'a, Result<caelix::HttpResponse>> {
        next.run()
    }
}

#[derive(Deserialize)]
struct Payload {
    name: String,
}

#[derive(Deserialize)]
struct Search {
    include: bool,
}

#[derive(Serialize)]
struct Output {
    id: String,
    name: String,
    include: bool,
}

struct HealthController;

impl Injectable for HealthController {
    fn dependencies() -> Vec<caelix::ProviderDependency> {
        caelix::provider_dependencies![]
    }

    fn create(_: &Container) -> BoxFuture<'_, Result<Self>> {
        Box::pin(async { Ok(Self) })
    }
}

#[controller("/health")]
#[use_guard(AllowGuard)]
#[use_interceptor(PassThroughInterceptor)]
impl HealthController {
    #[post("/{id}")]
    async fn create(
        &self,
        #[param] id: String,
        #[body] payload: Payload,
        #[query] search: Search,
    ) -> Result<Response<Output>> {
        Ok(Response::Body(Output {
            id,
            name: payload.name,
            include: search.include,
        }))
    }
}

struct AppModule;

impl Module for AppModule {
    fn register() -> ModuleMetadata {
        ModuleMetadata::new()
            .provider::<AllowGuard>()
            .provider::<PassThroughInterceptor>()
            .controller::<HealthController>()
    }
}

#[caelix::test]
async fn generated_controller_routes_and_extractors_work_on_axum() {
    let app = Application::new::<AppModule>().await.unwrap().into_router();
    let response = app
        .oneshot(
            caelix::__axum::http::Request::builder()
                .method("POST")
                .uri("/health/42?include=true")
                .header("content-type", "application/json")
                .body(caelix::__axum::body::Body::from(r#"{"name":"Ada"}"#))
                .unwrap(),
        )
        .await
        .unwrap();

    assert_eq!(response.status(), caelix::__axum::http::StatusCode::OK);
    let body = response.into_body().collect().await.unwrap().to_bytes();
    assert_eq!(body.as_ref(), br#"{"id":"42","name":"Ada","include":true}"#);
}

#[caelix::test]
async fn axum_runtime_macro_can_be_used_more_than_once() {}

#[derive(Default)]
struct EchoGateway;

impl Injectable for EchoGateway {
    fn dependencies() -> Vec<caelix::ProviderDependency> {
        caelix::provider_dependencies![]
    }

    fn create(_: &Container) -> BoxFuture<'_, Result<Self>> {
        Box::pin(async { Ok(Self) })
    }
}

#[gateway("/echo")]
impl WebSocketGateway for EchoGateway {
    fn on_text(&self, session: Arc<WebSocketSession>, text: String) -> BoxFuture<'_, Result<()>> {
        Box::pin(async move { session.send_text(text).await })
    }
}

struct WebSocketModule;

impl Module for WebSocketModule {
    fn register() -> ModuleMetadata {
        ModuleMetadata::new().gateway::<EchoGateway>()
    }
}

#[caelix::test]
async fn decorated_websocket_gateway_mounts_on_axum() {
    let app = Application::new::<WebSocketModule>()
        .await
        .unwrap()
        .into_router();
    let listener = caelix::__tokio::net::TcpListener::bind("127.0.0.1:0")
        .await
        .unwrap();
    let address = listener.local_addr().unwrap();
    let server = caelix::__tokio::spawn(async move {
        caelix::__axum::serve(listener, app).await.unwrap();
    });

    assert_eq!(
        {
            let (mut socket, _) = connect_async(format!("ws://{address}/echo")).await.unwrap();
            socket.send(Message::Text("hello".into())).await.unwrap();
            socket.next().await.unwrap().unwrap()
        },
        Message::Text("hello".into())
    );
    server.abort();
}