Skip to main content

codama_attributes/
codama_program_macro.rs

1use crate::utils::SetOnce;
2use codama_nodes::CamelCaseString;
3use codama_syn_helpers::{extensions::*, Meta};
4use proc_macro2::TokenStream;
5
6/// The parsed arguments of a `codama_program!` function-like macro invocation.
7///
8/// Unlike the item-level `#[codama(program(...))]` directive — which declares a
9/// *distinct* program and therefore requires both `name` and `address` — the
10/// `codama_program!` macro overrides the *primary* program's metadata. Both
11/// fields are optional (though at least one is required); any field left unset
12/// keeps the crate-derived default (the Cargo.toml package name and the
13/// `declare_id!` / `package.metadata.solana.program-id` address).
14#[derive(Debug, PartialEq)]
15pub struct CodamaProgramMacro {
16    pub name: Option<CamelCaseString>,
17    pub address: Option<String>,
18}
19
20impl CodamaProgramMacro {
21    /// Parse the raw token stream passed to `codama_program!(...)`.
22    pub fn parse(tokens: TokenStream) -> syn::Result<Self> {
23        // Reuse the `Meta` grammar by wrapping the arguments in a path list.
24        let meta: Meta = syn::parse_quote! { codama_program(#tokens) };
25        let pl = meta.as_path_list()?;
26
27        let mut name = SetOnce::<CamelCaseString>::new("name");
28        let mut address = SetOnce::<String>::new("address");
29
30        pl.each(|ref meta| match meta.path_str().as_str() {
31            "name" => name.set(meta.as_value()?.as_expr()?.as_string()?.into(), meta),
32            "address" => address.set(meta.as_value()?.as_expr()?.as_string()?, meta),
33            _ => Err(meta.error("unrecognized attribute")),
34        })?;
35
36        let name = name.option();
37        let address = address.option();
38        if name.is_none() && address.is_none() {
39            return Err(meta.error("expected at least one of `name` or `address`"));
40        }
41
42        Ok(Self { name, address })
43    }
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49    use quote::quote;
50
51    #[test]
52    fn name_only() {
53        let program = CodamaProgramMacro::parse(quote! { name = "associatedToken" }).unwrap();
54        assert_eq!(
55            program,
56            CodamaProgramMacro {
57                name: Some(CamelCaseString::from("associatedToken")),
58                address: None,
59            }
60        );
61    }
62
63    #[test]
64    fn address_only() {
65        let program = CodamaProgramMacro::parse(
66            quote! { address = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" },
67        )
68        .unwrap();
69        assert_eq!(
70            program,
71            CodamaProgramMacro {
72                name: None,
73                address: Some("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL".to_string()),
74            }
75        );
76    }
77
78    #[test]
79    fn name_and_address() {
80        let program = CodamaProgramMacro::parse(
81            quote! { name = "associatedToken", address = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" },
82        )
83        .unwrap();
84        assert_eq!(
85            program,
86            CodamaProgramMacro {
87                name: Some(CamelCaseString::from("associatedToken")),
88                address: Some("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL".to_string()),
89            }
90        );
91    }
92
93    #[test]
94    fn empty() {
95        let error = CodamaProgramMacro::parse(quote! {}).unwrap_err();
96        assert_eq!(
97            error.to_string(),
98            "expected at least one of `name` or `address`"
99        );
100    }
101
102    #[test]
103    fn unrecognized_attribute() {
104        let error =
105            CodamaProgramMacro::parse(quote! { name = "foo", version = "1.0.0" }).unwrap_err();
106        assert_eq!(error.to_string(), "unrecognized attribute");
107    }
108}