use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
use serde::Deserialize;
use serde_json::Value;
use crate::core::errors::McpifyError;
#[derive(Debug, Deserialize)]
struct OperationSchemas {
#[serde(rename = "inputSchema")]
input_schema: Value,
#[serde(rename = "outputSchema")]
output_schema: Value,
}
fn schemas_zst_for(api_version: &str) -> Option<&'static [u8]> {
match api_version {
"gh-2026-03-10" => Some(include_bytes!("generated_schemas.json.zst")),
"ghec-2026-03-10" => Some(include_bytes!("generated_schemas_vghec-2026-03-10.json.zst")),
"ghes-3.21" => Some(include_bytes!("generated_schemas_vghes-3.21.json.zst")),
"ghes-3.20" => Some(include_bytes!("generated_schemas_vghes-3.20.json.zst")),
"ghes-3.19" => Some(include_bytes!("generated_schemas_vghes-3.19.json.zst")),
"ghes-3.18" => Some(include_bytes!("generated_schemas_vghes-3.18.json.zst")),
"ghes-3.17" => Some(include_bytes!("generated_schemas_vghes-3.17.json.zst")),
"ghes-3.16" => Some(include_bytes!("generated_schemas_vghes-3.16.json.zst")),
"ghes-3.15" => Some(include_bytes!("generated_schemas_vghes-3.15.json.zst")),
"ghes-3.14" => Some(include_bytes!("generated_schemas_vghes-3.14.json.zst")),
"ghes-3.13" => Some(include_bytes!("generated_schemas_vghes-3.13.json.zst")),
"ghes-3.12" => Some(include_bytes!("generated_schemas_vghes-3.12.json.zst")),
"ghes-3.11" => Some(include_bytes!("generated_schemas_vghes-3.11.json.zst")),
"ghes-3.10" => Some(include_bytes!("generated_schemas_vghes-3.10.json.zst")),
"ghes-3.9" => Some(include_bytes!("generated_schemas_vghes-3.9.json.zst")),
"ghes-3.8" => Some(include_bytes!("generated_schemas_vghes-3.8.json.zst")),
"ghes-3.7" => Some(include_bytes!("generated_schemas_vghes-3.7.json.zst")),
"ghes-3.6" => Some(include_bytes!("generated_schemas_vghes-3.6.json.zst")),
"ghes-3.5" => Some(include_bytes!("generated_schemas_vghes-3.5.json.zst")),
"ghes-3.4" => Some(include_bytes!("generated_schemas_vghes-3.4.json.zst")),
"ghes-3.3" => Some(include_bytes!("generated_schemas_vghes-3.3.json.zst")),
"ghes-3.2" => Some(include_bytes!("generated_schemas_vghes-3.2.json.zst")),
"ghes-3.1" => Some(include_bytes!("generated_schemas_vghes-3.1.json.zst")),
"ghes-3.0" => Some(include_bytes!("generated_schemas_vghes-3.0.json.zst")),
"ghes-2.22" => Some(include_bytes!("generated_schemas_vghes-2.22.json.zst")),
"ghes-2.21" => Some(include_bytes!("generated_schemas_vghes-2.21.json.zst")),
"ghes-2.20" => Some(include_bytes!("generated_schemas_vghes-2.20.json.zst")),
"ghes-2.19" => Some(include_bytes!("generated_schemas_vghes-2.19.json.zst")),
"ghes-2.18" => Some(include_bytes!("generated_schemas_vghes-2.18.json.zst")),
_ => None,
}
}
fn schemas_by_operation(api_version: &str) -> &'static HashMap<String, OperationSchemas> {
static SCHEMAS: OnceLock<Mutex<HashMap<String, &'static HashMap<String, OperationSchemas>>>> =
OnceLock::new();
static EMPTY: OnceLock<HashMap<String, OperationSchemas>> = OnceLock::new();
let empty = EMPTY.get_or_init(HashMap::new);
let cache = SCHEMAS.get_or_init(|| Mutex::new(HashMap::new()));
let mut cache = cache.lock().unwrap();
if let Some(schemas) = cache.get(api_version) {
return schemas;
}
let Some(compressed) = schemas_zst_for(api_version) else {
return empty;
};
let parsed: HashMap<String, OperationSchemas> = zstd::decode_all(compressed)
.ok()
.and_then(|json| serde_json::from_slice(&json).ok())
.unwrap_or_default();
let leaked: &'static HashMap<String, OperationSchemas> = Box::leak(Box::new(parsed));
cache.insert(api_version.to_string(), leaked);
leaked
}
fn validate_against(
cache: &Mutex<HashMap<String, jsonschema::Validator>>,
operation_id: &str,
schema: &Value,
data: &Value,
) -> Result<(), Vec<String>> {
let mut cache = cache.lock().unwrap();
let validator = cache.entry(operation_id.to_string()).or_insert_with(|| {
jsonschema::validator_for(schema).unwrap_or_else(|_| {
jsonschema::validator_for(&Value::Object(Default::default())).unwrap()
})
});
if validator.is_valid(data) {
return Ok(());
}
let errors = validator
.iter_errors(data)
.map(|error| error.to_string())
.collect::<Vec<_>>();
Err(errors)
}
pub fn validate_input(
api_version: &str,
operation_id: &str,
data: &Value,
) -> Result<(), McpifyError> {
static CACHE: OnceLock<Mutex<HashMap<String, jsonschema::Validator>>> = OnceLock::new();
let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
let empty = Value::Object(Default::default());
let schema = schemas_by_operation(api_version)
.get(operation_id)
.map(|schemas| &schemas.input_schema)
.unwrap_or(&empty);
let cache_key = format!("{api_version} {operation_id}");
validate_against(cache, &cache_key, schema, data).map_err(|errors| McpifyError::Validation {
message: format!("invalid input for '{operation_id}'"),
details: Some(serde_json::json!(errors)),
})
}
pub fn validate_output(
api_version: &str,
operation_id: &str,
data: &Value,
) -> Result<(), McpifyError> {
static CACHE: OnceLock<Mutex<HashMap<String, jsonschema::Validator>>> = OnceLock::new();
let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
let empty = Value::Object(Default::default());
let schema = schemas_by_operation(api_version)
.get(operation_id)
.map(|schemas| &schemas.output_schema)
.unwrap_or(&empty);
let cache_key = format!("{api_version} {operation_id}");
validate_against(cache, &cache_key, schema, data).map_err(|errors| McpifyError::Validation {
message: format!("unexpected response shape for '{operation_id}'"),
details: Some(serde_json::json!(errors)),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_unknown_operation_id_falls_back_to_an_always_valid_schema() {
assert!(
validate_input(
"gh-2026-03-10",
"__unknown_operation__",
&serde_json::json!({"anything": true})
)
.is_ok()
);
assert!(
validate_output(
"gh-2026-03-10",
"__unknown_operation__",
&serde_json::json!(42)
)
.is_ok()
);
}
#[test]
fn an_unknown_api_version_also_falls_back_to_an_always_valid_schema() {
assert!(
validate_input(
"__unknown_version__",
"__unknown_operation__",
&serde_json::json!({"anything": true})
)
.is_ok()
);
}
#[test]
fn validate_against_reports_every_schema_violation() {
static CACHE: OnceLock<Mutex<HashMap<String, jsonschema::Validator>>> = OnceLock::new();
let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
let schema = serde_json::json!({
"type": "object",
"required": ["name"],
"properties": { "name": { "type": "string" } },
});
let ok = validate_against(
cache,
"test_op_ok",
&schema,
&serde_json::json!({"name": "widget"}),
);
assert!(ok.is_ok());
let err = validate_against(cache, "test_op_missing", &schema, &serde_json::json!({}));
assert!(err.is_err());
}
}