use std::path::Path;
use ggen_config::{manifest::GgenManifest, ConfigSchemaClassification};
use crate::{
config::GgenConfig,
error::{AppError, Result},
};
#[derive(Debug)]
pub(crate) enum ParsedGgenToml {
DeclarativeRules(Box<GgenManifest>),
Frontmatter(Box<GgenConfig>),
}
pub(crate) fn load(root: &Path) -> Result<ParsedGgenToml> {
let ggen_toml_path = root.join("ggen.toml");
let Ok(raw) = std::fs::read_to_string(&ggen_toml_path) else {
return GgenConfig::load(&ggen_toml_path).map(|c| ParsedGgenToml::Frontmatter(Box::new(c)));
};
match ggen_config::classify_ggen_toml(&raw) {
ConfigSchemaClassification::DeclarativeRules => {
let manifest = ggen_config::manifest::ManifestParser::parse_and_validate(
&ggen_toml_path,
)
.map_err(|e| {
AppError::fm_config(
3,
format!(
"declarative ggen.toml at `{}` failed to parse or validate: {e}. \
Remediation: fix the reported field(s) under [generation]/[[generation.rules]].",
ggen_toml_path.display()
),
)
})?;
Ok(ParsedGgenToml::DeclarativeRules(Box::new(manifest)))
}
ConfigSchemaClassification::Frontmatter => {
let config = GgenConfig::load(&ggen_toml_path)?;
Ok(ParsedGgenToml::Frontmatter(Box::new(config)))
}
ConfigSchemaClassification::Ambiguous { matched } => Err(AppError::Config(format!(
"[{}] ggen.toml at `{}` is ambiguous between the declarative-rules and \
frontmatter schemas: conflicting structural markers {matched:?}. Remediation: \
remove whichever schema's tables/fields were added by mistake.",
ggen_config::CONFIG_SCHEMA_AMBIGUOUS,
ggen_toml_path.display(),
))),
ConfigSchemaClassification::Unsupported { observed_markers } => {
Err(AppError::Config(format!(
"[{}] ggen.toml at `{}` matches neither the declarative-rules nor the \
frontmatter schema: observed top-level tables {observed_markers:?}. \
Remediation: add the required [project]/[ontology]/[templates] (frontmatter) \
or [project]/[ontology]/[generation] (declarative-rules) fields.",
ggen_config::CONFIG_SCHEMA_UNSUPPORTED,
ggen_toml_path.display(),
)))
}
ConfigSchemaClassification::Malformed { diagnostic } => Err(AppError::Config(format!(
"ggen.toml at `{}`: {diagnostic}",
ggen_toml_path.display(),
))),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use tempfile::TempDir;
fn write(dir: &TempDir, contents: &str) {
std::fs::write(dir.path().join("ggen.toml"), contents).expect("write ggen.toml");
}
#[test]
fn missing_file_falls_through_to_canonical_fm_config_001() {
let dir = TempDir::new().expect("tempdir");
let err = load(dir.path()).expect_err("must fail");
assert!(err.to_string().contains("FM-CONFIG-001"), "{err}");
}
#[test]
fn declarative_rules_document_parses_as_declarative_rules() {
let dir = TempDir::new().expect("tempdir");
write(
&dir,
r#"
[project]
name = "x"
version = "1.0.0"
[ontology]
source = "o.ttl"
[[generation.rules]]
name = "r"
query = { inline = "SELECT * WHERE { ?s ?p ?o } ORDER BY ?s" }
template = { inline = "hi" }
output_file = "out.txt"
"#,
);
std::fs::write(dir.path().join("o.ttl"), "").expect("write o.ttl");
match load(dir.path()).expect("must classify+parse") {
ParsedGgenToml::DeclarativeRules(_) => {}
ParsedGgenToml::Frontmatter(_) => panic!("expected DeclarativeRules"),
}
}
#[test]
fn frontmatter_document_parses_as_frontmatter() {
let dir = TempDir::new().expect("tempdir");
write(
&dir,
r#"
[project]
name = "x"
[ontology]
source = "o.ttl"
[templates]
dir = "templates"
"#,
);
match load(dir.path()).expect("must classify+parse") {
ParsedGgenToml::Frontmatter(_) => {}
ParsedGgenToml::DeclarativeRules(_) => panic!("expected Frontmatter"),
}
}
#[test]
fn ambiguous_document_reports_typed_code() {
let dir = TempDir::new().expect("tempdir");
write(
&dir,
"[project]\nname = \"x\"\n\n[ontology]\nsource = \"o.ttl\"\n\n[templates]\ndir = \"t\"\n\n[mcp]\nenabled = true\n",
);
let err = load(dir.path()).expect_err("must fail");
assert!(
err.to_string()
.contains(ggen_config::CONFIG_SCHEMA_AMBIGUOUS),
"{err}"
);
}
#[test]
fn unsupported_document_reports_typed_code() {
let dir = TempDir::new().expect("tempdir");
write(&dir, "[some_other_tool]\nkey = \"value\"\n");
let err = load(dir.path()).expect_err("must fail");
assert!(
err.to_string()
.contains(ggen_config::CONFIG_SCHEMA_UNSUPPORTED),
"{err}"
);
}
#[test]
fn malformed_document_reports_typed_code() {
let dir = TempDir::new().expect("tempdir");
write(&dir, "not [ valid toml");
let err = load(dir.path()).expect_err("must fail");
assert!(
err.to_string().contains(ggen_config::CONFIG_PARSE_FAILED),
"{err}"
);
}
#[test]
fn bare_project_version_with_full_frontmatter_shape_is_ambiguous_not_declarative_rules() {
let dir = TempDir::new().expect("tempdir");
write(
&dir,
"[project]\nname = \"x\"\nversion = \"1.0.0\"\n\n[ontology]\nsource = \"o.ttl\"\n\n[templates]\ndir = \"templates\"\n",
);
let err = load(dir.path()).expect_err("must fail");
assert!(
err.to_string()
.contains(ggen_config::CONFIG_SCHEMA_AMBIGUOUS),
"{err}"
);
}
}