1use std::{env::current_dir, fs::read_to_string, path::PathBuf};
2
3use derive_syn_parse::Parse;
4use proc_macro::TokenStream;
5use proc_macro2::{Span, TokenStream as TokenStream2};
6use quote::{quote, ToTokens};
7use syn::{parse2, Error, Expr, LitStr, Result, Token};
8use toml::{Table, Value};
9use walkdir::WalkDir;
10
11#[proc_macro]
12pub fn settings(tokens: TokenStream) -> TokenStream {
13 match settings_internal(tokens) {
14 Ok(tokens) => tokens.into(),
15 Err(err) => err.to_compile_error().into(),
16 }
17}
18
19#[derive(Parse)]
20struct SettingsProcArgs {
21 crate_name: LitStr,
22 #[prefix(Token![,])]
23 key: LitStr,
24 _comma2: Option<Token![,]>,
25 #[parse_if(_comma2.is_some())]
26 default: Option<Expr>,
27}
28
29#[derive(PartialEq, Copy, Clone)]
30enum ValueType {
31 String,
32 Integer,
33 Float,
34 Boolean,
35 Datetime,
36 Array,
37 Table,
38}
39
40trait GetValueType {
41 fn value_type(&self) -> ValueType;
42}
43
44impl GetValueType for Value {
45 fn value_type(&self) -> ValueType {
46 use ValueType::*;
47 match self {
48 Value::String(_) => String,
49 Value::Integer(_) => Integer,
50 Value::Float(_) => Float,
51 Value::Boolean(_) => Boolean,
52 Value::Datetime(_) => Datetime,
53 Value::Array(_) => Array,
54 Value::Table(_) => Table,
55 }
56 }
57}
58
59fn emit_toml_value(value: Value) -> Result<TokenStream2> {
60 match value {
61 Value::String(string) => Ok(quote!(#string)),
62 Value::Integer(integer) => Ok(quote!(#integer)),
63 Value::Float(float) => Ok(quote!(#float)),
64 Value::Boolean(bool) => Ok(quote!(#bool)),
65 Value::Datetime(date_time) => {
66 let date_time = date_time.to_string();
67 Ok(quote!(#date_time))
68 }
69 Value::Array(arr) => {
70 let mut new_arr: Vec<TokenStream2> = Vec::new();
71 let mut current_type: Option<ValueType> = None;
72 for value in arr.iter() {
73 if let Some(typ) = current_type {
74 if typ != value.value_type() {
75 let arr = arr.iter().map(|item| match item.as_str() {
76 Some(st) => String::from(st),
77 None => item.to_string(),
78 });
79 return Ok(quote!([#(#arr),*]));
80 }
81 } else {
82 current_type = Some(value.value_type());
83 }
84 new_arr.push(emit_toml_value(value.clone())?)
85 }
86 Ok(quote!([#(#new_arr),*]))
87 }
88 Value::Table(table) => {
89 let st = format!("{{ {} }}", table.to_string().trim().replace("\n", ", "));
90 Ok(quote!(#st))
91 }
92 }
93}
94
95fn workspace_root() -> PathBuf {
98 let mut current_dir = current_dir().expect("Failed to read current directory.");
99 let mut best_match = current_dir.clone();
100 loop {
101 let cargo_toml = current_dir.join("Cargo.toml");
102 if let Ok(cargo_toml) = read_to_string(&cargo_toml) {
103 best_match = current_dir.clone();
104 if let Ok(cargo_toml) = cargo_toml.parse::<Table>() {
105 if cargo_toml.contains_key("workspace") {
106 return best_match;
107 }
108 } else if cargo_toml.contains("[workspace]") || {
109 let mut cargo_toml = cargo_toml.clone();
110 cargo_toml.retain(|c| !c.is_whitespace());
111 cargo_toml.contains("workspace=")
112 } {
113 return best_match;
115 }
116 }
117 match current_dir.parent() {
118 Some(dir) => current_dir = dir.to_path_buf(),
119 None => break,
120 }
121 }
122 best_match
123}
124
125fn crate_root<S: AsRef<str>>(crate_name: S, current_dir: &PathBuf) -> PathBuf {
126 let root = workspace_root();
127 for entry in WalkDir::new(root).into_iter().filter_map(|e| e.ok()) {
128 let path = entry.path();
129 let Some(file_name) = path.file_name() else { continue };
130 if file_name != "Cargo.toml" {
131 continue;
132 }
133 let Ok(cargo_toml) = read_to_string(path) else { continue };
134 let Ok(cargo_toml) = cargo_toml.parse::<Table>() else { continue };
135 let Some(package) = cargo_toml.get("package") else { continue };
136 let Some(name) = package.get("name") else { continue };
137 let Value::String(name) = name else { continue };
138 if name == crate_name.as_ref() {
139 return path.parent().unwrap().to_path_buf();
140 }
141 }
142 current_dir.clone()
143}
144
145fn settings_internal_helper(
146 crate_name: String,
147 key: String,
148 current_dir: PathBuf,
149) -> Result<TokenStream2> {
150 println!("checking {}", current_dir.display());
151 println!(
152 "CARGO_MANIFEST_DIR: {}",
153 std::env::var("CARGO_MANIFEST_DIR").unwrap()
154 );
155 println!("OUT_DIR: {:?}", std::env::var("OUT_DIR"));
156 let parent_dir = match current_dir.parent() {
157 Some(parent_dir) => {
158 let parent_toml = parent_dir.join("Cargo.toml");
159 match parent_toml.exists() {
160 true => Some(parent_dir.to_path_buf()),
161 false => None,
162 }
163 }
164 None => None,
165 };
166 let cargo_toml_path = current_dir.join("Cargo.toml");
167 let Ok(cargo_toml) = read_to_string(&cargo_toml_path) else {
168 if let Some(parent_dir) = parent_dir {
169 return settings_internal_helper(crate_name, key, parent_dir);
170 }
171 return Err(Error::new(Span::call_site(), format!(
172 "Failed to read '{}'",
173 cargo_toml_path.display(),
174 )));
175 };
176 let Ok(cargo_toml) = cargo_toml.parse::<Table>() else {
177 if let Some(parent_dir) = parent_dir {
178 return settings_internal_helper(crate_name, key, parent_dir);
179 }
180 return Err(Error::new(Span::call_site(), format!(
181 "Failed to parse '{}' as valid TOML.",
182 cargo_toml_path.display(),
183 )));
184 };
185 let Some(package) = cargo_toml.get("package") else {
186 if let Some(parent_dir) = parent_dir {
187 return settings_internal_helper(crate_name, key, parent_dir);
188 }
189 return Err(Error::new(Span::call_site(), format!(
190 "Failed to find table 'package' in '{}'.",
191 cargo_toml_path.display(),
192 )));
193 };
194 let Some(metadata) = package.get("metadata") else {
195 if let Some(parent_dir) = parent_dir {
196 return settings_internal_helper(crate_name, key, parent_dir);
197 }
198 return Err(Error::new(Span::call_site(), format!(
199 "Failed to find table 'package.metadata' in '{}'.",
200 cargo_toml_path.display(),
201 )));
202 };
203 let Some(settings) = metadata.get("settings") else {
204 if let Some(parent_dir) = parent_dir {
205 return settings_internal_helper(crate_name, key, parent_dir);
206 }
207 return Err(Error::new(Span::call_site(), format!(
208 "Failed to find table 'package.metadata.settings' in '{}'.",
209 cargo_toml_path.display(),
210 )));
211 };
212 let Some(crate_name_table) = settings.get(&crate_name) else {
213 if let Some(parent_dir) = parent_dir {
214 return settings_internal_helper(crate_name, key, parent_dir);
215 }
216 return Err(Error::new(Span::call_site(), format!(
217 "Failed to find table 'package.metadata.settings.{}' in '{}'.",
218 crate_name,
219 cargo_toml_path.display(),
220 )));
221 };
222 let Some(value) = crate_name_table.get(&key) else {
223 if let Some(parent_dir) = parent_dir {
224 return settings_internal_helper(crate_name, key, parent_dir);
225 }
226 return Err(Error::new(Span::call_site(), format!(
227 "Failed to find table 'package.metadata.settings.{}.{}' in '{}'.",
228 crate_name,
229 key,
230 cargo_toml_path.display(),
231 )));
232 };
233 emit_toml_value(value.clone())
234}
235
236fn settings_internal(tokens: impl Into<TokenStream2>) -> Result<TokenStream2> {
237 let args = parse2::<SettingsProcArgs>(tokens.into())?;
238 let Ok(current_dir) = current_dir() else {
239 return Err(Error::new(Span::call_site(), "Failed to read current directory."));
240 };
241 let starting_dir = crate_root(args.crate_name.value(), ¤t_dir);
242 match settings_internal_helper(args.crate_name.value(), args.key.value(), starting_dir) {
243 Ok(tokens) => Ok(tokens),
244 Err(err) => {
245 if let Some(default) = args.default {
246 return Ok(default.to_token_stream());
247 }
248 Err(err)
249 }
250 }
251}