product-os-server 0.0.55

Product OS : Server provides a full functioning advanced server capable of acting as a web server, command and control distributed network, authentication server, crawling server and more. Fully featured with high level of flexibility.
Documentation
//! Serverless / Vercel function support for `ProductOSServer`.
//!
//! Enables invoking the same axum router used by `collab-server` without binding TCP.

#![cfg(all(feature = "serverless-vercel", feature = "framework_axum", feature = "executor_tokio"))]

use std::prelude::v1::*;

use core::str::FromStr;

use axum::middleware::{self, Next};
use axum::response::Response as AxumResponse;
use product_os_router::{Body, Request, Uri};
use tower::ServiceBuilder;
use vercel_runtime::axum::VercelLayer;

use crate::ProductOSServer;

impl<S, E, X> ProductOSServer<S, E, X>
where
    S: Clone + Send + Sync + 'static,
    E: product_os_async_executor::Executor<X>
        + product_os_async_executor::ExecutorPerform<X>
        + product_os_async_executor::Timer
        + 'static,
{
    /// Finalize route registration (e.g. compression layers) before serving requests.
    pub fn finalize(&mut self) {
        #[cfg(feature = "compression")]
        self.ensure_compression();
    }

    /// Build a finalized axum router for in-process or serverless dispatch.
    pub fn into_axum_router(&mut self) -> product_os_router::Router {
        self.finalize();
        self.get_router().clone().into_axum_router()
    }

    /// Dispatch a single HTTP request through the server router (tests, custom adapters).
    pub async fn handle_request(
        &mut self,
        request: Request<Body>,
    ) -> product_os_router::Response<Body> {
        let app = self.into_axum_router();
        match tower::ServiceExt::oneshot(app, request).await {
            Ok(response) => response,
            Err(error) => {
                tracing::error!("serverless handle_request failed: {error:?}");
                product_os_router::Response::builder()
                    .status(product_os_router::StatusCode::INTERNAL_SERVER_ERROR)
                    .body(Body::from("Internal Server Error"))
                    .unwrap_or_else(|_| product_os_router::Response::new(Body::empty()))
            }
        }
    }
}

/// Rewrite Vercel `/api?path=v1/health` back to `/api/v1/health` for axum routing.
pub fn rewrite_vercel_api_path(request: Request<Body>) -> Request<Body> {
    let path = request.uri().path();
    if !matches!(path, "/api" | "/api/" | "/api/index") {
        return request;
    }

    let Some(query) = request.uri().query() else {
        return request;
    };

    for (key, value) in url::form_urlencoded::parse(query.as_bytes()) {
        if key != "path" {
            continue;
        }

        let api_path = if value.starts_with('/') {
            format!("/api{value}")
        } else {
            format!("/api/{value}")
        };

        if let Ok(new_uri) = Uri::from_str(&api_path) {
            let (mut parts, body) = request.into_parts();
            parts.uri = new_uri;
            return Request::from_parts(parts, body);
        }
    }

    request
}

async fn vercel_path_rewrite_middleware(
    request: Request<Body>,
    next: Next,
) -> AxumResponse<Body> {
    next.run(rewrite_vercel_api_path(request)).await
}

/// Run this server as a Vercel function (blocks until the runtime exits).
pub async fn run_vercel<S, E, X>(
    server: &mut ProductOSServer<S, E, X>,
) -> Result<(), vercel_runtime::Error>
where
    S: Clone + Send + Sync + 'static,
    E: product_os_async_executor::Executor<X>
        + product_os_async_executor::ExecutorPerform<X>
        + product_os_async_executor::Timer
        + 'static,
{
    let router = server
        .into_axum_router()
        .layer(middleware::from_fn(vercel_path_rewrite_middleware));

    let app = ServiceBuilder::new()
        .layer(VercelLayer::new())
        .service(router);

    vercel_runtime::run(app).await
}

#[cfg(test)]
mod tests {
    use super::*;
    use product_os_async_executor::TokioExecutor;
    use product_os_router::{IntoResponse, StatusCode};

    #[tokio::test]
    async fn handle_request_routes_get() {
        let mut server: ProductOSServer<(), TokioExecutor, ()> =
            ProductOSServer::new_with_config(crate::ServerConfig::new());

        server.add_get("/api/v1/health", || async {
            (StatusCode::OK, "ok").into_response()
        });

        let response = server
            .handle_request(
                Request::builder()
                    .uri("/api/v1/health")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await;

        assert_eq!(response.status(), StatusCode::OK);
    }

    #[test]
    fn rewrite_vercel_query_path() {
        let request = Request::builder()
            .uri("/api?path=v1/health")
            .body(Body::empty())
            .unwrap();

        let rewritten = rewrite_vercel_api_path(request);
        assert_eq!(rewritten.uri().path(), "/api/v1/health");
    }
}