use std::sync::OnceLock;
use crate::error::{
OlError, OL_4210_SCHEMA_MISMATCH, OL_4320_PROVIDER_SCHEMA_INVALID, OL_4321_TOOL_SCHEMA_INVALID,
};
const MANIFEST_SCHEMA: &str = include_str!("../../schemas/manifest.schema.json");
const MANIFEST_EDITOR: &str = include_str!("../../schemas/manifest-editor.schema.json");
const MANIFEST_TOOL: &str = include_str!("../../schemas/manifest-tool.schema.json");
const MANIFEST_PROVIDER: &str = include_str!("../../schemas/manifest-provider.schema.json");
const MANIFEST_BINDING: &str = include_str!("../../schemas/manifest-binding.schema.json");
const MANIFEST_CAPABILITY: &str = include_str!("../../schemas/manifest-capability.schema.json");
const MANIFEST_PROCESS: &str = include_str!("../../schemas/manifest-process.schema.json");
const MANIFEST_TOOL_V2: &str = include_str!("../../schemas/manifest-tool-v2.schema.json");
const MANIFEST_PROVIDER_V2: &str = include_str!("../../schemas/manifest-provider-v2.schema.json");
const ENUMS: &str = include_str!("../../schemas/enums.schema.json");
fn validator() -> &'static jsonschema::Validator {
static V: OnceLock<jsonschema::Validator> = OnceLock::new();
V.get_or_init(|| build_validator().expect("manifest schema must compile"))
}
fn build_validator() -> Result<jsonschema::Validator, jsonschema::ValidationError<'static>> {
let resources = [
(
"https://schemas.openlatch.ai/provider/v1/manifest-editor.schema.json",
MANIFEST_EDITOR,
),
(
"https://schemas.openlatch.ai/provider/v1/manifest-tool.schema.json",
MANIFEST_TOOL,
),
(
"https://schemas.openlatch.ai/provider/v1/manifest-provider.schema.json",
MANIFEST_PROVIDER,
),
(
"https://schemas.openlatch.ai/provider/v1/manifest-binding.schema.json",
MANIFEST_BINDING,
),
(
"https://schemas.openlatch.ai/provider/v1/manifest-capability.schema.json",
MANIFEST_CAPABILITY,
),
(
"https://schemas.openlatch.ai/provider/v1/manifest-process.schema.json",
MANIFEST_PROCESS,
),
(
"https://schemas.openlatch.ai/client/v1/enums.schema.json",
ENUMS,
),
];
let parsed: serde_json::Value =
serde_json::from_str(MANIFEST_SCHEMA).expect("manifest.schema.json must parse");
let pairs: Vec<(String, serde_json::Value)> = resources
.iter()
.map(|(uri, raw)| {
let value: serde_json::Value =
serde_json::from_str(raw).expect("vendored manifest sub-schema must parse");
((*uri).to_string(), value)
})
.collect();
let registry = jsonschema::Registry::new()
.extend(pairs)
.expect("registry resource URIs must parse")
.prepare()
.expect("registry must prepare");
jsonschema::options()
.with_registry(®istry)
.build(&parsed)
}
pub fn validate(value: &serde_json::Value) -> Result<(), OlError> {
if let Err(err) = validator().validate(value) {
return Err(OlError::new(
OL_4210_SCHEMA_MISMATCH,
format!("schema mismatch at `{}`: {}", err.instance_path(), err),
));
}
Ok(())
}
fn build_v2_validator(
root_schema: &str,
) -> Result<jsonschema::Validator, jsonschema::ValidationError<'static>> {
let resources = [
(
"https://schemas.openlatch.ai/provider/v1/manifest-editor.schema.json",
MANIFEST_EDITOR,
),
(
"https://schemas.openlatch.ai/provider/v2/manifest-editor.schema.json",
MANIFEST_EDITOR,
),
(
"https://schemas.openlatch.ai/provider/v1/manifest-tool.schema.json",
MANIFEST_TOOL,
),
(
"https://schemas.openlatch.ai/provider/v2/manifest-tool.schema.json",
MANIFEST_TOOL,
),
(
"https://schemas.openlatch.ai/provider/v1/manifest-provider.schema.json",
MANIFEST_PROVIDER,
),
(
"https://schemas.openlatch.ai/provider/v2/manifest-provider.schema.json",
MANIFEST_PROVIDER,
),
(
"https://schemas.openlatch.ai/provider/v1/manifest-binding.schema.json",
MANIFEST_BINDING,
),
(
"https://schemas.openlatch.ai/provider/v1/manifest-capability.schema.json",
MANIFEST_CAPABILITY,
),
(
"https://schemas.openlatch.ai/provider/v2/manifest-capability.schema.json",
MANIFEST_CAPABILITY,
),
(
"https://schemas.openlatch.ai/provider/v1/manifest-process.schema.json",
MANIFEST_PROCESS,
),
(
"https://schemas.openlatch.ai/client/v1/enums.schema.json",
ENUMS,
),
];
let parsed: serde_json::Value =
serde_json::from_str(root_schema).expect("v2 schema must parse");
let pairs: Vec<(String, serde_json::Value)> = resources
.iter()
.map(|(uri, raw)| {
let value: serde_json::Value =
serde_json::from_str(raw).expect("v1 sub-schema must parse");
((*uri).to_string(), value)
})
.collect();
let registry = jsonschema::Registry::new()
.extend(pairs)
.expect("registry resource URIs must parse")
.prepare()
.expect("registry must prepare");
jsonschema::options()
.with_registry(®istry)
.build(&parsed)
}
fn provider_v2_validator() -> &'static jsonschema::Validator {
static V: OnceLock<jsonschema::Validator> = OnceLock::new();
V.get_or_init(|| {
build_v2_validator(MANIFEST_PROVIDER_V2).expect("provider v2 schema must compile")
})
}
fn tool_v2_validator() -> &'static jsonschema::Validator {
static V: OnceLock<jsonschema::Validator> = OnceLock::new();
V.get_or_init(|| build_v2_validator(MANIFEST_TOOL_V2).expect("tool v2 schema must compile"))
}
pub fn validate_provider_v2(value: &serde_json::Value) -> Result<(), OlError> {
if let Err(err) = provider_v2_validator().validate(value) {
return Err(OlError::new(
OL_4320_PROVIDER_SCHEMA_INVALID,
format!(
"provider v2 schema mismatch at `{}`: {}",
err.instance_path(),
err
),
));
}
Ok(())
}
pub fn validate_tool_v2(value: &serde_json::Value) -> Result<(), OlError> {
if let Err(err) = tool_v2_validator().validate(value) {
return Err(OlError::new(
OL_4321_TOOL_SCHEMA_INVALID,
format!(
"tool v2 schema mismatch at `{}`: {}",
err.instance_path(),
err
),
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_object_fails_required_fields() {
let v = serde_json::json!({});
let err = validate(&v).unwrap_err();
assert_eq!(err.code.code, "OL-4210");
}
#[test]
fn minimal_valid_manifest_accepted() {
let v = serde_json::json!({
"schema_version": 1,
"editor": {
"slug": "acme",
"display_name": "Acme Security",
"description": "x"
}
});
validate(&v).unwrap();
}
}