typst4ei 2.0.0-alpha

The Typst4EI building and serving tool/cli
mod config;
mod fetch_documents;
mod generate_html_pages;
mod inventory;
mod server;

use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::process;

use anyhow::Context;
use anyhow::Result;
use clap::Parser;
use clap::Subcommand;
use clap::crate_version;
use flexi_logger::Cleanup;
use flexi_logger::Criterion;
use flexi_logger::Duplicate;
use flexi_logger::FileSpec;
use flexi_logger::Logger;
use flexi_logger::Naming;
use flexi_logger::WriteMode;
use flexi_logger::colored_default_format;
use flexi_logger::detailed_format;
use log::error;
use log::info;

use crate::config::Config;
use crate::config::Site;
use crate::config::Source;
use crate::fetch_documents::Document;
use crate::fetch_documents::fetch_documents;
use crate::generate_html_pages::generate_html_error_pages;
use crate::generate_html_pages::generate_html_index_pages;
use crate::inventory::Inventory;
use crate::inventory::Module;
use crate::server::serve;

#[derive(Debug, Parser)]
#[command(version, about, long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Command,
}

#[derive(Debug, Subcommand)]
enum Command {
    /// Validate TOML config file and exit immediately
    ValidateConfig {
        /// Path to TOML config file
        #[arg(long, value_name = "PATH")]
        file: PathBuf,
    },

    /// Validate JSON inventory file and exit immediately
    ValidateInventory {
        /// Path to JSON inventory file
        #[arg(long, value_name = "PATH")]
        file: PathBuf,
    },

    /// Fetch documents and generate HTML on demand
    Serve {
        /// Address and port to listen on
        #[arg(long, default_value = "0.0.0.0:8080")]
        address: String,

        /// Path to TOML config file
        #[arg(long, default_value = "config.toml", value_name = "PATH")]
        config: PathBuf,
    },
}

#[tokio::main]
async fn main() {
    let cli = Cli::parse();

    let _logger = Logger::try_with_str("info")
        .unwrap()
        .log_to_file(
            FileSpec::default()
                .directory("logs")
                .basename("typst4ei")
                .suffix("log"),
        )
        .rotate(
            Criterion::Size(30 * 1024 * 1024), // ~ 20 MB
            Naming::Timestamps,
            Cleanup::KeepLogFiles(15),
        )
        .duplicate_to_stderr(Duplicate::Info)
        .format_for_files(detailed_format)
        .format_for_stderr(colored_default_format)
        .write_mode(WriteMode::Async)
        .print_message()
        .start()
        .unwrap();

    info!("starting Typst4EI v{}", crate_version!());

    match run(cli).await {
        Ok(_) => process::exit(0),
        Err(err) => {
            error!("{:?}", err);
            process::exit(1);
        }
    }
}

static CSS: &str = include_str!("../static/pico.min.css");

async fn run(cli: Cli) -> Result<()> {
    match cli.command {
        Command::ValidateConfig { file } => {
            Config::load(&file)?;
            info!("config `{}` is valid", file.display());
            Ok(())
        }
        Command::ValidateInventory { file } => {
            let data = fs::read(&file)
                .with_context(|| format!("unable to read from `{}`", file.display()))?;
            Inventory::load(
                String::from_utf8(data)
                    .with_context(|| format!("unable to parse `{}` as UTF-8", file.display()))?,
            )?;
            info!("inventory `{}` is valid", file.display());
            Ok(())
        }
        Command::Serve { address, config } => {
            let config = Config::load(&config)?;
            fs::create_dir_all(config.site.static_dir.clone()).with_context(|| {
                format!("unable to create directory `{}`", config.site.static_dir)
            })?;
            fs::create_dir_all(config.site.files_dir.clone()).with_context(|| {
                format!("unable to create directory `{}`", config.site.files_dir)
            })?;

            let css_path = Path::new(&config.site.static_dir).join("pico.min.css");
            fs::write(&css_path, CSS)
                .with_context(|| format!("unable to create file `{}`", css_path.display()))?;

            serve(config, address).await
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn verify_cli() {
        use clap::CommandFactory;
        Cli::command().debug_assert();
    }
}