ferogram_derive/lib.rs
1/*
2 * Copyright (c) 2026 Ankit Chaubey <ankitchaubey.dev@gmail.com>
3 * https://github.com/ankit-chaubey
4 *
5 * Project: ferogram
6 * Website: https://ferogram.dev
7 *
8 * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
9 * https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
10 * <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
11 * This file may not be copied, modified, or distributed except according
12 * to those terms.
13 */
14
15#![cfg_attr(docsrs, feature(doc_cfg))]
16#![doc(html_root_url = "https://docs.rs/ferogram-derive/0.6.5")]
17//! Procedural macros for ferogram.
18//!
19//! This crate is part of [ferogram](https://crates.io/crates/ferogram), an async Rust
20//! MTProto client built by [Ankit Chaubey](https://github.com/ankit-chaubey).
21//!
22//! - Channel: [t.me/Ferogram](https://t.me/Ferogram)
23//! - Chat: [t.me/FerogramChat](https://t.me/FerogramChat)
24//!
25//! You do not depend on this crate directly. It is re-exported through
26//! `ferogram` and `ferogram-fsm`. Add those crates to your `Cargo.toml`
27//! instead.
28//!
29//! # What's in here
30//!
31//! - **`#[derive(FsmState)]`**: Implements the `ferogram_fsm::FsmState`
32//! trait for a unit-variant enum. Generates `as_key` (module path + enum
33//! name + variant name → `String`) and `from_key` (string → `Option<Self>`,
34//! with a fallback for keys written by older versions of this macro).
35//! Tuple/struct variants and generic enums are rejected at compile time.
36//!
37//! # Example
38//!
39//! ```rust,ignore
40//! use ferogram::FsmState;
41//!
42//! #[derive(FsmState, Clone, Debug, PartialEq)]
43//! enum CheckoutState {
44//! Cart,
45//! Address,
46//! Payment,
47//! Confirmation,
48//! }
49//! ```
50
51#![deny(unsafe_code)]
52
53use proc_macro::TokenStream;
54use proc_macro2::TokenStream as TokenStream2;
55use quote::quote;
56use syn::{Data, DeriveInput, Fields, parse_macro_input, spanned::Spanned};
57
58/// Derive the `ferogram_fsm::FsmState` trait for an enum.
59///
60/// Only **unit variants** (no fields) are supported. Tuple or struct variants
61/// are rejected with a compile error. **Generic enums are also rejected**:
62/// the key can't disambiguate different type parameter instantiations, so
63/// this fails to compile rather than silently colliding at runtime.
64///
65/// # What gets generated
66///
67/// - `as_key(&self) -> String` - returns `"module::path::EnumName::Variant"`,
68/// namespaced by full module path and enum name so identically-named
69/// variants -- even on identically-named enums in different modules --
70/// don't collide.
71/// - `from_key(key: &str) -> Option<Self>` - parses that key back into the
72/// enum. Falls back to matching on the trailing `"::"`-segment so state
73/// written by older versions of this derive (bare `"Variant"` or
74/// `"EnumName::Variant"`) still deserializes after an upgrade, on a
75/// best-effort basis.
76///
77/// # Example
78///
79/// ```rust,ignore
80/// use ferogram::FsmState;
81///
82/// #[derive(FsmState, Clone, Debug, PartialEq)]
83/// enum RegistrationState {
84/// Start,
85/// WaitingName,
86/// WaitingPhone,
87/// WaitingCity,
88/// Done,
89/// }
90/// ```
91#[proc_macro_derive(FsmState)]
92pub fn derive_fsm_state(input: TokenStream) -> TokenStream {
93 let input = parse_macro_input!(input as DeriveInput);
94 match fsm_state_impl(input) {
95 Ok(ts) => ts.into(),
96 Err(e) => e.to_compile_error().into(),
97 }
98}
99
100fn fsm_state_impl(input: DeriveInput) -> syn::Result<TokenStream2> {
101 let name = &input.ident;
102 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
103
104 // Reject generics outright. A generic enum like `State<T>` would need the
105 // key to disambiguate by concrete `T` as well, which `module_path!()` +
106 // enum/variant name cannot do (it's resolved once, at the enum's
107 // declaration site, not per-monomorphization). Rather than silently
108 // producing colliding keys for `State<Deposit>` vs `State<Withdraw>`,
109 // refuse to compile so the gap is visible instead of a runtime bug.
110 if !input.generics.params.is_empty() {
111 return Err(syn::Error::new(
112 input.generics.span(),
113 "`#[derive(FsmState)]` does not support generic enums. \
114 The generated key cannot disambiguate different type parameter \
115 instantiations (e.g. `State<Deposit>` vs `State<Withdraw>` would \
116 collide). Define separate concrete enums instead.",
117 ));
118 }
119
120 let data_enum = match &input.data {
121 Data::Enum(e) => e,
122 _ => {
123 return Err(syn::Error::new(
124 input.ident.span(),
125 "`#[derive(FsmState)]` can only be applied to enums",
126 ));
127 }
128 };
129
130 // Validate: only unit variants allowed.
131 for variant in &data_enum.variants {
132 match &variant.fields {
133 Fields::Unit => {}
134 _ => {
135 return Err(syn::Error::new(
136 variant.span(),
137 "`#[derive(FsmState)]` only supports unit variants (no fields). \
138 Tuple and struct variants are not supported.",
139 ));
140 }
141 }
142 }
143
144 // Generate `as_key` match arms.
145 //
146 // Keys are namespaced as "module::path::EnumName::Variant" using
147 // `module_path!()`, resolved at the enum's declaration site. This
148 // disambiguates not just same-named variants on differently-named enums
149 // (DepositState::AwaitingAmount vs WithdrawState::AwaitingAmount), but
150 // also identically-named enums declared in different modules
151 // (deposit::State::AwaitingAmount vs withdraw::State::AwaitingAmount).
152 let as_key_arms = data_enum.variants.iter().map(|v| {
153 let ident = &v.ident;
154 quote! {
155 #name::#ident => ::std::concat!(
156 ::std::module_path!(), "::", ::std::stringify!(#name), "::", ::std::stringify!(#ident)
157 )
158 }
159 });
160
161 // Generate `from_key` match arms for the current, fully-qualified format.
162 let from_key_arms = data_enum.variants.iter().map(|v| {
163 let ident = &v.ident;
164 quote! {
165 ::std::concat!(
166 ::std::module_path!(), "::", ::std::stringify!(#name), "::", ::std::stringify!(#ident)
167 ) => ::std::option::Option::Some(#name::#ident)
168 }
169 });
170
171 // Legacy fallback arms, matched against just the variant name (the
172 // segment after the last "::"). This covers keys written by older
173 // versions of this macro: bare `"Variant"` (pre-namespacing) and
174 // `"EnumName::Variant"` (namespaced but without the module path). Those
175 // older formats were themselves ambiguous across enums/modules that
176 // shared a name -- this is a best-effort migration path so existing
177 // persisted state doesn't just vanish across an upgrade, not a
178 // guarantee that old, already-colliding data resolves correctly.
179 let legacy_from_key_arms = data_enum.variants.iter().map(|v| {
180 let ident = &v.ident;
181 let key = ident.to_string();
182 quote! { #key => ::std::option::Option::Some(#name::#ident) }
183 });
184
185 Ok(quote! {
186 #[automatically_derived]
187 impl #impl_generics ::ferogram::FsmState
188 for #name #ty_generics
189 #where_clause
190 {
191 fn as_key(&self) -> ::std::string::String {
192 match self {
193 #(#as_key_arms),*
194 }
195 .to_string()
196 }
197
198 fn from_key(key: &str) -> ::std::option::Option<Self> {
199 match key {
200 #(#from_key_arms,)*
201 _ => {
202 // Not the current fully-qualified format. Fall back
203 // to matching on the last "::"-delimited segment so
204 // state written by older versions of this derive
205 // still deserializes instead of being dropped.
206 let short = key.rsplit("::").next().unwrap_or(key);
207 match short {
208 #(#legacy_from_key_arms,)*
209 _ => ::std::option::Option::None,
210 }
211 }
212 }
213 }
214 }
215 })
216}