use crate::bounded::ParseLimits;
use crate::{Document, schema};
pub(crate) fn digest_schema() -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"pattern": "^sha256:[0-9a-f]{64}$"
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EmittedSchema {
pub file: String,
pub bytes: Vec<u8>,
}
const COMMENT: &str = "Descriptive, derived from the types the bounded parser reads; the parser \
is the only authority on validity. Where the parser reads two versions of a document with one \
type, this schema admits what that type admits, including members the later version adds. \
Byte, depth and count bounds are recorded in x-rustev-parse-limits (a string length here counts \
characters, the parser counts bytes), and rules applied when a plan is compiled are stated in \
field descriptions, not enforced here.";
fn emit<T: schemars::JsonSchema + Document>(version: &str, part: Option<&str>) -> EmittedSchema {
emit_adjusted::<T>(version, part, |_| {})
}
fn emit_adjusted<T: schemars::JsonSchema + Document>(
version: &str,
part: Option<&str>,
adjust: impl FnOnce(&mut serde_json::Value),
) -> EmittedSchema {
let generator = schemars::generate::SchemaSettings::draft2020_12().into_generator();
let root = generator.into_root_schema_for::<T>();
let mut v = serde_json::to_value(&root).expect("a schema is JSON");
adjust(&mut v);
let title = match part {
Some(p) => format!("{version} {p}"),
None => version.to_string(),
};
v["title"] = title.clone().into();
v["$comment"] = COMMENT.into();
v.get_mut("properties")
.and_then(|p| p.as_object_mut())
.unwrap_or_else(|| panic!("{title}: the derived schema has no properties"))
.insert(
"schema".into(),
serde_json::json!({ "type": "string", "const": version }),
);
v["x-rustev-parse-limits"] = limits(&T::LIMITS);
let file = format!(
"{}{}.schema.json",
version.replace('/', "-"),
part.map(|p| format!(".{p}")).unwrap_or_default()
);
let bytes = crate::canonical::canonical_value_bytes(&v).expect("a schema has canonical bytes");
EmittedSchema { file, bytes }
}
const RUN_V2_MEMBERS: [&str; 2] = ["remote_evidence", "remote_evidence_truncated"];
fn run_members(version: &str) -> impl FnOnce(&mut serde_json::Value) + '_ {
move |v| {
if version != schema::RUN_V3 {
v["$defs"]["EvidenceRecord"]["properties"]
.as_object_mut()
.unwrap()
.remove("generation");
v["$defs"]["EvidenceRecord"]["properties"]
.as_object_mut()
.unwrap()
.remove("acquisitions");
v["$defs"]["StepEvidence"]["properties"]
.as_object_mut()
.unwrap()
.remove("constructed");
v["$defs"]["StepStatus"]["oneOf"]
.as_array_mut()
.unwrap()
.retain(|v| v["properties"].get("skipped").is_none());
v["properties"]
.as_object_mut()
.unwrap()
.remove("acquisitions");
v["$defs"]["RequestRecord"]["properties"]
.as_object_mut()
.unwrap()
.remove("screen");
v["$defs"]["RequestResult"]["oneOf"]
.as_array_mut()
.unwrap()
.retain(|variant| variant["const"] != "withdrawn");
prune_defs(v);
}
if version == schema::RUN_V2 {
v["required"]
.as_array_mut()
.expect("the run record has required members")
.push(RUN_V2_MEMBERS[0].into());
let list = &mut v["properties"][RUN_V2_MEMBERS[0]];
assert_eq!(
list["type"],
serde_json::json!(["array", "null"]),
"the remote evidence list is derived as an optional array"
);
list["type"] = "array".into();
return;
}
let props = v["properties"]
.as_object_mut()
.expect("the run record has properties");
for m in RUN_V2_MEMBERS {
props
.remove(m)
.unwrap_or_else(|| panic!("the run record has no {m} member"));
}
prune_defs(v);
}
}
fn plan_members(version: &str) -> impl FnOnce(&mut serde_json::Value) + '_ {
move |v| {
if version != schema::PLAN_V3 {
if let Some(variants) = v["$defs"]["PlanStepDetail"]["oneOf"].as_array_mut() {
variants.retain(|v| v["properties"].get("generate").is_none());
}
v["properties"]
.as_object_mut()
.unwrap()
.remove("acquisitions");
v["properties"]
.as_object_mut()
.expect("the plan has properties")
.remove("instantiation")
.expect("the plan has an instantiation member");
v["properties"]
.as_object_mut()
.unwrap()
.remove("screening")
.expect("the plan has a screening member");
prune_defs(v);
}
}
}
fn prune_defs(v: &mut serde_json::Value) {
fn refs(v: &serde_json::Value, out: &mut Vec<String>) {
match v {
serde_json::Value::Object(m) => {
if let Some(r) = m.get("$ref").and_then(|r| r.as_str())
&& let Some(name) = r.strip_prefix("#/$defs/")
{
out.push(name.to_string());
}
m.values().for_each(|x| refs(x, out));
}
serde_json::Value::Array(a) => a.iter().for_each(|x| refs(x, out)),
_ => {}
}
}
let Some(defs) = v.get("$defs").cloned() else {
return;
};
let mut root = v.clone();
root.as_object_mut().unwrap().remove("$defs");
let mut pending = Vec::new();
refs(&root, &mut pending);
let mut reached = std::collections::BTreeSet::new();
while let Some(name) = pending.pop() {
if reached.insert(name.clone())
&& let Some(d) = defs.get(&name)
{
refs(d, &mut pending);
}
}
let kept: serde_json::Map<String, serde_json::Value> = defs
.as_object()
.unwrap()
.iter()
.filter(|(k, _)| reached.contains(*k))
.map(|(k, d)| (k.clone(), d.clone()))
.collect();
v["$defs"] = kept.into();
}
fn limits(l: &ParseLimits) -> serde_json::Value {
serde_json::json!({
"max_bytes": l.max_bytes,
"max_depth": l.max_depth,
"max_string_bytes": l.max_string_bytes,
"max_collection_len": l.max_collection_len,
"max_total_values": l.max_total_values,
"max_number_bytes": l.max_number_bytes,
"allow_fractional_numbers": l.allow_fractional_numbers,
})
}
pub fn document_schemas() -> Vec<EmittedSchema> {
use crate::{
calibration::CalibrationArtifact, definition::Definition, descriptor::BackendDescriptor,
eval_report::EvalReport, evidence::EvidenceRecord, execution::ExecutionPolicy,
judgment::Judgment, output::BackendOutputDoc, plan::Plan, reason::RuntimeReasonDoc, remote,
replay::ReplayBundle, request::RequestDoc, run::RunRecord, snapshot::Snapshot,
source::SourceDescriptor,
};
use crate::{
dynamic::{DynamicCapability, DynamicOutcome, DynamicQuestion},
live::LiveReexecution,
redaction::{REDACTION, RedactionPolicy},
trace::{TRACE, TraceLine},
};
let mut out = vec![
emit::<crate::explanation::Explanation>(schema::EXPLANATION, None),
emit::<crate::generation_result::TextResult>(schema::TEXT_RESULT, None),
emit::<crate::generation::PromptTemplate>(schema::PROMPT_TEMPLATE, None),
emit::<crate::generation::GenerateRequest>(schema::GENERATE_REQUEST, None),
emit::<crate::extraction_documents::ExtractRules>(schema::EXTRACT_RULES, None),
emit::<crate::extraction_documents::ExtractRequest>(schema::EXTRACT_REQUEST, None),
emit::<crate::profile::Profile>(schema::PROFILE, None),
emit::<crate::plan_diff::PlanDiff>(schema::PLAN_DIFF, None),
emit::<Definition>(schema::DEFINITION, None),
emit::<Definition>(schema::DEFINITION_V2, None),
emit_adjusted::<Plan>(schema::PLAN, None, plan_members(schema::PLAN)),
emit_adjusted::<Plan>(schema::PLAN_V3, None, plan_members(schema::PLAN_V3)),
emit::<Snapshot>(schema::SNAPSHOT, None),
emit::<Snapshot>(schema::SNAPSHOT_V2, None),
emit_adjusted::<EvidenceRecord>(schema::EVIDENCE, None, |v| {
v["properties"].as_object_mut().unwrap().remove("screening");
v["properties"]
.as_object_mut()
.unwrap()
.remove("generation");
v["properties"]
.as_object_mut()
.unwrap()
.remove("acquisitions");
v["$defs"]["StepEvidence"]["properties"]
.as_object_mut()
.unwrap()
.remove("constructed");
v["$defs"]["StepStatus"]["oneOf"]
.as_array_mut()
.unwrap()
.retain(|v| v["properties"].get("skipped").is_none());
prune_defs(v);
}),
emit::<EvidenceRecord>(schema::EVIDENCE_V2, None),
emit_adjusted::<RunRecord>(schema::RUN, None, run_members(schema::RUN)),
emit_adjusted::<RunRecord>(schema::RUN_V2, None, run_members(schema::RUN_V2)),
emit_adjusted::<RunRecord>(schema::RUN_V3, None, run_members(schema::RUN_V3)),
emit::<Judgment>(schema::JUDGMENT, None),
emit::<Judgment>(schema::JUDGMENT_V2, None),
emit::<BackendDescriptor>(schema::BACKEND, None),
emit::<CalibrationArtifact>(schema::CALIBRATION, None),
emit::<BackendOutputDoc>(schema::BACKEND_OUTPUT, None),
emit::<EvalReport>(schema::EVAL_REPORT, None),
emit::<ExecutionPolicy>(schema::EXECUTION, None),
emit_adjusted::<ReplayBundle>(schema::REPLAY, None, |v| {
for key in ["sources", "acquisition_pages"] {
v["properties"].as_object_mut().unwrap().remove(key);
}
v["$defs"]["SupplyEntry"]["properties"]
.as_object_mut()
.unwrap()
.remove("failure_context");
prune_defs(v);
}),
emit::<ReplayBundle>(schema::REPLAY_V2, None),
emit::<RequestDoc>(schema::REQUEST, None),
emit::<RuntimeReasonDoc>(schema::RUNTIME_REASON, None),
emit::<crate::acquisition::AcquisitionRequest>(schema::ACQUISITION_REQUEST, None),
emit::<crate::acquisition::AcquisitionPage>(schema::ACQUISITION_PAGE, None),
emit::<SourceDescriptor>(schema::SOURCE, None),
emit::<remote::DescribeRequest>(schema::REMOTE_DESCRIBE, Some("request")),
emit::<remote::DescribeResponse>(schema::REMOTE_DESCRIBE, Some("response")),
emit::<remote::InferRequest>(schema::REMOTE_INFER, Some("request")),
emit::<remote::InferResponse>(schema::REMOTE_INFER, Some("response")),
emit::<remote::CancelRequest>(schema::REMOTE_CANCEL, Some("request")),
emit::<remote::CancelResponse>(schema::REMOTE_CANCEL, Some("response")),
emit::<remote::ExchangeRecord>(schema::REMOTE_EXCHANGE, None),
emit::<DynamicQuestion>(schema::DYNAMIC_QUESTION, None),
emit::<DynamicCapability>(schema::DYNAMIC_CAPABILITY, None),
emit::<DynamicOutcome>(schema::DYNAMIC_OUTCOME, None),
emit::<LiveReexecution>(schema::LIVE_REEXECUTION, None),
emit::<RedactionPolicy>(REDACTION, None),
emit::<TraceLine>(TRACE, None),
];
out.extend([
emit::<crate::remote_context::DescribeRequest>(
crate::remote_context::DESCRIBE,
Some("request"),
),
emit::<crate::remote_context::DescribeResponse>(
crate::remote_context::DESCRIBE,
Some("response"),
),
emit::<crate::remote_context::FetchRequest>(crate::remote_context::FETCH, Some("request")),
emit::<crate::remote_context::FetchResponse>(
crate::remote_context::FETCH,
Some("response"),
),
emit::<crate::remote_context::CancelRequest>(
crate::remote_context::CANCEL,
Some("request"),
),
emit::<crate::remote_context::CancelResponse>(
crate::remote_context::CANCEL,
Some("response"),
),
emit::<crate::remote_context::Invalidation>(crate::remote_context::INVALIDATION, None),
emit::<crate::remote_context::InvalidationsRequest>(
crate::remote_context::INVALIDATIONS,
Some("request"),
),
emit::<crate::remote_context::InvalidationsResponse>(
crate::remote_context::INVALIDATIONS,
Some("response"),
),
emit::<crate::remote_context::ExchangeRecord>(crate::remote_context::EXCHANGE, None),
emit::<crate::remote_sink::DescribeRequest>(crate::remote_sink::DESCRIBE, Some("request")),
emit::<crate::remote_sink::DescribeResponse>(
crate::remote_sink::DESCRIBE,
Some("response"),
),
emit::<crate::remote_sink::DeliverRequest>(crate::remote_sink::DELIVER, Some("request")),
emit::<crate::remote_sink::DeliverResponse>(crate::remote_sink::DELIVER, Some("response")),
emit::<crate::serve::Serve>(crate::serve::Serve::SCHEMA, None),
emit::<crate::serve::ServeConfig>(crate::serve::ServeConfig::SCHEMA, None),
emit::<crate::serve::ServeService>(crate::serve::ServeService::SCHEMA, None),
emit::<crate::serve::ServeRegister>(crate::serve::ServeRegister::SCHEMA, None),
emit::<crate::serve::ServeDecide>(crate::serve::ServeDecide::SCHEMA, None),
emit::<crate::serve::ServeDecision>(crate::serve::ServeDecision::SCHEMA, None),
emit::<crate::serve::ServeDecisionState>(crate::serve::ServeDecisionState::SCHEMA, None),
emit::<crate::serve::ServeCancel>(crate::serve::ServeCancel::SCHEMA, None),
emit::<crate::serve::ServeEvent>(crate::serve::ServeEvent::SCHEMA, None),
emit::<crate::serve::ServeRefusal>(crate::serve::ServeRefusal::SCHEMA, None),
emit::<crate::serve::ServeReplay>(crate::serve::ServeReplay::SCHEMA, None),
emit::<crate::serve::ServeEvaluate>(crate::serve::ServeEvaluate::SCHEMA, None),
emit::<crate::serve::ServeReadiness>(crate::serve::ServeReadiness::SCHEMA, None),
emit::<crate::serve::ServeShutdown>(crate::serve::ServeShutdown::SCHEMA, None),
emit::<crate::serve::ServeInvalidations>(crate::serve::ServeInvalidations::SCHEMA, None),
emit::<crate::serve::ServeInvalidationsAck>(
crate::serve::ServeInvalidationsAck::SCHEMA,
None,
),
emit::<crate::plan_bundle::PlanBundle>(crate::plan_bundle::PlanBundle::SCHEMA, None),
emit::<crate::plan_bundle::PlanBundleSignature>(
crate::plan_bundle::PlanBundleSignature::SCHEMA,
None,
),
emit::<crate::plan_bundle::PlanRegistration>(
crate::plan_bundle::PlanRegistration::SCHEMA,
None,
),
]);
out.sort_by(|a, b| a.file.cmp(&b.file));
out
}