cargo-sync-rdme 0.8.0

Cargo subcommand to synchronize README with crate documentation
use std::collections::HashMap;

use serde::Deserialize;

use crate::config::ApplyLayer;

#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub(crate) struct Rustdoc {
    #[serde(default)]
    pub(crate) toolchain: Option<String>,
    #[serde(default)]
    pub(crate) features: Option<Vec<String>>,
    #[serde(default)]
    pub(crate) all_features: Option<bool>,
    #[serde(default)]
    pub(crate) no_default_features: Option<bool>,
    #[serde(default)]
    pub(crate) standard_library_url_mode: Option<StandardLibraryUrlMode>,
    #[serde(default)]
    pub(crate) html_root_url: Option<String>,
    #[serde(default)]
    pub(crate) mappings: HashMap<String, String>,
    #[expect(clippy::struct_field_names)]
    #[serde(default)]
    pub(crate) rustdoc_args: Option<Vec<String>>,
    #[serde(default)]
    pub(crate) cargo_args: Option<Vec<String>>,
}

impl ApplyLayer for Rustdoc {
    fn apply_layer(&mut self, layer: &Self) {
        let Self {
            toolchain,
            features,
            all_features,
            no_default_features,
            standard_library_url_mode,
            html_root_url,
            mappings,
            rustdoc_args,
            cargo_args,
        } = self;
        toolchain.apply_layer(&layer.toolchain);
        features.apply_layer(&layer.features);
        all_features.apply_layer(&layer.all_features);
        no_default_features.apply_layer(&layer.no_default_features);
        standard_library_url_mode.apply_layer(&layer.standard_library_url_mode);
        html_root_url.apply_layer(&layer.html_root_url);
        mappings.apply_layer(&layer.mappings);
        rustdoc_args.apply_layer(&layer.rustdoc_args);
        cargo_args.apply_layer(&layer.cargo_args);
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum StandardLibraryUrlMode {
    #[default]
    Channel,
    Version,
    AsIs,
}

impl ApplyLayer for StandardLibraryUrlMode {
    fn apply_layer(&mut self, layer: &Self) {
        *self = *layer;
    }
}

#[cfg(test)]
mod tests {
    use indoc::indoc;
    use similar_asserts::assert_eq;

    use crate::config::testing;

    use super::*;

    #[test]
    fn rustdoc_apply_layer_updates_html_root_url_and_mappings() {
        let mut target = Rustdoc {
            toolchain: Some("stable".to_owned()),
            standard_library_url_mode: Some(StandardLibraryUrlMode::Version),
            html_root_url: Some("https://docs.example.com/target/".to_owned()),
            mappings: HashMap::from([
                (
                    "target::TargetType".to_owned(),
                    "https://reference.example.com/items/target-type".to_owned(),
                ),
                (
                    "target::SharedType".to_owned(),
                    "https://reference.example.com/items/shared-type-from-target".to_owned(),
                ),
            ]),
            ..Default::default()
        };
        let layer = Rustdoc {
            toolchain: Some("nightly".to_owned()),
            standard_library_url_mode: Some(StandardLibraryUrlMode::AsIs),
            html_root_url: Some("https://docs.example.com/layer/".to_owned()),
            mappings: HashMap::from([
                (
                    "target::SharedType".to_owned(),
                    "https://reference.example.com/items/shared-type-from-layer".to_owned(),
                ),
                (
                    "target::LayerType".to_owned(),
                    "https://reference.example.com/items/layer-type".to_owned(),
                ),
            ]),
            ..Default::default()
        };

        target.apply_layer(&layer);

        assert_eq!(
            target,
            Rustdoc {
                toolchain: Some("nightly".to_owned()),
                standard_library_url_mode: Some(StandardLibraryUrlMode::AsIs),
                html_root_url: Some("https://docs.example.com/layer/".to_owned()),
                mappings: HashMap::from([
                    (
                        "target::TargetType".to_owned(),
                        "https://reference.example.com/items/target-type".to_owned(),
                    ),
                    (
                        "target::SharedType".to_owned(),
                        "https://reference.example.com/items/shared-type-from-layer".to_owned(),
                    ),
                    (
                        "target::LayerType".to_owned(),
                        "https://reference.example.com/items/layer-type".to_owned(),
                    ),
                ]),
                ..Default::default()
            }
        );
    }

    #[test]
    fn deserialize_rustdoc_parses_valid_maps() {
        let source = testing::rustdoc_manifest(indoc! {r#"
            toolchain = "stable"
            features = ["feature1", "feature2"]
            all-features = true
            no-default-features = false
            standard-library-url-mode = "version"
            html-root-url = "https://docs.example.com/my-crate/"
            mappings = {
              "std::io::Result" = "https://doc.rust-lang.org/stable/std/io/error/type.Result.html",
              "crate::SomeTrait" = "https://reference.example.com/items/some-trait",
            }
            rustdoc-args = ["--extern-html-root-takes-precedence", "--cfg=docsrs"]
            cargo-args = ["-Zrustdoc-scrape-examples"]
        "#});
        let rustdoc = testing::parse_rustdoc(&source);
        assert_eq!(
            rustdoc,
            Rustdoc {
                toolchain: Some("stable".to_owned()),
                features: Some(vec!["feature1".to_owned(), "feature2".to_owned()]),
                all_features: Some(true),
                no_default_features: Some(false),
                standard_library_url_mode: Some(StandardLibraryUrlMode::Version),
                html_root_url: Some("https://docs.example.com/my-crate/".to_owned()),
                mappings: HashMap::from([
                    (
                        "std::io::Result".to_owned(),
                        "https://doc.rust-lang.org/stable/std/io/error/type.Result.html".to_owned(),
                    ),
                    (
                        "crate::SomeTrait".to_owned(),
                        "https://reference.example.com/items/some-trait".to_owned(),
                    ),
                ]),
                rustdoc_args: Some(vec![
                    "--extern-html-root-takes-precedence".to_owned(),
                    "--cfg=docsrs".to_owned(),
                ]),
                cargo_args: Some(vec!["-Zrustdoc-scrape-examples".to_owned()]),
            }
        );
    }
}