1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3
4use syn::{Attribute, Fields, Item, ItemStruct, Type};
5
6use crate::ModelError;
7use crate::code::derives;
8use crate::syntax::{doc_lines, first_type_argument, line_of, parsed_or_read, type_last_name};
9
10pub const DATABASE_URL: &str = "DATABASE_URL";
11
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct ModuleConfig {
14 pub type_name: String,
15 pub file: PathBuf,
16 pub line: usize,
17 pub declared: bool,
18 pub keys: Vec<ConfigKey>,
19}
20
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct ConfigKey {
23 pub name: String,
24 pub doc: Vec<String>,
25 pub kind: KeyKind,
26 pub required: bool,
27 pub fallback: Option<String>,
28 pub secret: bool,
30}
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum KeyKind {
34 Text,
36 Boolean,
37 Integer,
38 Count,
39 Decimal,
40 Switch,
41 Address,
42}
43
44impl KeyKind {
45 fn of(type_name: &str) -> Self {
46 match type_name {
47 "bool" => Self::Boolean,
48 "i8" | "i16" | "i32" | "i64" | "i128" | "isize" => Self::Integer,
49 "u8" | "u16" | "u32" | "u64" | "u128" | "usize" => Self::Count,
50 "f32" | "f64" => Self::Decimal,
51 _ => Self::Text,
52 }
53 }
54
55 #[must_use]
57 pub fn accepts(self, value: &str) -> bool {
58 match self {
59 Self::Text => true,
60 Self::Boolean => value.parse::<bool>().is_ok(),
61 Self::Integer => value.parse::<i128>().is_ok(),
62 Self::Count => value.parse::<u128>().is_ok(),
63 Self::Decimal => value.parse::<f64>().is_ok(),
64 Self::Switch => matches!(value, "on" | "off"),
65 Self::Address => value.parse::<std::net::SocketAddr>().is_ok(),
66 }
67 }
68
69 #[must_use]
70 pub fn description(self) -> &'static str {
71 match self {
72 Self::Text => "text",
73 Self::Boolean => "`true` or `false`",
74 Self::Integer => "a whole number",
75 Self::Count => "a whole number of 0 or more",
76 Self::Decimal => "a number",
77 Self::Switch => "`on` or `off`",
78 Self::Address => "an address such as 127.0.0.1:8080",
79 }
80 }
81}
82
83pub(crate) fn read_config(
84 directory: &Path,
85 crate_name: &str,
86 syntax: &BTreeMap<PathBuf, syn::File>,
87) -> Result<Option<ModuleConfig>, ModelError> {
88 let file = directory.join("src").join("config.rs");
89 if !file.is_file() {
90 return Ok(None);
91 }
92 let parsed = parsed_or_read(syntax, &file)?;
93 let structs = structs_of(&parsed);
94 let type_name = config_type_name(crate_name);
95 let root = structs.get(&type_name).ok_or_else(|| {
96 ModelError::in_file(
97 &file,
98 format!("declare the module's config as `#[derive(sofie::Config)] pub struct {type_name} {{ … }}`"),
99 )
100 })?;
101 let mut keys = Vec::new();
102 Group {
103 structs: &structs,
104 file: &file,
105 }
106 .collect(root, &crate_name.to_ascii_uppercase(), false, &mut keys)?;
107 Ok(Some(ModuleConfig {
108 type_name,
109 line: line_of(&root.ident),
110 declared: syntax.contains_key(&file),
111 file,
112 keys,
113 }))
114}
115
116pub(crate) fn settings_keys(
117 syntax: &BTreeMap<PathBuf, syn::File>,
118 settings: Option<&str>,
119 prefix: &str,
120) -> Result<Vec<ConfigKey>, ModelError> {
121 let mut keys = Vec::new();
122 for (file, parsed) in syntax {
123 let structs = structs_of(parsed);
124 if let Some(root) = settings.and_then(|name| structs.get(name)) {
125 Group {
126 structs: &structs,
127 file,
128 }
129 .collect(root, prefix, false, &mut keys)?;
130 break;
131 }
132 }
133 Ok(keys)
134}
135
136fn structs_of(parsed: &syn::File) -> BTreeMap<String, &ItemStruct> {
137 parsed
138 .items
139 .iter()
140 .filter_map(|item| match item {
141 Item::Struct(declared) if derives(&declared.attrs, "Config") => {
142 Some((declared.ident.to_string(), declared))
143 }
144 _ => None,
145 })
146 .collect()
147}
148
149#[must_use]
150pub fn config_type_name(crate_name: &str) -> String {
151 crate::naming::pascal_case(crate_name) + "Config"
152}
153
154struct Group<'file> {
155 structs: &'file BTreeMap<String, &'file ItemStruct>,
156 file: &'file Path,
157}
158
159impl Group<'_> {
160 fn collect(
161 &self,
162 declared: &ItemStruct,
163 prefix: &str,
164 optional: bool,
165 keys: &mut Vec<ConfigKey>,
166 ) -> Result<(), ModelError> {
167 let Fields::Named(fields) = &declared.fields else {
168 return Err(ModelError::new(
169 self.file,
170 line_of(&declared.ident),
171 "a config struct has named fields, one per key",
172 ));
173 };
174 for field in &fields.named {
175 let Some(ident) = &field.ident else { continue };
176 let name = format!("{prefix}_{}", ident.to_string().to_ascii_uppercase());
177 let (field_type, is_option) = unwrapped(&field.ty, "Option");
178 let (field_type, secret) = unwrapped(field_type, "Secret");
179 let optional = optional || is_option;
180 let type_name = type_last_name(field_type).unwrap_or_default();
181 if let Some(nested) = self.structs.get(&type_name) {
182 self.collect(nested, &name, optional, keys)?;
183 } else if matches!(type_name.as_str(), "Database" | "Queue") {
184 keys.push(ConfigKey {
185 secret: true,
186 ..key(
187 format!("{name}_URL"),
188 &field.attrs,
189 KeyKind::Text,
190 false,
191 Some(DATABASE_URL),
192 )
193 });
194 } else {
195 keys.push(ConfigKey {
196 secret,
197 ..key(name, &field.attrs, KeyKind::of(&type_name), !optional, None)
198 });
199 }
200 }
201 Ok(())
202 }
203}
204
205fn key(
206 name: String,
207 attributes: &[Attribute],
208 kind: KeyKind,
209 required: bool,
210 fallback: Option<&str>,
211) -> ConfigKey {
212 ConfigKey {
213 name,
214 doc: doc_lines(attributes),
215 kind,
216 required,
217 fallback: fallback.map(str::to_owned),
218 secret: false,
219 }
220}
221
222fn unwrapped<'a>(written: &'a Type, wrapper: &str) -> (&'a Type, bool) {
223 if let Type::Path(path) = written
224 && let Some(last) = path.path.segments.last()
225 && last.ident == wrapper
226 && let Some(inner) = first_type_argument(last)
227 {
228 return (inner, true);
229 }
230 (written, false)
231}
232
233#[cfg(test)]
234mod tests {
235 use super::config_type_name;
236
237 #[test]
238 fn the_config_type_is_named_after_the_module() {
239 assert_eq!(config_type_name("order"), "OrderConfig");
240 assert_eq!(config_type_name("sofie_identity"), "SofieIdentityConfig");
241 }
242}