joaju 0.1.0

A scientific academic compiler and typographical orchestrator in Rust for Typst, clinical pedigrees, and tables
Documentation
//! # Joaju
//!
//! Orquestrador tipográfico acadêmico e compilador científico nativo em Rust.
//!
//! > ⚠️ **Aviso de Versão em Desenvolvimento Ativo (pre-1.0 / dev)**: O Joaju encontra-se
//! > em desenvolvimento inicial ativo (`0.2.x`). A API e contratos HTTP estão sujeitos a
//! > melhorias e ajustes antes da versão estável 1.0.0.
//!
//! ## Exemplo Rápido
//!
//! ```rust
//! use joaju::create_app;
//!
//! let app = create_app();
//! ```

pub mod compiler;
pub mod engine;
pub mod preprocessor;
pub mod unfurl;

use axum::{
    extract::State,
    http::{header, HeaderMap, HeaderValue, Method, StatusCode},
    response::Json,
    routing::{get, post},
    Router,
};
use serde::Serialize;
use std::sync::Arc;
use std::time::Instant;
use tower_http::cors::{Any, CorsLayer};
use tower_http::services::ServeDir;

#[derive(Clone)]
pub struct AppState {
    pub start_time: Instant,
    pub version: &'static str,
}

#[derive(Serialize)]
pub struct HealthResponse {
    pub status: &'static str,
    pub name: &'static str,
    pub version: &'static str,
    pub uptime: f64,
}

pub async fn health_handler(State(state): State<Arc<AppState>>) -> Json<HealthResponse> {
    Json(HealthResponse {
        status: "ok",
        name: "joaju",
        version: state.version,
        uptime: state.start_time.elapsed().as_secs_f64(),
    })
}

pub async fn render_handler(
    Json(payload): Json<engine::pedigree::RenderRequest>,
) -> Result<
    Json<engine::pedigree::RenderResponse>,
    (StatusCode, Json<engine::pedigree::RenderErrorResponse>),
> {
    let is_chem = payload
        .format
        .as_deref()
        .map(|f| matches!(f, "openchemlib" | "openbabel"))
        .unwrap_or(false)
        || payload.code.contains("```openchemlib")
        || payload.code.contains("```openbabel");

    let is_table = payload
        .format
        .as_deref()
        .map(|f| f == "table")
        .unwrap_or(false)
        || payload.code.contains("```table");

    if is_table {
        let clean = payload.code.trim();
        let body = if clean.starts_with("```table") {
            clean
                .trim_start_matches("```table")
                .trim_end_matches("```")
                .trim()
        } else {
            clean
        };
        return match preprocessor::render_single_table(body) {
            Ok(typst) => Ok(Json(engine::pedigree::RenderResponse {
                format: "table".to_string(),
                r#type: "typst".to_string(),
                svg: typst,
            })),
            Err(err) => Err((
                StatusCode::BAD_REQUEST,
                Json(engine::pedigree::RenderErrorResponse {
                    error: err.to_string(),
                }),
            )),
        };
    }

    let result = if is_chem {
        engine::chem::render_chem(&payload.code, payload.format.as_deref())
    } else {
        engine::pedigree::render_pedigree(&payload.code, payload.format.as_deref())
    };

    match result {
        Ok((fmt, svg)) => Ok(Json(engine::pedigree::RenderResponse {
            format: fmt,
            r#type: "svg".to_string(),
            svg,
        })),
        Err(err) => Err((
            StatusCode::BAD_REQUEST,
            Json(engine::pedigree::RenderErrorResponse { error: err }),
        )),
    }
}

pub async fn compile_handler(
    Json(payload): Json<compiler::CompileRequest>,
) -> Result<(HeaderMap, Vec<u8>), (StatusCode, String)> {
    let md = payload.markdown.unwrap_or_default();
    match compiler::compile_markdown_to_pdf_with_options(&md, payload.template.as_deref()) {
        Ok(pdf_bytes) => {
            let mut headers = HeaderMap::new();
            headers.insert(
                header::CONTENT_TYPE,
                HeaderValue::from_static("application/pdf"),
            );
            Ok((headers, pdf_bytes))
        }
        Err(err) => Err((StatusCode::BAD_REQUEST, err)),
    }
}

pub async fn unfurl_handler(
    Json(payload): Json<unfurl::UnfurlRequest>,
) -> Result<Json<unfurl::UnfurlResponse>, (StatusCode, Json<unfurl::UnfurlErrorResponse>)> {
    let url = match payload.url {
        Some(u) if !u.trim().is_empty() => u.trim().to_string(),
        _ => {
            return Err((
                StatusCode::BAD_REQUEST,
                Json(unfurl::UnfurlErrorResponse {
                    ok: false,
                    error: "Parâmetro \"url\" é obrigatório.".to_string(),
                }),
            ));
        }
    };

    match unfurl::unfurl_url(&url, payload.archive).await {
        Ok(resp) => Ok(Json(resp)),
        Err(err) => {
            let status = if err.contains("Protocolo inválido")
                || err.contains("URL inválida")
                || err.contains("não é permitido")
            {
                StatusCode::BAD_REQUEST
            } else {
                StatusCode::UNPROCESSABLE_ENTITY
            };
            Err((
                status,
                Json(unfurl::UnfurlErrorResponse {
                    ok: false,
                    error: err,
                }),
            ))
        }
    }
}

pub fn create_app() -> Router {
    let state = Arc::new(AppState {
        start_time: Instant::now(),
        version: env!("CARGO_PKG_VERSION"),
    });

    let cors = CorsLayer::new()
        .allow_origin(Any)
        .allow_methods([Method::GET, Method::POST, Method::OPTIONS, Method::HEAD])
        .allow_headers(Any);

    Router::new()
        .route("/health", get(health_handler))
        .route("/render", post(render_handler))
        .route("/compile", post(compile_handler))
        .route("/unfurl", post(unfurl_handler))
        .nest_service("/locales", ServeDir::new("locales"))
        .nest_service("/site", ServeDir::new("site"))
        .nest_service("/public", ServeDir::new("public"))
        .layer(cors)
        .with_state(state)
}