solana_package_metadata_macro/
lib.rs

1//! Macro to access data from the `package.metadata` section of Cargo.toml
2
3extern crate proc_macro;
4
5use {
6    proc_macro::TokenStream,
7    quote::quote,
8    std::{env, fs},
9    syn::parse_macro_input,
10    toml::value::{Array, Value},
11};
12
13/// Macro for accessing data from the `package.metadata` section of the Cargo manifest
14///
15/// # Arguments
16/// * `key` - A string slice of a dot-separated path to the TOML key of interest
17///
18/// # Example
19/// Given the following `Cargo.toml`:
20/// ```ignore
21/// [package]
22/// name = "MyApp"
23/// version = "0.1.0"
24///
25/// [package.metadata]
26/// copyright = "Copyright (c) 2024 ACME Inc."
27/// ```
28///
29/// You can fetch the copyright with the following:
30/// ```ignore
31/// use solana_sdk_macro::package_metadata;
32///
33/// pub fn main() {
34///     let copyright = package_metadata!("copyright");
35///     assert_eq!(copyright, "Copyright (c) 2024 ACME Inc.");
36/// }
37/// ```
38///
39/// ## TOML Support
40/// This macro only supports static data:
41/// * Strings
42/// * Integers
43/// * Floating-point numbers
44/// * Booleans
45/// * Datetimes
46/// * Arrays
47///
48/// ## Array Example
49/// Given the following Cargo manifest:
50/// ```ignore
51/// [package.metadata.arrays]
52/// some_array = [ 1, 2, 3 ]
53/// ```
54///
55/// This is legal:
56/// ```ignore
57/// static ARR: [i64; 3] = package_metadata!("arrays.some_array");
58/// ```
59///
60/// It does *not* currently support accessing TOML array elements directly.
61/// TOML tables are not supported.
62#[proc_macro]
63pub fn package_metadata(input: TokenStream) -> TokenStream {
64    let key = parse_macro_input!(input as syn::LitStr);
65    let full_key = &key.value();
66    let path = format!("{}/Cargo.toml", env::var("CARGO_MANIFEST_DIR").unwrap());
67    let manifest = load_manifest(&path);
68    let value = package_metadata_value(&manifest, full_key);
69    toml_value_codegen(value).into()
70}
71
72fn package_metadata_value<'a>(manifest: &'a Value, full_key: &str) -> &'a Value {
73    let error_message =
74        format!("Key `package.metadata.{full_key}` must be present in the Cargo manifest");
75    manifest
76        .get("package")
77        .and_then(|package| package.get("metadata"))
78        .and_then(|metadata| {
79            let mut table = metadata
80                .as_table()
81                .expect("TOML property `package.metadata` must be a table");
82            let mut value = None;
83            for key in full_key.split('.') {
84                match table.get(key).expect(&error_message) {
85                    Value::Table(t) => {
86                        table = t;
87                    }
88                    v => {
89                        value = Some(v);
90                    }
91                }
92            }
93            value
94        })
95        .expect(&error_message)
96}
97
98fn toml_value_codegen(value: &Value) -> proc_macro2::TokenStream {
99    match value {
100        Value::String(s) => quote! {{ #s }},
101        Value::Integer(i) => quote! {{ #i }},
102        Value::Float(f) => quote! {{ #f }},
103        Value::Boolean(b) => quote! {{ #b }},
104        Value::Array(a) => toml_array_codegen(a),
105        Value::Datetime(d) => {
106            let date_str = toml::ser::to_string(d).unwrap();
107            quote! {{
108                #date_str
109            }}
110        }
111        Value::Table(_) => {
112            panic!("Tables are not supported");
113        }
114    }
115}
116
117fn toml_array_codegen(array: &Array) -> proc_macro2::TokenStream {
118    let statements = array
119        .iter()
120        .flat_map(|val| {
121            let val = toml_value_codegen(val);
122            quote! {
123                #val,
124            }
125        })
126        .collect::<proc_macro2::TokenStream>();
127    quote! {{
128        [
129            #statements
130        ]
131    }}
132}
133
134fn load_manifest(path: &str) -> Value {
135    let contents = fs::read_to_string(path)
136        .unwrap_or_else(|err| panic!("error occurred reading Cargo manifest {path}: {err}"));
137    toml::from_str(&contents)
138        .unwrap_or_else(|err| panic!("error occurred parsing Cargo manifest {path}: {err}"))
139}
140
141#[cfg(test)]
142mod tests {
143    use {super::*, std::str::FromStr};
144
145    #[test]
146    fn package_metadata_string() {
147        let copyright = "Copyright (c) 2024 ACME Inc.";
148        let manifest = toml::from_str(&format!(
149            r#"
150            [package.metadata]
151            copyright = "{copyright}"
152        "#
153        ))
154        .unwrap();
155        assert_eq!(
156            package_metadata_value(&manifest, "copyright")
157                .as_str()
158                .unwrap(),
159            copyright
160        );
161    }
162
163    #[test]
164    fn package_metadata_nested() {
165        let program_id = "11111111111111111111111111111111";
166        let manifest = toml::from_str(&format!(
167            r#"
168            [package.metadata.solana]
169            program-id = "{program_id}"
170        "#
171        ))
172        .unwrap();
173        assert_eq!(
174            package_metadata_value(&manifest, "solana.program-id")
175                .as_str()
176                .unwrap(),
177            program_id
178        );
179    }
180
181    #[test]
182    fn package_metadata_bool() {
183        let manifest = toml::from_str(
184            r#"
185            [package.metadata]
186            is-ok = true
187        "#,
188        )
189        .unwrap();
190        assert!(package_metadata_value(&manifest, "is-ok")
191            .as_bool()
192            .unwrap());
193    }
194
195    #[test]
196    fn package_metadata_int() {
197        let number = 123;
198        let manifest = toml::from_str(&format!(
199            r#"
200            [package.metadata]
201            number = {number}
202        "#
203        ))
204        .unwrap();
205        assert_eq!(
206            package_metadata_value(&manifest, "number")
207                .as_integer()
208                .unwrap(),
209            number
210        );
211    }
212
213    #[test]
214    fn package_metadata_float() {
215        let float = 123.456;
216        let manifest = toml::from_str(&format!(
217            r#"
218            [package.metadata]
219            float = {float}
220        "#
221        ))
222        .unwrap();
223        assert_eq!(
224            package_metadata_value(&manifest, "float")
225                .as_float()
226                .unwrap(),
227            float
228        );
229    }
230
231    #[test]
232    fn package_metadata_array() {
233        let array = ["1", "2", "3"];
234        let manifest = toml::from_str(&format!(
235            r#"
236            [package.metadata]
237            array = {array:?}
238        "#
239        ))
240        .unwrap();
241        assert_eq!(
242            package_metadata_value(&manifest, "array")
243                .as_array()
244                .unwrap()
245                .iter()
246                .map(|x| x.as_str().unwrap())
247                .collect::<Vec<_>>(),
248            array
249        );
250    }
251
252    #[test]
253    fn package_metadata_datetime() {
254        let datetime = "1979-05-27T07:32:00Z";
255        let manifest = toml::from_str(&format!(
256            r#"
257            [package.metadata]
258            datetime = {datetime}
259        "#
260        ))
261        .unwrap();
262        let toml_datetime = toml::value::Datetime::from_str(datetime).unwrap();
263        assert_eq!(
264            package_metadata_value(&manifest, "datetime")
265                .as_datetime()
266                .unwrap(),
267            &toml_datetime
268        );
269    }
270}