Skip to main content

foundry_schema/
lib.rs

1use anyhow::{anyhow, Context, Result};
2use foundry_types::{Archetype, TemplateEngineKind};
3use serde::{Deserialize, Serialize};
4use std::path::Path;
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
7pub struct FoundryConfig {
8    pub project_name: String,
9    pub archetype: Archetype,
10    pub template_engine: TemplateEngineKind,
11    pub profile: String,
12    pub run_post_gen_checks: bool,
13}
14
15impl Default for FoundryConfig {
16    fn default() -> Self {
17        Self {
18            project_name: "sample-project".to_string(),
19            archetype: Archetype::Tooling,
20            template_engine: TemplateEngineKind::MiniJinja,
21            profile: "default".to_string(),
22            run_post_gen_checks: true,
23        }
24    }
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
28#[serde(deny_unknown_fields)]
29pub struct PartialConfig {
30    pub project_name: Option<String>,
31    pub archetype: Option<Archetype>,
32    pub template_engine: Option<TemplateEngineKind>,
33    pub profile: Option<String>,
34    pub run_post_gen_checks: Option<bool>,
35}
36
37pub fn load_partial_from_file(path: &Path) -> Result<PartialConfig> {
38    let content = std::fs::read_to_string(path)
39        .with_context(|| format!("failed to read config at {}", path.display()))?;
40    let value: toml::Value =
41        toml::from_str(&content).with_context(|| format!("invalid TOML at {}", path.display()))?;
42    validate_partial_config(&value)
43        .map_err(|e| anyhow!("invalid config at {}: {}", path.display(), e))?;
44    let parsed: PartialConfig = toml::from_str(&content)
45        .map_err(|e| anyhow!("invalid TOML at {}: {}", path.display(), e))?;
46    Ok(parsed)
47}
48
49fn validate_partial_config(value: &toml::Value) -> Result<()> {
50    let table = value
51        .as_table()
52        .ok_or_else(|| anyhow!("expected top-level TOML table"))?;
53
54    for (key, raw) in table {
55        match key.as_str() {
56            "project_name" | "profile" => {
57                if !raw.is_str() {
58                    return Err(anyhow!(
59                        "invalid type for `{}`: expected string; omit the field to use layered defaults",
60                        key
61                    ));
62                }
63            }
64            "archetype" => {
65                let Some(v) = raw.as_str() else {
66                    return Err(anyhow!(
67                        "invalid type for `archetype`: expected string; omit the field to use layered defaults"
68                    ));
69                };
70                if !matches!(v, "web" | "tui" | "tooling") {
71                    return Err(anyhow!(
72                        "invalid value for `archetype`: `{}`; allowed: web, tui, tooling",
73                        v
74                    ));
75                }
76            }
77            "template_engine" => {
78                let Some(v) = raw.as_str() else {
79                    return Err(anyhow!(
80                        "invalid type for `template_engine`: expected string; omit the field to use layered defaults"
81                    ));
82                };
83                if !matches!(v, "handlebars" | "mini_jinja") {
84                    return Err(anyhow!(
85                        "invalid value for `template_engine`: `{}`; allowed: handlebars, mini_jinja",
86                        v
87                    ));
88                }
89            }
90            "run_post_gen_checks" => {
91                if !raw.is_bool() {
92                    return Err(anyhow!(
93                        "invalid type for `run_post_gen_checks`: expected boolean; omit the field to use layered defaults"
94                    ));
95                }
96            }
97            _ => {
98                return Err(anyhow!(
99                    "unknown field `{}`; allowed keys: project_name, archetype, template_engine, profile, run_post_gen_checks",
100                    key
101                ));
102            }
103        }
104    }
105
106    Ok(())
107}
108
109pub fn merge(base: &FoundryConfig, partial: &PartialConfig) -> FoundryConfig {
110    FoundryConfig {
111        project_name: partial
112            .project_name
113            .clone()
114            .unwrap_or_else(|| base.project_name.clone()),
115        archetype: partial
116            .archetype
117            .clone()
118            .unwrap_or_else(|| base.archetype.clone()),
119        template_engine: partial
120            .template_engine
121            .clone()
122            .unwrap_or_else(|| base.template_engine.clone()),
123        profile: partial
124            .profile
125            .clone()
126            .unwrap_or_else(|| base.profile.clone()),
127        run_post_gen_checks: partial
128            .run_post_gen_checks
129            .unwrap_or(base.run_post_gen_checks),
130    }
131}