ferrosite 0.1.0

A railway-oriented static site generator for personal homepages
Documentation
use thiserror::Error;

#[derive(Debug, Error)]
pub enum SiteError {
    #[error("Configuration error: {0}")]
    Config(String),
    #[error("Missing required config field '{field}' in '{file}'")]
    MissingConfig { field: String, file: String },
    #[error("Content error in '{path}': {message}")]
    Content { path: String, message: String },
    #[error("Frontmatter parse error in '{path}': {message}")]
    Frontmatter { path: String, message: String },
    #[error("Unknown slot type '{slot}' in '{path}'")]
    UnknownSlot { slot: String, path: String },
    #[error("Article '{path}' missing required field '{field}' for slot '{slot}'")]
    MissingArticleField {
        path: String,
        field: String,
        slot: String,
    },
    #[error("Template '{template}' not found")]
    TemplateNotFound { template: String },
    #[error("Template render error in '{template}': {message}")]
    TemplateRenderMsg { template: String, message: String },
    #[error("Layout '{layout}' not found for page type '{page_type}'")]
    LayoutNotFound { layout: String, page_type: String },
    #[error("SSR error: {0}")]
    Ssr(String),
    #[error("Component error in '{component}': {message}")]
    Component { component: String, message: String },
    #[error("Plugin manifest error in '{plugin}': {message}")]
    Plugin { plugin: String, message: String },
    #[error("Plugin slot conflict: plugin '{plugin}' claims slot '{slot}'")]
    PluginSlotConflict { plugin: String, slot: String },
    #[error("Build error: {0}")]
    Build(String),
    #[error("Asset error for '{asset}': {message}")]
    Asset { asset: String, message: String },
    #[error("Deploy error for provider '{provider}': {message}")]
    Deploy { provider: String, message: String },
    #[error("Missing deploy credential: {0}")]
    MissingCredential(String),
    #[error("Missing path while {action}: '{path}'")]
    MissingPath { action: String, path: String },
    #[error("IO error while {action} '{path}': {source}")]
    IoPath {
        action: String,
        path: String,
        #[source]
        source: std::io::Error,
    },
    #[error("IO error: {source}")]
    Io {
        #[from]
        source: std::io::Error,
    },
    #[error("TOML deserialize error: {source}")]
    TomlDeserialize {
        #[from]
        source: toml::de::Error,
    },
    #[error("TOML serialize error: {source}")]
    TomlSerialize {
        #[from]
        source: toml::ser::Error,
    },
    #[error("JSON error: {source}")]
    Json {
        #[from]
        source: serde_json::Error,
    },
    #[error("Template engine error: {source}")]
    TemplateEngine {
        #[from]
        source: minijinja::Error,
    },
    #[error("Directory walk error: {source}")]
    WalkDir {
        #[from]
        source: walkdir::Error,
    },
    #[error("Path strip prefix error: {source}")]
    StripPrefix {
        #[from]
        source: std::path::StripPrefixError,
    },
    #[error("Multiple errors:\n{}", .0.iter().enumerate().map(|(i,e)| format!("  {}: {}", i+1, e)).collect::<Vec<_>>().join("\n"))]
    Multiple(Vec<SiteError>),
}

pub type SiteResult<T> = Result<T, SiteError>;

pub fn io_with_path(
    path: impl AsRef<std::path::Path>,
    action: impl Into<String>,
) -> impl FnOnce(std::io::Error) -> SiteError {
    let path = path.as_ref().display().to_string();
    let action = action.into();

    move |source| {
        if source.kind() == std::io::ErrorKind::NotFound {
            SiteError::MissingPath {
                action,
                path: path.clone(),
            }
        } else {
            SiteError::IoPath {
                action,
                path,
                source,
            }
        }
    }
}

pub fn collect_results<T>(results: Vec<SiteResult<T>>) -> SiteResult<Vec<T>> {
    let (oks, errs): (Vec<_>, Vec<_>) = results.into_iter().partition(Result::is_ok);
    if errs.is_empty() {
        Ok(oks.into_iter().map(|r| r.unwrap()).collect())
    } else {
        let errors: Vec<SiteError> = errs
            .into_iter()
            .map(|r| match r {
                Ok(_) => unreachable!("partition(Result::is_ok) guarantees only errors here"),
                Err(err) => err,
            })
            .collect();
        if errors.len() == 1 {
            Err(errors.into_iter().next().unwrap())
        } else {
            Err(SiteError::Multiple(errors))
        }
    }
}

pub fn with_path<T>(path: &str, result: SiteResult<T>) -> SiteResult<T> {
    result.map_err(|e| SiteError::Content {
        path: path.to_string(),
        message: e.to_string(),
    })
}