field_kinds_derive/
lib.rs1mod 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 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 #[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 #[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 #[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 #[test]
173 fn crate_path_falls_back_when_not_a_dependency() {
174 assert_eq!(resolve_crate_path().to_string(), ":: field_kinds");
175 }
176}