use std::collections::BTreeMap;
use std::path::Path;
use serde::{Deserialize, Serialize};
pub fn lock_version() -> u32 {
1
}
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct SchemaRegistry {
version: u32,
#[serde(default)]
events: BTreeMap<String, EventEntry>,
}
impl SchemaRegistry {
pub fn new() -> Self {
SchemaRegistry {
version: lock_version(),
events: BTreeMap::new(),
}
}
pub fn get(&self, key: &str) -> Option<&EventEntry> {
self.events.get(key)
}
pub fn insert(&mut self, key: String, entry: EventEntry) {
self.events.insert(key, entry);
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct EventEntry {
pub schema: i64,
pub fields: Vec<FieldShape>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FieldShape {
pub name: String,
#[serde(rename = "type")]
pub ty: String,
pub default: bool,
}
pub fn parse(existing: Option<&str>, project_root: &Path) -> Result<SchemaRegistry, String> {
let Some(text) = existing else {
return Ok(SchemaRegistry::new());
};
let path = project_root.join("bynk.schema.lock");
if text.trim().is_empty() {
return Err(format!(
"schema registry `{}` is empty or truncated (corrupt); refusing \
to treat it as a fresh project — restore it from version control",
path.display()
));
}
let reg: SchemaRegistry = toml::from_str(text).map_err(|e| {
format!(
"schema registry `{}` is corrupt ({e}) — restore it from version control",
path.display()
)
})?;
if reg.version != lock_version() {
return Err(format!(
"unsupported schema registry version {} (`{}`)",
reg.version,
path.display()
));
}
Ok(reg)
}
pub fn serialize(reg: &SchemaRegistry) -> String {
toml::to_string_pretty(reg).expect("SchemaRegistry always serializes")
}
#[cfg(test)]
mod tests {
use super::*;
fn shape(name: &str, ty: &str, default: bool) -> FieldShape {
FieldShape {
name: name.to_string(),
ty: ty.to_string(),
default,
}
}
fn test_root() -> &'static Path {
Path::new("/project")
}
#[test]
fn new_registry_is_empty_at_the_current_lock_version() {
let reg = SchemaRegistry::new();
assert_eq!(reg.version, lock_version());
assert!(reg.events.is_empty());
}
#[test]
fn insert_then_get_round_trips_an_entry() {
let mut reg = SchemaRegistry::new();
let entry = EventEntry {
schema: 2,
fields: vec![shape("orderId", "String", false)],
};
reg.insert("commerce.order.PaymentConfirmed".to_string(), entry.clone());
assert_eq!(reg.get("commerce.order.PaymentConfirmed"), Some(&entry));
assert_eq!(reg.get("no.such.key"), None);
}
#[test]
fn parse_of_absent_content_is_an_empty_registry() {
let reg = parse(None, test_root()).unwrap();
assert!(reg.events.is_empty());
}
#[test]
fn parse_round_trips_serialized_content() {
let mut reg = SchemaRegistry::new();
reg.insert(
"commerce.order.PaymentConfirmed".to_string(),
EventEntry {
schema: 2,
fields: vec![shape("orderId", "String", false)],
},
);
let body = serialize(®);
assert_eq!(parse(Some(&body), test_root()).unwrap(), reg);
}
#[test]
fn a_truncated_file_is_corruption_not_a_fresh_project() {
let err = parse(Some(" \n"), test_root()).unwrap_err();
assert!(err.contains("restore it from version control"));
assert!(err.contains("bynk.schema.lock"));
}
#[test]
fn an_unparseable_file_is_corruption() {
let err = parse(Some("not valid toml {{{"), test_root()).unwrap_err();
assert!(err.contains("corrupt"));
assert!(err.contains("bynk.schema.lock"));
}
#[test]
fn an_unsupported_lock_version_is_corruption() {
let err = parse(Some("version = 99\n"), test_root()).unwrap_err();
assert!(err.contains("unsupported schema registry version 99"));
}
}