seventy_macros/
lib.rs

1//! Procedural macros for [Seventy](https://crates.io/crates/seventy).
2
3use proc_macro::TokenStream;
4use syn::{parse::Parser, punctuated::Punctuated, Error, ItemStruct, Meta, Token};
5
6mod seventy;
7
8/// Newtype attribute.
9///
10/// Automatically implements the `Newtype`, `Sanitizable`, and `Validatable`
11/// traits.
12///
13/// Other functionality may also be implemented depending on enabled upgrades.
14///
15/// # Upgrades
16///
17/// ## as_ref
18///
19/// Implements `AsRef` for the newtype. The `Newtype` trait already
20/// has an equivalent `to_inner` method, but this provides compatability with
21/// APIs that expect `AsRef`.
22///
23/// ```
24/// use seventy::{core::Newtype, seventy};
25///
26/// #[seventy(upgrades(as_ref))]
27/// pub struct Velocity(f64);
28///
29/// assert_eq!(*Velocity::try_new(70.0).unwrap().as_ref(), 70.0);
30/// ```
31///
32/// ## deref
33///
34/// Implements `Deref` for the newtype.
35///
36/// ```
37/// use seventy::{seventy, Newtype};
38///
39/// #[seventy(upgrades(deref))]
40/// pub struct Sentence(String);
41///
42/// let sentence = Sentence::try_new("Hello, World!").unwrap();
43/// assert_eq!(*sentence, "Hello, World!");
44/// ```
45///
46/// ## display
47///
48/// Implements `Display` using the inner value's `Display` implementation.
49///
50/// ```
51/// use seventy::{seventy, Newtype};
52///
53/// #[seventy(upgrades(display))]
54/// pub struct Email(String);
55///
56/// let email = Email::try_new("example@example.com").unwrap();
57/// assert_eq!(format!("{email}"), "example@example.com");
58/// ```
59///
60/// ## try_from
61///
62/// Implements `TryFrom` for the newtype. The `Newtype` trait
63/// already has the method `Newtype::try_new`, which is similar to
64/// `TryFrom::try_from`, however the latter expects a concrete type, whereas
65/// the former `Newtype::try_new` does not.
66///
67/// ```
68/// use seventy::{seventy, Newtype};
69///
70/// #[seventy(upgrades(try_from))]
71/// pub struct Number(i32);
72///
73/// assert!(Number::try_from(5).is_ok());
74/// ```
75///
76/// ## deserializable
77///
78/// Implements `serde::Deserialize` for the newtype. You
79/// must have `serde` as a dependency!
80///
81/// ```
82/// use seventy::{seventy, Newtype};
83///
84/// #[seventy(upgrades(deserializable))]
85/// pub struct Message(String);
86///
87/// let json = "\"Seventy is a cool crate\"";
88///
89/// let message: Message = serde_json::from_str(json).unwrap();
90/// assert_eq!(message.into_inner(), "Seventy is a cool crate");
91/// ```
92///
93/// ## serializable
94///
95/// Implements `serde::Serialize` for the newtype. You must
96/// have `serde` as a dependency!
97///
98/// ```
99/// use seventy::{seventy, Newtype};
100///
101/// #[seventy(upgrades(serializable))]
102/// pub struct Message(String);
103///
104/// let message = Message::try_new("Seventy is a cool crate").unwrap();
105/// let json = serde_json::to_string(&message).unwrap();
106///
107/// assert_eq!(json, "\"Seventy is a cool crate\"");
108/// ```
109///
110/// ## bypassable
111///
112/// Enables bypass functionality for the newtype.
113///
114/// ```
115/// use seventy::{
116///     builtins::{compare::*, string::*},
117///     core::{Bypassable, Validatable},
118///     seventy, Newtype,
119/// };
120///
121/// #[seventy(
122///     upgrades(bypassable),
123///     sanitize(trim),
124///     validate(ascii, length::chars(within(5..=20)))
125/// )]
126/// pub struct Username(String);
127///
128/// /* `Bypassable::new_unchecked` */
129///
130/// let username = unsafe { Username::new_unchecked("   username!   ") };
131/// assert_eq!(username.as_inner(), "   username!   ");
132///
133/// /* `Bypassable::new_unsanitized` */
134///
135/// let username = unsafe { Username::new_unsanitized("   username   ") }.unwrap();
136/// assert_eq!(username.as_inner(), "   username   ");
137///
138/// /* `Bypassable::new_unvalidated` */
139///
140/// let username = unsafe { Username::new_unvalidated("   username!   ") };
141/// assert_eq!(username.as_inner(), "username!");
142///
143/// /* `Bypassable::as_inner_mut` */
144///
145/// let mut username = Username::try_new("username").unwrap();
146///
147/// // Passes validation.
148/// assert!(Username::validate(username.as_inner()));
149///
150/// // Unsafely mutate the value.
151/// unsafe { username.as_inner_mut() }.push_str("\u{00BF}");
152///
153/// // Fails validation.
154/// assert!(!Username::validate(username.as_inner()));
155/// ```
156///
157/// ## inherent
158///
159/// Makes the `Newtype` trait methods callable without the trait
160/// in scope.
161///
162/// The code below fails to compile, since the `Newtype` trait is not in scope.
163///
164/// ```compile_fail,E0599
165/// use seventy::{builtins::compare::*, seventy};
166///
167/// #[seventy(
168///     validate(within(1..=10))
169/// )]
170/// pub struct Rating(u8);
171///
172/// assert!(Rating::try_new(5).is_ok());
173/// ```
174///
175/// The code below compiles due to the `inherent` upgrade.
176///
177/// ```
178/// use seventy::{builtins::compare::*, seventy};
179///
180/// #[seventy(
181///     upgrades(inherent),
182///     validate(within(1..=10))
183/// )]
184/// pub struct Rating(u8);
185///
186/// assert!(Rating::try_new(5).is_ok());
187/// ```
188///
189/// ## shared
190///
191/// Sanitizers and validators are typically created each time they are used,
192/// which works well for simple validations. However, it may be inefficient for
193/// more complex sanitizers and validators to be constructed per-use. This
194/// upgrade makes it so each newtype instance shares its sanitizers and
195/// validators with other instances.
196///
197/// This upgrade takes away support for generics, and introduces some
198/// performance overhead.
199///
200/// ## unexposed
201///
202/// Prevents accessing the field directly from the same module.
203///
204/// **NOTE**:
205/// When this upgrade is enabled, all attributes (such as derives) must be below
206/// the `seventy` macro.
207///
208/// The code below modifies a newtype's value by directly accessing the field,
209/// which is not good!
210///
211/// ```
212/// use seventy::{seventy, Newtype};
213///
214/// #[seventy()]
215/// pub struct ExposedToModule(i32);
216///
217/// let mut etm = ExposedToModule::try_new(70).unwrap();
218/// etm.0 = 444;
219///
220/// assert_eq!(etm.into_inner(), 444);
221/// ```
222///
223/// The code below unexposes the inner field, so the bad code now produces a
224/// compilation error.
225///
226/// ```compile_fail,E0616
227/// use seventy::{Newtype, seventy};
228///
229/// #[seventy(upgrades(unexposed))]
230/// pub struct UnexposedToModule(i32);
231///
232/// let mut utm = UnexposedToModule::try_new(70).unwrap();
233/// utm.0 = 444;
234/// ```
235#[proc_macro_attribute]
236pub fn seventy(metas: TokenStream, item: TokenStream) -> TokenStream {
237    let metas = match Punctuated::<Meta, Token![,]>::parse_terminated.parse(metas) {
238        Ok(metas) => metas,
239        Err(error) => return error.into_compile_error().into(),
240    };
241
242    let item = match syn::parse::<ItemStruct>(item) {
243        Ok(item) => item,
244        Err(error) => return error.into_compile_error().into(),
245    };
246
247    seventy::expand(metas, item)
248        .unwrap_or_else(Error::into_compile_error)
249        .into()
250}