cargo-mend 0.1.0

Opinionated visibility auditing for Rust crates and workspaces
use std::collections::BTreeSet;

use anyhow::Result;

use super::cli::FixCli;

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum FixKind {
    ShortenImport,
    FixPubUse,
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FixSelection {
    kinds: BTreeSet<FixKind>,
}

impl FixSelection {
    pub fn from_cli(fix_cli: &FixCli) -> Self {
        let mut kinds = BTreeSet::new();
        if fix_cli.fix {
            kinds.insert(FixKind::ShortenImport);
        }
        if fix_cli.fix_pub_use {
            kinds.insert(FixKind::FixPubUse);
        }
        Self { kinds }
    }

    pub fn contains(&self, kind: FixKind) -> bool { self.kinds.contains(&kind) }

    pub fn is_empty(&self) -> bool { self.kinds.is_empty() }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OperationIntent {
    ReadOnly,
    DryRun,
    Apply,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OperationMode {
    pub intent: OperationIntent,
    pub fixes:  FixSelection,
}

impl OperationMode {
    pub fn from_cli(fix_cli: &FixCli) -> Result<Self> {
        let fixes = FixSelection::from_cli(fix_cli);
        if fix_cli.dry_run {
            if fixes.is_empty() {
                anyhow::bail!("`--dry-run` requires `--fix` or `--fix-pub-use`");
            }
            return Ok(Self {
                intent: OperationIntent::DryRun,
                fixes,
            });
        }

        if fixes.is_empty() {
            Ok(Self {
                intent: OperationIntent::ReadOnly,
                fixes,
            })
        } else {
            Ok(Self {
                intent: OperationIntent::Apply,
                fixes,
            })
        }
    }
}

#[cfg(test)]
mod tests {
    use super::FixKind;
    use super::OperationIntent;
    use super::OperationMode;
    use crate::cli::FixCli;

    #[test]
    fn operation_mode_is_read_only_without_fix_flags() {
        let cli = FixCli {
            fix:         false,
            fix_pub_use: false,
            dry_run:     false,
        };
        let Ok(mode) = OperationMode::from_cli(&cli) else {
            unreachable!("read-only mode should parse");
        };
        assert_eq!(mode.intent, OperationIntent::ReadOnly);
    }

    #[test]
    fn operation_mode_rejects_dry_run_without_fix_flags() {
        let cli = FixCli {
            fix:         false,
            fix_pub_use: false,
            dry_run:     true,
        };
        let mode = OperationMode::from_cli(&cli);
        let Err(err) = mode else {
            unreachable!("dry run without fix flags should be rejected");
        };
        assert!(
            err.to_string()
                .contains("`--dry-run` requires `--fix` or `--fix-pub-use`")
        );
    }

    #[test]
    fn operation_mode_allows_previewing_multiple_fix_kinds() {
        let cli = FixCli {
            fix:         true,
            fix_pub_use: true,
            dry_run:     true,
        };
        let Ok(mode) = OperationMode::from_cli(&cli) else {
            unreachable!("preview mode should parse");
        };
        assert_eq!(mode.intent, OperationIntent::DryRun);
        assert!(mode.fixes.contains(FixKind::ShortenImport));
        assert!(mode.fixes.contains(FixKind::FixPubUse));
    }

    #[test]
    fn operation_mode_allows_applying_multiple_fix_kinds() {
        let cli = FixCli {
            fix:         true,
            fix_pub_use: true,
            dry_run:     false,
        };
        let Ok(mode) = OperationMode::from_cli(&cli) else {
            unreachable!("apply mode should parse");
        };
        assert_eq!(mode.intent, OperationIntent::Apply);
        assert!(mode.fixes.contains(FixKind::ShortenImport));
        assert!(mode.fixes.contains(FixKind::FixPubUse));
    }
}