synapse_application/
schema.rs1use std::collections::BTreeMap;
2
3use jsonschema::Validator;
4use serde::Deserialize;
5use serde_json::Value;
6use soma_ops::{OperationName, OperationSpec, SchemaId};
7
8use crate::{
9 CompatibilityError, DiagnosticProjection, LegacyOperationBinding, catalog::contract_error,
10};
11
12pub struct OperationSchemaContract {
14 schema_id: SchemaId,
15 family: Option<String>,
16 schema: Value,
17 validator: Validator,
18}
19
20impl OperationSchemaContract {
21 pub(crate) fn new(
22 artifact: &'static str,
23 schema_id: SchemaId,
24 family: Option<String>,
25 schema: Value,
26 ) -> Result<Self, CompatibilityError> {
27 let validator = jsonschema::validator_for(&schema).map_err(|error| {
28 CompatibilityError::EmbeddedContract {
29 artifact,
30 message: format!("schema {schema_id} failed to compile: {error}"),
31 }
32 })?;
33 Ok(Self {
34 schema_id,
35 family,
36 schema,
37 validator,
38 })
39 }
40
41 #[must_use]
43 pub fn schema_id(&self) -> &SchemaId {
44 &self.schema_id
45 }
46
47 #[must_use]
49 pub fn family(&self) -> Option<&str> {
50 self.family.as_deref()
51 }
52
53 #[must_use]
55 pub fn schema(&self) -> &Value {
56 &self.schema
57 }
58
59 pub(crate) fn validate(
60 &self,
61 operation: &OperationName,
62 kind: &'static str,
63 value: &Value,
64 ) -> Result<(), CompatibilityError> {
65 let details = self
66 .validator
67 .iter_errors(value)
68 .map(|error| format!("{}: {error}", error.instance_path()))
69 .collect::<Vec<_>>();
70 if details.is_empty() {
71 Ok(())
72 } else {
73 Err(CompatibilityError::SchemaValidation {
74 operation: operation.clone(),
75 kind,
76 details: details.join("; "),
77 })
78 }
79 }
80}
81
82pub(crate) fn build_parameter_schemas(
83 bundle: ParameterBundle,
84 operations: &BTreeMap<OperationName, OperationSpec>,
85) -> Result<BTreeMap<OperationName, OperationSchemaContract>, CompatibilityError> {
86 let mut schemas = BTreeMap::new();
87 for record in bundle.schemas {
88 let operation = OperationName::new(record.operation_name.clone()).map_err(|error| {
89 CompatibilityError::EmbeddedContract {
90 artifact: "synapse-operation-parameters.json",
91 message: format!("{}: {error}", record.operation_name),
92 }
93 })?;
94 let spec = operations
95 .get(&operation)
96 .ok_or_else(|| CompatibilityError::UnknownOperation(operation.clone()))?;
97 if &record.schema_id != spec.parameter_schema() {
98 return contract_error(
99 "synapse-operation-parameters.json",
100 &format!("schema identity drift for {operation}"),
101 );
102 }
103 let contract = OperationSchemaContract::new(
104 "synapse-operation-parameters.json",
105 record.schema_id,
106 None,
107 record.schema,
108 )?;
109 if schemas.insert(operation, contract).is_some() {
110 return contract_error(
111 "synapse-operation-parameters.json",
112 "duplicate operation schema",
113 );
114 }
115 }
116 if schemas.len() != operations.len() {
117 return contract_error(
118 "synapse-operation-parameters.json",
119 "parameter schema coverage mismatch",
120 );
121 }
122 Ok(schemas)
123}
124
125pub(crate) fn build_result_schemas(
126 bundle: ResultBundle,
127 operations: &BTreeMap<OperationName, OperationSpec>,
128) -> Result<BTreeMap<OperationName, OperationSchemaContract>, CompatibilityError> {
129 let mut schemas = BTreeMap::new();
130 for record in bundle.schemas {
131 let operation = OperationName::new(record.operation_name.clone()).map_err(|error| {
132 CompatibilityError::EmbeddedContract {
133 artifact: "synapse-operation-results.json",
134 message: format!("{}: {error}", record.operation_name),
135 }
136 })?;
137 let spec = operations
138 .get(&operation)
139 .ok_or_else(|| CompatibilityError::UnknownOperation(operation.clone()))?;
140 if &record.schema_id != spec.result_schema() {
141 return contract_error(
142 "synapse-operation-results.json",
143 &format!("schema identity drift for {operation}"),
144 );
145 }
146 let contract = OperationSchemaContract::new(
147 "synapse-operation-results.json",
148 record.schema_id,
149 Some(record.family),
150 record.schema,
151 )?;
152 if schemas.insert(operation, contract).is_some() {
153 return contract_error(
154 "synapse-operation-results.json",
155 "duplicate operation schema",
156 );
157 }
158 }
159 if schemas.len() != operations.len() {
160 return contract_error(
161 "synapse-operation-results.json",
162 "result schema coverage mismatch",
163 );
164 }
165 Ok(schemas)
166}
167
168#[derive(Deserialize)]
169pub(crate) struct LegacyBundle {
170 pub(crate) operations: Vec<LegacyOperationBinding>,
171}
172
173#[derive(Deserialize)]
174pub(crate) struct CanonicalBundle {
175 pub(crate) classification_sha256: String,
176 pub(crate) operations: Vec<OperationSpec>,
177}
178
179#[derive(Deserialize)]
180pub(crate) struct DiagnosticBundle {
181 pub(crate) classification_sha256: String,
182 pub(crate) mappings: Vec<DiagnosticProjection>,
183}
184
185#[derive(Deserialize)]
186pub(crate) struct ParameterBundle {
187 pub(crate) classification_sha256: String,
188 pub(crate) schemas: Vec<ParameterRecord>,
189}
190
191#[derive(Deserialize)]
192pub(crate) struct ParameterRecord {
193 operation_name: String,
194 schema_id: SchemaId,
195 schema: Value,
196}
197
198#[derive(Deserialize)]
199pub(crate) struct ResultBundle {
200 pub(crate) classification_sha256: String,
201 pub(crate) schemas: Vec<ResultRecord>,
202}
203
204#[derive(Deserialize)]
205pub(crate) struct ResultRecord {
206 operation_name: String,
207 schema_id: SchemaId,
208 family: String,
209 schema: Value,
210}
211
212#[cfg(test)]
213#[path = "schema_tests.rs"]
214mod tests;