1use crate::schema::{lookup_crdt, lookup_delta_type, Entity, EntityVersion, Field, SchemaConfig};
2use std::collections::HashSet;
3use std::fmt::Write;
4
5const HEADER: &str = "\
10// ============================================================================
11// AUTO-GENERATED by crdt-codegen -- DO NOT EDIT
12//
13// This file was generated from a crdt-schema.toml definition.
14// Any manual changes will be overwritten on the next `crdt generate` run.
15//
16// To modify this code, edit the schema file and re-run:
17// crdt generate --schema crdt-schema.toml
18// ============================================================================
19
20#![allow(dead_code, unused_imports)]
21";
22
23pub(crate) fn to_snake_case(s: &str) -> String {
27 let mut out = String::new();
28 for (i, ch) in s.chars().enumerate() {
29 if ch.is_ascii_uppercase() {
30 if i > 0 {
31 out.push('_');
32 }
33 out.push(ch.to_ascii_lowercase());
34 } else {
35 out.push(ch);
36 }
37 }
38 out
39}
40
41pub(crate) fn to_pascal_case(s: &str) -> String {
43 s.split('_')
44 .map(|part| {
45 let mut chars = part.chars();
46 match chars.next() {
47 None => String::new(),
48 Some(first) => {
49 let mut result = first.to_ascii_uppercase().to_string();
50 result.extend(chars);
51 result
52 }
53 }
54 })
55 .collect()
56}
57
58fn field_rust_type(field: &Field) -> String {
64 if let Some(crdt_name) = &field.crdt {
65 if let Some(info) = lookup_crdt(crdt_name) {
66 if info.is_generic {
67 return format!("{}<{}>", info.name, field.field_type);
68 } else {
69 return info.name.to_string();
70 }
71 }
72 }
73 field.field_type.clone()
74}
75
76fn entity_uses_crdt(entity: &Entity) -> bool {
78 entity
79 .versions
80 .iter()
81 .any(|v| v.fields.iter().any(|f| f.crdt.is_some()))
82}
83
84fn entity_relation_fields(entity: &Entity) -> Vec<&Field> {
86 entity
87 .versions
88 .last()
89 .map(|v| v.fields.iter().filter(|f| f.relation.is_some()).collect())
90 .unwrap_or_default()
91}
92
93fn entity_crdt_fields(entity: &Entity) -> Vec<&Field> {
95 entity
96 .versions
97 .last()
98 .map(|v| v.fields.iter().filter(|f| f.crdt.is_some()).collect())
99 .unwrap_or_default()
100}
101
102fn entity_delta_fields(entity: &Entity) -> Vec<&Field> {
104 entity
105 .versions
106 .last()
107 .map(|v| {
108 v.fields
109 .iter()
110 .filter(|f| f.crdt.as_ref().and_then(|c| lookup_delta_type(c)).is_some())
111 .collect()
112 })
113 .unwrap_or_default()
114}
115
116fn field_snapshot_type(field: &Field) -> String {
125 match field.crdt.as_deref() {
126 Some("GCounter") => "u64".into(),
127 Some("PNCounter") => "i64".into(),
128 Some("LWWRegister") => field.field_type.clone(),
129 Some("MVRegister") => format!("Vec<{}>", field.field_type),
130 Some("GSet" | "TwoPSet" | "ORSet") => format!("Vec<{}>", field.field_type),
131 _ => field.field_type.clone(),
132 }
133}
134
135fn delta_field_type(field: &Field) -> Option<String> {
139 let crdt_name = field.crdt.as_ref()?;
140 let delta_type = lookup_delta_type(crdt_name)?;
141 let crdt_info = lookup_crdt(crdt_name)?;
142 if crdt_info.is_generic {
143 Some(format!("{}<{}>", delta_type, field.field_type))
144 } else {
145 Some(delta_type.to_string())
146 }
147}
148
149fn relation_param_type(field_type: &str) -> String {
151 if field_type == "String" {
152 "&str".into()
153 } else {
154 field_type.into()
155 }
156}
157
158pub fn generate_entity_file(entity: &Entity) -> (String, String) {
165 let snake = to_snake_case(&entity.name);
166 let filename = format!("{snake}.rs");
167
168 let mut buf = String::new();
169 writeln!(buf, "{HEADER}").unwrap();
170 writeln!(buf, "use crdt_migrate::crdt_schema;").unwrap();
171 writeln!(buf, "use serde::{{Deserialize, Serialize}};").unwrap();
172 if entity_uses_crdt(entity) {
173 writeln!(buf, "use crdt_kit::prelude::*;").unwrap();
174 }
175 writeln!(buf).unwrap();
176
177 let latest = entity.versions.iter().map(|v| v.version).max().unwrap_or(1);
178
179 for ver in &entity.versions {
180 let is_latest = ver.version == latest;
181 let suffix = if is_latest { " (current)" } else { "" };
182 writeln!(
183 buf,
184 "/// {} entity -- version {}{suffix}",
185 entity.name, ver.version
186 )
187 .unwrap();
188 writeln!(
189 buf,
190 "#[crdt_schema(version = {}, table = \"{}\")]",
191 ver.version, entity.table
192 )
193 .unwrap();
194 writeln!(
195 buf,
196 "#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]"
197 )
198 .unwrap();
199 writeln!(buf, "pub struct {}V{} {{", entity.name, ver.version).unwrap();
200 for field in &ver.fields {
201 let rust_type = field_rust_type(field);
202 writeln!(buf, " pub {}: {},", field.name, rust_type).unwrap();
203 }
204 writeln!(buf, "}}").unwrap();
205 writeln!(buf).unwrap();
206 }
207
208 writeln!(buf, "/// Type alias for the current version.").unwrap();
210 writeln!(buf, "pub type {} = {}V{latest};", entity.name, entity.name).unwrap();
211
212 (filename, buf)
213}
214
215fn can_auto_migrate(from: &EntityVersion, to: &EntityVersion) -> bool {
220 let from_fields: HashSet<(&str, &str)> = from
221 .fields
222 .iter()
223 .map(|f| (f.name.as_str(), f.field_type.as_str()))
224 .collect();
225
226 for field in &to.fields {
227 let key = (field.name.as_str(), field.field_type.as_str());
228 if !from_fields.contains(&key) {
229 let has_crdt_default =
231 field.crdt.is_some() && lookup_crdt(field.crdt.as_deref().unwrap_or("")).is_some();
232 if field.default.is_none() && !has_crdt_default {
233 return false;
234 }
235 }
236 }
237
238 let to_names: HashSet<&str> = to.fields.iter().map(|f| f.name.as_str()).collect();
240 for field in &from.fields {
241 if !to_names.contains(field.name.as_str()) {
242 return false;
243 }
244 }
245
246 true
247}
248
249fn new_fields<'a>(from: &EntityVersion, to: &'a EntityVersion) -> Vec<&'a Field> {
251 let from_names: HashSet<&str> = from.fields.iter().map(|f| f.name.as_str()).collect();
252 to.fields
253 .iter()
254 .filter(|f| !from_names.contains(f.name.as_str()))
255 .collect()
256}
257
258pub fn generate_migration_file(entity: &Entity) -> (String, String) {
264 let snake = to_snake_case(&entity.name);
265 let filename = format!("{snake}_migrations.rs");
266
267 let mut buf = String::new();
268 writeln!(buf, "{HEADER}").unwrap();
269 if entity_uses_crdt(entity) {
270 writeln!(buf, "use crdt_kit::prelude::*;").unwrap();
271 }
272 writeln!(buf, "use crdt_migrate::migration;").unwrap();
273
274 let mut import_versions: Vec<u32> = Vec::new();
276 for window in entity.versions.windows(2) {
277 import_versions.push(window[0].version);
278 import_versions.push(window[1].version);
279 }
280 import_versions.sort();
281 import_versions.dedup();
282
283 let imports: Vec<String> = import_versions
284 .iter()
285 .map(|v| format!("{}V{v}", entity.name))
286 .collect();
287 writeln!(buf, "use super::super::models::{{{}}};", imports.join(", ")).unwrap();
288 writeln!(buf).unwrap();
289
290 for window in entity.versions.windows(2) {
291 let from_ver = &window[0];
292 let to_ver = &window[1];
293 let fn_name = format!(
294 "migrate_{snake}_v{}_to_v{}",
295 from_ver.version, to_ver.version
296 );
297
298 if can_auto_migrate(from_ver, to_ver) {
299 let added = new_fields(from_ver, to_ver);
300 let added_desc: Vec<String> = added
301 .iter()
302 .map(|f| {
303 let default_desc = if let Some(d) = &f.default {
304 d.clone()
305 } else if let Some(crdt_name) = &f.crdt {
306 lookup_crdt(crdt_name)
307 .map(|c| c.default_expr.to_string())
308 .unwrap_or_else(|| "?".into())
309 } else {
310 "?".into()
311 };
312 format!("{} (default: {})", f.name, default_desc)
313 })
314 .collect();
315
316 writeln!(
317 buf,
318 "/// Auto-generated migration: {} v{} -> v{}",
319 entity.name, from_ver.version, to_ver.version
320 )
321 .unwrap();
322 if !added_desc.is_empty() {
323 writeln!(buf, "///").unwrap();
324 writeln!(buf, "/// Added fields: {}", added_desc.join(", ")).unwrap();
325 }
326 writeln!(
327 buf,
328 "#[migration(from = {}, to = {})]",
329 from_ver.version, to_ver.version
330 )
331 .unwrap();
332 writeln!(
333 buf,
334 "pub fn {fn_name}(old: {}V{}) -> {}V{} {{",
335 entity.name, from_ver.version, entity.name, to_ver.version
336 )
337 .unwrap();
338 writeln!(buf, " {}V{} {{", entity.name, to_ver.version).unwrap();
339
340 let from_names: HashSet<&str> =
342 from_ver.fields.iter().map(|f| f.name.as_str()).collect();
343 for field in &to_ver.fields {
344 if from_names.contains(field.name.as_str()) {
345 writeln!(buf, " {}: old.{},", field.name, field.name).unwrap();
346 } else {
347 let default = if let Some(d) = &field.default {
349 d.clone()
350 } else if let Some(crdt_name) = &field.crdt {
351 lookup_crdt(crdt_name)
352 .map(|c| c.default_expr.to_string())
353 .unwrap_or_else(|| "Default::default()".into())
354 } else {
355 "Default::default()".into()
356 };
357 writeln!(buf, " {}: {default},", field.name).unwrap();
358 }
359 }
360
361 writeln!(buf, " }}").unwrap();
362 writeln!(buf, "}}").unwrap();
363 } else {
364 writeln!(
366 buf,
367 "/// Migration: {} v{} -> v{}",
368 entity.name, from_ver.version, to_ver.version
369 )
370 .unwrap();
371 writeln!(buf, "///").unwrap();
372 writeln!(
373 buf,
374 "/// WARNING: This migration requires manual implementation."
375 )
376 .unwrap();
377 writeln!(
378 buf,
379 "#[migration(from = {}, to = {})]",
380 from_ver.version, to_ver.version
381 )
382 .unwrap();
383 writeln!(
384 buf,
385 "pub fn {fn_name}(old: {}V{}) -> {}V{} {{",
386 entity.name, from_ver.version, entity.name, to_ver.version
387 )
388 .unwrap();
389 writeln!(
390 buf,
391 " todo!(\"Implement migration from {} v{} to v{}\")",
392 entity.name, from_ver.version, to_ver.version
393 )
394 .unwrap();
395 writeln!(buf, "}}").unwrap();
396 }
397 writeln!(buf).unwrap();
398 }
399
400 (filename, buf)
401}
402
403pub fn generate_helpers_file(entities: &[Entity]) -> String {
410 let mut buf = String::new();
411 writeln!(buf, "{HEADER}").unwrap();
412 writeln!(buf, "use crdt_store::{{CrdtDb, MemoryStore, StateStore}};").unwrap();
413
414 let mut register_fns: Vec<String> = Vec::new();
416
417 for entity in entities {
418 if entity.versions.len() <= 1 {
419 continue;
420 }
421 let snake = to_snake_case(&entity.name);
422
423 for window in entity.versions.windows(2) {
424 let fn_name = format!(
425 "register_migrate_{snake}_v{}_to_v{}",
426 window[0].version, window[1].version
427 );
428 register_fns.push(format!("super::{snake}_migrations::{fn_name}"));
429 }
430 }
431
432 writeln!(buf).unwrap();
433
434 let max_version = entities
436 .iter()
437 .flat_map(|e| e.versions.iter().map(|v| v.version))
438 .max()
439 .unwrap_or(1);
440
441 writeln!(
442 buf,
443 "/// Create a [`CrdtDb`] with all generated migrations registered."
444 )
445 .unwrap();
446 writeln!(buf, "///").unwrap();
447 writeln!(
448 buf,
449 "/// The database is configured for schema version {max_version} (the latest"
450 )
451 .unwrap();
452 writeln!(buf, "/// version defined in the schema).").unwrap();
453 writeln!(
454 buf,
455 "pub fn create_db<S: StateStore>(store: S) -> CrdtDb<S> {{"
456 )
457 .unwrap();
458
459 if register_fns.is_empty() {
460 writeln!(buf, " CrdtDb::with_store(store)").unwrap();
461 } else {
462 writeln!(buf, " CrdtDb::builder(store, {max_version})").unwrap();
463 for reg in ®ister_fns {
464 writeln!(buf, " .register_migration({reg}())").unwrap();
465 }
466 writeln!(buf, " .build()").unwrap();
467 }
468
469 writeln!(buf, "}}").unwrap();
470 writeln!(buf).unwrap();
471
472 writeln!(
473 buf,
474 "/// Create a [`CrdtDb`] with [`MemoryStore`] for testing."
475 )
476 .unwrap();
477 writeln!(buf, "pub fn create_memory_db() -> CrdtDb<MemoryStore> {{").unwrap();
478 writeln!(buf, " create_db(MemoryStore::new())").unwrap();
479 writeln!(buf, "}}").unwrap();
480
481 buf
482}
483
484pub fn generate_models_mod_file(entities: &[Entity]) -> String {
488 let mut buf = String::new();
489 writeln!(buf, "{HEADER}").unwrap();
490 writeln!(buf).unwrap();
491
492 for entity in entities {
493 let snake = to_snake_case(&entity.name);
494 writeln!(buf, "mod {snake};").unwrap();
495 }
496 writeln!(buf).unwrap();
497
498 for entity in entities {
499 let snake = to_snake_case(&entity.name);
500 writeln!(buf, "pub use {snake}::*;").unwrap();
501 }
502
503 buf
504}
505
506pub fn generate_migrations_mod_file(entities: &[Entity]) -> String {
508 let mut buf = String::new();
509 writeln!(buf, "{HEADER}").unwrap();
510 writeln!(buf).unwrap();
511
512 writeln!(buf, "mod helpers;").unwrap();
513 for entity in entities {
514 if entity.versions.len() > 1 {
515 let snake = to_snake_case(&entity.name);
516 writeln!(buf, "mod {snake}_migrations;").unwrap();
517 }
518 }
519 writeln!(buf).unwrap();
520
521 writeln!(buf, "pub use helpers::*;").unwrap();
522 for entity in entities {
523 if entity.versions.len() > 1 {
524 let snake = to_snake_case(&entity.name);
525 writeln!(buf, "pub use {snake}_migrations::*;").unwrap();
526 }
527 }
528
529 buf
530}
531
532pub fn generate_repositories_mod_file(entities: &[Entity]) -> String {
534 let mut buf = String::new();
535 writeln!(buf, "{HEADER}").unwrap();
536 writeln!(buf).unwrap();
537
538 writeln!(buf, "pub mod traits;").unwrap();
539 for entity in entities {
540 let snake = to_snake_case(&entity.name);
541 writeln!(buf, "pub mod {snake}_repo;").unwrap();
542 }
543 writeln!(buf).unwrap();
544
545 writeln!(buf, "pub use traits::*;").unwrap();
546 for entity in entities {
547 let snake = to_snake_case(&entity.name);
548 writeln!(buf, "pub use {snake}_repo::*;").unwrap();
549 }
550
551 buf
552}
553
554pub fn generate_events_mod_file(entities: &[Entity]) -> String {
556 let mut buf = String::new();
557 writeln!(buf, "{HEADER}").unwrap();
558 writeln!(buf).unwrap();
559
560 writeln!(buf, "mod policies;").unwrap();
561 for entity in entities {
562 let snake = to_snake_case(&entity.name);
563 writeln!(buf, "mod {snake}_events;").unwrap();
564 }
565 writeln!(buf).unwrap();
566
567 writeln!(buf, "pub use policies::*;").unwrap();
568 for entity in entities {
569 let snake = to_snake_case(&entity.name);
570 writeln!(buf, "pub use {snake}_events::*;").unwrap();
571 }
572
573 buf
574}
575
576pub fn generate_sync_mod_file(entities: &[Entity]) -> String {
578 let mut buf = String::new();
579 writeln!(buf, "{HEADER}").unwrap();
580 writeln!(buf).unwrap();
581
582 let crdt_entities: Vec<&Entity> = entities.iter().filter(|e| entity_uses_crdt(e)).collect();
583
584 for entity in &crdt_entities {
585 let snake = to_snake_case(&entity.name);
586 writeln!(buf, "mod {snake}_sync;").unwrap();
587 }
588 writeln!(buf).unwrap();
589
590 for entity in &crdt_entities {
591 let snake = to_snake_case(&entity.name);
592 writeln!(buf, "pub use {snake}_sync::*;").unwrap();
593 }
594
595 buf
596}
597
598pub fn generate_repository_traits_file(entities: &[Entity]) -> String {
606 let mut buf = String::new();
607 writeln!(buf, "{HEADER}").unwrap();
608 writeln!(buf, "use std::fmt;").unwrap();
609 writeln!(buf).unwrap();
610
611 let imports: Vec<String> = entities.iter().map(|e| e.name.clone()).collect();
613 writeln!(buf, "use super::super::models::{{{}}};", imports.join(", ")).unwrap();
614 writeln!(buf).unwrap();
615
616 for entity in entities {
617 let name = &entity.name;
618 let relation_fields = entity_relation_fields(entity);
619
620 writeln!(
621 buf,
622 "/// Repository trait for {name} entities (port in hexagonal architecture)."
623 )
624 .unwrap();
625 writeln!(buf, "///").unwrap();
626 writeln!(
627 buf,
628 "/// Implement this trait to provide persistence for {name} entities."
629 )
630 .unwrap();
631 writeln!(
632 buf,
633 "/// The default adapter uses `CrdtDb<S>` (see `{name}RepositoryAccess`)."
634 )
635 .unwrap();
636 writeln!(buf, "pub trait {name}Repository {{").unwrap();
637 writeln!(buf, " /// Error type returned by repository operations.").unwrap();
638 writeln!(buf, " type Error: fmt::Debug + fmt::Display;").unwrap();
639 writeln!(buf).unwrap();
640
641 writeln!(buf, " /// Load a {name} by its identifier.").unwrap();
643 writeln!(
644 buf,
645 " fn get(&mut self, id: &str) -> Result<Option<{name}>, Self::Error>;"
646 )
647 .unwrap();
648 writeln!(buf).unwrap();
649
650 writeln!(buf, " /// Persist a {name} with the given identifier.").unwrap();
652 writeln!(
653 buf,
654 " fn save(&mut self, id: &str, entity: &{name}) -> Result<(), Self::Error>;"
655 )
656 .unwrap();
657 writeln!(buf).unwrap();
658
659 writeln!(buf, " /// Delete a {name} by its identifier.").unwrap();
661 writeln!(
662 buf,
663 " fn delete(&mut self, id: &str) -> Result<(), Self::Error>;"
664 )
665 .unwrap();
666 writeln!(buf).unwrap();
667
668 writeln!(
670 buf,
671 " /// List all {name} entities as `(id, entity)` pairs."
672 )
673 .unwrap();
674 writeln!(
675 buf,
676 " fn list(&mut self) -> Result<Vec<(String, {name})>, Self::Error>;"
677 )
678 .unwrap();
679 writeln!(buf).unwrap();
680
681 writeln!(
683 buf,
684 " /// Check whether a {name} with the given identifier exists."
685 )
686 .unwrap();
687 writeln!(
688 buf,
689 " fn exists(&self, id: &str) -> Result<bool, Self::Error>;"
690 )
691 .unwrap();
692
693 for field in &relation_fields {
695 let param_type = relation_param_type(&field.field_type);
696 let rel_name = field.relation.as_ref().unwrap();
697 writeln!(buf).unwrap();
698 writeln!(
699 buf,
700 " /// Find all {name} entities related to a {rel_name} by `{}`.",
701 field.name
702 )
703 .unwrap();
704 writeln!(
705 buf,
706 " fn find_by_{}(&mut self, val: {param_type}) -> Result<Vec<(String, {name})>, Self::Error>;",
707 field.name
708 )
709 .unwrap();
710 }
711
712 writeln!(buf, "}}").unwrap();
713 writeln!(buf).unwrap();
714 }
715
716 buf
717}
718
719pub fn generate_repository_impl_file(entity: &Entity) -> (String, String) {
724 let name = &entity.name;
725 let snake = to_snake_case(name);
726 let filename = format!("{snake}_repo.rs");
727 let table = &entity.table;
728
729 let mut buf = String::new();
730 writeln!(buf, "{HEADER}").unwrap();
731 writeln!(buf, "use crdt_store::{{CrdtDb, DbError, StateStore}};").unwrap();
732 writeln!(buf).unwrap();
733 writeln!(buf, "use super::super::models::{name};").unwrap();
734 writeln!(buf, "use super::traits::{name}Repository;").unwrap();
735 writeln!(buf).unwrap();
736
737 writeln!(
739 buf,
740 "/// Repository adapter for {name} entities backed by `CrdtDb<S>`."
741 )
742 .unwrap();
743 writeln!(buf, "///").unwrap();
744 writeln!(
745 buf,
746 "/// This is the \"adapter\" in hexagonal architecture. It implements"
747 )
748 .unwrap();
749 writeln!(
750 buf,
751 "/// `{name}Repository` using the CRDT-aware versioned store."
752 )
753 .unwrap();
754 writeln!(
755 buf,
756 "pub struct {name}RepositoryAccess<'a, S: StateStore> {{"
757 )
758 .unwrap();
759 writeln!(buf, " db: &'a mut CrdtDb<S>,").unwrap();
760 writeln!(buf, "}}").unwrap();
761 writeln!(buf).unwrap();
762
763 writeln!(
765 buf,
766 "impl<'a, S: StateStore> {name}RepositoryAccess<'a, S> {{"
767 )
768 .unwrap();
769 writeln!(
770 buf,
771 " /// Create a new repository accessor with a mutable reference to the database."
772 )
773 .unwrap();
774 writeln!(buf, " pub fn new(db: &'a mut CrdtDb<S>) -> Self {{").unwrap();
775 writeln!(buf, " Self {{ db }}").unwrap();
776 writeln!(buf, " }}").unwrap();
777 writeln!(buf, "}}").unwrap();
778 writeln!(buf).unwrap();
779
780 writeln!(
782 buf,
783 "impl<S: StateStore> {name}Repository for {name}RepositoryAccess<'_, S> {{"
784 )
785 .unwrap();
786 writeln!(buf, " type Error = DbError<S::Error>;").unwrap();
787 writeln!(buf).unwrap();
788
789 writeln!(
791 buf,
792 " fn get(&mut self, id: &str) -> Result<Option<{name}>, Self::Error> {{"
793 )
794 .unwrap();
795 writeln!(buf, " self.db.load_ns(\"{table}\", id)").unwrap();
796 writeln!(buf, " }}").unwrap();
797 writeln!(buf).unwrap();
798
799 writeln!(
801 buf,
802 " fn save(&mut self, id: &str, entity: &{name}) -> Result<(), Self::Error> {{"
803 )
804 .unwrap();
805 writeln!(buf, " self.db.save_ns(\"{table}\", id, entity)").unwrap();
806 writeln!(buf, " }}").unwrap();
807 writeln!(buf).unwrap();
808
809 writeln!(
811 buf,
812 " fn delete(&mut self, id: &str) -> Result<(), Self::Error> {{"
813 )
814 .unwrap();
815 writeln!(buf, " self.db.delete_ns(\"{table}\", id)").unwrap();
816 writeln!(buf, " }}").unwrap();
817 writeln!(buf).unwrap();
818
819 writeln!(
821 buf,
822 " fn list(&mut self) -> Result<Vec<(String, {name})>, Self::Error> {{"
823 )
824 .unwrap();
825 writeln!(
826 buf,
827 " let keys = self.db.list_keys_ns(\"{table}\")?;"
828 )
829 .unwrap();
830 writeln!(buf, " let mut result = Vec::new();").unwrap();
831 writeln!(buf, " for key in keys {{").unwrap();
832 writeln!(
833 buf,
834 " if let Some(entity) = self.db.load_ns(\"{table}\", &key)? {{"
835 )
836 .unwrap();
837 writeln!(buf, " result.push((key, entity));").unwrap();
838 writeln!(buf, " }}").unwrap();
839 writeln!(buf, " }}").unwrap();
840 writeln!(buf, " Ok(result)").unwrap();
841 writeln!(buf, " }}").unwrap();
842 writeln!(buf).unwrap();
843
844 writeln!(
846 buf,
847 " fn exists(&self, id: &str) -> Result<bool, Self::Error> {{"
848 )
849 .unwrap();
850 writeln!(buf, " self.db.exists_ns(\"{table}\", id)").unwrap();
851 writeln!(buf, " }}").unwrap();
852
853 let relation_fields = entity_relation_fields(entity);
855 for field in &relation_fields {
856 let param_type = relation_param_type(&field.field_type);
857 writeln!(buf).unwrap();
858 writeln!(
859 buf,
860 " fn find_by_{}(&mut self, val: {param_type}) -> Result<Vec<(String, {name})>, Self::Error> {{",
861 field.name
862 )
863 .unwrap();
864 writeln!(buf, " let all = self.list()?;").unwrap();
865 writeln!(
866 buf,
867 " Ok(all.into_iter().filter(|(_, e)| e.{} == val).collect())",
868 field.name
869 )
870 .unwrap();
871 writeln!(buf, " }}").unwrap();
872 }
873
874 writeln!(buf, "}}").unwrap();
875
876 (filename, buf)
877}
878
879pub fn generate_store_file(entities: &[Entity]) -> String {
887 let mut buf = String::new();
888 writeln!(buf, "{HEADER}").unwrap();
889 writeln!(buf, "use crdt_store::{{CrdtDb, MemoryStore, StateStore}};").unwrap();
890 writeln!(buf).unwrap();
891 writeln!(buf, "use super::migrations::create_db;").unwrap();
892
893 for entity in entities {
895 let name = &entity.name;
896 let snake = to_snake_case(name);
897 writeln!(
898 buf,
899 "use super::repositories::{snake}_repo::{name}RepositoryAccess;"
900 )
901 .unwrap();
902 }
903 writeln!(buf).unwrap();
904
905 writeln!(
907 buf,
908 "/// Unified persistence layer providing repository access for all entities."
909 )
910 .unwrap();
911 writeln!(buf, "///").unwrap();
912 writeln!(
913 buf,
914 "/// Owns a single `CrdtDb<S>` and provides scoped, typed access"
915 )
916 .unwrap();
917 writeln!(
918 buf,
919 "/// to each entity's repository through borrow-checked accessor methods."
920 )
921 .unwrap();
922 writeln!(buf, "///").unwrap();
923 writeln!(buf, "/// # Example").unwrap();
924 writeln!(buf, "///").unwrap();
925 writeln!(buf, "/// ```ignore").unwrap();
926 writeln!(
927 buf,
928 "/// let mut persistence = create_memory_persistence();"
929 )
930 .unwrap();
931
932 if let Some(first) = entities.first() {
933 let snake = to_snake_case(&first.name);
934 writeln!(buf, "/// let mut repo = persistence.{snake}s();").unwrap();
935 writeln!(buf, "/// repo.save(\"id-1\", &entity)?;").unwrap();
936 }
937
938 writeln!(buf, "/// ```").unwrap();
939 writeln!(buf, "pub struct Persistence<S: StateStore> {{").unwrap();
940 writeln!(buf, " db: CrdtDb<S>,").unwrap();
941 writeln!(buf, "}}").unwrap();
942 writeln!(buf).unwrap();
943
944 writeln!(buf, "impl<S: StateStore> Persistence<S> {{").unwrap();
945
946 writeln!(
948 buf,
949 " /// Create a new persistence layer with the given store."
950 )
951 .unwrap();
952 writeln!(buf, " ///").unwrap();
953 writeln!(buf, " /// All migrations are automatically registered.").unwrap();
954 writeln!(buf, " pub fn new(store: S) -> Self {{").unwrap();
955 writeln!(buf, " Persistence {{ db: create_db(store) }}").unwrap();
956 writeln!(buf, " }}").unwrap();
957 writeln!(buf).unwrap();
958
959 for entity in entities {
961 let name = &entity.name;
962 let snake = to_snake_case(name);
963 writeln!(buf, " /// Access the {name} repository.").unwrap();
964 writeln!(
965 buf,
966 " pub fn {snake}s(&mut self) -> {name}RepositoryAccess<'_, S> {{"
967 )
968 .unwrap();
969 writeln!(buf, " {name}RepositoryAccess::new(&mut self.db)").unwrap();
970 writeln!(buf, " }}").unwrap();
971 writeln!(buf).unwrap();
972 }
973
974 writeln!(buf, " /// Access the underlying `CrdtDb` (read-only).").unwrap();
976 writeln!(buf, " pub fn db(&self) -> &CrdtDb<S> {{").unwrap();
977 writeln!(buf, " &self.db").unwrap();
978 writeln!(buf, " }}").unwrap();
979 writeln!(buf).unwrap();
980
981 writeln!(buf, " /// Access the underlying `CrdtDb` (mutable).").unwrap();
982 writeln!(buf, " pub fn db_mut(&mut self) -> &mut CrdtDb<S> {{").unwrap();
983 writeln!(buf, " &mut self.db").unwrap();
984 writeln!(buf, " }}").unwrap();
985
986 writeln!(buf, "}}").unwrap();
987 writeln!(buf).unwrap();
988
989 writeln!(
991 buf,
992 "/// Create a persistence layer backed by an in-memory store."
993 )
994 .unwrap();
995 writeln!(buf, "///").unwrap();
996 writeln!(buf, "/// Useful for testing and prototyping.").unwrap();
997 writeln!(
998 buf,
999 "pub fn create_memory_persistence() -> Persistence<MemoryStore> {{"
1000 )
1001 .unwrap();
1002 writeln!(buf, " Persistence::new(MemoryStore::new())").unwrap();
1003 writeln!(buf, "}}").unwrap();
1004
1005 buf
1006}
1007
1008pub fn generate_event_types_file(entity: &Entity) -> (String, String) {
1015 let name = &entity.name;
1016 let snake = to_snake_case(name);
1017 let filename = format!("{snake}_events.rs");
1018
1019 let latest_fields = &entity
1021 .versions
1022 .last()
1023 .expect("entity must have at least one version")
1024 .fields;
1025
1026 let mut buf = String::new();
1027 writeln!(buf, "{HEADER}").unwrap();
1028 writeln!(buf, "use serde::{{Deserialize, Serialize}};").unwrap();
1029 writeln!(buf).unwrap();
1030
1031 writeln!(
1033 buf,
1034 "/// Events representing state changes for {name} entities."
1035 )
1036 .unwrap();
1037 writeln!(buf, "///").unwrap();
1038 writeln!(
1039 buf,
1040 "/// Used for event sourcing — each event captures a logical mutation"
1041 )
1042 .unwrap();
1043 writeln!(
1044 buf,
1045 "/// independent of the underlying CRDT representation."
1046 )
1047 .unwrap();
1048 writeln!(
1049 buf,
1050 "#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]"
1051 )
1052 .unwrap();
1053 writeln!(buf, "pub enum {name}Event {{").unwrap();
1054 writeln!(
1055 buf,
1056 " /// The entity was created with this initial state."
1057 )
1058 .unwrap();
1059 writeln!(buf, " Created({name}Snapshot),").unwrap();
1060 writeln!(buf, " /// A field was updated.").unwrap();
1061 writeln!(buf, " FieldUpdated({name}FieldUpdate),").unwrap();
1062 writeln!(buf, " /// The entity was deleted.").unwrap();
1063 writeln!(buf, " Deleted,").unwrap();
1064 writeln!(buf, "}}").unwrap();
1065 writeln!(buf).unwrap();
1066
1067 writeln!(
1069 buf,
1070 "/// Complete snapshot of a {name} entity's logical state."
1071 )
1072 .unwrap();
1073 writeln!(buf, "///").unwrap();
1074 writeln!(
1075 buf,
1076 "/// Field types represent logical values (not CRDT wrappers)."
1077 )
1078 .unwrap();
1079 writeln!(
1080 buf,
1081 "#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]"
1082 )
1083 .unwrap();
1084 writeln!(buf, "pub struct {name}Snapshot {{").unwrap();
1085 for field in latest_fields {
1086 let snapshot_type = field_snapshot_type(field);
1087 writeln!(buf, " pub {}: {snapshot_type},", field.name).unwrap();
1088 }
1089 writeln!(buf, "}}").unwrap();
1090 writeln!(buf).unwrap();
1091
1092 writeln!(buf, "/// Individual field updates for {name} entities.").unwrap();
1094 writeln!(
1095 buf,
1096 "#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]"
1097 )
1098 .unwrap();
1099 writeln!(buf, "pub enum {name}FieldUpdate {{").unwrap();
1100 for field in latest_fields {
1101 let variant = to_pascal_case(&field.name);
1102 let snapshot_type = field_snapshot_type(field);
1103 writeln!(buf, " /// Update to the `{}` field.", field.name).unwrap();
1104 writeln!(buf, " {variant}({snapshot_type}),").unwrap();
1105 }
1106 writeln!(buf, "}}").unwrap();
1107
1108 (filename, buf)
1109}
1110
1111pub fn generate_snapshot_policy_file(threshold: u64) -> String {
1113 let mut buf = String::new();
1114 writeln!(buf, "{HEADER}").unwrap();
1115 writeln!(buf, "use serde::{{Deserialize, Serialize}};").unwrap();
1116 writeln!(buf).unwrap();
1117
1118 writeln!(
1119 buf,
1120 "/// Policy for determining when to create entity snapshots."
1121 )
1122 .unwrap();
1123 writeln!(buf, "///").unwrap();
1124 writeln!(
1125 buf,
1126 "/// When event count reaches the threshold, a snapshot should be"
1127 )
1128 .unwrap();
1129 writeln!(
1130 buf,
1131 "/// taken to compact the event log and speed up entity reconstruction."
1132 )
1133 .unwrap();
1134 writeln!(buf, "#[derive(Debug, Clone, Serialize, Deserialize)]").unwrap();
1135 writeln!(buf, "pub struct SnapshotPolicy {{").unwrap();
1136 writeln!(
1137 buf,
1138 " /// Number of events before a snapshot is recommended."
1139 )
1140 .unwrap();
1141 writeln!(buf, " pub event_threshold: u64,").unwrap();
1142 writeln!(buf, "}}").unwrap();
1143 writeln!(buf).unwrap();
1144
1145 writeln!(buf, "impl SnapshotPolicy {{").unwrap();
1146 writeln!(
1147 buf,
1148 " /// Check whether a snapshot should be created based on the event count."
1149 )
1150 .unwrap();
1151 writeln!(
1152 buf,
1153 " pub fn should_snapshot(&self, event_count: u64) -> bool {{"
1154 )
1155 .unwrap();
1156 writeln!(buf, " event_count >= self.event_threshold").unwrap();
1157 writeln!(buf, " }}").unwrap();
1158 writeln!(buf, "}}").unwrap();
1159 writeln!(buf).unwrap();
1160
1161 writeln!(
1162 buf,
1163 "/// Default snapshot policy (threshold = {threshold})."
1164 )
1165 .unwrap();
1166 writeln!(
1167 buf,
1168 "pub const DEFAULT_POLICY: SnapshotPolicy = SnapshotPolicy {{"
1169 )
1170 .unwrap();
1171 writeln!(buf, " event_threshold: {threshold},").unwrap();
1172 writeln!(buf, "}};").unwrap();
1173
1174 buf
1175}
1176
1177pub fn generate_sync_file(entity: &Entity) -> (String, String) {
1185 let name = &entity.name;
1186 let snake = to_snake_case(name);
1187 let filename = format!("{snake}_sync.rs");
1188
1189 let crdt_fields = entity_crdt_fields(entity);
1190 let delta_fields = entity_delta_fields(entity);
1191 let has_deltas = !delta_fields.is_empty();
1192
1193 let mut buf = String::new();
1194 writeln!(buf, "{HEADER}").unwrap();
1195 writeln!(buf, "use crdt_kit::prelude::*;").unwrap();
1196 writeln!(buf).unwrap();
1197 writeln!(buf, "use super::super::models::{name};").unwrap();
1198 writeln!(buf).unwrap();
1199
1200 if has_deltas {
1202 writeln!(
1203 buf,
1204 "/// Delta state for incremental sync of {name} entities."
1205 )
1206 .unwrap();
1207 writeln!(buf, "///").unwrap();
1208 writeln!(
1209 buf,
1210 "/// Only includes fields backed by `DeltaCrdt`-capable types."
1211 )
1212 .unwrap();
1213 writeln!(
1214 buf,
1215 "/// Fields using state-based-only CRDTs (e.g., LWWRegister) are"
1216 )
1217 .unwrap();
1218 writeln!(buf, "/// synced via full-state merge instead.").unwrap();
1219 writeln!(buf, "#[derive(Debug, Clone)]").unwrap();
1220 writeln!(buf, "pub struct {name}Delta {{").unwrap();
1221 for field in &delta_fields {
1222 let delta_type = delta_field_type(field).unwrap();
1223 writeln!(buf, " /// Delta for the `{}` field.", field.name).unwrap();
1224 writeln!(buf, " pub {}: Option<{delta_type}>,", field.name).unwrap();
1225 }
1226 writeln!(buf, "}}").unwrap();
1227 writeln!(buf).unwrap();
1228
1229 writeln!(
1231 buf,
1232 "/// Compute the delta that `local` needs to catch up with `remote`."
1233 )
1234 .unwrap();
1235 writeln!(buf, "///").unwrap();
1236 writeln!(buf, "/// Returns what `remote` has that `local` doesn't.").unwrap();
1237 writeln!(
1238 buf,
1239 "pub fn compute_{snake}_delta(local: &{name}, remote: &{name}) -> {name}Delta {{"
1240 )
1241 .unwrap();
1242 writeln!(buf, " {name}Delta {{").unwrap();
1243 for field in &delta_fields {
1244 writeln!(
1245 buf,
1246 " {}: Some(remote.{}.delta(&local.{})),",
1247 field.name, field.name, field.name
1248 )
1249 .unwrap();
1250 }
1251 writeln!(buf, " }}").unwrap();
1252 writeln!(buf, "}}").unwrap();
1253 writeln!(buf).unwrap();
1254
1255 writeln!(buf, "/// Apply a delta to a {name} entity.").unwrap();
1257 writeln!(
1258 buf,
1259 "pub fn apply_{snake}_delta(entity: &mut {name}, delta: &{name}Delta) {{"
1260 )
1261 .unwrap();
1262 for field in &delta_fields {
1263 writeln!(buf, " if let Some(d) = &delta.{} {{", field.name).unwrap();
1264 writeln!(buf, " entity.{}.apply_delta(d);", field.name).unwrap();
1265 writeln!(buf, " }}").unwrap();
1266 }
1267 writeln!(buf, "}}").unwrap();
1268 writeln!(buf).unwrap();
1269 }
1270
1271 writeln!(buf, "/// Full-state merge for {name} entities.").unwrap();
1273 writeln!(buf, "///").unwrap();
1274 writeln!(
1275 buf,
1276 "/// Merges all CRDT fields using the `Crdt::merge` trait method."
1277 )
1278 .unwrap();
1279 writeln!(
1280 buf,
1281 "pub fn merge_{snake}(local: &mut {name}, remote: &{name}) {{"
1282 )
1283 .unwrap();
1284 for field in &crdt_fields {
1285 writeln!(
1286 buf,
1287 " local.{}.merge(&remote.{});",
1288 field.name, field.name
1289 )
1290 .unwrap();
1291 }
1292 writeln!(buf, "}}").unwrap();
1293
1294 (filename, buf)
1295}
1296
1297pub fn generate_persistence_mod_file(entities: &[Entity], config: &SchemaConfig) -> String {
1303 let mut buf = String::new();
1304 writeln!(buf, "{HEADER}").unwrap();
1305 writeln!(buf).unwrap();
1306
1307 writeln!(buf, "pub mod models;").unwrap();
1309 writeln!(buf, "pub mod migrations;").unwrap();
1310 writeln!(buf, "pub mod repositories;").unwrap();
1311 writeln!(buf, "pub mod store;").unwrap();
1312
1313 let events_enabled = config.events.as_ref().map(|e| e.enabled).unwrap_or(false);
1315 let sync_enabled = config.sync.as_ref().map(|s| s.enabled).unwrap_or(false);
1316
1317 if events_enabled {
1318 writeln!(buf, "pub mod events;").unwrap();
1319 }
1320 if sync_enabled {
1321 writeln!(buf, "pub mod sync;").unwrap();
1322 }
1323 writeln!(buf).unwrap();
1324
1325 writeln!(buf, "pub use models::*;").unwrap();
1327 writeln!(buf, "pub use migrations::{{create_db, create_memory_db}};").unwrap();
1328 writeln!(buf, "pub use repositories::traits::*;").unwrap();
1329 writeln!(buf, "pub use store::*;").unwrap();
1330
1331 if events_enabled {
1332 writeln!(buf, "pub use events::*;").unwrap();
1333 }
1334 if sync_enabled {
1335 writeln!(buf, "pub use sync::*;").unwrap();
1336 }
1337
1338 writeln!(buf).unwrap();
1340 writeln!(buf, "// Entities defined in this persistence layer:").unwrap();
1341 for entity in entities {
1342 let latest = entity.versions.iter().map(|v| v.version).max().unwrap_or(1);
1343 writeln!(
1344 buf,
1345 "// {} (table: \"{}\", latest version: v{latest})",
1346 entity.name, entity.table
1347 )
1348 .unwrap();
1349 }
1350
1351 buf
1352}
1353
1354#[cfg(test)]
1355mod tests {
1356 use super::*;
1357 use crate::schema::*;
1358
1359 fn make_field(name: &str, ty: &str, default: Option<&str>) -> Field {
1360 Field {
1361 name: name.into(),
1362 field_type: ty.into(),
1363 default: default.map(|s| s.into()),
1364 crdt: None,
1365 relation: None,
1366 }
1367 }
1368
1369 fn make_crdt_field(name: &str, ty: &str, crdt: &str) -> Field {
1370 Field {
1371 name: name.into(),
1372 field_type: ty.into(),
1373 default: None,
1374 crdt: Some(crdt.into()),
1375 relation: None,
1376 }
1377 }
1378
1379 fn make_relation_field(name: &str, ty: &str, relation: &str) -> Field {
1380 Field {
1381 name: name.into(),
1382 field_type: ty.into(),
1383 default: None,
1384 crdt: None,
1385 relation: Some(relation.into()),
1386 }
1387 }
1388
1389 fn make_entity_v1() -> Entity {
1390 Entity {
1391 name: "Task".into(),
1392 table: "tasks".into(),
1393 versions: vec![EntityVersion {
1394 version: 1,
1395 fields: vec![
1396 make_field("title", "String", None),
1397 make_field("done", "bool", None),
1398 ],
1399 }],
1400 }
1401 }
1402
1403 fn make_entity_v2() -> Entity {
1404 Entity {
1405 name: "Task".into(),
1406 table: "tasks".into(),
1407 versions: vec![
1408 EntityVersion {
1409 version: 1,
1410 fields: vec![
1411 make_field("title", "String", None),
1412 make_field("done", "bool", None),
1413 ],
1414 },
1415 EntityVersion {
1416 version: 2,
1417 fields: vec![
1418 make_field("title", "String", None),
1419 make_field("done", "bool", None),
1420 make_field("priority", "Option<u8>", Some("None")),
1421 ],
1422 },
1423 ],
1424 }
1425 }
1426
1427 fn make_crdt_entity() -> Entity {
1428 Entity {
1429 name: "Project".into(),
1430 table: "projects".into(),
1431 versions: vec![EntityVersion {
1432 version: 1,
1433 fields: vec![
1434 make_crdt_field("name", "String", "LWWRegister"),
1435 make_crdt_field("members", "String", "ORSet"),
1436 ],
1437 }],
1438 }
1439 }
1440
1441 fn make_entity_with_relation() -> Entity {
1442 Entity {
1443 name: "Task".into(),
1444 table: "tasks".into(),
1445 versions: vec![EntityVersion {
1446 version: 1,
1447 fields: vec![
1448 make_field("title", "String", None),
1449 make_relation_field("project_id", "String", "Project"),
1450 ],
1451 }],
1452 }
1453 }
1454
1455 fn make_config() -> SchemaConfig {
1456 SchemaConfig {
1457 output: "src/persistence".into(),
1458 events: None,
1459 sync: None,
1460 }
1461 }
1462
1463 #[test]
1466 fn to_snake_case_works() {
1467 assert_eq!(to_snake_case("Task"), "task");
1468 assert_eq!(to_snake_case("SensorReading"), "sensor_reading");
1469 assert_eq!(to_snake_case("IODevice"), "i_o_device");
1470 }
1471
1472 #[test]
1473 fn to_pascal_case_works() {
1474 assert_eq!(to_pascal_case("title"), "Title");
1475 assert_eq!(to_pascal_case("project_id"), "ProjectId");
1476 assert_eq!(to_pascal_case("done"), "Done");
1477 assert_eq!(to_pascal_case("created_at"), "CreatedAt");
1478 }
1479
1480 #[test]
1483 fn entity_file_single_version() {
1484 let entity = make_entity_v1();
1485 let (filename, content) = generate_entity_file(&entity);
1486 assert_eq!(filename, "task.rs");
1487 assert!(content.contains("AUTO-GENERATED"));
1488 assert!(content.contains("pub struct TaskV1"));
1489 assert!(content.contains("pub type Task = TaskV1;"));
1490 assert!(content.contains("#[crdt_schema(version = 1, table = \"tasks\")]"));
1491 assert!(content.contains("pub title: String,"));
1492 assert!(content.contains("pub done: bool,"));
1493 }
1494
1495 #[test]
1496 fn entity_file_two_versions() {
1497 let entity = make_entity_v2();
1498 let (filename, content) = generate_entity_file(&entity);
1499 assert_eq!(filename, "task.rs");
1500 assert!(content.contains("pub struct TaskV1"));
1501 assert!(content.contains("pub struct TaskV2"));
1502 assert!(content.contains("pub type Task = TaskV2;"));
1503 assert!(content.contains("pub priority: Option<u8>,"));
1504 }
1505
1506 #[test]
1507 fn crdt_field_wraps_type() {
1508 let entity = Entity {
1509 name: "Counter".into(),
1510 table: "counters".into(),
1511 versions: vec![EntityVersion {
1512 version: 1,
1513 fields: vec![
1514 make_crdt_field("title", "String", "LWWRegister"),
1515 make_crdt_field("views", "u64", "GCounter"),
1516 make_crdt_field("tags", "String", "ORSet"),
1517 ],
1518 }],
1519 };
1520 let (_filename, content) = generate_entity_file(&entity);
1521 assert!(content.contains("use crdt_kit::prelude::*;"));
1522 assert!(content.contains("pub title: LWWRegister<String>,"));
1523 assert!(content.contains("pub views: GCounter,"));
1524 assert!(content.contains("pub tags: ORSet<String>,"));
1525 }
1526
1527 #[test]
1528 fn no_crdt_import_without_crdt_fields() {
1529 let entity = make_entity_v1();
1530 let (_filename, content) = generate_entity_file(&entity);
1531 assert!(!content.contains("crdt_kit"));
1532 }
1533
1534 #[test]
1535 fn relation_field_plain_type() {
1536 let entity = make_entity_with_relation();
1537 let (_filename, content) = generate_entity_file(&entity);
1538 assert!(content.contains("pub project_id: String,"));
1539 }
1540
1541 #[test]
1544 fn migration_auto_generated() {
1545 let entity = make_entity_v2();
1546 let (filename, content) = generate_migration_file(&entity);
1547 assert_eq!(filename, "task_migrations.rs");
1548 assert!(content.contains("AUTO-GENERATED"));
1549 assert!(content.contains("#[migration(from = 1, to = 2)]"));
1550 assert!(content.contains("pub fn migrate_task_v1_to_v2"));
1551 assert!(content.contains("title: old.title,"));
1552 assert!(content.contains("done: old.done,"));
1553 assert!(content.contains("priority: None,"));
1554 assert!(content.contains("use super::super::models::{TaskV1, TaskV2};"));
1556 }
1557
1558 #[test]
1559 fn migration_todo_for_removed_field() {
1560 let entity = Entity {
1561 name: "Task".into(),
1562 table: "tasks".into(),
1563 versions: vec![
1564 EntityVersion {
1565 version: 1,
1566 fields: vec![
1567 make_field("title", "String", None),
1568 make_field("legacy", "String", None),
1569 ],
1570 },
1571 EntityVersion {
1572 version: 2,
1573 fields: vec![make_field("title", "String", None)],
1574 },
1575 ],
1576 };
1577 let (_filename, content) = generate_migration_file(&entity);
1578 assert!(content.contains("todo!"));
1579 assert!(content.contains("WARNING"));
1580 }
1581
1582 #[test]
1583 fn crdt_field_auto_default_in_migration() {
1584 let entity = Entity {
1585 name: "Task".into(),
1586 table: "tasks".into(),
1587 versions: vec![
1588 EntityVersion {
1589 version: 1,
1590 fields: vec![make_field("title", "String", None)],
1591 },
1592 EntityVersion {
1593 version: 2,
1594 fields: vec![
1595 make_field("title", "String", None),
1596 make_crdt_field("views", "u64", "GCounter"),
1597 ],
1598 },
1599 ],
1600 };
1601 let (_filename, content) = generate_migration_file(&entity);
1602 assert!(!content.contains("todo!"));
1603 assert!(content.contains("views: GCounter::new(\"_migrated\"),"));
1604 assert!(content.contains("use crdt_kit::prelude::*;"));
1605 }
1606
1607 #[test]
1610 fn helpers_file_with_migrations() {
1611 let entity = make_entity_v2();
1612 let content = generate_helpers_file(&[entity]);
1613 assert!(content.contains("AUTO-GENERATED"));
1614 assert!(content.contains("fn create_db"));
1615 assert!(content.contains("fn create_memory_db"));
1616 assert!(content.contains("register_migrate_task_v1_to_v2"));
1617 assert!(content.contains("CrdtDb::builder(store, 2)"));
1618 }
1619
1620 #[test]
1621 fn helpers_file_no_migrations() {
1622 let entity = make_entity_v1();
1623 let content = generate_helpers_file(&[entity]);
1624 assert!(content.contains("CrdtDb::with_store(store)"));
1625 assert!(!content.contains("register_migrate"));
1626 }
1627
1628 #[test]
1631 fn models_mod_file() {
1632 let entities = vec![make_entity_v1(), make_crdt_entity()];
1633 let content = generate_models_mod_file(&entities);
1634 assert!(content.contains("mod task;"));
1635 assert!(content.contains("mod project;"));
1636 assert!(content.contains("pub use task::*;"));
1637 assert!(content.contains("pub use project::*;"));
1638 }
1639
1640 #[test]
1641 fn migrations_mod_file() {
1642 let entities = vec![make_entity_v2(), make_crdt_entity()];
1643 let content = generate_migrations_mod_file(&entities);
1644 assert!(content.contains("mod helpers;"));
1645 assert!(content.contains("mod task_migrations;"));
1646 assert!(content.contains("pub use helpers::*;"));
1647 assert!(content.contains("pub use task_migrations::*;"));
1648 assert!(!content.contains("mod project_migrations;"));
1650 }
1651
1652 #[test]
1653 fn repositories_mod_file() {
1654 let entities = vec![make_entity_v1(), make_crdt_entity()];
1655 let content = generate_repositories_mod_file(&entities);
1656 assert!(content.contains("pub mod traits;"));
1657 assert!(content.contains("pub mod task_repo;"));
1658 assert!(content.contains("pub mod project_repo;"));
1659 assert!(content.contains("pub use traits::*;"));
1660 }
1661
1662 #[test]
1665 fn repository_traits_basic() {
1666 let entities = vec![make_entity_v1()];
1667 let content = generate_repository_traits_file(&entities);
1668 assert!(content.contains("pub trait TaskRepository"));
1669 assert!(content.contains("type Error: fmt::Debug + fmt::Display;"));
1670 assert!(
1671 content.contains("fn get(&mut self, id: &str) -> Result<Option<Task>, Self::Error>;")
1672 );
1673 assert!(content
1674 .contains("fn save(&mut self, id: &str, entity: &Task) -> Result<(), Self::Error>;"));
1675 assert!(content.contains("fn delete(&mut self, id: &str) -> Result<(), Self::Error>;"));
1676 assert!(content.contains("fn list(&mut self) -> Result<Vec<(String, Task)>, Self::Error>;"));
1677 assert!(content.contains("fn exists(&self, id: &str) -> Result<bool, Self::Error>;"));
1678 }
1679
1680 #[test]
1681 fn repository_traits_with_relations() {
1682 let entities = vec![make_entity_with_relation()];
1683 let content = generate_repository_traits_file(&entities);
1684 assert!(content.contains("fn find_by_project_id(&mut self, val: &str) -> Result<Vec<(String, Task)>, Self::Error>;"));
1685 }
1686
1687 #[test]
1688 fn repository_impl_file() {
1689 let entity = make_entity_with_relation();
1690 let (filename, content) = generate_repository_impl_file(&entity);
1691 assert_eq!(filename, "task_repo.rs");
1692 assert!(content.contains("pub struct TaskRepositoryAccess<'a, S: StateStore>"));
1693 assert!(
1694 content.contains("impl<S: StateStore> TaskRepository for TaskRepositoryAccess<'_, S>")
1695 );
1696 assert!(content.contains("type Error = DbError<S::Error>;"));
1697 assert!(content.contains("self.db.load_ns(\"tasks\", id)"));
1698 assert!(content.contains("self.db.save_ns(\"tasks\", id, entity)"));
1699 assert!(content.contains("self.db.delete_ns(\"tasks\", id)"));
1700 assert!(content.contains("self.db.exists_ns(\"tasks\", id)"));
1701 assert!(content.contains("fn find_by_project_id"));
1702 assert!(content.contains("e.project_id == val"));
1703 }
1704
1705 #[test]
1708 fn store_file() {
1709 let entities = vec![make_entity_v1(), make_crdt_entity()];
1710 let content = generate_store_file(&entities);
1711 assert!(content.contains("pub struct Persistence<S: StateStore>"));
1712 assert!(content.contains("pub fn tasks(&mut self) -> TaskRepositoryAccess<'_, S>"));
1713 assert!(content.contains("pub fn projects(&mut self) -> ProjectRepositoryAccess<'_, S>"));
1714 assert!(content.contains("pub fn db(&self) -> &CrdtDb<S>"));
1715 assert!(content.contains("pub fn db_mut(&mut self) -> &mut CrdtDb<S>"));
1716 assert!(content.contains("pub fn create_memory_persistence()"));
1717 }
1718
1719 #[test]
1722 fn event_types_file() {
1723 let entity = make_entity_v1();
1724 let (filename, content) = generate_event_types_file(&entity);
1725 assert_eq!(filename, "task_events.rs");
1726 assert!(content.contains("pub enum TaskEvent"));
1727 assert!(content.contains("Created(TaskSnapshot)"));
1728 assert!(content.contains("FieldUpdated(TaskFieldUpdate)"));
1729 assert!(content.contains("Deleted"));
1730 assert!(content.contains("pub struct TaskSnapshot"));
1731 assert!(content.contains("pub title: String,"));
1732 assert!(content.contains("pub done: bool,"));
1733 assert!(content.contains("pub enum TaskFieldUpdate"));
1734 assert!(content.contains("Title(String)"));
1735 assert!(content.contains("Done(bool)"));
1736 }
1737
1738 #[test]
1739 fn event_types_crdt_entity_uses_inner_types() {
1740 let entity = make_crdt_entity();
1741 let (_, content) = generate_event_types_file(&entity);
1742 assert!(content.contains("pub name: String,")); assert!(content.contains("pub members: Vec<String>,")); }
1746
1747 #[test]
1748 fn snapshot_policy_file() {
1749 let content = generate_snapshot_policy_file(200);
1750 assert!(content.contains("pub struct SnapshotPolicy"));
1751 assert!(content.contains("pub fn should_snapshot"));
1752 assert!(content.contains("event_threshold: 200,"));
1753 }
1754
1755 #[test]
1758 fn sync_file_with_delta_fields() {
1759 let entity = make_crdt_entity();
1760 let (filename, content) = generate_sync_file(&entity);
1761 assert_eq!(filename, "project_sync.rs");
1762 assert!(content.contains("pub struct ProjectDelta"));
1764 assert!(content.contains("pub members: Option<ORSetDelta<String>>,"));
1765 assert!(!content.contains("name: Option<")); assert!(content.contains("pub fn compute_project_delta"));
1768 assert!(content.contains("pub fn apply_project_delta"));
1769 assert!(content.contains("pub fn merge_project"));
1771 assert!(content.contains("local.name.merge(&remote.name);"));
1772 assert!(content.contains("local.members.merge(&remote.members);"));
1773 }
1774
1775 #[test]
1776 fn sync_file_state_only_crdts() {
1777 let entity = Entity {
1779 name: "Config".into(),
1780 table: "configs".into(),
1781 versions: vec![EntityVersion {
1782 version: 1,
1783 fields: vec![make_crdt_field("value", "String", "LWWRegister")],
1784 }],
1785 };
1786 let (_, content) = generate_sync_file(&entity);
1787 assert!(!content.contains("pub struct ConfigDelta"));
1789 assert!(!content.contains("compute_config_delta"));
1790 assert!(content.contains("pub fn merge_config"));
1792 assert!(content.contains("local.value.merge(&remote.value);"));
1793 }
1794
1795 #[test]
1798 fn persistence_mod_file_basic() {
1799 let entities = vec![make_entity_v1()];
1800 let config = make_config();
1801 let content = generate_persistence_mod_file(&entities, &config);
1802 assert!(content.contains("pub mod models;"));
1803 assert!(content.contains("pub mod migrations;"));
1804 assert!(content.contains("pub mod repositories;"));
1805 assert!(content.contains("pub mod store;"));
1806 assert!(!content.contains("pub mod events;"));
1807 assert!(!content.contains("pub mod sync;"));
1808 assert!(content.contains("pub use models::*;"));
1809 assert!(content.contains("pub use store::*;"));
1810 }
1811
1812 #[test]
1813 fn persistence_mod_file_with_events_sync() {
1814 let entities = vec![make_entity_v1()];
1815 let config = SchemaConfig {
1816 output: "src/persistence".into(),
1817 events: Some(EventsConfig {
1818 enabled: true,
1819 snapshot_threshold: 100,
1820 }),
1821 sync: Some(SyncConfig { enabled: true }),
1822 };
1823 let content = generate_persistence_mod_file(&entities, &config);
1824 assert!(content.contains("pub mod events;"));
1825 assert!(content.contains("pub mod sync;"));
1826 assert!(content.contains("pub use events::*;"));
1827 assert!(content.contains("pub use sync::*;"));
1828 }
1829}