use std::{collections::BTreeMap, fmt};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SchemaDescriptor {
Primitive(&'static str),
Unit,
Option(Box<SchemaDescriptor>),
Seq(Box<SchemaDescriptor>),
Map(Box<SchemaDescriptor>, Box<SchemaDescriptor>),
Tuple(Vec<SchemaDescriptor>),
Struct {
name: &'static str,
fields: Vec<(&'static str, SchemaDescriptor)>,
},
Enum {
name: &'static str,
variants: Vec<(&'static str, SchemaDescriptor)>,
},
}
impl SchemaDescriptor {
#[must_use]
pub const fn primitive(name: &'static str) -> Self {
Self::Primitive(name)
}
#[must_use]
pub fn option(inner: Self) -> Self {
Self::Option(Box::new(inner))
}
#[must_use]
pub fn seq(inner: Self) -> Self {
Self::Seq(Box::new(inner))
}
#[must_use]
pub fn map(key: Self, value: Self) -> Self {
Self::Map(Box::new(key), Box::new(value))
}
#[must_use]
pub fn tuple(elements: Vec<Self>) -> Self {
Self::Tuple(elements)
}
#[must_use]
pub fn structure(name: &'static str, fields: Vec<(&'static str, Self)>) -> Self {
Self::Struct { name, fields }
}
#[must_use]
pub fn enumeration(name: &'static str, variants: Vec<(&'static str, Self)>) -> Self {
Self::Enum { name, variants }
}
#[must_use]
pub fn fingerprint(&self) -> SchemaFingerprint {
SchemaFingerprint::of(self)
}
const fn kind_name(&self) -> &'static str {
match self {
Self::Primitive(_) => "primitive",
Self::Unit => "unit",
Self::Option(_) => "option",
Self::Seq(_) => "seq",
Self::Map(..) => "map",
Self::Tuple(_) => "tuple",
Self::Struct { .. } => "struct",
Self::Enum { .. } => "enum",
}
}
fn canonical_encode(&self, out: &mut Vec<u8>) {
match self {
Self::Primitive(name) => {
out.push(0);
encode_str(name, out);
}
Self::Unit => out.push(1),
Self::Option(inner) => {
out.push(2);
inner.canonical_encode(out);
}
Self::Seq(inner) => {
out.push(3);
inner.canonical_encode(out);
}
Self::Map(key, value) => {
out.push(4);
key.canonical_encode(out);
value.canonical_encode(out);
}
Self::Tuple(elements) => {
out.push(5);
encode_len(elements.len(), out);
for element in elements {
element.canonical_encode(out);
}
}
Self::Struct { name, fields } => {
out.push(6);
encode_str(name, out);
encode_len(fields.len(), out);
for (field_name, field_schema) in fields {
encode_str(field_name, out);
field_schema.canonical_encode(out);
}
}
Self::Enum { name, variants } => {
out.push(7);
encode_str(name, out);
encode_len(variants.len(), out);
for (variant_name, variant_schema) in variants {
encode_str(variant_name, out);
variant_schema.canonical_encode(out);
}
}
}
}
#[must_use]
pub fn diff(expected: &Self, actual: &Self) -> Option<SchemaMismatch> {
diff_at(expected, actual, "$")
}
}
fn encode_str(value: &str, out: &mut Vec<u8>) {
encode_len(value.len(), out);
out.extend_from_slice(value.as_bytes());
}
fn encode_len(len: usize, out: &mut Vec<u8>) {
out.extend_from_slice(&(len as u64).to_le_bytes());
}
fn fnv1a64(bytes: &[u8]) -> u64 {
const OFFSET_BASIS: u64 = 0xcbf29ce484222325;
const PRIME: u64 = 0x0000_0100_0000_01b3;
let mut hash = OFFSET_BASIS;
for &byte in bytes {
hash ^= byte as u64;
hash = hash.wrapping_mul(PRIME);
}
hash
}
#[allow(clippy::explicit_auto_deref)]
fn diff_at(
expected: &SchemaDescriptor,
actual: &SchemaDescriptor,
path: &str,
) -> Option<SchemaMismatch> {
use SchemaDescriptor as D;
match (expected, actual) {
(D::Primitive(e), D::Primitive(a)) => {
if e == a {
None
} else {
Some(SchemaMismatch::new(
path,
SchemaMismatchKind::PrimitiveChanged {
expected: *e,
actual: *a,
},
))
}
}
(D::Unit, D::Unit) => None,
(D::Option(e), D::Option(a)) => diff_at(e, a, &format!("{path}?")),
(D::Seq(e), D::Seq(a)) => diff_at(e, a, &format!("{path}[]")),
(D::Map(ek, ev), D::Map(ak, av)) => diff_at(ek, ak, &format!("{path}.<key>"))
.or_else(|| diff_at(ev, av, &format!("{path}.<value>"))),
(D::Tuple(e), D::Tuple(a)) => {
if e.len() != a.len() {
return Some(SchemaMismatch::new(
path,
SchemaMismatchKind::ArityChanged {
expected: e.len(),
actual: a.len(),
},
));
}
for (index, (ei, ai)) in e.iter().zip(a.iter()).enumerate() {
if let Some(mismatch) = diff_at(ei, ai, &format!("{path}.{index}")) {
return Some(mismatch);
}
}
None
}
(
D::Struct {
name: en,
fields: ef,
},
D::Struct {
name: an,
fields: af,
},
) => {
if en != an {
return Some(SchemaMismatch::new(
path,
SchemaMismatchKind::NameChanged {
expected: (*en).to_string(),
actual: (*an).to_string(),
},
));
}
diff_named(ef, af, path)
}
(
D::Enum {
name: en,
variants: ev,
},
D::Enum {
name: an,
variants: av,
},
) => {
if en != an {
return Some(SchemaMismatch::new(
path,
SchemaMismatchKind::NameChanged {
expected: (*en).to_string(),
actual: (*an).to_string(),
},
));
}
diff_named(ev, av, path)
}
(e, a) => Some(SchemaMismatch::new(
path,
SchemaMismatchKind::KindChanged {
expected: e.kind_name(),
actual: a.kind_name(),
},
)),
}
}
fn diff_named(
expected: &[(&'static str, SchemaDescriptor)],
actual: &[(&'static str, SchemaDescriptor)],
path: &str,
) -> Option<SchemaMismatch> {
for (name, expected_schema) in expected {
match actual.iter().find(|(other, _)| other == name) {
Some((_, actual_schema)) => {
if let Some(mismatch) =
diff_at(expected_schema, actual_schema, &format!("{path}.{name}"))
{
return Some(mismatch);
}
}
None => {
return Some(SchemaMismatch::new(
path,
SchemaMismatchKind::FieldMissing {
field: (*name).to_string(),
},
));
}
}
}
for (name, _) in actual {
if !expected.iter().any(|(other, _)| other == name) {
return Some(SchemaMismatch::new(
path,
SchemaMismatchKind::FieldExtra {
field: (*name).to_string(),
},
));
}
}
None
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SchemaFingerprint(u64);
impl SchemaFingerprint {
#[must_use]
pub fn of(descriptor: &SchemaDescriptor) -> Self {
let mut buffer = Vec::new();
descriptor.canonical_encode(&mut buffer);
Self(fnv1a64(&buffer))
}
#[must_use]
pub const fn to_u64(self) -> u64 {
self.0
}
}
impl fmt::Display for SchemaFingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:016x}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SchemaMismatch {
pub path: String,
pub kind: SchemaMismatchKind,
}
impl SchemaMismatch {
fn new(path: &str, kind: SchemaMismatchKind) -> Self {
Self {
path: path.to_string(),
kind,
}
}
}
impl fmt::Display for SchemaMismatch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "schema mismatch at `{}`: {}", self.path, self.kind)
}
}
impl std::error::Error for SchemaMismatch {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SchemaMismatchKind {
KindChanged {
expected: &'static str,
actual: &'static str,
},
PrimitiveChanged {
expected: &'static str,
actual: &'static str,
},
NameChanged {
expected: String,
actual: String,
},
FieldMissing {
field: String,
},
FieldExtra {
field: String,
},
ArityChanged {
expected: usize,
actual: usize,
},
}
impl fmt::Display for SchemaMismatchKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::KindChanged { expected, actual } => {
write!(f, "kind changed: expected `{expected}`, found `{actual}`")
}
Self::PrimitiveChanged { expected, actual } => write!(
f,
"primitive type changed: expected `{expected}`, found `{actual}`"
),
Self::NameChanged { expected, actual } => {
write!(f, "type renamed: expected `{expected}`, found `{actual}`")
}
Self::FieldMissing { field } => write!(f, "missing field `{field}`"),
Self::FieldExtra { field } => write!(f, "unexpected extra field `{field}`"),
Self::ArityChanged { expected, actual } => {
write!(
f,
"tuple arity changed: expected {expected}, found {actual}"
)
}
}
}
}
pub trait HasSchema {
fn schema() -> SchemaDescriptor;
#[must_use]
fn schema_fingerprint() -> SchemaFingerprint {
SchemaFingerprint::of(&Self::schema())
}
}
macro_rules! impl_primitive_schema {
($($t:ty => $name:literal),+ $(,)?) => {
$(
impl HasSchema for $t {
fn schema() -> SchemaDescriptor {
SchemaDescriptor::Primitive($name)
}
}
)+
};
}
impl_primitive_schema! {
u8 => "u8", u16 => "u16", u32 => "u32", u64 => "u64", u128 => "u128", usize => "usize",
i8 => "i8", i16 => "i16", i32 => "i32", i64 => "i64", i128 => "i128", isize => "isize",
bool => "bool", char => "char", f32 => "f32", f64 => "f64", String => "String",
}
impl HasSchema for () {
fn schema() -> SchemaDescriptor {
SchemaDescriptor::Unit
}
}
impl<T: HasSchema> HasSchema for Option<T> {
fn schema() -> SchemaDescriptor {
SchemaDescriptor::option(T::schema())
}
}
impl<T: HasSchema> HasSchema for Vec<T> {
fn schema() -> SchemaDescriptor {
SchemaDescriptor::seq(T::schema())
}
}
impl<T: HasSchema> HasSchema for Box<T> {
fn schema() -> SchemaDescriptor {
T::schema()
}
}
impl<A: HasSchema, B: HasSchema> HasSchema for (A, B) {
fn schema() -> SchemaDescriptor {
SchemaDescriptor::tuple(vec![A::schema(), B::schema()])
}
}
impl<A: HasSchema, B: HasSchema, C: HasSchema> HasSchema for (A, B, C) {
fn schema() -> SchemaDescriptor {
SchemaDescriptor::tuple(vec![A::schema(), B::schema(), C::schema()])
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RegisteredComputationSchema {
name: String,
input_schema: SchemaDescriptor,
output_schema: SchemaDescriptor,
input_fingerprint: SchemaFingerprint,
output_fingerprint: SchemaFingerprint,
}
impl RegisteredComputationSchema {
#[must_use]
pub fn new(
name: impl Into<String>,
input_schema: SchemaDescriptor,
output_schema: SchemaDescriptor,
) -> Self {
let input_fingerprint = input_schema.fingerprint();
let output_fingerprint = output_schema.fingerprint();
Self {
name: name.into(),
input_schema,
output_schema,
input_fingerprint,
output_fingerprint,
}
}
#[must_use]
pub fn typed<I, O>(name: impl Into<String>) -> Self
where
I: HasSchema,
O: HasSchema,
{
Self::new(name, I::schema(), O::schema())
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub const fn input_schema(&self) -> &SchemaDescriptor {
&self.input_schema
}
#[must_use]
pub const fn output_schema(&self) -> &SchemaDescriptor {
&self.output_schema
}
#[must_use]
pub const fn input_fingerprint(&self) -> SchemaFingerprint {
self.input_fingerprint
}
#[must_use]
pub const fn output_fingerprint(&self) -> SchemaFingerprint {
self.output_fingerprint
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ComputationSchemaRegistry {
entries: BTreeMap<String, RegisteredComputationSchema>,
}
impl ComputationSchemaRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register(
&mut self,
entry: RegisteredComputationSchema,
) -> Result<(), ComputationSchemaRegistryError> {
let name = entry.name.clone();
if self.entries.contains_key(&name) {
return Err(ComputationSchemaRegistryError::DuplicateName { name });
}
self.entries.insert(name, entry);
Ok(())
}
pub fn register_typed<I, O>(
&mut self,
name: impl Into<String>,
) -> Result<(), ComputationSchemaRegistryError>
where
I: HasSchema,
O: HasSchema,
{
self.register(RegisteredComputationSchema::typed::<I, O>(name))
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&RegisteredComputationSchema> {
self.entries.get(name)
}
#[must_use]
pub fn contains(&self, name: &str) -> bool {
self.entries.contains_key(name)
}
pub fn iter(&self) -> impl ExactSizeIterator<Item = &RegisteredComputationSchema> + '_ {
self.entries.values()
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ComputationSchemaRegistryError {
DuplicateName {
name: String,
},
}
impl fmt::Display for ComputationSchemaRegistryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::DuplicateName { name } => {
write!(f, "duplicate remote computation schema `{name}`")
}
}
}
}
impl std::error::Error for ComputationSchemaRegistryError {}
#[cfg(test)]
mod tests {
use super::*;
fn person() -> SchemaDescriptor {
SchemaDescriptor::structure(
"Person",
vec![
("id", u64::schema()),
("name", String::schema()),
("tags", <Vec<String>>::schema()),
],
)
}
#[test]
fn fingerprint_is_deterministic_and_distinct() {
assert_eq!(person().fingerprint(), person().fingerprint());
assert_eq!(u64::schema_fingerprint(), u64::schema_fingerprint());
assert_ne!(u64::schema_fingerprint(), u32::schema_fingerprint());
assert_ne!(
<Vec<u8>>::schema_fingerprint(),
<Vec<u16>>::schema_fingerprint()
);
assert_ne!(
<Option<u64>>::schema_fingerprint(),
u64::schema_fingerprint()
);
}
#[test]
fn fingerprint_not_type_id_based() {
let a = SchemaDescriptor::structure("P", vec![("x", u8::schema())]);
let b = SchemaDescriptor::structure("P", vec![("x", u8::schema())]);
assert_eq!(a.fingerprint(), b.fingerprint());
}
#[test]
fn diff_detects_type_change() {
let expected = person();
let actual = SchemaDescriptor::structure(
"Person",
vec![
("id", u32::schema()), ("name", String::schema()),
("tags", <Vec<String>>::schema()),
],
);
let mismatch = SchemaDescriptor::diff(&expected, &actual).expect("should differ");
assert_eq!(mismatch.path, "$.id");
assert!(matches!(
mismatch.kind,
SchemaMismatchKind::PrimitiveChanged {
expected: "u64",
actual: "u32"
}
));
}
#[test]
fn diff_detects_missing_field() {
let expected = person();
let actual = SchemaDescriptor::structure(
"Person",
vec![("id", u64::schema()), ("name", String::schema())], );
let mismatch = SchemaDescriptor::diff(&expected, &actual).expect("should differ");
assert!(matches!(
mismatch.kind,
SchemaMismatchKind::FieldMissing { ref field } if field == "tags"
));
}
#[test]
fn diff_detects_extra_field() {
let expected = SchemaDescriptor::structure("P", vec![("a", u8::schema())]);
let actual =
SchemaDescriptor::structure("P", vec![("a", u8::schema()), ("b", u8::schema())]);
let mismatch = SchemaDescriptor::diff(&expected, &actual).expect("should differ");
assert!(matches!(
mismatch.kind,
SchemaMismatchKind::FieldExtra { ref field } if field == "b"
));
}
#[test]
fn diff_detects_rename() {
let expected = SchemaDescriptor::structure("Old", vec![("a", u8::schema())]);
let actual = SchemaDescriptor::structure("New", vec![("a", u8::schema())]);
let mismatch = SchemaDescriptor::diff(&expected, &actual).expect("should differ");
assert!(matches!(
mismatch.kind,
SchemaMismatchKind::NameChanged { .. }
));
}
#[test]
fn identical_schema_has_no_diff() {
assert!(SchemaDescriptor::diff(&person(), &person()).is_none());
}
#[test]
fn mismatch_messages_name_the_difference() {
let expected = person();
let actual = SchemaDescriptor::structure(
"Person",
vec![
("id", u32::schema()),
("name", String::schema()),
("tags", <Vec<String>>::schema()),
],
);
let mismatch = SchemaDescriptor::diff(&expected, &actual).expect("should differ");
let rendered = mismatch.to_string();
assert!(rendered.contains("$.id"), "rendered: {rendered}");
assert!(rendered.contains("u64"), "rendered: {rendered}");
assert!(rendered.contains("u32"), "rendered: {rendered}");
}
#[test]
fn computation_registry_enumerates_entries_in_name_order() {
let mut registry = ComputationSchemaRegistry::new();
registry
.register_typed::<u64, String>("zeta.encode")
.unwrap();
registry
.register_typed::<(u64, String), Vec<u8>>("alpha.decode")
.unwrap();
assert_eq!(registry.len(), 2);
assert!(registry.contains("alpha.decode"));
let names: Vec<&str> = registry
.iter()
.map(RegisteredComputationSchema::name)
.collect();
assert_eq!(names, vec!["alpha.decode", "zeta.encode"]);
let alpha = registry.get("alpha.decode").unwrap();
assert_eq!(
alpha.input_fingerprint(),
<(u64, String)>::schema_fingerprint()
);
assert_eq!(alpha.output_fingerprint(), <Vec<u8>>::schema_fingerprint());
}
#[test]
fn computation_registry_rejects_duplicate_names() {
let mut registry = ComputationSchemaRegistry::new();
registry.register_typed::<u64, u64>("work").unwrap();
let err = registry
.register_typed::<String, String>("work")
.expect_err("duplicate computation names must fail closed");
assert!(matches!(
err,
ComputationSchemaRegistryError::DuplicateName { ref name } if name == "work"
));
assert_eq!(registry.len(), 1);
let work = registry.get("work").unwrap();
assert_eq!(work.input_fingerprint(), u64::schema_fingerprint());
assert_eq!(work.output_fingerprint(), u64::schema_fingerprint());
}
}