Skip to main content

turnframe_core/
schema.rs

1//! JSON Schema helpers.
2//!
3//! Model-facing structures derive [`schemars::JsonSchema`]; the runtime records
4//! a [`SchemaFingerprint`] of the interpreter schema in every replay record so a
5//! turn can be reproduced against the exact schema that was in force.
6
7use std::fmt;
8
9use schemars::{JsonSchema, Schema, SchemaGenerator};
10use serde::{Deserialize, Serialize};
11
12use crate::hash::{Digest, HashError, canonical_digest};
13
14/// Generates the root JSON Schema for `T` with the library's default settings.
15#[must_use]
16pub fn schema_for<T: JsonSchema>() -> Schema {
17    SchemaGenerator::default().into_root_schema_for::<T>()
18}
19
20/// BLAKE3 digest of the canonical JSON rendering of a schema.
21#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
22#[serde(transparent)]
23pub struct SchemaFingerprint(pub Digest);
24
25impl SchemaFingerprint {
26    /// Fingerprints the schema of `T`.
27    pub fn of<T: JsonSchema>() -> Result<Self, HashError> {
28        Self::of_schema(&schema_for::<T>())
29    }
30
31    /// Fingerprints an already generated schema.
32    pub fn of_schema(schema: &Schema) -> Result<Self, HashError> {
33        canonical_digest(schema).map(Self)
34    }
35
36    /// Borrows the hexadecimal digest.
37    #[must_use]
38    pub fn as_str(&self) -> &str {
39        self.0.as_str()
40    }
41}
42
43impl fmt::Display for SchemaFingerprint {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        fmt::Display::fmt(&self.0, f)
46    }
47}
48
49/// Validates `instance` against `schema`, reporting paths only.
50///
51/// # Errors
52///
53/// [`SchemaCheckError`](crate::error::SchemaCheckError) when the schema does not
54/// compile or the instance violates it.
55pub fn validate_against(
56    schema: &schemars::Schema,
57    instance: &serde_json::Value,
58) -> Result<(), crate::error::SchemaCheckError> {
59    let validator = jsonschema::validator_for(schema.as_value())
60        .map_err(|_| crate::error::SchemaCheckError::InvalidSchema)?;
61    validator.validate(instance).map_err(|error| {
62        crate::error::SchemaCheckError::Violation(crate::error::SchemaValidationError {
63            instance_path: error.instance_path().to_string(),
64            schema_path: error.schema_path().to_string(),
65        })
66    })
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[derive(JsonSchema)]
74    #[allow(dead_code)]
75    struct A {
76        x: u32,
77    }
78
79    #[derive(JsonSchema)]
80    #[allow(dead_code)]
81    struct B {
82        x: String,
83    }
84
85    #[test]
86    fn fingerprint_distinguishes_schemas_and_is_stable() {
87        let a1 = SchemaFingerprint::of::<A>().unwrap();
88        let a2 = SchemaFingerprint::of::<A>().unwrap();
89        let b = SchemaFingerprint::of::<B>().unwrap();
90        assert_eq!(a1, a2);
91        assert_ne!(a1, b);
92    }
93}