zyn 0.5.4

A proc macro framework with templates, composable elements, and built-in diagnostics
Documentation
use zyn::FromArg;
use zyn::meta::Args;

#[derive(zyn::Attribute)]
#[zyn("my_attr", about = "test attribute")]
struct MyAttr {
    #[zyn(about = "the name")]
    name: String,
    #[zyn(default)]
    count: i64,
    enabled: bool,
    tag: Option<String>,
}

#[test]
fn attribute_mode_full_extraction() {
    let args: Args = zyn::parse!("name = \"hello\", count = 3, enabled, tag = \"v1\"").unwrap();
    let attr = MyAttr::from_args(&args).unwrap();

    assert_eq!(attr.name, "hello");
    assert_eq!(attr.count, 3);
    assert!(attr.enabled);
    assert_eq!(attr.tag, Some("v1".to_string()));
}

#[test]
fn attribute_mode_optional_absent() {
    let args: Args = zyn::parse!("name = \"hi\"").unwrap();
    let attr = MyAttr::from_args(&args).unwrap();

    assert_eq!(attr.name, "hi");
    assert_eq!(attr.count, 0);
    assert!(!attr.enabled);
    assert_eq!(attr.tag, None);
}

#[test]
fn attribute_mode_missing_required_is_err() {
    let args: Args = zyn::parse!("").unwrap();
    assert!(MyAttr::from_args(&args).is_err());
}

#[derive(zyn::Attribute)]
struct ArgMode {
    a: i64,
    b: String,
}

#[test]
fn argument_mode_from_args() {
    let args: Args = zyn::parse!("a = 42, b = \"hello\"").unwrap();
    let v = ArgMode::from_args(&args).unwrap();

    assert_eq!(v.a, 42);
    assert_eq!(v.b, "hello");
}

#[test]
fn argument_mode_from_arg_via_list() {
    let arg: zyn::meta::Arg = zyn::parse!("inner(a = 7, b = \"world\")").unwrap();
    let v = ArgMode::from_arg(&arg).unwrap();

    assert_eq!(v.a, 7);
    assert_eq!(v.b, "world");
}

#[test]
fn argument_mode_from_arg_non_list_is_err() {
    let arg: zyn::meta::Arg = zyn::parse!("skip").unwrap();
    assert!(ArgMode::from_arg(&arg).is_err());
}

#[derive(zyn::Attribute)]
#[zyn("outer")]
struct Outer {
    inner: ArgMode,
}

#[test]
fn recursive_nesting() {
    let args: Args = zyn::parse!("inner(a = 5, b = \"nested\")").unwrap();
    let v = Outer::from_args(&args).unwrap();

    assert_eq!(v.inner.a, 5);
    assert_eq!(v.inner.b, "nested");
}

#[derive(zyn::Attribute)]
#[zyn("positioned")]
struct Positional {
    #[zyn(0)]
    first: String,
    #[zyn(1)]
    second: i64,
}

#[test]
fn positional_args() {
    let args: Args = zyn::parse!("\"hello\", 42").unwrap();
    let v = Positional::from_args(&args).unwrap();

    assert_eq!(v.first, "hello");
    assert_eq!(v.second, 42);
}

#[derive(zyn::Attribute)]
#[zyn("renamed")]
struct Renamed {
    #[zyn("my_key")]
    value: String,
}

#[test]
fn name_override() {
    let args: Args = zyn::parse!("my_key = \"found\"").unwrap();
    let v = Renamed::from_args(&args).unwrap();

    assert_eq!(v.value, "found");
}

#[derive(zyn::Attribute)]
#[zyn("defaulted")]
struct WithDefault {
    #[zyn(default = "fallback")]
    label: String,
    #[zyn(default)]
    count: i64,
}

#[test]
fn default_expr_used_when_absent() {
    let args: Args = zyn::parse!("").unwrap();
    let v = WithDefault::from_args(&args).unwrap();

    assert_eq!(v.label, "fallback");
    assert_eq!(v.count, 0);
}

#[test]
fn default_overridden_when_present() {
    let args: Args = zyn::parse!("label = \"custom\", count = 9").unwrap();
    let v = WithDefault::from_args(&args).unwrap();

    assert_eq!(v.label, "custom");
    assert_eq!(v.count, 9);
}

#[derive(zyn::Attribute)]
#[zyn("skipped")]
struct WithSkip {
    name: String,
    #[zyn(skip)]
    internal: i64,
}

#[test]
fn skip_field_always_default() {
    let args: Args = zyn::parse!("name = \"hi\"").unwrap();
    let v = WithSkip::from_args(&args).unwrap();

    assert_eq!(v.name, "hi");
    assert_eq!(v.internal, 0);
}

#[test]
fn about_generated() {
    let about = MyAttr::about();

    assert!(about.contains("#[my_attr(...)]"));
    assert!(about.contains("test attribute"));
    assert!(about.contains("name"));
    assert!(about.contains("the name"));
}

#[test]
fn about_shows_required_status() {
    let about = MyAttr::about();
    assert!(about.contains("(required)"));
}

fn expect_err<T>(result: zyn::Result<T>) -> zyn::Diagnostic {
    match result {
        Err(e) => e,
        Ok(_) => panic!("expected error"),
    }
}

#[test]
fn multiple_missing_fields_all_reported() {
    let args: Args = zyn::parse!("").unwrap();
    let err = expect_err(ArgMode::from_args(&args));
    let combined = format!("{err}");

    assert!(combined.contains("missing required field `a`"));
    assert!(combined.contains("missing required field `b`"));
    assert_eq!(err.len(), 2);
}

#[test]
fn unknown_key_reported() {
    let args: Args = zyn::parse!("name = \"hello\", bogus = 1").unwrap();
    let err = expect_err(MyAttr::from_args(&args));
    let combined = format!("{err}");

    assert!(combined.contains("unknown argument `bogus`"));
}

#[test]
fn did_you_mean_suggestion() {
    let args: Args = zyn::parse!("naem = \"hello\"").unwrap();
    let err = expect_err(MyAttr::from_args(&args));
    let combined = format!("{err}");

    assert!(combined.contains("unknown argument `naem`"));
    assert!(combined.contains("did you mean"));
}

#[test]
fn unknown_key_no_suggestion_when_distant() {
    let args: Args = zyn::parse!("name = \"hello\", zzzzzzzzz = 1").unwrap();
    let err = expect_err(MyAttr::from_args(&args));
    let combined = format!("{err}");

    assert!(combined.contains("unknown argument `zzzzzzzzz`"));
}

#[test]
fn about_text_in_missing_error() {
    let args: Args = zyn::parse!("count = 1").unwrap();
    let err = expect_err(MyAttr::from_args(&args));
    let combined = format!("{err}");

    assert!(combined.contains("missing required field `name`"));
    assert!(combined.contains("the name"));
}

#[test]
fn valid_args_still_work_after_accumulation() {
    let args: Args = zyn::parse!("name = \"test\", count = 5, enabled, tag = \"v2\"").unwrap();
    let attr = MyAttr::from_args(&args).unwrap();

    assert_eq!(attr.name, "test");
    assert_eq!(attr.count, 5);
    assert!(attr.enabled);
    assert_eq!(attr.tag, Some("v2".to_string()));
}

#[test]
fn type_mismatch_and_missing_both_reported() {
    let args: Args = zyn::parse!("a = \"not_an_int\", b = 42").unwrap();
    let err = expect_err(ArgMode::from_args(&args));
    let combined = format!("{err}");

    assert!(err.is_error());
    assert!(combined.contains("expected integer literal"));
    assert!(combined.contains("expected string literal"));
    assert_eq!(err.len(), 2);
}

#[test]
fn unknown_key_with_valid_fields_still_errors() {
    let args: Args = zyn::parse!("a = 1, b = \"ok\", bogus = 3").unwrap();
    let err = expect_err(ArgMode::from_args(&args));
    let combined = format!("{err}");

    assert!(combined.contains("unknown argument `bogus`"));
    assert_eq!(err.len(), 1);
}