codama_attributes/codama_directives/
display_directive.rs1use crate::{
2 utils::{FromMeta, SetOnce},
3 Attribute, CodamaAttribute, CodamaDirective,
4};
5use codama_errors::CodamaError;
6use codama_nodes::{DisplaySkip, NumberDisplayNode};
7use codama_syn_helpers::{extensions::*, Meta};
8
9#[derive(Debug, Default, PartialEq, Clone)]
10pub struct DisplayDirective {
11 pub intent: Option<String>,
12 pub interpolated_intent: Option<String>,
13 pub label: Option<String>,
14 pub skip: Option<DisplaySkip>,
15 pub flatten: Option<bool>,
16 pub flatten_prefix: Option<String>,
17 pub number_display: Option<NumberDisplayNode>,
18}
19
20impl DisplayDirective {
21 pub fn parse(meta: &Meta) -> syn::Result<Self> {
22 let pl = meta.assert_directive("display")?.as_path_list()?;
23 let mut intent: SetOnce<String> = SetOnce::new("intent");
24 let mut interpolated_intent: SetOnce<String> = SetOnce::new("interpolated_intent");
25 let mut label: SetOnce<String> = SetOnce::new("label");
26 let mut skip: SetOnce<DisplaySkip> = SetOnce::new("skip");
27 let mut flatten: SetOnce<bool> = SetOnce::new("flatten");
28 let mut flatten_prefix: SetOnce<String> = SetOnce::new("flatten_prefix");
29 let mut number_display: SetOnce<NumberDisplayNode> = SetOnce::new("number display");
30
31 pl.each(|ref meta| match meta.path_str().as_str() {
32 "intent" => intent.set(meta.as_value()?.as_expr()?.as_string()?, meta),
33 "interpolated_intent" => {
34 interpolated_intent.set(meta.as_value()?.as_expr()?.as_string()?, meta)
35 }
36 "label" => label.set(meta.as_value()?.as_expr()?.as_string()?, meta),
37 "skip" => skip.set(DisplaySkip::from_meta(meta.as_value()?)?, meta),
38 "flatten" => flatten.set(bool::from_meta(meta)?, meta),
39 "flatten_prefix" => flatten_prefix.set(meta.as_value()?.as_expr()?.as_string()?, meta),
40 "amount" | "date_time" | "duration" | "injected" => {
41 number_display.set(NumberDisplayNode::from_meta(meta)?, meta)
42 }
43 "string" => Err(meta.error("string display is not supported yet")),
44 "skip_inner_data" => Err(meta.error("enum variant display is not supported yet")),
45 _ => Err(meta.error("unrecognized display attribute")),
46 })?;
47
48 let display = Self {
49 intent: intent.option(),
50 interpolated_intent: interpolated_intent.option(),
51 label: label.option(),
52 skip: skip.option(),
53 flatten: flatten.option(),
54 flatten_prefix: flatten_prefix.option(),
55 number_display: number_display.option(),
56 };
57 if display == Self::default() {
58 return Err(meta.error("display requires at least one attribute"));
59 }
60 Ok(display)
61 }
62}
63
64impl<'a> TryFrom<&'a CodamaAttribute<'a>> for &'a DisplayDirective {
65 type Error = CodamaError;
66
67 fn try_from(attribute: &'a CodamaAttribute) -> Result<Self, Self::Error> {
68 match attribute.directive.as_ref() {
69 CodamaDirective::Display(ref display) => Ok(display),
70 _ => Err(CodamaError::InvalidCodamaDirective {
71 expected: "display".to_string(),
72 actual: attribute.directive.name().to_string(),
73 }),
74 }
75 }
76}
77
78impl<'a> TryFrom<&'a Attribute<'a>> for &'a DisplayDirective {
79 type Error = CodamaError;
80
81 fn try_from(attribute: &'a Attribute) -> Result<Self, Self::Error> {
82 <&CodamaAttribute>::try_from(attribute)?.try_into()
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use codama_nodes::{
90 AmountNumberDisplayNode, DateTimeNumberDisplayNode, InstructionAccountDisplayNode,
91 NumberValueNode, StringValueNode,
92 };
93
94 fn parse_display(tokens: proc_macro2::TokenStream) -> syn::Result<DisplayDirective> {
95 let meta: Meta = syn::parse2(tokens)?;
96 DisplayDirective::parse(&meta)
97 }
98
99 #[test]
100 fn parses_instruction_display() {
101 let display = parse_display(quote::quote! {
102 display(
103 intent = "Transfer tokens",
104 interpolated_intent = "Transfer ${data.amount} to ${accounts.destination}"
105 )
106 })
107 .unwrap();
108 assert_eq!(
109 display,
110 DisplayDirective {
111 intent: Some("Transfer tokens".to_string()),
112 interpolated_intent: Some(
113 "Transfer ${data.amount} to ${accounts.destination}".to_string()
114 ),
115 ..Default::default()
116 }
117 );
118 }
119
120 #[test]
121 fn parses_struct_field_display() {
122 let display = parse_display(quote::quote! {
123 display(
124 label = "Authorities",
125 skip = when_injected,
126 flatten,
127 flatten_prefix = "Authority "
128 )
129 })
130 .unwrap();
131 assert_eq!(
132 display,
133 DisplayDirective {
134 label: Some("Authorities".to_string()),
135 skip: Some(DisplaySkip::WhenInjected),
136 flatten: Some(true),
137 flatten_prefix: Some("Authority ".to_string()),
138 ..Default::default()
139 }
140 );
141 }
142
143 #[test]
144 fn parses_amount_display() {
145 let display = parse_display(quote::quote! {
146 display(amount(decimals = 9, unit = "SOL"))
147 })
148 .unwrap();
149 assert_eq!(
150 display.number_display,
151 Some(NumberDisplayNode::Amount(AmountNumberDisplayNode {
152 decimals: Box::new(Some(NumberValueNode::new(9u64).into())),
153 unit: Box::new(Some(StringValueNode::new("SOL").into())),
154 }))
155 );
156 }
157
158 #[test]
159 fn parses_empty_amount_display() {
160 let display = parse_display(quote::quote! { display(amount) }).unwrap();
161 assert_eq!(
162 display.number_display,
163 Some(NumberDisplayNode::Amount(Default::default()))
164 );
165 }
166
167 #[test]
168 fn parses_partial_amount_displays() {
169 let decimals = parse_display(quote::quote! {
170 display(amount(decimals = 6))
171 })
172 .unwrap();
173 assert_eq!(
174 decimals.number_display,
175 Some(NumberDisplayNode::Amount(AmountNumberDisplayNode {
176 decimals: Box::new(Some(NumberValueNode::new(6u64).into())),
177 unit: Box::new(None),
178 }))
179 );
180
181 let unit = parse_display(quote::quote! {
182 display(amount(unit = "bytes"))
183 })
184 .unwrap();
185 assert_eq!(
186 unit.number_display,
187 Some(NumberDisplayNode::Amount(AmountNumberDisplayNode {
188 decimals: Box::new(None),
189 unit: Box::new(Some(StringValueNode::new("bytes").into())),
190 }))
191 );
192 }
193
194 #[test]
195 fn parses_date_time_display() {
196 let display = parse_display(quote::quote! {
197 display(date_time(ticks_per_second = 1_000))
198 })
199 .unwrap();
200 assert_eq!(
201 display.number_display,
202 Some(NumberDisplayNode::DateTime(DateTimeNumberDisplayNode {
203 ticks_per_second: Some(1_000),
204 }))
205 );
206 }
207
208 #[test]
209 fn parses_empty_date_time_display() {
210 let display = parse_display(quote::quote! { display(date_time) }).unwrap();
211 assert_eq!(
212 display.number_display,
213 Some(NumberDisplayNode::DateTime(Default::default()))
214 );
215 }
216
217 #[test]
218 fn parses_instruction_account_display() {
219 let meta: Meta = syn::parse_quote! { display(label = "Payer", skip = always) };
220 assert_eq!(
221 InstructionAccountDisplayNode::from_meta(&meta).unwrap(),
222 InstructionAccountDisplayNode {
223 label: Some("Payer".to_string()),
224 skip: Some(DisplaySkip::Always),
225 }
226 );
227 }
228
229 #[test]
230 fn parses_flatten_prefix_without_flatten() {
231 let display = parse_display(quote::quote! {
232 display(flatten_prefix = "Authority ")
233 })
234 .unwrap();
235 assert_eq!(display.flatten, None);
236 assert_eq!(display.flatten_prefix, Some("Authority ".to_string()));
237 }
238
239 #[test]
240 fn parses_explicit_false_flatten() {
241 let display = parse_display(quote::quote! { display(flatten = false) }).unwrap();
242 assert_eq!(display.flatten, Some(false));
243 }
244
245 #[test]
246 fn rejects_multiple_number_displays() {
247 let error = parse_display(quote::quote! {
248 display(amount, date_time)
249 })
250 .unwrap_err();
251 assert_eq!(error.to_string(), "number display is already set");
252 }
253
254 #[test]
255 fn rejects_unsupported_display_nodes() {
256 for (tokens, expected) in [
257 (
258 quote::quote! { display(duration) },
259 "duration display is not supported yet",
260 ),
261 (
262 quote::quote! { display(string) },
263 "string display is not supported yet",
264 ),
265 (
266 quote::quote! { display(skip_inner_data = true) },
267 "enum variant display is not supported yet",
268 ),
269 ] {
270 assert_eq!(parse_display(tokens).unwrap_err().to_string(), expected);
271 }
272 }
273
274 #[test]
275 fn rejects_injected_amount_values() {
276 let error = parse_display(quote::quote! {
277 display(amount(decimals = injected("mint_decimals")))
278 })
279 .unwrap_err();
280 assert_eq!(
281 error.to_string(),
282 "injected display values are not supported yet"
283 );
284 }
285
286 #[test]
287 fn rejects_positional_attributes() {
288 for tokens in [
289 quote::quote! { display("Transfer tokens") },
290 quote::quote! { display(amount(9, "SOL")) },
291 quote::quote! { display(date_time(1_000)) },
292 ] {
293 assert!(parse_display(tokens).is_err());
294 }
295 }
296
297 #[test]
298 fn rejects_invalid_skip() {
299 let error = parse_display(quote::quote! { display(skip = sometimes) }).unwrap_err();
300 assert_eq!(
301 error.to_string(),
302 "expected always, never, or when_injected"
303 );
304 }
305
306 #[test]
307 fn rejects_empty_display() {
308 let error = parse_display(quote::quote! { display() }).unwrap_err();
309 assert_eq!(error.to_string(), "display requires at least one attribute");
310 }
311}