Skip to main content

greentic_types/
schema_id.rs

1//! Schema identifiers and reference sources built on canonical CBOR.
2use alloc::{format, string::String};
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6use crate::{cbor::canonical, cbor_bytes::CborBytes};
7
8const SCHEMA_ID_PREFIX: &str = "schema:v1:";
9
10/// Stable identifier derived from canonical schema CBOR.
11#[derive(Clone, Debug, PartialEq, Eq)]
12#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
13pub struct SchemaId(String);
14
15impl SchemaId {
16    /// Parse an existing `schema:v1:` identifier.
17    pub fn parse(value: &str) -> Result<Self, SchemaIdError> {
18        if !value.starts_with(SCHEMA_ID_PREFIX) {
19            return Err(SchemaIdError::InvalidPrefix);
20        }
21        let encoded = &value[SCHEMA_ID_PREFIX.len()..];
22        canonical::decode_base32_crockford(encoded)?;
23        Ok(Self(value.to_owned()))
24    }
25
26    /// Serialize as `schema:v1:<base32>` string.
27    pub fn as_str(&self) -> &str {
28        &self.0
29    }
30}
31
32impl core::fmt::Display for SchemaId {
33    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
34        f.write_str(&self.0)
35    }
36}
37
38/// Compute the canonical schema ID for the provided CBOR bytes.
39pub fn schema_id_for_cbor(schema_cbor: &[u8]) -> Result<SchemaId, SchemaIdError> {
40    canonical::ensure_canonical(schema_cbor)?;
41    let digest = canonical::blake3_128(schema_cbor);
42    let encoded = canonical::encode_base32_crockford(&digest);
43    Ok(SchemaId(format!("{SCHEMA_ID_PREFIX}{encoded}")))
44}
45
46/// Errors emitted while parsing or deriving schema IDs.
47#[derive(Debug, Error)]
48pub enum SchemaIdError {
49    /// Identifier does not have the required `schema:v1:` prefix.
50    #[error("schema ID must begin with {SCHEMA_ID_PREFIX}")]
51    InvalidPrefix,
52    /// Payload part of the ID is not valid Crockford Base32.
53    #[error("invalid base32 payload: {0}")]
54    Base32(#[from] canonical::Base32Error),
55    /// Canonical enforcement failed while deriving the ID.
56    #[error(transparent)]
57    Canonical(#[from] canonical::CanonicalError),
58}
59
60/// Schema references used for both runtime I/O and QA answers.
61#[derive(Clone, Debug, PartialEq, Eq)]
62#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
63pub enum SchemaSource {
64    /// Refer to a schema by its canonical ID.
65    CborSchemaId(SchemaId),
66    /// Embed canonical CBOR schema bytes inline.
67    InlineCbor(CborBytes),
68    /// Embed JSON schema bytes for debugging (feature gated).
69    #[cfg(feature = "json-compat")]
70    InlineJson(String),
71    /// Refer to a schema stored in another pack.
72    RefPackPath(String),
73    /// Refer to a schema hosted at an arbitrary URI.
74    RefUri(String),
75}
76
77/// QA schema references reuse the same set of sources today.
78pub type QaSchemaSource = SchemaSource;
79/// Invoke-time schemas are currently the same as QA schemas.
80pub type IoSchemaSource = SchemaSource;
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn schema_id_roundtrip() {
88        let schema = canon_schema();
89        let id = match schema_id_for_cbor(&schema) {
90            Ok(value) => value,
91            Err(err) => panic!("id generation failed: {err:?}"),
92        };
93        let parsed = match SchemaId::parse(id.as_str()) {
94            Ok(value) => value,
95            Err(err) => panic!("parse failed: {err:?}"),
96        };
97        assert_eq!(parsed.as_str(), id.as_str());
98    }
99
100    fn canon_schema() -> Vec<u8> {
101        match canonical::to_canonical_cbor(&"schema") {
102            Ok(bytes) => bytes,
103            Err(err) => panic!("canonicalize schema failed: {err:?}"),
104        }
105    }
106}