use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct SchemaFile {
pub config: SchemaConfig,
#[serde(rename = "entity")]
pub entities: Vec<Entity>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SchemaConfig {
pub output: String,
pub events: Option<EventsConfig>,
pub sync: Option<SyncConfig>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct EventsConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_snapshot_threshold")]
pub snapshot_threshold: u64,
}
fn default_snapshot_threshold() -> u64 {
100
}
#[derive(Debug, Clone, Deserialize)]
pub struct SyncConfig {
#[serde(default)]
pub enabled: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Entity {
pub name: String,
pub table: String,
pub versions: Vec<EntityVersion>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct EntityVersion {
pub version: u32,
pub fields: Vec<Field>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Field {
pub name: String,
#[serde(rename = "type")]
pub field_type: String,
pub default: Option<String>,
pub crdt: Option<String>,
pub relation: Option<String>,
}
pub const SUPPORTED_CRDTS: &[CrdtInfo] = &[
CrdtInfo {
name: "GCounter",
is_generic: false,
default_expr: "GCounter::new(\"_migrated\")",
},
CrdtInfo {
name: "PNCounter",
is_generic: false,
default_expr: "PNCounter::new(\"_migrated\")",
},
CrdtInfo {
name: "LWWRegister",
is_generic: true,
default_expr: "LWWRegister::with_timestamp(\"_migrated\", Default::default(), 0)",
},
CrdtInfo {
name: "MVRegister",
is_generic: true,
default_expr: "MVRegister::new(\"_migrated\")",
},
CrdtInfo {
name: "GSet",
is_generic: true,
default_expr: "GSet::new()",
},
CrdtInfo {
name: "TwoPSet",
is_generic: true,
default_expr: "TwoPSet::new()",
},
CrdtInfo {
name: "ORSet",
is_generic: true,
default_expr: "ORSet::new(\"_migrated\")",
},
];
pub struct CrdtInfo {
pub name: &'static str,
pub is_generic: bool,
pub default_expr: &'static str,
}
pub fn lookup_crdt(name: &str) -> Option<&'static CrdtInfo> {
SUPPORTED_CRDTS.iter().find(|c| c.name == name)
}
pub const DELTA_CRDTS: &[(&str, &str)] = &[
("GCounter", "GCounterDelta"),
("PNCounter", "PNCounterDelta"),
("ORSet", "ORSetDelta"),
];
pub fn lookup_delta_type(crdt_name: &str) -> Option<&'static str> {
DELTA_CRDTS
.iter()
.find(|(name, _)| *name == crdt_name)
.map(|(_, delta)| *delta)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_minimal_schema() {
let toml = r#"
[config]
output = "src/generated"
[[entity]]
name = "Task"
table = "tasks"
[[entity.versions]]
version = 1
fields = [
{ name = "title", type = "String" },
{ name = "done", type = "bool" },
]
"#;
let schema: SchemaFile = toml::from_str(toml).unwrap();
assert_eq!(schema.config.output, "src/generated");
assert_eq!(schema.entities.len(), 1);
assert_eq!(schema.entities[0].name, "Task");
assert_eq!(schema.entities[0].table, "tasks");
assert_eq!(schema.entities[0].versions[0].fields.len(), 2);
}
#[test]
fn parse_crdt_and_relation_fields() {
let toml = r#"
[config]
output = "out"
[[entity]]
name = "Task"
table = "tasks"
[[entity.versions]]
version = 1
fields = [
{ name = "title", type = "String", crdt = "LWWRegister" },
{ name = "views", type = "u64", crdt = "GCounter" },
{ name = "project_id", type = "String", relation = "Project" },
]
"#;
let schema: SchemaFile = toml::from_str(toml).unwrap();
let fields = &schema.entities[0].versions[0].fields;
assert_eq!(fields[0].crdt.as_deref(), Some("LWWRegister"));
assert_eq!(fields[1].crdt.as_deref(), Some("GCounter"));
assert_eq!(fields[2].relation.as_deref(), Some("Project"));
}
#[test]
fn parse_events_and_sync_config() {
let toml = r#"
[config]
output = "src/persistence"
[config.events]
enabled = true
snapshot_threshold = 200
[config.sync]
enabled = true
[[entity]]
name = "Task"
table = "tasks"
[[entity.versions]]
version = 1
fields = [
{ name = "title", type = "String" },
]
"#;
let schema: SchemaFile = toml::from_str(toml).unwrap();
let events = schema.config.events.unwrap();
assert!(events.enabled);
assert_eq!(events.snapshot_threshold, 200);
let sync = schema.config.sync.unwrap();
assert!(sync.enabled);
}
#[test]
fn parse_config_without_events_sync() {
let toml = r#"
[config]
output = "out"
[[entity]]
name = "Task"
table = "tasks"
[[entity.versions]]
version = 1
fields = [
{ name = "title", type = "String" },
]
"#;
let schema: SchemaFile = toml::from_str(toml).unwrap();
assert!(schema.config.events.is_none());
assert!(schema.config.sync.is_none());
}
#[test]
fn lookup_delta_type_works() {
assert_eq!(lookup_delta_type("GCounter"), Some("GCounterDelta"));
assert_eq!(lookup_delta_type("PNCounter"), Some("PNCounterDelta"));
assert_eq!(lookup_delta_type("ORSet"), Some("ORSetDelta"));
assert_eq!(lookup_delta_type("LWWRegister"), None);
assert_eq!(lookup_delta_type("GSet"), None);
}
#[test]
fn parse_multi_version_schema() {
let toml = r#"
[config]
output = "out"
[[entity]]
name = "Sensor"
table = "sensors"
[[entity.versions]]
version = 1
fields = [
{ name = "device_id", type = "String" },
{ name = "temperature", type = "f32" },
]
[[entity.versions]]
version = 2
fields = [
{ name = "device_id", type = "String" },
{ name = "temperature", type = "f32" },
{ name = "humidity", type = "Option<f32>", default = "None" },
]
"#;
let schema: SchemaFile = toml::from_str(toml).unwrap();
let sensor = &schema.entities[0];
assert_eq!(sensor.versions.len(), 2);
assert_eq!(sensor.versions[1].fields[2].name, "humidity");
assert_eq!(
sensor.versions[1].fields[2].default.as_deref(),
Some("None")
);
}
}