use std::sync::OnceLock;
use anyhow::{anyhow, Result};
use jsonschema::{Draft, JSONSchema};
use serde_json::Value;
use crate::canonical::Envelope;
macro_rules! compiled {
($name:literal) => {{
static CELL: OnceLock<JSONSchema> = OnceLock::new();
CELL.get_or_init(|| {
let raw = include_str!(concat!("../schemas/", $name));
let json: Value = serde_json::from_str(raw)
.unwrap_or_else(|e| panic!("schema {}: invalid JSON: {e}", $name));
JSONSchema::options()
.with_draft(Draft::Draft202012)
.compile(&json)
.unwrap_or_else(|e| panic!("schema {}: compile failed: {e}", $name))
})
}};
}
fn schema_for(capturer: &str) -> Option<&'static JSONSchema> {
Some(match capturer {
"deps.pip-audit" => compiled!("capture-deps-pip-audit.schema.json"),
"deps.pnpm-audit" => compiled!("capture-deps-pnpm-audit.schema.json"),
"deps.cargo-audit" => compiled!("capture-deps-cargo-audit.schema.json"),
"deps.osv-query" => compiled!("capture-deps-osv-query.schema.json"),
"github.dependabot-alerts" => {
compiled!("capture-github-dependabot-alerts.schema.json")
}
"github.branch-protection" => {
compiled!("capture-github-branch-protection.schema.json")
}
"github.org-members" => compiled!("capture-github-org-members.schema.json"),
"github.audit-log" => compiled!("capture-github-audit-log.schema.json"),
"github.codeql-alerts" => compiled!("capture-github-codeql-alerts.schema.json"),
_ => return None,
})
}
pub fn validate(envelope: &Envelope) -> Result<Vec<String>> {
let value = serde_json::to_value(envelope)?;
let schema = schema_for(&envelope.capturer)
.ok_or_else(|| anyhow!("no schema registered for capturer `{}`", envelope.capturer))?;
let errs = match schema.validate(&value) {
Ok(()) => Vec::new(),
Err(it) => it.map(|e| format!("{}: {}", e.instance_path, e)).collect(),
};
Ok(errs)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn pip_audit_schema_accepts_minimal_envelope() {
let env = Envelope::new(
"deps.pip-audit",
"1",
json!({ "path": "/x" }),
json!({ "dependencies": [] }),
);
assert!(validate(&env).unwrap().is_empty());
}
#[test]
fn pip_audit_schema_rejects_wrong_capturer_const() {
let mut env = Envelope::new(
"deps.pip-audit",
"1",
json!({ "path": "/x" }),
json!({ "dependencies": [] }),
);
env.capturer = "deps.pip-audit".into();
env.captured_at = "not-a-timestamp".into();
let errs = validate(&env).unwrap();
assert!(!errs.is_empty(), "expected captured_at pattern to fail");
}
#[test]
fn dependabot_alerts_schema_accepts_minimal_envelope() {
let env = Envelope::new(
"github.dependabot-alerts",
"1",
json!({ "repo": "o/r", "state": "all" }),
json!({ "alerts": [{"number": 1, "state": "open"}] }),
);
assert!(validate(&env).unwrap().is_empty());
}
#[test]
fn unknown_capturer_returns_err() {
let env = Envelope::new("made.up", "1", json!({}), json!({}));
assert!(validate(&env).is_err());
}
}