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 depends_on: Vec<String>,
55}
56
57#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
58pub struct WorkspaceSection {
59 pub name: Option<String>,
60 #[serde(default)]
61 pub schemas: Vec<String>,
62 pub include_dirs: Option<Vec<String>>,
63 pub output_base_dir: Option<String>,
64 pub custom_header: Option<String>,
65 pub go_module: Option<String>,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(deny_unknown_fields)]
73pub struct TargetConfig {
74 pub target: String,
75 pub output: String,
76 pub enabled: Option<bool>,
77 pub backend: Option<String>,
78 #[serde(default)]
79 pub features: Vec<String>,
80 pub package: Option<String>,
81 pub namespace: Option<String>,
82 pub strict_facets: Option<bool>,
83 pub slots: Option<bool>,
84 pub kw_only: Option<bool>,
85 pub zero_copy: Option<bool>,
86 pub codecs: Option<bool>,
87 pub standard: Option<String>,
88 pub derive_traits: Option<Vec<String>>,
89 pub box_cycles: Option<bool>,
90 pub modules: Option<bool>,
91 pub mode: Option<String>,
92 pub serializer: Option<String>,
93 pub style: Option<String>,
94 pub custom_header: Option<String>,
95}
96
97#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
98#[serde(deny_unknown_fields)]
99pub struct CodegenTargetConfig {
100 pub enabled: Option<bool>,
101 pub output: Option<String>,
102 pub backend: Option<String>,
103 #[serde(default)]
104 pub features: Vec<String>,
105 pub package: Option<String>,
106 pub namespace: Option<String>,
107 pub strict_facets: Option<bool>,
108 pub slots: Option<bool>,
109 pub kw_only: Option<bool>,
110 pub zero_copy: Option<bool>,
111 pub codecs: Option<bool>,
112 pub standard: Option<String>,
113 pub derive_traits: Option<Vec<String>>,
114 pub box_cycles: Option<bool>,
115 pub modules: Option<bool>,
116 pub mode: Option<String>,
117 pub serializer: Option<String>,
118 pub style: Option<String>,
119 pub custom_header: Option<String>,
120}
121
122impl std::str::FromStr for WorkspaceManifest {
123 type Err = ConfigError;
124
125 fn from_str(toml_str: &str) -> Result<Self, Self::Err> {
126 let manifest: WorkspaceManifest = toml::from_str(toml_str)?;
127 Ok(manifest)
128 }
129}
130
131impl WorkspaceManifest {
132 pub fn from_file(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
133 let content = fs::read_to_string(path)?;
134 content.parse()
135 }
136
137 pub fn resolved_targets(&self) -> Vec<TargetConfig> {
140 let mut targets = Vec::new();
141
142 let ws_header = self
143 .workspace
144 .as_ref()
145 .and_then(|w| w.custom_header.clone());
146
147 for gen in &self.generate {
149 if gen.enabled.unwrap_or(true) {
150 let mut target = gen.clone();
151 if target.custom_header.is_none() {
152 target.custom_header = ws_header.clone();
153 }
154 targets.push(target);
155 }
156 }
157
158 if let Some(ref codegen_map) = self.codegen {
160 for (lang, cfg) in codegen_map {
161 if cfg.enabled.unwrap_or(true) {
162 let output = cfg
163 .output
164 .clone()
165 .unwrap_or_else(|| format!("generated/{}", lang));
166
167 targets.push(TargetConfig {
168 target: lang.clone(),
169 output,
170 enabled: cfg.enabled,
171 backend: cfg.backend.clone(),
172 features: cfg.features.clone(),
173 package: cfg.package.clone(),
174 namespace: cfg.namespace.clone(),
175 strict_facets: cfg.strict_facets,
176 slots: cfg.slots,
177 kw_only: cfg.kw_only,
178 zero_copy: cfg.zero_copy,
179 codecs: cfg.codecs,
180 standard: cfg.standard.clone(),
181 derive_traits: cfg.derive_traits.clone(),
182 box_cycles: cfg.box_cycles,
183 modules: cfg.modules,
184 mode: cfg.mode.clone(),
185 serializer: cfg.serializer.clone(),
186 style: cfg.style.clone(),
187 custom_header: cfg.custom_header.clone().or_else(|| ws_header.clone()),
188 });
189 }
190 }
191 }
192
193 targets
194 }
195
196 pub fn expand_schemas(&self, base_dir: &Path) -> Result<Vec<PathBuf>, ConfigError> {
198 let Some(ref ws) = self.workspace else {
199 return Ok(Vec::new());
200 };
201
202 expand_patterns(&ws.schemas, base_dir)
203 }
204
205 pub fn expand_module_schemas(
206 &self,
207 name: &str,
208 base_dir: &Path,
209 ) -> Result<Vec<PathBuf>, ConfigError> {
210 let Some(module) = self.modules.get(name) else {
211 return Ok(Vec::new());
212 };
213 expand_patterns(&module.schemas, base_dir)
214 }
215
216 pub fn module_order(&self) -> Result<Vec<String>, ConfigError> {
217 for name in self.modules.keys() {
218 let mut chars = name.chars();
219 if !chars
220 .next()
221 .is_some_and(|ch| ch.is_ascii_alphabetic() || ch == '_')
222 || !chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
223 {
224 return Err(ConfigError::InvalidModuleName(name.clone()));
225 }
226 }
227 let mut order = Vec::new();
228 let mut visiting = HashSet::new();
229 let mut visited = HashSet::new();
230 fn visit(
231 name: &str,
232 manifest: &WorkspaceManifest,
233 visiting: &mut HashSet<String>,
234 visited: &mut HashSet<String>,
235 order: &mut Vec<String>,
236 ) -> Result<(), ConfigError> {
237 if visited.contains(name) {
238 return Ok(());
239 }
240 if !visiting.insert(name.to_string()) {
241 return Err(ConfigError::ModuleCycle(name.to_string()));
242 }
243 let module = &manifest.modules[name];
244 for dependency in &module.depends_on {
245 if !manifest.modules.contains_key(dependency) {
246 return Err(ConfigError::UnknownModuleDependency {
247 module: name.to_string(),
248 dependency: dependency.clone(),
249 });
250 }
251 visit(dependency, manifest, visiting, visited, order)?;
252 }
253 visiting.remove(name);
254 visited.insert(name.to_string());
255 order.push(name.to_string());
256 Ok(())
257 }
258 for name in self.modules.keys() {
259 visit(name, self, &mut visiting, &mut visited, &mut order)?;
260 }
261 Ok(order)
262 }
263}
264
265fn expand_patterns(patterns: &[String], base_dir: &Path) -> Result<Vec<PathBuf>, ConfigError> {
266 let mut paths = Vec::new();
267 for pattern in patterns {
268 let full_pattern = if Path::new(pattern).is_absolute() {
269 pattern.clone()
270 } else {
271 base_dir.join(pattern).to_string_lossy().to_string()
272 };
273
274 let entries = glob(&full_pattern).map_err(|e| ConfigError::GlobPattern {
275 pattern: full_pattern.clone(),
276 error: e,
277 })?;
278
279 for entry in entries {
280 let path = entry?;
281 if path.is_file() {
282 paths.push(path);
283 }
284 }
285 }
286
287 paths.sort();
288 paths.dedup();
289 Ok(paths)
290}