rullst-orm-macros 12.3.0

Procedural macros and derive attributes for the rullst-orm Active Record pattern.
Documentation
use super::*;
use syn::parse_quote;

#[test]
fn generates_typed_metadata_and_explicit_widgets() {
    let input: DeriveInput = parse_quote! {
        #[nexus(table = "articles", label = "Articles", icon = "📰")]
        struct Article {
            #[nexus(primary_key)]
            uuid: String,
            #[nexus(kind = "textarea", label = "Article body")]
            body: String,
            #[nexus(kind = "enum", options = "draft, published")]
            status: String,
            published: bool,
        }
    };
    let output = expand_nexus(&input)
        .expect("valid Nexus derive")
        .to_string();
    assert!(output.contains("fn nexus_table"));
    assert!(output.contains("\"articles\""));
    assert!(output.contains("FieldKind :: Textarea"));
    assert!(output.contains("FieldKind :: Enum"));
    assert!(output.contains("\"draft\""));
    assert!(output.contains("FieldKind :: Boolean"));
    assert!(output.contains("\"uuid\""));
}

#[test]
fn rejects_invalid_widget_configuration_and_primary_key() {
    let invalid_kind: DeriveInput = parse_quote! {
        struct Article {
            id: i64,
            #[nexus(kind = "magic")]
            body: String,
        }
    };
    assert!(
        expand_nexus(&invalid_kind)
            .expect_err("invalid widget must fail")
            .to_string()
            .contains("unsupported Nexus field kind")
    );

    let invalid_pk: DeriveInput = parse_quote! {
        #[nexus(primary_key = "missing")]
        struct Article { id: i64 }
    };
    assert!(
        expand_nexus(&invalid_pk)
            .expect_err("missing primary key must fail")
            .to_string()
            .contains("is not a field")
    );

    let invalid_tenant: DeriveInput = parse_quote! {
        #[nexus(tenant = "organization_id")]
        struct Article { id: i64 }
    };
    assert!(
        expand_nexus(&invalid_tenant)
            .expect_err("missing tenant column must fail")
            .to_string()
            .contains("tenant column `organization_id` is not a field")
    );

    let invalid_tenant_type: DeriveInput = parse_quote! {
        #[nexus(tenant = "organization_id")]
        struct Article {
            id: i64,
            organization_id: Option<String>,
        }
    };
    assert!(
        expand_nexus(&invalid_tenant_type)
            .expect_err("nullable tenant column must fail")
            .to_string()
            .contains("must use a non-optional `String` with text metadata")
    );

    let invalid_tenant_widget: DeriveInput = parse_quote! {
        #[nexus(tenant = "organization_id")]
        struct Article {
            id: i64,
            #[nexus(kind = "textarea")]
            organization_id: String,
        }
    };
    assert!(
        expand_nexus(&invalid_tenant_widget)
            .expect_err("non-text tenant metadata must fail")
            .to_string()
            .contains("must use a non-optional `String` with text metadata")
    );
}

#[test]
fn skips_shared_orm_options_and_relation_fields() {
    let input: DeriveInput = parse_quote! {
        #[orm(
            table_name = "projects",
            tenant_column = "tenant_id",
            policy = "ProjectPolicy",
            soft_delete(column = "removed_at"),
            auditable
        )]
        struct Project {
            id: i32,
            tenant_id: String,
            #[orm(has_many = "Tag", foreign_key = "project_id", cascade_soft_delete)]
            tags: Option<Vec<Tag>>,
            #[orm(belongs_to = "Owner", foreign_key = "owner_id")]
            owner: Option<Owner>,
            owner_id: i32,
            #[orm(masked)]
            title: String,
            removed_at: Option<String>,
        }
    };
    let output = expand_nexus(&input)
        .expect("shared ORM options must not break the Nexus derive")
        .to_string();
    assert!(output.contains("fn nexus_table () -> & 'static str { \"projects\" }"));
    for column in ["id", "tenant_id", "owner_id", "title", "removed_at"] {
        assert!(output.contains(&format!("name : \"{column}\"")), "{column}");
    }
    for relation in ["tags", "owner"] {
        assert!(
            !output.contains(&format!("name : \"{relation}\"")),
            "{relation}"
        );
    }
    assert!(!output.contains("fn nexus_tenant_column"));
}

#[test]
fn generates_readonly_hidden_tenant_scope_from_orm_metadata() {
    let input: DeriveInput = parse_quote! {
        #[orm(table = "articles", tenant = "organization_id")]
        struct Article {
            id: i64,
            organization_id: String,
            title: String,
        }
    };
    let output = expand_nexus(&input)
        .expect("valid tenant-scoped Nexus derive")
        .to_string();

    assert!(output.contains("fn nexus_tenant_column"));
    assert!(output.contains("Some (\"organization_id\")"));
    let tenant_field = output
        .split("name : \"organization_id\"")
        .nth(1)
        .expect("tenant field metadata emitted");
    assert!(tenant_field.contains("hidden : true"));
    assert!(tenant_field.contains("readonly : true"));
}

/// The generated `FieldMeta` of `name`, or `None` when it is not emitted.
fn field_meta(output: &str, name: &str) -> Option<String> {
    let rest = output.split(&format!("name : \"{name}\"")).nth(1)?;
    Some(rest.split('}').next().unwrap_or_default().to_string())
}

#[test]
fn honours_orm_skip_and_confidentiality_markers() {
    let input: DeriveInput = parse_quote! {
        #[orm(table = "users")]
        struct User {
            id: i32,
            name: String,
            #[orm(skip)]
            display_name: String,
            #[sqlx(default, skip)]
            cached_total: i64,
            #[orm(hidden)]
            reset_token: String,
            #[orm(encrypted)]
            tax_id: String,
            cpf: SecretString,
            backup_cpf: Option<rullst_orm::SecretString>,
            #[orm(masked)]
            api_token: String,
            #[orm(masked)]
            #[nexus(kind = "text")]
            recovery_hint: String,
            #[orm(hidden)]
            #[nexus(kind = "password")]
            password: String,
            #[sqlx(json)]
            settings: Json<Settings>,
        }
    };
    let output = expand_nexus(&input)
        .expect("ORM markers are valid Nexus input")
        .to_string();

    for skipped in ["display_name", "cached_total"] {
        assert!(field_meta(&output, skipped).is_none(), "{skipped}");
    }
    // 12.x Nexus lists and pre-fills `Password` fields, so even an explicit
    // `kind = "password"` keeps an `#[orm(hidden)]` field concealed.
    for sealed in ["reset_token", "tax_id", "cpf", "backup_cpf", "password"] {
        let meta = field_meta(&output, sealed).expect("protected field metadata");
        assert!(meta.contains("FieldKind :: Password"), "{sealed}: {meta}");
        assert!(meta.contains("hidden : true"), "{sealed}: {meta}");
        assert!(meta.contains("readonly : true"), "{sealed}: {meta}");
    }
    let masked = field_meta(&output, "api_token").expect("masked field metadata");
    assert!(masked.contains("FieldKind :: Password"), "{masked}");
    assert!(masked.contains("hidden : false"), "{masked}");
    assert!(masked.contains("readonly : false"), "{masked}");
    let relaxed = field_meta(&output, "recovery_hint").expect("masked override");
    assert!(relaxed.contains("FieldKind :: Text"), "{relaxed}");
    let settings = field_meta(&output, "settings").expect("json column is kept");
    assert!(settings.contains("hidden : false"), "{settings}");
}

#[test]
fn widget_overrides_never_expose_protected_orm_fields() {
    let cases: [(DeriveInput, &str); 3] = [
        (
            parse_quote! {
                struct Patient {
                    id: i32,
                    #[orm(encrypted)]
                    #[nexus(kind = "textarea")]
                    diagnosis: String,
                }
            },
            "diagnosis",
        ),
        (
            parse_quote! {
                struct Patient {
                    id: i32,
                    #[nexus(kind = "enum", options = "a, b")]
                    cpf: SecretString,
                }
            },
            "cpf",
        ),
        (
            parse_quote! {
                struct Account {
                    id: i32,
                    #[orm(hidden)]
                    #[nexus(kind = "text")]
                    reset_token: String,
                }
            },
            "reset_token",
        ),
    ];
    for (input, field) in cases {
        let output = expand_nexus(&input)
            .expect("existing widget overrides keep compiling")
            .to_string();
        let meta = field_meta(&output, field).expect("protected field metadata");
        assert!(meta.contains("FieldKind :: Password"), "{field}: {meta}");
        assert!(meta.contains("hidden : true"), "{field}: {meta}");
        assert!(meta.contains("readonly : true"), "{field}: {meta}");
    }

    let invalid: DeriveInput = parse_quote! {
        struct Patient {
            id: i32,
            #[orm(encrypted)]
            #[nexus(kind = "magic")]
            diagnosis: String,
        }
    };
    assert!(
        expand_nexus(&invalid)
            .expect_err("an invalid widget still fails")
            .to_string()
            .contains("unsupported Nexus field kind")
    );
}