Skip to main content

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}