1use serde::Deserialize;
2
3#[derive(Debug, Clone, Deserialize)]
5pub struct SchemaFile {
6 pub config: SchemaConfig,
8 #[serde(rename = "entity")]
10 pub entities: Vec<Entity>,
11}
12
13#[derive(Debug, Clone, Deserialize)]
15pub struct SchemaConfig {
16 pub output: String,
18 pub events: Option<EventsConfig>,
20 pub sync: Option<SyncConfig>,
22}
23
24#[derive(Debug, Clone, Deserialize)]
26pub struct EventsConfig {
27 #[serde(default)]
29 pub enabled: bool,
30 #[serde(default = "default_snapshot_threshold")]
32 pub snapshot_threshold: u64,
33}
34
35fn default_snapshot_threshold() -> u64 {
36 100
37}
38
39#[derive(Debug, Clone, Deserialize)]
41pub struct SyncConfig {
42 #[serde(default)]
44 pub enabled: bool,
45}
46
47#[derive(Debug, Clone, Deserialize)]
49pub struct Entity {
50 pub name: String,
52 pub table: String,
54 pub versions: Vec<EntityVersion>,
56}
57
58#[derive(Debug, Clone, Deserialize)]
60pub struct EntityVersion {
61 pub version: u32,
63 pub fields: Vec<Field>,
65}
66
67#[derive(Debug, Clone, Deserialize)]
69pub struct Field {
70 pub name: String,
72 #[serde(rename = "type")]
74 pub field_type: String,
75 pub default: Option<String>,
78 pub crdt: Option<String>,
83 pub relation: Option<String>,
86}
87
88pub const SUPPORTED_CRDTS: &[CrdtInfo] = &[
90 CrdtInfo {
91 name: "GCounter",
92 is_generic: false,
93 default_expr: "GCounter::new(\"_migrated\")",
94 },
95 CrdtInfo {
96 name: "PNCounter",
97 is_generic: false,
98 default_expr: "PNCounter::new(\"_migrated\")",
99 },
100 CrdtInfo {
101 name: "LWWRegister",
102 is_generic: true,
103 default_expr: "LWWRegister::with_timestamp(\"_migrated\", Default::default(), 0)",
104 },
105 CrdtInfo {
106 name: "MVRegister",
107 is_generic: true,
108 default_expr: "MVRegister::new(\"_migrated\")",
109 },
110 CrdtInfo {
111 name: "GSet",
112 is_generic: true,
113 default_expr: "GSet::new()",
114 },
115 CrdtInfo {
116 name: "TwoPSet",
117 is_generic: true,
118 default_expr: "TwoPSet::new()",
119 },
120 CrdtInfo {
121 name: "ORSet",
122 is_generic: true,
123 default_expr: "ORSet::new(\"_migrated\")",
124 },
125];
126
127pub struct CrdtInfo {
129 pub name: &'static str,
131 pub is_generic: bool,
133 pub default_expr: &'static str,
135}
136
137pub fn lookup_crdt(name: &str) -> Option<&'static CrdtInfo> {
139 SUPPORTED_CRDTS.iter().find(|c| c.name == name)
140}
141
142pub const DELTA_CRDTS: &[(&str, &str)] = &[
144 ("GCounter", "GCounterDelta"),
145 ("PNCounter", "PNCounterDelta"),
146 ("ORSet", "ORSetDelta"),
147];
148
149pub fn lookup_delta_type(crdt_name: &str) -> Option<&'static str> {
151 DELTA_CRDTS
152 .iter()
153 .find(|(name, _)| *name == crdt_name)
154 .map(|(_, delta)| *delta)
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn parse_minimal_schema() {
163 let toml = r#"
164[config]
165output = "src/generated"
166
167[[entity]]
168name = "Task"
169table = "tasks"
170
171[[entity.versions]]
172version = 1
173fields = [
174 { name = "title", type = "String" },
175 { name = "done", type = "bool" },
176]
177"#;
178 let schema: SchemaFile = toml::from_str(toml).unwrap();
179 assert_eq!(schema.config.output, "src/generated");
180 assert_eq!(schema.entities.len(), 1);
181 assert_eq!(schema.entities[0].name, "Task");
182 assert_eq!(schema.entities[0].table, "tasks");
183 assert_eq!(schema.entities[0].versions[0].fields.len(), 2);
184 }
185
186 #[test]
187 fn parse_crdt_and_relation_fields() {
188 let toml = r#"
189[config]
190output = "out"
191
192[[entity]]
193name = "Task"
194table = "tasks"
195
196[[entity.versions]]
197version = 1
198fields = [
199 { name = "title", type = "String", crdt = "LWWRegister" },
200 { name = "views", type = "u64", crdt = "GCounter" },
201 { name = "project_id", type = "String", relation = "Project" },
202]
203"#;
204 let schema: SchemaFile = toml::from_str(toml).unwrap();
205 let fields = &schema.entities[0].versions[0].fields;
206 assert_eq!(fields[0].crdt.as_deref(), Some("LWWRegister"));
207 assert_eq!(fields[1].crdt.as_deref(), Some("GCounter"));
208 assert_eq!(fields[2].relation.as_deref(), Some("Project"));
209 }
210
211 #[test]
212 fn parse_events_and_sync_config() {
213 let toml = r#"
214[config]
215output = "src/persistence"
216
217[config.events]
218enabled = true
219snapshot_threshold = 200
220
221[config.sync]
222enabled = true
223
224[[entity]]
225name = "Task"
226table = "tasks"
227
228[[entity.versions]]
229version = 1
230fields = [
231 { name = "title", type = "String" },
232]
233"#;
234 let schema: SchemaFile = toml::from_str(toml).unwrap();
235 let events = schema.config.events.unwrap();
236 assert!(events.enabled);
237 assert_eq!(events.snapshot_threshold, 200);
238 let sync = schema.config.sync.unwrap();
239 assert!(sync.enabled);
240 }
241
242 #[test]
243 fn parse_config_without_events_sync() {
244 let toml = r#"
245[config]
246output = "out"
247
248[[entity]]
249name = "Task"
250table = "tasks"
251
252[[entity.versions]]
253version = 1
254fields = [
255 { name = "title", type = "String" },
256]
257"#;
258 let schema: SchemaFile = toml::from_str(toml).unwrap();
259 assert!(schema.config.events.is_none());
260 assert!(schema.config.sync.is_none());
261 }
262
263 #[test]
264 fn lookup_delta_type_works() {
265 assert_eq!(lookup_delta_type("GCounter"), Some("GCounterDelta"));
266 assert_eq!(lookup_delta_type("PNCounter"), Some("PNCounterDelta"));
267 assert_eq!(lookup_delta_type("ORSet"), Some("ORSetDelta"));
268 assert_eq!(lookup_delta_type("LWWRegister"), None);
269 assert_eq!(lookup_delta_type("GSet"), None);
270 }
271
272 #[test]
273 fn parse_multi_version_schema() {
274 let toml = r#"
275[config]
276output = "out"
277
278[[entity]]
279name = "Sensor"
280table = "sensors"
281
282[[entity.versions]]
283version = 1
284fields = [
285 { name = "device_id", type = "String" },
286 { name = "temperature", type = "f32" },
287]
288
289[[entity.versions]]
290version = 2
291fields = [
292 { name = "device_id", type = "String" },
293 { name = "temperature", type = "f32" },
294 { name = "humidity", type = "Option<f32>", default = "None" },
295]
296"#;
297 let schema: SchemaFile = toml::from_str(toml).unwrap();
298 let sensor = &schema.entities[0];
299 assert_eq!(sensor.versions.len(), 2);
300 assert_eq!(sensor.versions[1].fields[2].name, "humidity");
301 assert_eq!(
302 sensor.versions[1].fields[2].default.as_deref(),
303 Some("None")
304 );
305 }
306}