Newer
Older
TelosDB / src / backend / src / dev_static.rs
//! 開発時のみ使用: フロントエンド静的ファイル配信用のパス解決とルーター。
//! モノリシック化で beforeDevCommand (Node) を廃止し、同一プロセスで 8474 を listen するために使う。

use std::path::{Path, PathBuf};
use axum::Router;
use tower_http::services::ServeDir;

/// 開発時用のフロントエンドルート(`src/frontend`)の絶対パスを返す。
/// テスト時は CARGO_MANIFEST_DIR、実行時は exe_dir から解決する。
fn resolve_dev_frontend_dir(exe_dir: Option<&Path>) -> Option<PathBuf> {
    // テスト時: CARGO_MANIFEST_DIR が設定されている(src/backend の兄弟 = src/frontend)
    if let Some(manifest_dir) = std::env::var_os("CARGO_MANIFEST_DIR")
        .map(PathBuf::from)
        .filter(|p| p.ends_with("backend"))
    {
        let parent = manifest_dir.parent()?;
        let frontend = parent.join("frontend");
        if frontend.join("index.html").exists() {
            return Some(frontend);
        }
    }
    // 実行時: バイナリは target/debug にあるので 2 階層上がプロジェクトルート
    let exe_dir = exe_dir?;
    let project_root = exe_dir.parent()?.parent()?;
    let frontend = project_root.join("src").join("frontend");
    if frontend.join("index.html").exists() {
        Some(frontend)
    } else {
        None
    }
}

/// 開発時用のフロントエンドルート(テスト用。CARGO_MANIFEST_DIR のみ使用)。
pub fn dev_frontend_dir() -> Option<PathBuf> {
    resolve_dev_frontend_dir(None)
}

/// 実行時用: exe の親ディレクトリからプロジェクトルートを辿って frontend を解決する。
pub fn dev_frontend_dir_from_exe(exe_dir: &Path) -> Option<PathBuf> {
    resolve_dev_frontend_dir(Some(exe_dir))
}

/// 開発用フロントルートが解決できる場合、静的配信用の Axum Router を返す。
/// exe_dir: 実行時は Some(バイナリの親ディレクトリ)。テスト時は None で CARGO_MANIFEST_DIR を使用。
pub fn dev_static_router(exe_dir: Option<&Path>) -> Option<Router> {
    let dir = resolve_dev_frontend_dir(exe_dir)?;
    let serve_dir = ServeDir::new(dir).append_index_html_on_directories(true);
    Some(Router::new().fallback_service(serve_dir))
}

#[cfg(test)]
mod tests {
    use super::*;
    use axum::body::Body;
    use axum::http::Request;
    use tower::util::ServiceExt;

    #[test]
    fn dev_frontend_dir_resolves_to_path_with_index_html() {
        let dir = dev_frontend_dir();
        assert!(
            dir.is_some(),
            "dev_frontend_dir() should return Some when run from workspace (crate at src/backend)"
        );
        let path = dir.unwrap();
        assert!(
            path.join("index.html").exists(),
            "resolved path must contain index.html: {:?}",
            path
        );
        assert!(
            path.to_string_lossy().contains("frontend"),
            "path should contain 'frontend': {:?}",
            path
        );
    }

    #[tokio::test]
    async fn dev_static_router_returns_200_for_root() {
        let router = dev_static_router(None).expect("dev_static_router(None) should be Some when run from workspace");
        let req = Request::get("/").body(Body::empty()).unwrap();
        let response = router.oneshot(req).await.unwrap();
        assert_eq!(response.status(), 200, "GET / should return 200 when serving dev frontend");
    }
}