Skip to main content

codama_attributes/codama_directives/
program_directive.rs

1use crate::{
2    utils::SetOnce, Attribute, AttributeContext, CodamaAttribute, CodamaDirective, TryFromFilter,
3};
4use codama_errors::CodamaError;
5use codama_nodes::{CamelCaseString, Node, ProgramNode};
6use codama_syn_helpers::{extensions::*, Meta};
7
8/// The `#[codama(program(name = ..., address = ...))]` directive.
9///
10/// Its required arguments depend on the scope it is attached to:
11///
12/// - At the **crate root** ([`AttributeContext::Crate`]) it *overrides* the
13///   primary program's identity, so both `name` and `address` are optional —
14///   any omitted field falls back to the crate defaults (the Cargo.toml package
15///   name and the `declare_id!`/`package.metadata.solana.program-id` address).
16/// - At any **other scope** (an item, a module block or a file-module) it
17///   declares a *distinct* program that has no defaults to fall back on, so both
18///   `name` and `address` are required.
19#[derive(Debug, PartialEq)]
20pub struct ProgramDirective {
21    pub name: Option<CamelCaseString>,
22    pub address: Option<String>,
23}
24
25impl ProgramDirective {
26    pub fn parse(meta: &Meta, ctx: &AttributeContext) -> syn::Result<Self> {
27        let pl = meta.assert_directive("program")?.as_path_list()?;
28
29        let mut name = SetOnce::<CamelCaseString>::new("name");
30        let mut address = SetOnce::<String>::new("address");
31
32        pl.each(|ref meta| match meta.path_str().as_str() {
33            "name" => name.set(meta.as_value()?.as_expr()?.as_string()?.into(), meta),
34            "address" => address.set(meta.as_value()?.as_expr()?.as_string()?, meta),
35            _ => Err(meta.error("unrecognized attribute")),
36        })?;
37
38        // Both arguments are optional at the crate root (override), required elsewhere.
39        if let AttributeContext::Crate(_) = ctx {
40            Ok(Self {
41                name: name.option(),
42                address: address.option(),
43            })
44        } else {
45            Ok(Self {
46                name: Some(name.take(meta)?),
47                address: Some(address.take(meta)?),
48            })
49        }
50    }
51
52    pub fn apply(attributes: &crate::Attributes, node: Node) -> Node {
53        match attributes.get_last(Self::filter) {
54            Some(pd) => pd.update_or_wrap_program_node(node),
55            None => node,
56        }
57    }
58
59    pub fn update_or_wrap_program_node(&self, node: Node) -> Node {
60        // Wrapping a leaf node only happens outside the crate root, where both fields are set.
61        let name = self.name.clone().unwrap_or_default();
62        let public_key = self.address.clone().unwrap_or_default();
63        match node {
64            // Updating an existing program only overrides the fields the directive set.
65            Node::Root(mut root) => {
66                if let Some(name) = self.name.clone() {
67                    root.program.name = name;
68                }
69                if let Some(public_key) = self.address.clone() {
70                    root.program.public_key = public_key;
71                }
72                root.into()
73            }
74            Node::Program(mut program) => {
75                if let Some(name) = self.name.clone() {
76                    program.name = name;
77                }
78                if let Some(public_key) = self.address.clone() {
79                    program.public_key = public_key;
80                }
81                program.into()
82            }
83            Node::Account(account) => ProgramNode {
84                name,
85                public_key,
86                accounts: vec![account],
87                ..ProgramNode::default()
88            }
89            .into(),
90            Node::Constant(constant) => ProgramNode {
91                name,
92                public_key,
93                constants: vec![constant],
94                ..ProgramNode::default()
95            }
96            .into(),
97            Node::Instruction(instruction) => ProgramNode {
98                name,
99                public_key,
100                instructions: vec![instruction],
101                ..ProgramNode::default()
102            }
103            .into(),
104            Node::Error(error) => ProgramNode {
105                name,
106                public_key,
107                errors: vec![error],
108                ..ProgramNode::default()
109            }
110            .into(),
111            Node::Pda(pda) => ProgramNode {
112                name,
113                public_key,
114                pdas: vec![pda],
115                ..ProgramNode::default()
116            }
117            .into(),
118            Node::Event(event) => ProgramNode {
119                name,
120                public_key,
121                events: vec![event],
122                ..ProgramNode::default()
123            }
124            .into(),
125            other => other,
126        }
127    }
128}
129
130impl<'a> TryFrom<&'a CodamaAttribute<'a>> for &'a ProgramDirective {
131    type Error = CodamaError;
132
133    fn try_from(attribute: &'a CodamaAttribute) -> Result<Self, Self::Error> {
134        match attribute.directive.as_ref() {
135            CodamaDirective::Program(ref a) => Ok(a),
136            _ => Err(CodamaError::InvalidCodamaDirective {
137                expected: "program".to_string(),
138                actual: attribute.directive.name().to_string(),
139            }),
140        }
141    }
142}
143
144impl<'a> TryFrom<&'a Attribute<'a>> for &'a ProgramDirective {
145    type Error = CodamaError;
146
147    fn try_from(attribute: &'a Attribute) -> Result<Self, Self::Error> {
148        <&CodamaAttribute>::try_from(attribute)?.try_into()
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    fn item_ctx() -> syn::Item {
157        syn::parse_quote! { struct Foo; }
158    }
159
160    fn crate_file() -> syn::File {
161        syn::parse_quote! {}
162    }
163
164    #[test]
165    fn ok() {
166        let item = item_ctx();
167        let ctx = AttributeContext::Item(&item);
168        let meta: Meta = syn::parse_quote! { program(name = "associatedToken", address = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL") };
169        let directive = ProgramDirective::parse(&meta, &ctx).unwrap();
170        assert_eq!(
171            directive,
172            ProgramDirective {
173                name: Some(CamelCaseString::from("associatedToken")),
174                address: Some("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL".to_string()),
175            }
176        );
177    }
178
179    #[test]
180    fn name_missing_at_item_scope() {
181        let item = item_ctx();
182        let ctx = AttributeContext::Item(&item);
183        let meta: Meta =
184            syn::parse_quote! { program(address = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL") };
185        let error = ProgramDirective::parse(&meta, &ctx).unwrap_err();
186        assert_eq!(error.to_string(), "name is missing");
187    }
188
189    #[test]
190    fn address_missing_at_item_scope() {
191        let item = item_ctx();
192        let ctx = AttributeContext::Item(&item);
193        let meta: Meta = syn::parse_quote! { program(name = "associatedToken") };
194        let error = ProgramDirective::parse(&meta, &ctx).unwrap_err();
195        assert_eq!(error.to_string(), "address is missing");
196    }
197
198    #[test]
199    fn name_only_at_crate_scope() {
200        let file = crate_file();
201        let ctx = AttributeContext::Crate(&file);
202        let meta: Meta = syn::parse_quote! { program(name = "associatedToken") };
203        let directive = ProgramDirective::parse(&meta, &ctx).unwrap();
204        assert_eq!(
205            directive,
206            ProgramDirective {
207                name: Some(CamelCaseString::from("associatedToken")),
208                address: None,
209            }
210        );
211    }
212
213    #[test]
214    fn address_only_at_crate_scope() {
215        let file = crate_file();
216        let ctx = AttributeContext::Crate(&file);
217        let meta: Meta =
218            syn::parse_quote! { program(address = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL") };
219        let directive = ProgramDirective::parse(&meta, &ctx).unwrap();
220        assert_eq!(
221            directive,
222            ProgramDirective {
223                name: None,
224                address: Some("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL".to_string()),
225            }
226        );
227    }
228
229    #[test]
230    fn empty_at_crate_scope() {
231        let file = crate_file();
232        let ctx = AttributeContext::Crate(&file);
233        let meta: Meta = syn::parse_quote! { program() };
234        let directive = ProgramDirective::parse(&meta, &ctx).unwrap();
235        assert_eq!(
236            directive,
237            ProgramDirective {
238                name: None,
239                address: None,
240            }
241        );
242    }
243}