garde 0.23.0

Validation library
Documentation
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};

const RUN_ENV: &str = "GARDE_RUN_FEATURE_FLAG_TESTS";

struct TempProject {
    path: PathBuf,
}

impl TempProject {
    fn new(name: &str, garde_features: &[&str]) -> Self {
        let mut path = std::env::temp_dir();
        path.push(format!(
            "garde-{name}-{}-{}",
            std::process::id(),
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap()
                .as_nanos()
        ));

        fs::create_dir_all(path.join("src")).unwrap();

        let features = garde_features
            .iter()
            .map(|feature| format!("\"{feature}\""))
            .collect::<Vec<_>>()
            .join(", ");

        fs::write(
            path.join("Cargo.toml"),
            format!(
                r#"[package]
name = "{name}"
version = "0.0.0"
edition = "2021"

[dependencies]
garde = {{ path = "{}", default-features = false, features = [{features}] }}
"#,
                Path::new(env!("CARGO_MANIFEST_DIR")).display()
            ),
        )
        .unwrap();

        Self { path }
    }

    fn write_lib(&self, contents: &str) {
        fs::write(self.path.join("src/lib.rs"), contents).unwrap();
    }

    fn cargo_check(&self) -> std::process::Output {
        Command::new(env!("CARGO"))
            .arg("check")
            .arg("--quiet")
            .arg("--manifest-path")
            .arg(self.path.join("Cargo.toml"))
            .output()
            .unwrap()
    }
}

impl Drop for TempProject {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.path);
    }
}

#[test]
fn feature_gated_rules_report_missing_feature_flags() {
    if std::env::var_os(RUN_ENV).is_none() {
        eprintln!("skipping; set {RUN_ENV}=1 or run `cargo xtask test feature-flags`");
        return;
    }

    for (rule, feature) in [
        ("email", "email"),
        ("url", "url"),
        ("credit_card", "credit-card"),
        ("phone_number", "phone-number"),
    ] {
        let project = TempProject::new(
            &format!("missing-{}", feature.replace('-', "_")),
            &["derive"],
        );
        project.write_lib(&format!(
            r#"#[derive(garde::Validate)]
struct Test {{
    #[garde({rule})]
    value: String,
}}
"#
        ));

        let output = project.cargo_check();
        assert!(
            !output.status.success(),
            "`#[garde({rule})]` unexpectedly compiled without the `{feature}` feature"
        );

        let stderr = String::from_utf8_lossy(&output.stderr);
        let expected = format!("validation rule `{rule}` requires the `{feature}` feature flag");
        assert!(
            stderr.contains(&expected),
            "missing expected error `{expected}` in stderr:\n{stderr}"
        );
    }
}

#[test]
fn feature_gated_rules_compile_when_their_features_are_enabled() {
    if std::env::var_os(RUN_ENV).is_none() {
        eprintln!("skipping; set {RUN_ENV}=1 or run `cargo xtask test feature-flags`");
        return;
    }

    for (rule, feature) in [
        ("email", "email"),
        ("url", "url"),
        ("credit_card", "credit-card"),
        ("phone_number", "phone-number"),
    ] {
        let project = TempProject::new(
            &format!("enabled-{}", feature.replace('-', "_")),
            &["derive", feature],
        );
        project.write_lib(&format!(
            r#"#[derive(garde::Validate)]
struct Test {{
    #[garde({rule})]
    value: String,
}}
"#
        ));

        let output = project.cargo_check();
        assert!(
            output.status.success(),
            "`#[garde({rule})]` did not compile with the `{feature}` feature:\n{}",
            String::from_utf8_lossy(&output.stderr)
        );
    }
}