1use std::collections::HashMap;
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
27#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
29pub struct WorkspaceManifest {
30 pub workspace: Option<WorkspaceSection>,
31 #[serde(default)]
32 pub generate: Vec<TargetConfig>,
33 pub codegen: Option<HashMap<String, CodegenTargetConfig>>,
34}
35
36#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
37pub struct WorkspaceSection {
38 pub name: Option<String>,
39 #[serde(default)]
40 pub schemas: Vec<String>,
41 pub include_dirs: Option<Vec<String>>,
42 pub output_base_dir: Option<String>,
43 pub custom_header: Option<String>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct TargetConfig {
49 pub target: String,
50 pub output: String,
51 pub enabled: Option<bool>,
52 pub backend: Option<String>,
53 pub package: Option<String>,
54 pub namespace: Option<String>,
55 pub strict_facets: Option<bool>,
56 pub slots: Option<bool>,
57 pub kw_only: Option<bool>,
58 pub zero_copy: Option<bool>,
59 pub codecs: Option<bool>,
60 pub standard: Option<String>,
61 pub derive_traits: Option<Vec<String>>,
62 pub box_cycles: Option<bool>,
63 pub modules: Option<bool>,
64 pub mode: Option<String>,
65 pub serializer: Option<String>,
66 pub zod: Option<bool>,
67 pub source_gen: Option<bool>,
68 pub record_kind: Option<String>,
69 pub style: Option<String>,
70 pub builder: Option<bool>,
71 pub codec: Option<String>,
72 pub rkyv: Option<bool>,
73 pub custom_header: Option<String>,
74}
75
76#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
77pub struct CodegenTargetConfig {
78 pub enabled: Option<bool>,
79 pub output: Option<String>,
80 pub backend: Option<String>,
81 pub package: Option<String>,
82 pub namespace: Option<String>,
83 pub strict_facets: Option<bool>,
84 pub slots: Option<bool>,
85 pub kw_only: Option<bool>,
86 pub zero_copy: Option<bool>,
87 pub codecs: Option<bool>,
88 pub standard: Option<String>,
89 pub derive_traits: Option<Vec<String>>,
90 pub box_cycles: Option<bool>,
91 pub modules: Option<bool>,
92 pub mode: Option<String>,
93 pub serializer: Option<String>,
94 pub zod: Option<bool>,
95 pub source_gen: Option<bool>,
96 pub record_kind: Option<String>,
97 pub style: Option<String>,
98 pub builder: Option<bool>,
99 pub codec: Option<String>,
100 pub rkyv: Option<bool>,
101 pub custom_header: Option<String>,
102}
103
104impl std::str::FromStr for WorkspaceManifest {
105 type Err = ConfigError;
106
107 fn from_str(toml_str: &str) -> Result<Self, Self::Err> {
108 let manifest: WorkspaceManifest = toml::from_str(toml_str)?;
109 Ok(manifest)
110 }
111}
112
113impl WorkspaceManifest {
114 pub fn from_file(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
115 let content = fs::read_to_string(path)?;
116 content.parse()
117 }
118
119 pub fn resolved_targets(&self) -> Vec<TargetConfig> {
122 let mut targets = Vec::new();
123
124 let ws_header = self
125 .workspace
126 .as_ref()
127 .and_then(|w| w.custom_header.clone());
128
129 for gen in &self.generate {
131 if gen.enabled.unwrap_or(true) {
132 let mut target = gen.clone();
133 if target.custom_header.is_none() {
134 target.custom_header = ws_header.clone();
135 }
136 targets.push(target);
137 }
138 }
139
140 if let Some(ref codegen_map) = self.codegen {
142 for (lang, cfg) in codegen_map {
143 if cfg.enabled.unwrap_or(true) {
144 let output = cfg
145 .output
146 .clone()
147 .unwrap_or_else(|| format!("generated/{}", lang));
148
149 targets.push(TargetConfig {
150 target: lang.clone(),
151 output,
152 enabled: cfg.enabled,
153 backend: cfg.backend.clone(),
154 package: cfg.package.clone(),
155 namespace: cfg.namespace.clone(),
156 strict_facets: cfg.strict_facets,
157 slots: cfg.slots,
158 kw_only: cfg.kw_only,
159 zero_copy: cfg.zero_copy,
160 codecs: cfg.codecs,
161 standard: cfg.standard.clone(),
162 derive_traits: cfg.derive_traits.clone(),
163 box_cycles: cfg.box_cycles,
164 modules: cfg.modules,
165 mode: cfg.mode.clone(),
166 serializer: cfg.serializer.clone(),
167 zod: cfg.zod,
168 source_gen: cfg.source_gen,
169 record_kind: cfg.record_kind.clone(),
170 style: cfg.style.clone(),
171 builder: cfg.builder,
172 codec: cfg.codec.clone(),
173 rkyv: cfg.rkyv,
174 custom_header: cfg.custom_header.clone().or_else(|| ws_header.clone()),
175 });
176 }
177 }
178 }
179
180 targets
181 }
182
183 pub fn expand_schemas(&self, base_dir: &Path) -> Result<Vec<PathBuf>, ConfigError> {
185 let mut paths = Vec::new();
186
187 let Some(ref ws) = self.workspace else {
188 return Ok(paths);
189 };
190
191 for pattern in &ws.schemas {
192 let full_pattern = if Path::new(pattern).is_absolute() {
193 pattern.clone()
194 } else {
195 base_dir.join(pattern).to_string_lossy().to_string()
196 };
197
198 let entries = glob(&full_pattern).map_err(|e| ConfigError::GlobPattern {
199 pattern: full_pattern.clone(),
200 error: e,
201 })?;
202
203 for entry in entries {
204 let path = entry?;
205 if path.is_file() {
206 paths.push(path);
207 }
208 }
209 }
210
211 paths.sort();
212 paths.dedup();
213 Ok(paths)
214 }
215}