serde-evolve-macros 0.1.0

Procedural macros for the serde-evolve crate
Documentation
use crate::parse::ParsedInput;
use quote::format_ident;

#[derive(Debug, Clone)]
pub struct ValidatedInput {
    pub domain_ident: syn::Ident,
    pub rep_ident: syn::Ident,
    pub mode: Mode,
    pub transparent: bool,
    pub versions: Vec<syn::Path>,
}

#[derive(Debug, Clone)]
pub enum Mode {
    Infallible,
    Fallible { error: syn::Path },
}

pub fn validate(parsed: ParsedInput) -> Result<ValidatedInput, syn::Error> {
    let ParsedInput {
        ident,
        representation,
        mode,
        error,
        transparent,
        versions,
    } = parsed;

    if versions.is_empty() {
        return Err(syn::Error::new_spanned(
            &ident,
            "chain must contain at least one version type",
        ));
    }

    let rep_ident = representation.unwrap_or_else(|| format_ident!("{}Versions", ident));

    let validated_mode = match mode.as_deref().unwrap_or("fallible") {
        "infallible" => Mode::Infallible,
        "fallible" => match error {
            Some(error) => Mode::Fallible { error },
            None => {
                return Err(syn::Error::new_spanned(
                    &ident,
                    "fallible mode requires 'error' attribute",
                ));
            }
        },
        other => {
            return Err(syn::Error::new_spanned(
                &ident,
                format!("invalid mode '{other}', expected 'infallible' or 'fallible'"),
            ));
        }
    };

    Ok(ValidatedInput {
        domain_ident: ident,
        rep_ident,
        mode: validated_mode,
        transparent,
        versions,
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use syn::{parse_quote, parse_str};

    fn base_parsed_input() -> ParsedInput {
        ParsedInput {
            ident: parse_str::<syn::Ident>("Example").unwrap(),
            representation: None,
            mode: None,
            error: Some(parse_quote!(ExampleError)),
            transparent: false,
            versions: vec![parse_quote!(Version1), parse_quote!(Version2)],
        }
    }

    #[test]
    fn infers_defaults_and_representation_name() {
        let parsed = base_parsed_input();
        let validated = validate(parsed).expect("validation should succeed");

        assert_eq!(validated.domain_ident.to_string(), "Example");
        assert_eq!(validated.rep_ident.to_string(), "ExampleVersions");
        assert!(matches!(validated.mode, Mode::Fallible { .. }));
        assert!(!validated.transparent);
        assert_eq!(validated.versions.len(), 2);
    }

    #[test]
    fn errors_when_missing_error_in_fallible_mode() {
        let mut parsed = base_parsed_input();
        parsed.error = None;
        let err = validate(parsed).expect_err("validation should fail");
        assert_eq!(err.to_string(), "fallible mode requires 'error' attribute");
    }

    #[test]
    fn errors_on_empty_version_chain() {
        let mut parsed = base_parsed_input();
        parsed.versions.clear();
        let err = validate(parsed).expect_err("validation should fail");
        assert_eq!(
            err.to_string(),
            "chain must contain at least one version type"
        );
    }
}