Skip to main content

polyxml_cli/
config.rs

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