atlas_package_metadata_macro/
lib.rs

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