use schemars::{JsonSchema, schema_for};
use crate::model::wire_envelope::WireEnvelope;
use serde_json::{Value, json};
use thiserror::Error;
use crate::model::batch::BatchPayload;
use crate::model::cache::{
CacheListPayload, CacheMutationPayload, CacheShowPayload, CacheStatsPayload,
};
use crate::model::check_payload::CheckPayload;
use crate::model::sibling_drift::SiblingDoctorReport;
use crate::model::summary::SummaryRow;
use crate::suites::SuitePlan;
pub use crate::envelope_version::{
CURRENT_SCHEMA_VERSION, MIN_EMBEDDED_VERSION, embedded_versions,
};
const SCHEMA_V1_FROZEN: &str = include_str!("schema_v1_snapshot.json");
const SCHEMA_V3_FROZEN: &str = include_str!("schema_v3_snapshot.json");
const SCHEMA_V5_FROZEN: &str = include_str!("schema_v5_snapshot.json");
const SCHEMA_V2_FROZEN: &str = include_str!("schema_v2_snapshot.json");
const SCHEMA_V7_FROZEN: &str = include_str!("schema_v7_snapshot.json");
const SCHEMA_V8_FROZEN: &str = include_str!("schema_v8_snapshot.json");
const SCHEMA_V9_FROZEN: &str = include_str!("schema_v9_snapshot.json");
const SCHEMA_V10_FROZEN: &str = include_str!("schema_v10_snapshot.json");
const SCHEMA_V11_FROZEN: &str = include_str!("schema_v11_snapshot.json");
const SCHEMA_V12_FROZEN: &str = include_str!("schema_v12_snapshot.json");
const SCHEMA_V13_FROZEN: &str = include_str!("schema_v13_snapshot.json");
const SCHEMA_V14_FROZEN: &str = include_str!("schema_v14_snapshot.json");
#[derive(Debug, Error)]
pub enum SchemaError {
#[error("no schema embedded for version {requested}; known versions: {known:?}")]
UnknownVersion { requested: u32, known: Vec<u32> },
}
pub fn rendered_schema(version: u32) -> Result<String, SchemaError> {
let value = schema_value_for(version)?;
let mut bytes = serde_json::to_vec_pretty(&value).expect("schemars output serialises");
bytes.push(b'\n');
Ok(String::from_utf8(bytes).expect("pretty json is utf-8"))
}
pub fn schema_value_for(version: u32) -> Result<Value, SchemaError> {
match version {
1 => Ok(serde_json::from_str(SCHEMA_V1_FROZEN).expect("frozen v1 snapshot is valid JSON")),
2 => Ok(serde_json::from_str(SCHEMA_V2_FROZEN).expect("frozen v2 snapshot is valid JSON")),
3 => Ok(serde_json::from_str(SCHEMA_V3_FROZEN).expect("frozen v3 snapshot is valid JSON")),
4 => Ok(combined_v4()),
5 => Ok(serde_json::from_str(SCHEMA_V5_FROZEN).expect("frozen v5 snapshot is valid JSON")),
6 => Ok(combined_v6()),
7 => Ok(serde_json::from_str(SCHEMA_V7_FROZEN).expect("frozen v7 snapshot is valid JSON")),
8 => Ok(serde_json::from_str(SCHEMA_V8_FROZEN).expect("frozen v8 snapshot is valid JSON")),
9 => Ok(serde_json::from_str(SCHEMA_V9_FROZEN).expect("frozen v9 snapshot is valid JSON")),
10 => {
Ok(serde_json::from_str(SCHEMA_V10_FROZEN).expect("frozen v10 snapshot is valid JSON"))
}
11 => {
Ok(serde_json::from_str(SCHEMA_V11_FROZEN).expect("frozen v11 snapshot is valid JSON"))
}
12 => {
Ok(serde_json::from_str(SCHEMA_V12_FROZEN).expect("frozen v12 snapshot is valid JSON"))
}
13 => {
Ok(serde_json::from_str(SCHEMA_V13_FROZEN).expect("frozen v13 snapshot is valid JSON"))
}
14 => {
Ok(serde_json::from_str(SCHEMA_V14_FROZEN).expect("frozen v14 snapshot is valid JSON"))
}
15 => Ok(combined_v15()),
other => Err(SchemaError::UnknownVersion {
requested: other,
known: embedded_versions(),
}),
}
}
fn combined_v4() -> Value {
let mut v3: Value =
serde_json::from_str(SCHEMA_V3_FROZEN).expect("frozen v3 snapshot is valid JSON");
if let Some(obj) = v3.as_object_mut() {
obj.insert("schema_version".into(), json!(4));
obj.insert("title".into(), json!("backhopper envelope v4"));
obj.insert(
"description".into(),
json!(
"v4 wires the path-translation pipeline: `Reason::PathRename` is emitted for \
translated paths and `InapplicableReason::PathsMissingOnTarget` now carries \
`Vec<RelativePath>` instead of `Vec<PathBuf>`. Reflected in the \
SeriesEvaluation payload."
),
);
}
v3
}
fn combined_v6() -> Value {
let mut v5: Value =
serde_json::from_str(SCHEMA_V5_FROZEN).expect("frozen v5 snapshot is valid JSON");
if let Some(obj) = v5.as_object_mut() {
obj.insert("schema_version".into(), json!(6));
obj.insert("title".into(), json!("backhopper envelope v6"));
obj.insert(
"description".into(),
json!(
"v6: operator-facing SHA inputs accept 7-to-40 hex prefixes (resolved through \
gix before any analyser sees them), new typed `GitError::AmbiguousSha` and \
`GitError::NotACommit` variants surface through the error path, the \
`rev resolve` companion verb expands a prefix to a full SHA, and \
`BisectPayload.commit` plus `MultiPayload.commit` carry typed `CommitSha` \
rather than raw strings."
),
);
}
v5
}
fn combined_v15() -> Value {
combined_live(
15,
"v15 adds `ExportedTypeUndefinedOnTarget` to `Reason`: an `-export_type` entry \
a patch adds naming a type the target version of the same module does not \
declare. An `erlc` error rather than the dialyzer-level concern `MissingType` \
reports, but non-blocking like the other target-tree axes, and carried on \
`target_findings` so an inapplicable pin cannot drop it.",
)
}
fn combined_live(version: u32, description: &str) -> Value {
let series = envelope_with_payload::<CheckPayload>(version, "check", description);
let summary_row =
serde_json::to_value(schema_for!(SummaryRow)).expect("SummaryRow schema serialises");
let batch_payload =
serde_json::to_value(schema_for!(BatchPayload)).expect("BatchPayload schema serialises");
let siblings_doctor_payload = serde_json::to_value(schema_for!(SiblingDoctorReport))
.expect("SiblingDoctorReport schema serialises");
let cache_stats_payload = serde_json::to_value(schema_for!(CacheStatsPayload))
.expect("CacheStatsPayload schema serialises");
let cache_list_payload = serde_json::to_value(schema_for!(CacheListPayload))
.expect("CacheListPayload schema serialises");
let cache_show_payload = serde_json::to_value(schema_for!(CacheShowPayload))
.expect("CacheShowPayload schema serialises");
let cache_mutation_payload = serde_json::to_value(schema_for!(CacheMutationPayload))
.expect("CacheMutationPayload schema serialises");
let suite_plan_payload =
serde_json::to_value(schema_for!(SuitePlan)).expect("SuitePlan schema serialises");
let Value::Object(mut obj) = series else {
unreachable!("envelope_with_payload always returns a Value::Object")
};
obj.insert("summary_row".into(), summary_row);
obj.insert("batch_payload".into(), batch_payload);
obj.insert("siblings_doctor_payload".into(), siblings_doctor_payload);
obj.insert("cache_stats_payload".into(), cache_stats_payload);
obj.insert("cache_list_payload".into(), cache_list_payload);
obj.insert("cache_show_payload".into(), cache_show_payload);
obj.insert("cache_mutation_payload".into(), cache_mutation_payload);
obj.insert("suite_plan_payload".into(), suite_plan_payload);
obj.insert("schema_version".into(), json!(version));
obj.insert(
"title".into(),
json!(format!("backhopper envelope v{version}")),
);
Value::Object(obj)
}
fn envelope_with_payload<T: JsonSchema>(
version: u32,
command_family: &str,
description: &str,
) -> Value {
let envelope =
serde_json::to_value(schema_for!(WireEnvelope<T>)).expect("envelope schema serialises");
json!({
"$schema": "https://json-schema.org/draft-07/schema",
"title": format!("backhopper envelope v{version}"),
"schema_version": version,
"command_family": command_family,
"description": description,
"envelope": envelope,
})
}