Skip to main content

field_kinds_derive/
lib.rs

1mod field;
2mod generate;
3mod parse;
4
5use proc_macro::TokenStream;
6use proc_macro_crate::{FoundCrate, crate_name};
7use quote::{format_ident, quote};
8use syn::parse_macro_input;
9
10#[proc_macro_derive(FieldKinds, attributes(field_kinds, field_tags, serde))]
11pub fn derive_field_kinds(input: TokenStream) -> TokenStream {
12    let input = parse_macro_input!(input as syn::DeriveInput);
13
14    match derive_impl(&input) {
15        Ok(tokens) => tokens.into(),
16        Err(err) => err.to_compile_error().into(),
17    }
18}
19
20fn derive_impl(
21    input: &syn::DeriveInput,
22) -> syn::Result<proc_macro2::TokenStream> {
23    let struct_name = &input.ident;
24    let generics = &input.generics;
25    let rename_all = parse::parse_rename_all(&input.attrs)?;
26    let fields = parse::parse_fields(input)?;
27    let crate_path = resolve_crate_path();
28
29    Ok(generate::generate_all(
30        struct_name,
31        generics,
32        &fields,
33        rename_all,
34        &crate_path,
35    ))
36}
37
38fn resolve_crate_path() -> proc_macro2::TokenStream {
39    match crate_name("field-kinds") {
40        Ok(FoundCrate::Name(name)) => {
41            let ident = format_ident!("{}", name);
42            quote! { ::#ident }
43        }
44        Ok(FoundCrate::Itself) | Err(_) => quote! { ::field_kinds },
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use super::{derive_impl, resolve_crate_path};
51
52    /// The rejections a user can hit. They are unreachable from an
53    /// integration test, since a `derive` that reports an error stops the
54    /// test crate from compiling at all.
55    fn error_for(input: &str) -> String {
56        let input = syn::parse_str(input).expect("input should parse");
57        match derive_impl(&input) {
58            Ok(_) => panic!("deriving should have been rejected"),
59            Err(error) => error.to_string(),
60        }
61    }
62
63    fn accepts(input: &str) -> bool {
64        let input = syn::parse_str(input).expect("input should parse");
65        derive_impl(&input).is_ok()
66    }
67
68    #[test]
69    fn tuple_structs_are_rejected() {
70        assert_eq!(
71            error_for("struct Point(u32, u32);"),
72            "FieldKinds only supports structs with named fields"
73        );
74    }
75
76    #[test]
77    fn unit_structs_are_rejected() {
78        assert_eq!(
79            error_for("struct Marker;"),
80            "FieldKinds only supports structs with named fields"
81        );
82    }
83
84    #[test]
85    fn enums_are_rejected() {
86        assert_eq!(
87            error_for("enum Shape { Circle, Square }"),
88            "FieldKinds only supports structs"
89        );
90    }
91
92    #[test]
93    fn unions_are_rejected() {
94        assert_eq!(
95            error_for("union Slot { a: u32, b: f32 }"),
96            "FieldKinds only supports structs"
97        );
98    }
99
100    /// Dropping the rule would leave the fields under their original names,
101    /// and the mismatch would only surface in the serialized output.
102    #[test]
103    fn unknown_rename_all_rule_is_rejected() {
104        let message = error_for(
105            r#"#[serde(rename_all = "bogusCase")] struct S { a: u32 }"#,
106        );
107        assert!(
108            message.contains("bogusCase"),
109            "the error should name the offending rule, got: {message}"
110        );
111    }
112
113    /// `field_kinds` is this crate's own attribute, so `skipp` is a typo
114    /// that must not pass silently.
115    #[test]
116    fn unknown_field_kinds_option_is_rejected() {
117        let message = error_for("struct S { #[field_kinds(skipp)] a: u32 }");
118        assert!(
119            message.contains("field_kinds"),
120            "the error should mention the attribute, got: {message}"
121        );
122    }
123
124    /// Without quotes the argument is not a literal at all, so the whole
125    /// attribute used to be dropped and the field silently lost every tag.
126    #[test]
127    fn unquoted_field_tag_is_rejected() {
128        let message = error_for("struct S { #[field_tags(primary)] a: u32 }");
129        assert!(
130            message.contains("field_tags"),
131            "the error should mention the attribute, got: {message}"
132        );
133    }
134
135    #[test]
136    fn non_string_field_tag_is_rejected() {
137        let message =
138            error_for(r#"struct S { #[field_tags("primary", 42)] a: u32 }"#);
139        assert!(
140            message.contains("field_tags"),
141            "the error should mention the attribute, got: {message}"
142        );
143    }
144
145    #[test]
146    fn string_field_tags_are_accepted() {
147        assert!(accepts(
148            r#"struct S { #[field_tags("primary", "indexed")] a: u32 }"#
149        ));
150    }
151
152    #[test]
153    fn known_rename_all_rules_are_accepted() {
154        assert!(accepts(
155            r#"#[serde(rename_all = "camelCase")] struct S { a: u32 }"#
156        ));
157    }
158
159    #[test]
160    fn field_kinds_options_are_accepted() {
161        assert!(accepts("struct S { #[field_kinds(skip)] a: u32 }"));
162        assert!(accepts("struct S { #[field_kinds(category = Text)] a: u32 }"));
163    }
164
165    #[test]
166    fn named_structs_are_accepted() {
167        assert!(accepts("struct User { id: u64, name: String }"));
168    }
169
170    /// Falls back to `::field_kinds` when the crate is not a dependency of
171    /// the manifest being compiled, which is the case for this very crate.
172    #[test]
173    fn crate_path_falls_back_when_not_a_dependency() {
174        assert_eq!(resolve_crate_path().to_string(), ":: field_kinds");
175    }
176}