use schemars::{JsonSchema, schema_for};
use crate::model::wire_envelope::WireEnvelope;
use serde_json::{Value, json};
use thiserror::Error;
use crate::jigsaw::{JigsawCheckPayload, JigsawPlanPayload};
use crate::model::batch::BatchPayload;
use crate::model::cache::{
CacheListPayload, CacheMutationPayload, CacheShowPayload, CacheStatsPayload,
};
use crate::model::check_payload::CheckPayload;
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");
const SCHEMA_V15_FROZEN: &str = include_str!("schema_v15_snapshot.json");
const SCHEMA_V16_FROZEN: &str = include_str!("schema_v16_snapshot.json");
const SCHEMA_V17_FROZEN: &str = include_str!("schema_v17_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"))
}
enum Embedded {
Frozen(&'static str),
Derived(fn() -> Value),
}
const EMBEDDED: &[(u32, Embedded)] = &[
(1, Embedded::Frozen(SCHEMA_V1_FROZEN)),
(2, Embedded::Frozen(SCHEMA_V2_FROZEN)),
(3, Embedded::Frozen(SCHEMA_V3_FROZEN)),
(4, Embedded::Derived(combined_v4)),
(5, Embedded::Frozen(SCHEMA_V5_FROZEN)),
(6, Embedded::Derived(combined_v6)),
(7, Embedded::Frozen(SCHEMA_V7_FROZEN)),
(8, Embedded::Frozen(SCHEMA_V8_FROZEN)),
(9, Embedded::Frozen(SCHEMA_V9_FROZEN)),
(10, Embedded::Frozen(SCHEMA_V10_FROZEN)),
(11, Embedded::Frozen(SCHEMA_V11_FROZEN)),
(12, Embedded::Frozen(SCHEMA_V12_FROZEN)),
(13, Embedded::Frozen(SCHEMA_V13_FROZEN)),
(14, Embedded::Frozen(SCHEMA_V14_FROZEN)),
(15, Embedded::Frozen(SCHEMA_V15_FROZEN)),
(16, Embedded::Frozen(SCHEMA_V16_FROZEN)),
(17, Embedded::Frozen(SCHEMA_V17_FROZEN)),
(18, Embedded::Derived(combined_v18)),
];
const _: () = {
assert!(
EMBEDDED.len() as u32 == CURRENT_SCHEMA_VERSION - MIN_EMBEDDED_VERSION + 1,
"EMBEDDED needs one row per version from MIN_EMBEDDED_VERSION to CURRENT_SCHEMA_VERSION"
);
let mut i = 0;
while i < EMBEDDED.len() {
assert!(
EMBEDDED[i].0 == MIN_EMBEDDED_VERSION + i as u32,
"EMBEDDED rows must run in ascending order from MIN_EMBEDDED_VERSION"
);
i += 1;
}
};
pub fn schema_value_for(version: u32) -> Result<Value, SchemaError> {
let row = version
.checked_sub(MIN_EMBEDDED_VERSION)
.and_then(|i| EMBEDDED.get(i as usize));
match row {
Some((_, Embedded::Frozen(text))) => {
Ok(serde_json::from_str(text).expect("frozen schema snapshot is valid JSON"))
}
Some((_, Embedded::Derived(f))) => Ok(f()),
None => Err(SchemaError::UnknownVersion {
requested: version,
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_v18() -> Value {
combined_live(
18,
"v18: the `jigsaw plan` and `jigsaw check` payloads (`jigsaw_plan_payload`, \
`jigsaw_check_payload`). No existing payload changed.",
)
}
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 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 jigsaw_plan_payload = serde_json::to_value(schema_for!(JigsawPlanPayload))
.expect("JigsawPlanPayload schema serialises");
let jigsaw_check_payload = serde_json::to_value(schema_for!(JigsawCheckPayload))
.expect("JigsawCheckPayload 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("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("jigsaw_plan_payload".into(), jigsaw_plan_payload);
obj.insert("jigsaw_check_payload".into(), jigsaw_check_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,
})
}