use std::fmt::Write;
use std::path::Path;
use crate::validate::ValidationError;
pub fn validate_against_schemas(
schema_dir: &Path,
file_paths: &[impl AsRef<Path>],
) -> Result<Vec<ValidationError>, crate::Error> {
let layers = discover_layer_schemas(schema_dir)?;
if layers.is_empty() {
return Ok(Vec::new());
}
let root_schema = build_root_schema(&layers);
let schema_doc = uppsala::parse(&root_schema).map_err(|e| {
crate::Error::Validation(format!("synthetic root schema parse failed: {e}"))
})?;
let virtual_root = schema_dir.join("_clayers_root.xsd");
let validator = uppsala::XsdValidator::from_schema_with_base_path(&schema_doc, Some(&virtual_root))
.map_err(|e| crate::Error::Validation(format!("XSD validator build failed: {e}")))?;
let mut errors = Vec::new();
for file_path in file_paths {
let path = file_path.as_ref();
let content = std::fs::read_to_string(path)?;
let doc = match uppsala::parse(&content) {
Ok(d) => d,
Err(e) => {
errors.push(ValidationError {
message: format!("{}: uppsala parse: {e}", path.display()),
});
continue;
}
};
for ve in validator.validate(&doc) {
errors.push(ValidationError {
message: format!("{}: {ve}", path.display()),
});
}
}
Ok(errors)
}
struct LayerSchema {
namespace: String,
file_name: String,
}
fn discover_layer_schemas(schema_dir: &Path) -> Result<Vec<LayerSchema>, crate::Error> {
let mut layers = Vec::new();
for entry in std::fs::read_dir(schema_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().is_none_or(|ext| ext != "xsd") {
continue;
}
let file_name = match path.file_name().and_then(|n| n.to_str()) {
Some(n) => n.to_string(),
None => continue,
};
let content = std::fs::read_to_string(&path)?;
let mut xot = xot::Xot::new();
let Ok(doc) = xot.parse(&content) else {
continue;
};
let Ok(root) = xot.document_element(doc) else {
continue;
};
let target_ns_attr = xot.add_name("targetNamespace");
let Some(ns) = xot.get_attribute(root, target_ns_attr) else {
continue;
};
layers.push(LayerSchema {
namespace: ns.to_string(),
file_name,
});
}
layers.sort_by(|a, b| a.file_name.cmp(&b.file_name));
Ok(layers)
}
fn build_root_schema(layers: &[LayerSchema]) -> String {
let mut s = String::from(
r#"<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="urn:clayers:internal:validation-root"
elementFormDefault="qualified"
version="1.1">
"#,
);
for layer in layers {
writeln!(
s,
" <xs:import namespace=\"{}\" schemaLocation=\"{}\"/>",
layer.namespace, layer.file_name
)
.expect("write to String");
}
s.push_str("</xs:schema>\n");
s
}
#[cfg(test)]
fn target_namespace_of(path: &Path) -> Option<String> {
let content = std::fs::read_to_string(path).ok()?;
let mut xot = xot::Xot::new();
let doc = xot.parse(&content).ok()?;
let root = xot.document_element(doc).ok()?;
let attr = xot.add_name("targetNamespace");
xot.get_attribute(root, attr).map(ToString::to_string)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn schemas_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../schemas")
.canonicalize()
.expect("resolve schemas/")
}
fn self_spec_files() -> Vec<PathBuf> {
let spec = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../clayers/clayers")
.canonicalize()
.expect("resolve clayers/clayers/");
crate::discovery::discover_spec_files(&spec.join("index.xml")).expect("discover")
}
#[test]
fn discovers_every_shipped_layer_schema() {
let layers = discover_layer_schemas(&schemas_dir()).expect("discover");
assert!(
layers.len() >= 17,
"expected 17+ layer schemas, got {}",
layers.len()
);
let ns: Vec<&str> = layers.iter().map(|l| l.namespace.as_str()).collect();
for required in &[
"urn:clayers:spec",
"urn:clayers:prose",
"urn:clayers:terminology",
"urn:clayers:artifact",
"urn:clayers:relation",
] {
assert!(ns.contains(required), "missing namespace {required}");
}
}
#[test]
fn root_schema_contains_imports_for_every_layer() {
let layers = discover_layer_schemas(&schemas_dir()).expect("discover");
let root = build_root_schema(&layers);
for layer in &layers {
let needle = format!(
"<xs:import namespace=\"{}\" schemaLocation=\"{}\"/>",
layer.namespace, layer.file_name
);
assert!(root.contains(&needle), "missing import for {needle}");
}
}
#[test]
fn validates_self_spec_returns_findings() {
let errs = validate_against_schemas(&schemas_dir(), &self_spec_files())
.expect("validation runs");
eprintln!("uppsala self-spec findings: {}", errs.len());
for e in errs.iter().take(5) {
eprintln!(" {}", e.message);
}
}
#[test]
fn target_ns_helper_reads_existing_schema() {
let p = schemas_dir().join("spec.xsd");
assert_eq!(
target_namespace_of(&p).as_deref(),
Some("urn:clayers:spec")
);
}
}