proc_micro/lib.rs
1//! # Proc Micro
2//!
3//! Small conveniences for high-quality macros.
4//!
5//! ## Use
6//!
7//! ```term
8//! $ cargo add proc_micro
9//! $ cargo add strum --features=derive
10//! ```
11//!
12//! ## Errors
13//!
14//! Normal rust code returns on the first error. Great macros accumulate as many
15//! errors as they can and show them all at once.
16//!
17//! - [MaybeError]: A container that holds zero or more [syn::Error]-s. When
18//! it holds error(s) they can be accumulated into a single [syn::Error] (which
19//! is a data structure for holding one or more macro errors).
20//! - [OkMaybe]: An alternative for Result that allows for returning both data
21//! and (maybe) errors at the same time. Use Result when an error means
22//! the data is unusable (untrustable) or [OkMaybe] when an error means
23//! the partial data might still be useful in generating additional error.
24//! information. The caller can convert an [OkMaybe] into a [Result], but cannot
25//! convert a [Result] into an [OkMaybe].
26//!
27//! ## Enum powered Attribute parsing
28//!
29//! These helpers work with attributes defined as enums with this library:
30//!
31//! - [WithSpan]: Holds the attributes you parsed and their spans
32//! - [parse_attrs]: Turns a [syn::Attribute] into a [Vec] of your parsed enums
33//! - [unique]: Guarantee each enum attribute is only listed once
34//! - [check_exclusive]: Check if an exclusive attribute is used in conjunction with others.
35//! - [known_attribute]: Convienence function for parsing an enum discriminant
36//!
37//! ## Tutorial
38//!
39//! Here's how you define a macro attribute that has a namespace of `my_macro` and
40//! accepts `rename = <string>` and `ignore` attributes using the [strum crate](https://crates.io/crates/strum):
41//!
42//! ```rust
43//! # #[allow(dead_code)]
44//! const NAMESPACE: proc_micro::AttrNamespace =
45//! proc_micro::AttrNamespace("my_macro");
46//!
47//! #[derive(strum::EnumDiscriminants, Debug, PartialEq)]
48//! #[strum_discriminants(
49//! name(KnownAttribute),
50//! derive(strum::EnumIter, strum::Display, strum::EnumString, Hash)
51//! )]
52//! enum ParseAttribute {
53//! // #[my_macro(rename = "<string>")]
54//! #[allow(non_camel_case_types)]
55//! rename(String),
56//! // #[my_macro(ignore)]
57//! #[allow(non_camel_case_types)]
58//! ignore,
59//! }
60//!
61//! impl syn::parse::Parse for ParseAttribute {
62//! fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
63//! let ident = input.parse::<syn::Ident>()?;
64//! match proc_micro::known_attribute(&ident)? {
65//! KnownAttribute::rename => {
66//! input.parse::<syn::Token![=]>()?;
67//! Ok(ParseAttribute::rename(
68//! input.parse::<syn::LitStr>()?.value(),
69//! ))
70//! }
71//! KnownAttribute::ignore => Ok(ParseAttribute::ignore),
72//! }
73//! }
74//! }
75//! ```
76//!
77//! Each parsed attribute is stored in our enum while the discriminant can be
78//! used as a lookup. By representing attributes as an enum, we can be confident
79//! our code handles attribute additions or modifications exhaustively.
80//!
81//! This also provides a platform for unit testing attribute logic:
82//!
83//! ```
84#![doc = include_str!("./fixtures/hidden_parse_attribute_rename_ignore.md")]
85//! let attribute = syn::parse_str::<ParseAttribute>(
86//! "rename = \"Ruby version\""
87//! ).unwrap();
88//! assert_eq!(ParseAttribute::rename("Ruby version".to_string()), attribute);
89//! ```
90//!
91//! Then within your macro code you can convert many comma separated attributes
92//! into enums while accumulating errors:
93//!
94//! ```
95#![doc = include_str!("./fixtures/hidden_parse_attribute_rename_ignore.md")]
96//! # use proc_micro::WithSpan;
97//! # const NAMESPACE: proc_micro::AttrNamespace =
98//! # proc_micro::AttrNamespace("my_macro");
99//!
100//! let mut errors = proc_micro::MaybeError::new();
101//!
102//! let field: syn::Field = syn::parse_quote! {
103//! #[my_macro(ignore, rename = "Ruby version")]
104//! version: String
105//! };
106//!
107//! let attributes: Vec<WithSpan<ParseAttribute>> = proc_micro::parse_attrs(
108//! &NAMESPACE, &field.attrs
109//! ).push_unwrap(&mut errors);
110//!
111//! assert_eq!(2, attributes.len());
112//! assert!(matches!(attributes.first(), Some(WithSpan(ParseAttribute::ignore, _))));
113//! assert!(errors.is_empty());
114//! ```
115//!
116//! Use this result with other helpers to validate your attribute requirements.
117//! For example [unique] requires that attributes are specified at most once i.e.
118//! `#[my_macro(ignore, ignore)]` is incorrect. And [check_exclusive] is called
119//! for attributes that must be used exclusively, i.e. using "ignore" with any
120//! other attribute is in valid as they would have no effect. And you can use
121//! the returned [WithSpan] information to build your own custom [syn::Error]
122//! errors.
123//!
124//! ```
125#![doc = include_str!("./fixtures/hidden_parse_attribute_rename_ignore.md")]
126//! # use proc_micro::WithSpan;
127//! # const NAMESPACE: proc_micro::AttrNamespace =
128//! # proc_micro::AttrNamespace("my_macro");
129//!
130//! use proc_micro::OkMaybe;
131//!
132//! // Make a structure to store your parsed configuration
133//! #[derive(Debug, Clone)]
134//! struct FieldConfig {
135//! # #[allow(dead_code)]
136//! ignore: bool,
137//! # #[allow(dead_code)]
138//! rename: Option<String>
139//! }
140//!
141//! // Use our building blocks to implement your desired logic
142//! fn field_config(field: &syn::Field) -> OkMaybe<FieldConfig, syn::Error> {
143//! let mut rename_config = None;
144//! let mut ignore_config = false;
145//! let mut errors = proc_micro::MaybeError::new();
146//!
147//! let attributes: Vec<WithSpan<ParseAttribute>> = proc_micro::parse_attrs(
148//! &NAMESPACE, &field.attrs
149//! ).push_unwrap(&mut errors);
150//!
151//! proc_micro::check_exclusive(KnownAttribute::ignore, &attributes)
152//! .push_unwrap(&mut errors);
153//! let mut unique = proc_micro::unique(attributes)
154//! .push_unwrap(&mut errors);
155//!
156//! for (_, WithSpan(attribute, _)) in unique.drain() {
157//! match attribute {
158//! ParseAttribute::ignore => ignore_config = true,
159//! ParseAttribute::rename(name) => rename_config = Some(name),
160//! }
161//! }
162//!
163//! OkMaybe(
164//! FieldConfig {
165//! ignore: ignore_config,
166//! rename: rename_config
167//! },
168//! errors.maybe()
169//! )
170//! }
171//!
172//! // No problems
173//! let _config = field_config(&syn::parse_quote! {
174//! #[my_macro(rename = "Ruby version")]
175//! version: String
176//! }).to_result().unwrap();
177//!
178//! // Problem with `check_exclusive`
179//! let result = field_config(&syn::parse_quote! {
180//! #[my_macro(rename = "Ruby version", ignore)]
181//! version: String
182//! }).to_result();
183//!
184//! assert!(result.is_err(), "Expected to be err but is {result:?}");
185//! let err = result.err().unwrap();
186//! assert_eq!(vec![
187//! "Exclusive attribute. Remove either `ignore` or `rename`".to_string(),
188//! "cannot be used with `ignore`".to_string()],
189//! err.into_iter().map(|e| e.to_string()).collect::<Vec<String>>()
190//! );
191//!
192//! // Problem with `unique`
193//! let result = field_config(&syn::parse_quote! {
194//! #[my_macro(ignore, ignore)]
195//! version: String
196//! }).to_result();
197//!
198//! assert!(result.is_err(), "Expected to be err but is {result:?}");
199//! let err = result.err().unwrap();
200//! assert_eq!(vec![
201//! "Duplicate attribute: `ignore`".to_string(),
202//! "previously `ignore` defined here".to_string()],
203//! err.into_iter().map(|e| e.to_string()).collect::<Vec<String>>()
204//! );
205//!
206//! // Multiple problems `unique` and unknown attribute
207//! let result = field_config(&syn::parse_quote! {
208//! #[my_macro(ignore, ignore)]
209//! #[my_macro(unknown)]
210//! version: String
211//! }).to_result();
212//!
213//! assert!(result.is_err(), "Expected to be err but is {result:?}");
214//! let err = result.err().unwrap();
215//! assert_eq!(vec![
216//! "Unknown attribute: `unknown`. Must be one of `rename`, `ignore`".to_string(),
217//! "Duplicate attribute: `ignore`".to_string(),
218//! "previously `ignore` defined here".to_string(),
219//! ],
220//! err.into_iter().map(|e| e.to_string()).collect::<Vec<String>>()
221//! );
222//! ```
223#![doc(test(attr(warn(unused))))]
224
225mod attr;
226mod error;
227
228pub use attr::{AttrNamespace, WithSpan, check_exclusive, known_attribute, parse_attrs, unique};
229pub use error::MaybeError;
230pub use error::OkMaybe;