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}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47pub struct TargetConfig {
48 pub target: String,
49 pub output: String,
50 pub enabled: Option<bool>,
51 pub backend: Option<String>,
52 pub package: Option<String>,
53 pub namespace: Option<String>,
54 pub strict_facets: Option<bool>,
55 pub slots: Option<bool>,
56 pub kw_only: Option<bool>,
57 pub zero_copy: Option<bool>,
58 pub codecs: Option<bool>,
59 pub standard: Option<String>,
60 pub derive_traits: Option<Vec<String>>,
61 pub box_cycles: Option<bool>,
62 pub modules: Option<bool>,
63 pub mode: Option<String>,
64 pub serializer: Option<String>,
65 pub zod: Option<bool>,
66}
67
68#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
69pub struct CodegenTargetConfig {
70 pub enabled: Option<bool>,
71 pub output: Option<String>,
72 pub backend: Option<String>,
73 pub package: Option<String>,
74 pub namespace: Option<String>,
75 pub strict_facets: Option<bool>,
76 pub slots: Option<bool>,
77 pub kw_only: Option<bool>,
78 pub zero_copy: Option<bool>,
79 pub codecs: Option<bool>,
80 pub standard: Option<String>,
81 pub derive_traits: Option<Vec<String>>,
82 pub box_cycles: Option<bool>,
83 pub modules: Option<bool>,
84 pub mode: Option<String>,
85 pub serializer: Option<String>,
86 pub zod: Option<bool>,
87}
88
89impl std::str::FromStr for WorkspaceManifest {
90 type Err = ConfigError;
91
92 fn from_str(toml_str: &str) -> Result<Self, Self::Err> {
93 let manifest: WorkspaceManifest = toml::from_str(toml_str)?;
94 Ok(manifest)
95 }
96}
97
98impl WorkspaceManifest {
99 pub fn from_file(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
100 let content = fs::read_to_string(path)?;
101 content.parse()
102 }
103
104 pub fn resolved_targets(&self) -> Vec<TargetConfig> {
107 let mut targets = Vec::new();
108
109 for gen in &self.generate {
111 if gen.enabled.unwrap_or(true) {
112 targets.push(gen.clone());
113 }
114 }
115
116 if let Some(ref codegen_map) = self.codegen {
118 for (lang, cfg) in codegen_map {
119 if cfg.enabled.unwrap_or(true) {
120 let output = cfg
121 .output
122 .clone()
123 .unwrap_or_else(|| format!("generated/{}", lang));
124
125 targets.push(TargetConfig {
126 target: lang.clone(),
127 output,
128 enabled: cfg.enabled,
129 backend: cfg.backend.clone(),
130 package: cfg.package.clone(),
131 namespace: cfg.namespace.clone(),
132 strict_facets: cfg.strict_facets,
133 slots: cfg.slots,
134 kw_only: cfg.kw_only,
135 zero_copy: cfg.zero_copy,
136 codecs: cfg.codecs,
137 standard: cfg.standard.clone(),
138 derive_traits: cfg.derive_traits.clone(),
139 box_cycles: cfg.box_cycles,
140 modules: cfg.modules,
141 mode: cfg.mode.clone(),
142 serializer: cfg.serializer.clone(),
143 zod: cfg.zod,
144 });
145 }
146 }
147 }
148
149 targets
150 }
151
152 pub fn expand_schemas(&self, base_dir: &Path) -> Result<Vec<PathBuf>, ConfigError> {
154 let mut paths = Vec::new();
155
156 let Some(ref ws) = self.workspace else {
157 return Ok(paths);
158 };
159
160 for pattern in &ws.schemas {
161 let full_pattern = if Path::new(pattern).is_absolute() {
162 pattern.clone()
163 } else {
164 base_dir.join(pattern).to_string_lossy().to_string()
165 };
166
167 let entries = glob(&full_pattern).map_err(|e| ConfigError::GlobPattern {
168 pattern: full_pattern.clone(),
169 error: e,
170 })?;
171
172 for entry in entries {
173 let path = entry?;
174 if path.is_file() {
175 paths.push(path);
176 }
177 }
178 }
179
180 paths.sort();
181 paths.dedup();
182 Ok(paths)
183 }
184}