Skip to main content

synapse_application/
schema.rs

1use 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
12/// One compiled parameter or result schema bound to a canonical operation.
13pub 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    /// Returns the stable schema identity.
42    #[must_use]
43    pub fn schema_id(&self) -> &SchemaId {
44        &self.schema_id
45    }
46
47    /// Returns the normalized result family, when this is a result contract.
48    #[must_use]
49    pub fn family(&self) -> Option<&str> {
50        self.family.as_deref()
51    }
52
53    /// Returns the closed Draft 2020-12 JSON Schema.
54    #[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;