#[path = "property/schema_value.rs"]
pub mod schema_value;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{LazyLock, Mutex};
use proptest::prelude::*;
use serde_json::Value;
use schema_value::{Defs, defs_of, value_for_def};
const CASES: u32 = 256;
fn spec_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.ancestors()
.nth(2)
.map_or_else(PathBuf::new, |root| {
root.join("crates/protocol-droid-schema/spec")
})
}
fn schema_for(revision: &str) -> Value {
let path = spec_dir().join(revision).join("schema.json");
std::fs::read_to_string(&path)
.ok()
.and_then(|text| serde_json::from_str(&text).ok())
.unwrap_or(Value::Null)
}
fn defs_for(revision: &str) -> Defs {
defs_of(&schema_for(revision))
}
fn values(revision: &str, definition: &str) -> BoxedStrategy<Value> {
let defs = defs_for(revision);
value_for_def(definition, &defs).unwrap_or_else(|| {
fail(&format!(
"{revision}: the case list names a definition the vendored \
schema does not have ({definition}); grep spec/{revision}/schema.json \
before adding a name"
))
})
}
static VALIDATORS: LazyLock<Mutex<HashMap<String, jsonschema::Validator>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
fn validator_for(revision: &str, definition: &str) -> jsonschema::Validator {
let key = format!("{revision}/{definition}");
if let Some(cached) = VALIDATORS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(&key)
{
return cached.clone();
}
let schema = schema_for(revision);
let container = if schema.get("$defs").is_some() {
"$defs"
} else {
"definitions"
};
if !defs_for(revision).contains_key(definition) {
fail(&format!("{revision}: no definition named {definition}"));
}
let mut document = schema;
let Some(root) = document.as_object_mut() else {
fail(&format!(
"{revision}: the vendored schema is not a JSON object"
));
};
root.insert(
"$ref".to_owned(),
Value::String(format!("#/{container}/{definition}")),
);
let compiled = jsonschema::validator_for(&document)
.unwrap_or_else(|err| fail(&format!("{key}: the document will not compile: {err}")));
VALIDATORS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(key, compiled.clone());
compiled
}
fn fail(message: &str) -> ! {
eprintln!("property test failed: {message}");
std::process::exit(1)
}
#[track_caller]
fn assert_round_trips<T>(revision: &str, definition: &str, value: &Value)
where
T: for<'de> serde::Deserialize<'de> + serde::Serialize,
{
let validator = validator_for(revision, definition);
if let Err(errors) = validator.validate(value) {
fail(&format!(
"{revision}/{definition}: the value generator produced something \
the schema rejects\n value: {value:#}\n error: {errors:#}"
));
}
let Ok(decoded) = serde_json::from_value::<T>(value.clone()) else {
let reason = decode_error::<T>(value);
fail(&format!(
"{revision}/{definition}: a schema-valid value failed to decode: \
{reason}\n value: {value:#}"
));
};
let Ok(re_encoded) = serde_json::to_value(&decoded) else {
let reason = serde_json::to_value(&decoded)
.err()
.map_or_else(|| "unknown".to_owned(), |err| err.to_string());
fail(&format!(
"{revision}/{definition}: a decoded value failed to encode: {reason}"
));
};
if let Err(errors) = validator.validate(&re_encoded) {
fail(&format!(
"{revision}/{definition}: re-encoding produced a value the schema \
rejects\n before: {value:#}\n after: {re_encoded:#}\n \
error: {errors:#}"
));
}
if re_encoded != *value {
fail(&format!(
"{revision}/{definition}: the round trip changed the value\n \
before: {value:#}\n after: {re_encoded:#}"
));
}
}
fn decode_error<T: for<'de> serde::Deserialize<'de>>(value: &Value) -> String {
serde_json::from_value::<T>(value.clone())
.err()
.map_or_else(|| "unknown".to_owned(), |err| err.to_string())
}
macro_rules! round_trips {
($($revision:literal / $definition:literal as $test:ident : $ty:ty),* $(,)?) => {$(
proptest! {
#![proptest_config(proptest::test_runner::Config {
cases: CASES,
failure_persistence: None,
..proptest::test_runner::Config::default()
})]
#[test]
fn $test(value in values($revision, $definition)) {
assert_round_trips::<$ty>($revision, $definition, &value);
}
}
)*};
}
round_trips! {
"2024-11-05" / "JSONRPCMessage" as jsonrpc_message_2024_11_05
: protocol_droid_schema::wire::v2024_11_05::JSONRPCMessage,
"2024-11-05" / "JSONRPCRequest" as jsonrpc_request_2024_11_05
: protocol_droid_schema::wire::v2024_11_05::JSONRPCRequest,
"2024-11-05" / "JSONRPCNotification" as jsonrpc_notification_2024_11_05
: protocol_droid_schema::wire::v2024_11_05::JSONRPCNotification,
"2024-11-05" / "JSONRPCResponse" as jsonrpc_response_2024_11_05
: protocol_droid_schema::wire::v2024_11_05::JSONRPCResponse,
"2024-11-05" / "JSONRPCError" as jsonrpc_error_2024_11_05
: protocol_droid_schema::wire::v2024_11_05::JSONRPCError,
"2024-11-05" / "CallToolRequest" as call_tool_request_2024_11_05
: protocol_droid_schema::wire::v2024_11_05::CallToolRequest,
"2024-11-05" / "CallToolResult" as call_tool_result_2024_11_05
: protocol_droid_schema::wire::v2024_11_05::CallToolResult,
"2024-11-05" / "Tool" as tool_2024_11_05
: protocol_droid_schema::wire::v2024_11_05::Tool,
"2024-11-05" / "Prompt" as prompt_2024_11_05
: protocol_droid_schema::wire::v2024_11_05::Prompt,
"2024-11-05" / "PromptMessage" as prompt_message_2024_11_05
: protocol_droid_schema::wire::v2024_11_05::PromptMessage,
"2024-11-05" / "ResourceContents" as resource_contents_2024_11_05
: protocol_droid_schema::wire::v2024_11_05::ResourceContents,
"2024-11-05" / "TextContent" as text_content_2024_11_05
: protocol_droid_schema::wire::v2024_11_05::TextContent,
"2024-11-05" / "ImageContent" as image_content_2024_11_05
: protocol_droid_schema::wire::v2024_11_05::ImageContent,
"2024-11-05" / "EmbeddedResource" as embedded_resource_2024_11_05
: protocol_droid_schema::wire::v2024_11_05::EmbeddedResource,
"2024-11-05" / "ListResourcesResult" as list_resources_result_2024_11_05
: protocol_droid_schema::wire::v2024_11_05::ListResourcesResult,
"2024-11-05" / "ListPromptsResult" as list_prompts_result_2024_11_05
: protocol_droid_schema::wire::v2024_11_05::ListPromptsResult,
"2024-11-05" / "ReadResourceResult" as read_resource_result_2024_11_05
: protocol_droid_schema::wire::v2024_11_05::ReadResourceResult,
"2024-11-05" / "LoggingLevel" as logging_level_2024_11_05
: protocol_droid_schema::wire::v2024_11_05::LoggingLevel,
"2024-11-05" / "ModelPreferences" as model_preferences_2024_11_05
: protocol_droid_schema::wire::v2024_11_05::ModelPreferences,
"2024-11-05" / "ClientCapabilities" as client_capabilities_2024_11_05
: protocol_droid_schema::wire::v2024_11_05::ClientCapabilities,
"2024-11-05" / "ServerCapabilities" as server_capabilities_2024_11_05
: protocol_droid_schema::wire::v2024_11_05::ServerCapabilities,
"2024-11-05" / "SamplingMessage" as sampling_message_2024_11_05
: protocol_droid_schema::wire::v2024_11_05::SamplingMessage,
"2024-11-05" / "CompleteRequest" as complete_request_2024_11_05
: protocol_droid_schema::wire::v2024_11_05::CompleteRequest,
"2025-03-26" / "CallToolResult" as call_tool_result_2025_03_26
: protocol_droid_schema::wire::v2025_03_26::CallToolResult,
"2025-03-26" / "ToolAnnotations" as tool_annotations_2025_03_26
: protocol_droid_schema::wire::v2025_03_26::ToolAnnotations,
"2025-03-26" / "AudioContent" as audio_content_2025_03_26
: protocol_droid_schema::wire::v2025_03_26::AudioContent,
"2025-03-26" / "ModelHint" as model_hint_2025_03_26
: protocol_droid_schema::wire::v2025_03_26::ModelHint,
"2025-03-26" / "ModelPreferences" as model_preferences_2025_03_26
: protocol_droid_schema::wire::v2025_03_26::ModelPreferences,
"2025-03-26" / "CompleteResult" as complete_result_2025_03_26
: protocol_droid_schema::wire::v2025_03_26::CompleteResult,
"2025-06-18" / "ContentBlock" as content_block_2025_06_18
: protocol_droid_schema::wire::v2025_06_18::ContentBlock,
"2025-06-18" / "CallToolResult" as call_tool_result_2025_06_18
: protocol_droid_schema::wire::v2025_06_18::CallToolResult,
"2025-06-18" / "ToolAnnotations" as tool_annotations_2025_06_18
: protocol_droid_schema::wire::v2025_06_18::ToolAnnotations,
"2025-06-18" / "ListRootsResult" as list_roots_result_2025_06_18
: protocol_droid_schema::wire::v2025_06_18::ListRootsResult,
"2025-06-18" / "Root" as root_2025_06_18
: protocol_droid_schema::wire::v2025_06_18::Root,
"2025-06-18" / "CreateMessageResult" as create_message_result_2025_06_18
: protocol_droid_schema::wire::v2025_06_18::CreateMessageResult,
"2025-11-25" / "ContentBlock" as content_block_2025_11_25
: protocol_droid_schema::wire::v2025_11_25::ContentBlock,
"2025-11-25" / "CallToolResult" as call_tool_result_2025_11_25
: protocol_droid_schema::wire::v2025_11_25::CallToolResult,
"2025-11-25" / "ElicitRequest" as elicit_request_2025_11_25
: protocol_droid_schema::wire::v2025_11_25::ElicitRequest,
"2025-11-25" / "ElicitResult" as elicit_result_2025_11_25
: protocol_droid_schema::wire::v2025_11_25::ElicitResult,
"2025-11-25" / "ElicitRequestFormParams" as elicit_form_2025_11_25
: protocol_droid_schema::wire::v2025_11_25::ElicitRequestFormParams,
"2025-11-25" / "CancelTaskRequest" as cancel_task_request_2025_11_25
: protocol_droid_schema::wire::v2025_11_25::CancelTaskRequest,
"2025-11-25" / "Icon" as icon_2025_11_25
: protocol_droid_schema::wire::v2025_11_25::Icon,
"2025-11-25" / "ClientCapabilities" as client_capabilities_2025_11_25
: protocol_droid_schema::wire::v2025_11_25::ClientCapabilities,
"2026-07-28" / "JSONValue" as json_value_2026_07_28
: protocol_droid_schema::wire::v2026_07_28::JSONValue,
"2026-07-28" / "JSONObject" as json_object_2026_07_28
: protocol_droid_schema::wire::v2026_07_28::JSONObject,
"2026-07-28" / "JSONArray" as json_array_2026_07_28
: protocol_droid_schema::wire::v2026_07_28::JSONArray,
"2026-07-28" / "JSONRPCMessage" as jsonrpc_message_2026_07_28
: protocol_droid_schema::wire::v2026_07_28::JSONRPCMessage,
"2026-07-28" / "ContentBlock" as content_block_2026_07_28
: protocol_droid_schema::wire::v2026_07_28::ContentBlock,
"2026-07-28" / "ToolAnnotations" as tool_annotations_2026_07_28
: protocol_droid_schema::wire::v2026_07_28::ToolAnnotations,
"2026-07-28" / "ToolChoice" as tool_choice_2026_07_28
: protocol_droid_schema::wire::v2026_07_28::ToolChoice,
"2026-07-28" / "ToolResultContent" as tool_result_content_2026_07_28
: protocol_droid_schema::wire::v2026_07_28::ToolResultContent,
"2026-07-28" / "ElicitRequest" as elicit_request_2026_07_28
: protocol_droid_schema::wire::v2026_07_28::ElicitRequest,
"2026-07-28" / "Icons" as icons_2026_07_28
: protocol_droid_schema::wire::v2026_07_28::Icons,
"2026-07-28" / "SubscriptionsListenResult" as listen_result_2026_07_28
: protocol_droid_schema::wire::v2026_07_28::SubscriptionsListenResult,
"draft" / "JSONValue" as json_value_draft
: protocol_droid_schema::wire::draft::JSONValue,
"draft" / "JSONRPCMessage" as jsonrpc_message_draft
: protocol_droid_schema::wire::draft::JSONRPCMessage,
"draft" / "ContentBlock" as content_block_draft
: protocol_droid_schema::wire::draft::ContentBlock,
"draft" / "CallToolResult" as call_tool_result_draft
: protocol_droid_schema::wire::draft::CallToolResult,
"draft" / "ToolChoice" as tool_choice_draft
: protocol_droid_schema::wire::draft::ToolChoice,
"draft" / "Icon" as icon_draft
: protocol_droid_schema::wire::draft::Icon,
"draft" / "ElicitRequestFormParams" as elicit_form_draft
: protocol_droid_schema::wire::draft::ElicitRequestFormParams,
"draft" / "ServerCapabilities" as server_capabilities_draft
: protocol_droid_schema::wire::draft::ServerCapabilities,
}