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 let parent_dir = match current_dir.parent() {
152 Some(parent_dir) => {
153 let parent_toml = parent_dir.join("Cargo.toml");
154 match parent_toml.exists() {
155 true => Some(parent_dir.to_path_buf()),
156 false => None,
157 }
158 }
159 None => None,
160 };
161 let cargo_toml_path = current_dir.join("Cargo.toml");
162 let Ok(cargo_toml) = read_to_string(&cargo_toml_path) else {
163 if let Some(parent_dir) = parent_dir {
164 return settings_internal_helper(crate_name, key, parent_dir);
165 }
166 return Err(Error::new(Span::call_site(), format!(
167 "Failed to read '{}'",
168 cargo_toml_path.display(),
169 )));
170 };
171 let Ok(cargo_toml) = cargo_toml.parse::<Table>() else {
172 if let Some(parent_dir) = parent_dir {
173 return settings_internal_helper(crate_name, key, parent_dir);
174 }
175 return Err(Error::new(Span::call_site(), format!(
176 "Failed to parse '{}' as valid TOML.",
177 cargo_toml_path.display(),
178 )));
179 };
180 let Some(package) = cargo_toml.get("package") else {
181 if let Some(parent_dir) = parent_dir {
182 return settings_internal_helper(crate_name, key, parent_dir);
183 }
184 return Err(Error::new(Span::call_site(), format!(
185 "Failed to find table 'package' in '{}'.",
186 cargo_toml_path.display(),
187 )));
188 };
189 let Some(metadata) = package.get("metadata") else {
190 if let Some(parent_dir) = parent_dir {
191 return settings_internal_helper(crate_name, key, parent_dir);
192 }
193 return Err(Error::new(Span::call_site(), format!(
194 "Failed to find table 'package.metadata' in '{}'.",
195 cargo_toml_path.display(),
196 )));
197 };
198 let Some(settings) = metadata.get("settings") else {
199 if let Some(parent_dir) = parent_dir {
200 return settings_internal_helper(crate_name, key, parent_dir);
201 }
202 return Err(Error::new(Span::call_site(), format!(
203 "Failed to find table 'package.metadata.settings' in '{}'.",
204 cargo_toml_path.display(),
205 )));
206 };
207 let Some(crate_name_table) = settings.get(&crate_name) else {
208 if let Some(parent_dir) = parent_dir {
209 return settings_internal_helper(crate_name, key, parent_dir);
210 }
211 return Err(Error::new(Span::call_site(), format!(
212 "Failed to find table 'package.metadata.settings.{}' in '{}'.",
213 crate_name,
214 cargo_toml_path.display(),
215 )));
216 };
217 let Some(value) = crate_name_table.get(&key) else {
218 if let Some(parent_dir) = parent_dir {
219 return settings_internal_helper(crate_name, key, parent_dir);
220 }
221 return Err(Error::new(Span::call_site(), format!(
222 "Failed to find table 'package.metadata.settings.{}.{}' in '{}'.",
223 crate_name,
224 key,
225 cargo_toml_path.display(),
226 )));
227 };
228 emit_toml_value(value.clone())
229}
230
231fn settings_internal(tokens: impl Into<TokenStream2>) -> Result<TokenStream2> {
232 let args = parse2::<SettingsProcArgs>(tokens.into())?;
233 let Ok(current_dir) = current_dir() else {
234 return Err(Error::new(Span::call_site(), "Failed to read current directory."));
235 };
236 let starting_dir = crate_root(args.crate_name.value(), ¤t_dir);
237 match settings_internal_helper(args.crate_name.value(), args.key.value(), starting_dir) {
238 Ok(tokens) => Ok(tokens),
239 Err(err) => {
240 if let Some(default) = args.default {
241 return Ok(default.to_token_stream());
242 }
243 Err(err)
244 }
245 }
246}