bookyard 0.1.0

Build and locally edit a bookshelf for multiple mdBook projects.
use axum::{
    Json, Router,
    extract::State,
    http::{StatusCode, header},
    response::{Html, IntoResponse, Response},
    routing::{get, post},
};
use bookyard_core::{BookyardConfig, BuildOptions, CONFIG_FILE, build_catalog, build_shelf};
use tower_http::services::ServeDir;

use crate::{render, server::ServerState};

pub fn router(state: ServerState) -> Router {
    let mut router = Router::new()
        .route("/__bookyard/edit", get(editor_html))
        .route("/__bookyard/editor/style.css", get(editor_css))
        .route("/__bookyard/editor/app.js", get(editor_js))
        .route("/__bookyard/api/config", get(get_config).put(put_config))
        .route("/__bookyard/api/catalog", get(get_catalog))
        .route("/__bookyard/api/build", post(post_build))
        .fallback_service(ServeDir::new(state.output_dir.clone()));
    if !state.edit {
        router = Router::new().fallback_service(ServeDir::new(state.output_dir.clone()));
    }
    router.with_state(state)
}

async fn editor_html(State(state): State<ServerState>) -> Response {
    if !state.edit {
        return StatusCode::NOT_FOUND.into_response();
    }
    Html(crate::server::static_files::EDITOR_HTML).into_response()
}

async fn editor_css(State(state): State<ServerState>) -> Response {
    if !state.edit {
        return StatusCode::NOT_FOUND.into_response();
    }
    (
        [(header::CONTENT_TYPE, "text/css; charset=utf-8")],
        crate::server::static_files::EDITOR_CSS,
    )
        .into_response()
}

async fn editor_js(State(state): State<ServerState>) -> Response {
    if !state.edit {
        return StatusCode::NOT_FOUND.into_response();
    }
    (
        [(header::CONTENT_TYPE, "text/javascript; charset=utf-8")],
        crate::server::static_files::EDITOR_JS,
    )
        .into_response()
}

async fn get_config(State(state): State<ServerState>) -> Response {
    if !state.edit {
        return StatusCode::NOT_FOUND.into_response();
    }
    match BookyardConfig::load_from(state.root_dir.join(CONFIG_FILE)) {
        Ok(config) => Json(config).into_response(),
        Err(err) => error_response(err),
    }
}

async fn put_config(
    State(state): State<ServerState>,
    Json(config): Json<BookyardConfig>,
) -> Response {
    if !state.edit {
        return StatusCode::NOT_FOUND.into_response();
    }
    let path = state.root_dir.join(CONFIG_FILE);
    if let Err(err) = config.save_to(&path) {
        return error_response(err);
    }
    if let Err(err) = render::shelf::write_shelf(&state.output_dir, &config) {
        return error_response(err);
    }
    Json(config).into_response()
}

async fn get_catalog(State(state): State<ServerState>) -> Response {
    if !state.edit {
        return StatusCode::NOT_FOUND.into_response();
    }
    match BookyardConfig::load_from(state.root_dir.join(CONFIG_FILE)) {
        Ok(config) => Json(build_catalog(&config)).into_response(),
        Err(err) => error_response(err),
    }
}

async fn post_build(State(state): State<ServerState>) -> Response {
    if !state.edit {
        return StatusCode::NOT_FOUND.into_response();
    }
    let config = match BookyardConfig::load_from(state.root_dir.join(CONFIG_FILE)) {
        Ok(config) => config,
        Err(err) => return error_response(err),
    };
    match build_shelf(&state.root_dir, &config, &BuildOptions::default()) {
        Ok(_) => match render::shelf::write_shelf(&state.output_dir, &config) {
            Ok(()) => Json(build_catalog(&config)).into_response(),
            Err(err) => error_response(err),
        },
        Err(err) => error_response(err),
    }
}

fn error_response(err: impl std::fmt::Display) -> Response {
    (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response()
}