cargo-sync-rdme 0.5.0

Cargo subcommand to synchronize README with crate documentation
Documentation
use std::fmt;

use cargo_metadata::{Metadata, Package};

use crate::App;

use super::{ManifestFile, marker::Replace};

mod badge;
mod rustdoc;
mod title;

pub(super) fn create_all(
    replaces: impl IntoIterator<Item = Replace>,
    app: &App,
    manifest: &ManifestFile,
    workspace: &Metadata,
    package: &Package,
) -> Result<Vec<Contents>, CreateAllContentsError> {
    let mut contents = vec![];
    let mut errors = vec![];
    for replace in replaces {
        let res = replace.create_content(app, manifest, workspace, package);
        match res {
            Ok(c) => contents.push(c),
            Err(err) => errors.push(err),
        }
    }

    if !errors.is_empty() {
        return Err(CreateAllContentsError { errors });
    }

    Ok(contents)
}

#[derive(Debug, thiserror::Error, miette::Diagnostic)]
#[error("failed to create contents of README")]
pub(super) struct CreateAllContentsError {
    #[related]
    errors: Vec<CreateContentsError>,
}

#[derive(Debug, thiserror::Error, miette::Diagnostic)]
pub(super) enum CreateContentsError {
    #[error(transparent)]
    #[diagnostic(transparent)]
    CreateBadge(#[from] badge::CreateAllBadgesError),
    #[error(transparent)]
    #[diagnostic(transparent)]
    CreateRustdoc(#[from] rustdoc::CreateRustdocError),
}

#[derive(Debug, Clone)]
pub(super) struct Contents {
    text: String,
}

impl Replace {
    fn create_content(
        self,
        app: &App,
        manifest: &ManifestFile,
        workspace: &Metadata,
        package: &Package,
    ) -> Result<Contents, CreateContentsError> {
        let text = match self {
            Replace::Title => title::create(package),
            Replace::Badge { name: _, badges } => {
                badge::create_all(badges, manifest, workspace, package)?
            }
            Replace::Rustdoc => rustdoc::create(app, manifest, workspace, package)?,
        };

        assert!(text.is_empty() || text.ends_with('\n'));

        Ok(Contents { text })
    }
}

impl Contents {
    pub(super) fn text(&self) -> &str {
        &self.text
    }
}

struct Escape<'s>(&'s str, &'s [char]);

impl fmt::Display for Escape<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut s = self.0;
        while let Some(idx) = s.find(self.1) {
            f.write_str(&s[..idx])?;
            write!(f, r"\{}", s.as_bytes()[idx] as char)?;
            s = &s[idx + 1..];
        }
        f.write_str(s)?;
        Ok(())
    }
}

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

    #[test]
    fn escape() {
        let need_escape = [
            '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!',
        ];

        assert_eq!(Escape(r"foo", &need_escape).to_string(), r"foo");
        assert_eq!(Escape(r"`foobar", &need_escape).to_string(), r"\`foobar");
        assert_eq!(Escape(r"foo*bar", &need_escape).to_string(), r"foo\*bar");
        assert_eq!(Escape(r"foobar_", &need_escape).to_string(), r"foobar\_");
        assert_eq!(
            Escape(r"`foo*bar_", &need_escape).to_string(),
            r"\`foo\*bar\_"
        );
        assert_eq!(
            Escape(r"\foo\bar\", &need_escape).to_string(),
            r"\\foo\\bar\\"
        );
        assert_eq!(Escape(r"*", &need_escape).to_string(), r"\*");
    }
}