1use std::collections::{BTreeMap, HashMap, HashSet};
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use glob::glob;
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9#[derive(Debug, Error)]
10pub enum ConfigError {
11 #[error("I/O error reading configuration: {0}")]
12 Io(#[from] std::io::Error),
13
14 #[error("TOML syntax error: {0}")]
15 Toml(#[from] toml::de::Error),
16
17 #[error("Invalid glob pattern '{pattern}': {error}")]
18 GlobPattern {
19 pattern: String,
20 error: glob::PatternError,
21 },
22
23 #[error("Failed to read glob path: {0}")]
24 Glob(#[from] glob::GlobError),
25
26 #[error("Module '{module}' depends on unknown module '{dependency}'")]
27 UnknownModuleDependency { module: String, dependency: String },
28
29 #[error("Cycle in workspace module dependencies involving '{0}'")]
30 ModuleCycle(String),
31
32 #[error("Invalid module name '{0}': use ASCII letters, digits, and underscores, starting with a letter or underscore")]
33 InvalidModuleName(String),
34}
35
36#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
38pub struct WorkspaceManifest {
39 pub workspace: Option<WorkspaceSection>,
40 #[serde(default)]
41 pub generate: Vec<TargetConfig>,
42 pub codegen: Option<HashMap<String, CodegenTargetConfig>>,
43 #[serde(default)]
44 pub modules: BTreeMap<String, ModuleConfig>,
45}
46
47#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(deny_unknown_fields)]
50pub struct ModuleConfig {
51 #[serde(default)]
52 pub schemas: Vec<String>,
53 #[serde(default)]
54 pub root_elements: Vec<String>,
55 #[serde(default)]
56 pub depends_on: Vec<String>,
57}
58
59#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
60pub struct WorkspaceSection {
61 pub name: Option<String>,
62 #[serde(default)]
63 pub schemas: Vec<String>,
64 #[serde(default)]
65 pub root_elements: Vec<String>,
66 pub include_dirs: Option<Vec<String>>,
67 pub output_base_dir: Option<String>,
68 pub custom_header: Option<String>,
69 pub go_module: Option<String>,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(deny_unknown_fields)]
77pub struct TargetConfig {
78 pub target: String,
79 pub output: String,
80 pub enabled: Option<bool>,
81 pub backend: Option<String>,
82 #[serde(default)]
83 pub features: Vec<String>,
84 pub package: Option<String>,
85 pub namespace: Option<String>,
86 pub strict_facets: Option<bool>,
87 pub slots: Option<bool>,
88 pub kw_only: Option<bool>,
89 pub zero_copy: Option<bool>,
90 pub codecs: Option<bool>,
91 pub standard: Option<String>,
92 pub derive_traits: Option<Vec<String>>,
93 pub box_cycles: Option<bool>,
94 pub modules: Option<bool>,
95 pub mode: Option<String>,
96 pub serializer: Option<String>,
97 pub style: Option<String>,
98 pub custom_header: Option<String>,
99 pub split_units: Option<bool>,
100 pub split_crates: Option<bool>,
101 pub chunk_size: Option<usize>,
102}
103
104#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
105#[serde(deny_unknown_fields)]
106pub struct CodegenTargetConfig {
107 pub enabled: Option<bool>,
108 pub output: Option<String>,
109 pub backend: Option<String>,
110 #[serde(default)]
111 pub features: Vec<String>,
112 pub package: Option<String>,
113 pub namespace: Option<String>,
114 pub strict_facets: Option<bool>,
115 pub slots: Option<bool>,
116 pub kw_only: Option<bool>,
117 pub zero_copy: Option<bool>,
118 pub codecs: Option<bool>,
119 pub standard: Option<String>,
120 pub derive_traits: Option<Vec<String>>,
121 pub box_cycles: Option<bool>,
122 pub modules: Option<bool>,
123 pub mode: Option<String>,
124 pub serializer: Option<String>,
125 pub style: Option<String>,
126 pub custom_header: Option<String>,
127 pub split_units: Option<bool>,
128 pub split_crates: Option<bool>,
129 pub chunk_size: Option<usize>,
130}
131
132impl std::str::FromStr for WorkspaceManifest {
133 type Err = ConfigError;
134
135 fn from_str(toml_str: &str) -> Result<Self, Self::Err> {
136 let manifest: WorkspaceManifest = toml::from_str(toml_str)?;
137 Ok(manifest)
138 }
139}
140
141impl WorkspaceManifest {
142 pub fn from_file(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
143 let content = fs::read_to_string(path)?;
144 content.parse()
145 }
146
147 pub fn resolved_targets(&self) -> Vec<TargetConfig> {
150 let mut targets = Vec::new();
151
152 let ws_header = self
153 .workspace
154 .as_ref()
155 .and_then(|w| w.custom_header.clone());
156
157 for gen in &self.generate {
159 if gen.enabled.unwrap_or(true) {
160 let mut target = gen.clone();
161 if target.custom_header.is_none() {
162 target.custom_header = ws_header.clone();
163 }
164 targets.push(target);
165 }
166 }
167
168 if let Some(ref codegen_map) = self.codegen {
170 for (lang, cfg) in codegen_map {
171 if cfg.enabled.unwrap_or(true) {
172 let output = cfg
173 .output
174 .clone()
175 .unwrap_or_else(|| format!("generated/{}", lang));
176
177 targets.push(TargetConfig {
178 target: lang.clone(),
179 output,
180 enabled: cfg.enabled,
181 backend: cfg.backend.clone(),
182 features: cfg.features.clone(),
183 package: cfg.package.clone(),
184 namespace: cfg.namespace.clone(),
185 strict_facets: cfg.strict_facets,
186 slots: cfg.slots,
187 kw_only: cfg.kw_only,
188 zero_copy: cfg.zero_copy,
189 codecs: cfg.codecs,
190 standard: cfg.standard.clone(),
191 derive_traits: cfg.derive_traits.clone(),
192 box_cycles: cfg.box_cycles,
193 modules: cfg.modules,
194 mode: cfg.mode.clone(),
195 serializer: cfg.serializer.clone(),
196 style: cfg.style.clone(),
197 custom_header: cfg.custom_header.clone().or_else(|| ws_header.clone()),
198 split_units: cfg.split_units,
199 split_crates: cfg.split_crates,
200 chunk_size: cfg.chunk_size,
201 });
202 }
203 }
204 }
205
206 targets
207 }
208
209 pub fn expand_schemas(&self, base_dir: &Path) -> Result<Vec<PathBuf>, ConfigError> {
211 let Some(ref ws) = self.workspace else {
212 return Ok(Vec::new());
213 };
214
215 expand_patterns(&ws.schemas, base_dir)
216 }
217
218 pub fn expand_module_schemas(
219 &self,
220 name: &str,
221 base_dir: &Path,
222 ) -> Result<Vec<PathBuf>, ConfigError> {
223 let Some(module) = self.modules.get(name) else {
224 return Ok(Vec::new());
225 };
226 expand_patterns(&module.schemas, base_dir)
227 }
228
229 pub fn module_order(&self) -> Result<Vec<String>, ConfigError> {
230 for name in self.modules.keys() {
231 let mut chars = name.chars();
232 if !chars
233 .next()
234 .is_some_and(|ch| ch.is_ascii_alphabetic() || ch == '_')
235 || !chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
236 {
237 return Err(ConfigError::InvalidModuleName(name.clone()));
238 }
239 }
240 let mut order = Vec::new();
241 let mut visiting = HashSet::new();
242 let mut visited = HashSet::new();
243 fn visit(
244 name: &str,
245 manifest: &WorkspaceManifest,
246 visiting: &mut HashSet<String>,
247 visited: &mut HashSet<String>,
248 order: &mut Vec<String>,
249 ) -> Result<(), ConfigError> {
250 if visited.contains(name) {
251 return Ok(());
252 }
253 if !visiting.insert(name.to_string()) {
254 return Err(ConfigError::ModuleCycle(name.to_string()));
255 }
256 let module = &manifest.modules[name];
257 for dependency in &module.depends_on {
258 if !manifest.modules.contains_key(dependency) {
259 return Err(ConfigError::UnknownModuleDependency {
260 module: name.to_string(),
261 dependency: dependency.clone(),
262 });
263 }
264 visit(dependency, manifest, visiting, visited, order)?;
265 }
266 visiting.remove(name);
267 visited.insert(name.to_string());
268 order.push(name.to_string());
269 Ok(())
270 }
271 for name in self.modules.keys() {
272 visit(name, self, &mut visiting, &mut visited, &mut order)?;
273 }
274 Ok(order)
275 }
276}
277
278fn expand_patterns(patterns: &[String], base_dir: &Path) -> Result<Vec<PathBuf>, ConfigError> {
279 let mut paths = Vec::new();
280 for pattern in patterns {
281 let full_pattern = if Path::new(pattern).is_absolute() {
282 pattern.clone()
283 } else {
284 base_dir.join(pattern).to_string_lossy().to_string()
285 };
286
287 let entries = glob(&full_pattern).map_err(|e| ConfigError::GlobPattern {
288 pattern: full_pattern.clone(),
289 error: e,
290 })?;
291
292 for entry in entries {
293 let path = entry?;
294 if path.is_file() {
295 paths.push(path);
296 }
297 }
298 }
299
300 paths.sort();
301 paths.dedup();
302 Ok(paths)
303}