solana_package_metadata_macro/
lib.rs1extern 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#[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}