Skip to main content

type_bridge_contract/
schema_lowering.rs

1//! Validated identity and fingerprint primitives for schema-lowering profiles.
2
3use std::error::Error;
4use std::fmt;
5
6use serde::de::Error as _;
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8
9use crate::codec::from_canonical_json;
10use crate::diagnostic::{Diagnostic, DiagnosticCategory};
11use crate::fingerprint::{
12    CanonicalizationVersion, Fingerprint, FingerprintDomain, SemanticProfileId,
13};
14
15/// The only schema-lowering profile admitted by this V2 contract revision.
16pub const TYPEDB_3_12_1_SCHEMA_LOWERING_PROFILE_ID: &str = "typedb-3.12.1-schema-lowering/v1";
17/// Fingerprint domain for schema-lowering profile documents.
18pub const SCHEMA_LOWERING_PROFILE_FINGERPRINT_DOMAIN: &str = "typebridge.schema.lowering-profile";
19/// Canonicalization version for schema-lowering profile documents.
20pub const SCHEMA_LOWERING_PROFILE_CANONICALIZATION: &str = "typebridge.schema-lowering-profile/v1";
21
22/// Validation failure for a schema-lowering profile identity or fingerprint.
23#[derive(Clone, Debug, Eq, PartialEq)]
24pub struct SchemaLoweringProfileValidationError {
25    message: String,
26}
27
28impl SchemaLoweringProfileValidationError {
29    fn new(message: impl Into<String>) -> Self {
30        Self {
31            message: message.into(),
32        }
33    }
34}
35
36impl fmt::Display for SchemaLoweringProfileValidationError {
37    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
38        formatter.write_str(&self.message)
39    }
40}
41
42impl Error for SchemaLoweringProfileValidationError {}
43
44/// A validated schema-lowering profile identifier.
45#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
46pub struct SchemaLoweringProfileId(String);
47
48impl SchemaLoweringProfileId {
49    /// Validates the fixed V2 schema-lowering profile identifier.
50    pub fn new(value: impl Into<String>) -> Result<Self, SchemaLoweringProfileValidationError> {
51        let value = value.into();
52        SemanticProfileId::new(value.clone()).map_err(|error| {
53            SchemaLoweringProfileValidationError::new(format!(
54                "invalid schema-lowering profile id: {error}"
55            ))
56        })?;
57        if value != TYPEDB_3_12_1_SCHEMA_LOWERING_PROFILE_ID {
58            return Err(SchemaLoweringProfileValidationError::new(format!(
59                "unsupported schema-lowering profile id: {value}"
60            )));
61        }
62        Ok(Self(value))
63    }
64
65    /// Returns the fixed TypeDB 3.12.1 profile identifier.
66    pub fn typedb_3_12_1() -> Self {
67        Self::new(TYPEDB_3_12_1_SCHEMA_LOWERING_PROFILE_ID)
68            .expect("the fixed schema-lowering profile id is valid")
69    }
70
71    /// Returns the wire spelling.
72    pub fn as_str(&self) -> &str {
73        &self.0
74    }
75}
76
77impl fmt::Display for SchemaLoweringProfileId {
78    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
79        formatter.write_str(self.as_str())
80    }
81}
82
83impl Serialize for SchemaLoweringProfileId {
84    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
85    where
86        S: Serializer,
87    {
88        serializer.serialize_str(self.as_str())
89    }
90}
91
92impl<'de> Deserialize<'de> for SchemaLoweringProfileId {
93    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
94    where
95        D: Deserializer<'de>,
96    {
97        let value = String::deserialize(deserializer)?;
98        Self::new(value).map_err(D::Error::custom)
99    }
100}
101
102/// A fingerprint whose metadata is pinned to the schema-lowering domain.
103#[derive(Clone, Debug, Eq, PartialEq)]
104pub struct SchemaLoweringProfileFingerprint(Fingerprint);
105
106impl SchemaLoweringProfileFingerprint {
107    /// Fingerprints canonical schema-lowering profile bytes with the fixed metadata.
108    pub fn compute(canonical_bytes: &[u8]) -> Self {
109        let domain = FingerprintDomain::new(SCHEMA_LOWERING_PROFILE_FINGERPRINT_DOMAIN)
110            .expect("the fixed schema-lowering fingerprint domain is valid");
111        let canonicalization =
112            CanonicalizationVersion::new(SCHEMA_LOWERING_PROFILE_CANONICALIZATION)
113                .expect("the fixed schema-lowering canonicalization is valid");
114        let profile = SemanticProfileId::new(TYPEDB_3_12_1_SCHEMA_LOWERING_PROFILE_ID)
115            .expect("the fixed schema-lowering profile id is a valid semantic profile id");
116        Self(Fingerprint::compute(
117            domain,
118            canonicalization,
119            Some(profile),
120            canonical_bytes,
121        ))
122    }
123
124    /// Borrows the generic contract fingerprint.
125    pub fn as_fingerprint(&self) -> &Fingerprint {
126        &self.0
127    }
128
129    fn validate_metadata(
130        fingerprint: &Fingerprint,
131    ) -> Result<(), SchemaLoweringProfileValidationError> {
132        let value = serde_json::to_value(fingerprint).map_err(|error| {
133            SchemaLoweringProfileValidationError::new(format!(
134                "cannot inspect schema-lowering fingerprint: {error}"
135            ))
136        })?;
137        let expected = [
138            ("domain", SCHEMA_LOWERING_PROFILE_FINGERPRINT_DOMAIN),
139            ("canonicalization", SCHEMA_LOWERING_PROFILE_CANONICALIZATION),
140            ("semantic_profile", TYPEDB_3_12_1_SCHEMA_LOWERING_PROFILE_ID),
141        ];
142        for (field, expected_value) in expected {
143            if value.get(field).and_then(serde_json::Value::as_str) != Some(expected_value) {
144                return Err(SchemaLoweringProfileValidationError::new(format!(
145                    "schema-lowering fingerprint has invalid {field}"
146                )));
147            }
148        }
149        Ok(())
150    }
151}
152
153impl Serialize for SchemaLoweringProfileFingerprint {
154    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
155    where
156        S: Serializer,
157    {
158        self.0.serialize(serializer)
159    }
160}
161
162impl<'de> Deserialize<'de> for SchemaLoweringProfileFingerprint {
163    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
164    where
165        D: Deserializer<'de>,
166    {
167        let fingerprint = Fingerprint::deserialize(deserializer)?;
168        Self::validate_metadata(&fingerprint).map_err(D::Error::custom)?;
169        Ok(Self(fingerprint))
170    }
171}
172
173/// A registry-resolved schema-lowering profile and exact content fingerprint.
174#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
175pub struct SchemaLoweringProfileBinding {
176    fingerprint: SchemaLoweringProfileFingerprint,
177    id: SchemaLoweringProfileId,
178}
179
180impl SchemaLoweringProfileBinding {
181    /// Bind the fixed profile identity to a complete canonical registry document.
182    ///
183    /// This validates canonical JSON and requires the document's top-level `id`
184    /// to equal the frozen profile identity before computing its content fingerprint.
185    pub fn from_canonical_profile_bytes(
186        canonical_profile_bytes: &[u8],
187    ) -> Result<Self, Diagnostic> {
188        let value: serde_json::Value = from_canonical_json(canonical_profile_bytes)?;
189        if value.get("id").and_then(serde_json::Value::as_str)
190            != Some(TYPEDB_3_12_1_SCHEMA_LOWERING_PROFILE_ID)
191        {
192            return Err(Diagnostic::stable(
193                DiagnosticCategory::InvalidContract,
194                "schema_lowering_profile_id_mismatch",
195                "canonical schema-lowering profile bytes do not carry the frozen profile id",
196            ));
197        }
198        Ok(Self {
199            fingerprint: SchemaLoweringProfileFingerprint::compute(canonical_profile_bytes),
200            id: SchemaLoweringProfileId::typedb_3_12_1(),
201        })
202    }
203
204    /// Return the exact lowering-profile identity.
205    pub const fn id(&self) -> &SchemaLoweringProfileId {
206        &self.id
207    }
208
209    /// Return the registry-owned lowering-profile content fingerprint.
210    pub const fn fingerprint(&self) -> &SchemaLoweringProfileFingerprint {
211        &self.fingerprint
212    }
213}