mdbookkit 2.0.1

Support library for mdBook preprocessors in the mdbookkit project
Documentation
use std::{
    fmt::Display,
    process::exit,
    sync::{
        LockResult,
        atomic::{AtomicU8, Ordering},
    },
};

use anyhow::{Context, Error, Result, anyhow};
use serde::Deserialize;
use tap::Pipe;
use tracing::{Event, Level, Subscriber};
use tracing_subscriber::{Layer, layer};

use crate::env::is_ci;

static MAX_SEVERITY: AtomicU8 = AtomicU8::new(0);

pub fn has_severity(level: Level) -> bool {
    MAX_SEVERITY.load(Ordering::Relaxed) >= level_to_severity(level)
}

#[inline]
pub fn put_severity(level: Level) {
    let severity = level_to_severity(level);
    MAX_SEVERITY.fetch_max(severity, Ordering::Relaxed);
}

pub struct EventLevelLayer;

impl<S: Subscriber> Layer<S> for EventLevelLayer {
    fn on_event(&self, event: &Event<'_>, _ctx: layer::Context<'_, S>) {
        put_severity(*event.metadata().level());
    }
}

fn level_to_severity(level: Level) -> u8 {
    if level <= Level::ERROR {
        50
    } else if level <= Level::WARN {
        40
    } else if level <= Level::INFO {
        30
    } else if level <= Level::DEBUG {
        20
    } else {
        10
    }
}

/// Flag indicating how the program should proceed when there are warnings.
///
/// Used in preprocessor options.
///
/// Doc comments for variants in this enum will show up in autogenerated docs.
#[derive(clap::ValueEnum, Deserialize, Debug, Default, Clone, Copy)]
#[serde(rename_all = "lowercase")]
pub enum OnWarning {
    /// Fail if the environment variable `CI` is set to a value other than `0` or `false`.
    /// Environments like GitHub Actions configure this automatically.
    #[default]
    #[serde(rename = "ci")]
    #[clap(name = "ci")]
    FailInCi,

    /// Fail as long as there are warnings, even in local use.
    AlwaysFail,
}

impl OnWarning {
    pub fn check(&self) -> Result<()> {
        if has_severity(Level::ERROR) {
            Err(anyhow!("Preprocessor has errors"))
        } else if has_severity(Level::WARN) {
            match (self, is_ci()) {
                (Self::AlwaysFail, _) => anyhow! { "Treating warnings as errors because the \
                `fail-on-warnings` option is set to \"always\"" }
                .pipe(Err),
                (Self::FailInCi, Some(ci)) => {
                    anyhow! { "Treating warnings as errors because CI={ci} and the \
                    `fail-on-warnings` option is set to \"ci\"" }
                    .pipe(Err)
                }
                (Self::FailInCi, None) => Ok(()),
            }
        } else {
            Ok(())
        }
    }

    pub fn adjusted<T, E>(&self, result: Result<Result<T, E>, E>) -> Result<Result<T, E>, E> {
        match result {
            Err(error) => Err(error),
            Ok(Err(error)) => match (self, is_ci()) {
                (Self::AlwaysFail, _) => Err(error),
                (Self::FailInCi, Some(_)) => Err(error),
                (Self::FailInCi, None) => Ok(Err(error)),
            },
            Ok(Ok(result)) => Ok(Ok(result)),
        }
    }
}

pub trait ExpectFmt {
    fn expect_fmt(self);
}

impl ExpectFmt for std::fmt::Result {
    #[inline(always)]
    fn expect_fmt(self) {
        self.expect("string formatting should not fail")
    }
}

pub trait ExpectLock<T> {
    fn expect_lock(self) -> T;
}

impl<T> ExpectLock<T> for LockResult<T> {
    #[inline(always)]
    fn expect_lock(self) -> T {
        self.expect("lock should not be poisoned")
    }
}

pub trait IntoAnyhow<T> {
    fn anyhow(self) -> Result<T>;
}

impl<T, E: Into<Error>> IntoAnyhow<T> for Result<T, E> {
    #[inline(always)]
    fn anyhow(self) -> Result<T> {
        self.map_err(Into::into)
    }
}

#[allow(async_fn_in_trait)]
pub trait FutureWithError<T> {
    async fn context<C>(self, context: C) -> Result<T>
    where
        C: Display + Send + Sync + 'static;

    async fn with_context<C, G>(self, context: G) -> Result<T>
    where
        C: Display + Send + Sync + 'static,
        G: FnOnce() -> C;
}

impl<F, T, E> FutureWithError<T> for F
where
    F: Future<Output = Result<T, E>>,
    E: Into<Error>,
{
    #[inline(always)]
    async fn context<C>(self, context: C) -> Result<T>
    where
        C: Display + Send + Sync + 'static,
    {
        match self.await {
            Ok(value) => Ok(value),
            Err(error) => Err(error.into()).context(context),
        }
    }

    #[inline(always)]
    async fn with_context<C, G>(self, context: G) -> Result<T>
    where
        C: Display + Send + Sync + 'static,
        G: FnOnce() -> C,
    {
        match self.await {
            Ok(value) => Ok(value),
            Err(error) => Err(error.into()).with_context(context),
        }
    }
}

pub trait ExitProcess<T> {
    fn exit(self, log: impl FnOnce(Error)) -> T;
}

impl<T> ExitProcess<T> for Result<T> {
    fn exit(self, log: impl FnOnce(Error)) -> T {
        match self {
            Ok(v) => v,
            Err(e) => {
                log(e);
                exit(1)
            }
        }
    }
}