crate_settings/
lib.rs

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
95/// Finds the root of the current workspace, falling back to the outer-most directory with a
96/// Cargo.toml, and then falling back to the current directory.
97fn 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                // only used if `Cargo.toml` is invalid TOML
114                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(), &current_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}