use crate::schema::{lookup_crdt, lookup_delta_type, Entity, EntityVersion, Field, SchemaConfig};
use std::collections::HashSet;
use std::fmt::Write;
const HEADER: &str = "\
// ============================================================================
// AUTO-GENERATED by crdt-codegen -- DO NOT EDIT
//
// This file was generated from a crdt-schema.toml definition.
// Any manual changes will be overwritten on the next `crdt generate` run.
//
// To modify this code, edit the schema file and re-run:
// crdt generate --schema crdt-schema.toml
// ============================================================================
#![allow(dead_code, unused_imports)]
";
pub(crate) fn to_snake_case(s: &str) -> String {
let mut out = String::new();
for (i, ch) in s.chars().enumerate() {
if ch.is_ascii_uppercase() {
if i > 0 {
out.push('_');
}
out.push(ch.to_ascii_lowercase());
} else {
out.push(ch);
}
}
out
}
pub(crate) fn to_pascal_case(s: &str) -> String {
s.split('_')
.map(|part| {
let mut chars = part.chars();
match chars.next() {
None => String::new(),
Some(first) => {
let mut result = first.to_ascii_uppercase().to_string();
result.extend(chars);
result
}
}
})
.collect()
}
fn field_rust_type(field: &Field) -> String {
if let Some(crdt_name) = &field.crdt {
if let Some(info) = lookup_crdt(crdt_name) {
if info.is_generic {
return format!("{}<{}>", info.name, field.field_type);
} else {
return info.name.to_string();
}
}
}
field.field_type.clone()
}
fn entity_uses_crdt(entity: &Entity) -> bool {
entity
.versions
.iter()
.any(|v| v.fields.iter().any(|f| f.crdt.is_some()))
}
fn entity_relation_fields(entity: &Entity) -> Vec<&Field> {
entity
.versions
.last()
.map(|v| v.fields.iter().filter(|f| f.relation.is_some()).collect())
.unwrap_or_default()
}
fn entity_crdt_fields(entity: &Entity) -> Vec<&Field> {
entity
.versions
.last()
.map(|v| v.fields.iter().filter(|f| f.crdt.is_some()).collect())
.unwrap_or_default()
}
fn entity_delta_fields(entity: &Entity) -> Vec<&Field> {
entity
.versions
.last()
.map(|v| {
v.fields
.iter()
.filter(|f| f.crdt.as_ref().and_then(|c| lookup_delta_type(c)).is_some())
.collect()
})
.unwrap_or_default()
}
fn field_snapshot_type(field: &Field) -> String {
match field.crdt.as_deref() {
Some("GCounter") => "u64".into(),
Some("PNCounter") => "i64".into(),
Some("LWWRegister") => field.field_type.clone(),
Some("MVRegister") => format!("Vec<{}>", field.field_type),
Some("GSet" | "TwoPSet" | "ORSet") => format!("Vec<{}>", field.field_type),
_ => field.field_type.clone(),
}
}
fn delta_field_type(field: &Field) -> Option<String> {
let crdt_name = field.crdt.as_ref()?;
let delta_type = lookup_delta_type(crdt_name)?;
let crdt_info = lookup_crdt(crdt_name)?;
if crdt_info.is_generic {
Some(format!("{}<{}>", delta_type, field.field_type))
} else {
Some(delta_type.to_string())
}
}
fn relation_param_type(field_type: &str) -> String {
if field_type == "String" {
"&str".into()
} else {
field_type.into()
}
}
pub fn generate_entity_file(entity: &Entity) -> (String, String) {
let snake = to_snake_case(&entity.name);
let filename = format!("{snake}.rs");
let mut buf = String::new();
writeln!(buf, "{HEADER}").unwrap();
writeln!(buf, "use crdt_migrate::crdt_schema;").unwrap();
writeln!(buf, "use serde::{{Deserialize, Serialize}};").unwrap();
if entity_uses_crdt(entity) {
writeln!(buf, "use crdt_kit::prelude::*;").unwrap();
}
writeln!(buf).unwrap();
let latest = entity.versions.iter().map(|v| v.version).max().unwrap_or(1);
for ver in &entity.versions {
let is_latest = ver.version == latest;
let suffix = if is_latest { " (current)" } else { "" };
writeln!(
buf,
"/// {} entity -- version {}{suffix}",
entity.name, ver.version
)
.unwrap();
writeln!(
buf,
"#[crdt_schema(version = {}, table = \"{}\")]",
ver.version, entity.table
)
.unwrap();
writeln!(
buf,
"#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]"
)
.unwrap();
writeln!(buf, "pub struct {}V{} {{", entity.name, ver.version).unwrap();
for field in &ver.fields {
let rust_type = field_rust_type(field);
writeln!(buf, " pub {}: {},", field.name, rust_type).unwrap();
}
writeln!(buf, "}}").unwrap();
writeln!(buf).unwrap();
}
writeln!(buf, "/// Type alias for the current version.").unwrap();
writeln!(buf, "pub type {} = {}V{latest};", entity.name, entity.name).unwrap();
(filename, buf)
}
fn can_auto_migrate(from: &EntityVersion, to: &EntityVersion) -> bool {
let from_fields: HashSet<(&str, &str)> = from
.fields
.iter()
.map(|f| (f.name.as_str(), f.field_type.as_str()))
.collect();
for field in &to.fields {
let key = (field.name.as_str(), field.field_type.as_str());
if !from_fields.contains(&key) {
let has_crdt_default =
field.crdt.is_some() && lookup_crdt(field.crdt.as_deref().unwrap_or("")).is_some();
if field.default.is_none() && !has_crdt_default {
return false;
}
}
}
let to_names: HashSet<&str> = to.fields.iter().map(|f| f.name.as_str()).collect();
for field in &from.fields {
if !to_names.contains(field.name.as_str()) {
return false;
}
}
true
}
fn new_fields<'a>(from: &EntityVersion, to: &'a EntityVersion) -> Vec<&'a Field> {
let from_names: HashSet<&str> = from.fields.iter().map(|f| f.name.as_str()).collect();
to.fields
.iter()
.filter(|f| !from_names.contains(f.name.as_str()))
.collect()
}
pub fn generate_migration_file(entity: &Entity) -> (String, String) {
let snake = to_snake_case(&entity.name);
let filename = format!("{snake}_migrations.rs");
let mut buf = String::new();
writeln!(buf, "{HEADER}").unwrap();
if entity_uses_crdt(entity) {
writeln!(buf, "use crdt_kit::prelude::*;").unwrap();
}
writeln!(buf, "use crdt_migrate::migration;").unwrap();
let mut import_versions: Vec<u32> = Vec::new();
for window in entity.versions.windows(2) {
import_versions.push(window[0].version);
import_versions.push(window[1].version);
}
import_versions.sort();
import_versions.dedup();
let imports: Vec<String> = import_versions
.iter()
.map(|v| format!("{}V{v}", entity.name))
.collect();
writeln!(buf, "use super::super::models::{{{}}};", imports.join(", ")).unwrap();
writeln!(buf).unwrap();
for window in entity.versions.windows(2) {
let from_ver = &window[0];
let to_ver = &window[1];
let fn_name = format!(
"migrate_{snake}_v{}_to_v{}",
from_ver.version, to_ver.version
);
if can_auto_migrate(from_ver, to_ver) {
let added = new_fields(from_ver, to_ver);
let added_desc: Vec<String> = added
.iter()
.map(|f| {
let default_desc = if let Some(d) = &f.default {
d.clone()
} else if let Some(crdt_name) = &f.crdt {
lookup_crdt(crdt_name)
.map(|c| c.default_expr.to_string())
.unwrap_or_else(|| "?".into())
} else {
"?".into()
};
format!("{} (default: {})", f.name, default_desc)
})
.collect();
writeln!(
buf,
"/// Auto-generated migration: {} v{} -> v{}",
entity.name, from_ver.version, to_ver.version
)
.unwrap();
if !added_desc.is_empty() {
writeln!(buf, "///").unwrap();
writeln!(buf, "/// Added fields: {}", added_desc.join(", ")).unwrap();
}
writeln!(
buf,
"#[migration(from = {}, to = {})]",
from_ver.version, to_ver.version
)
.unwrap();
writeln!(
buf,
"pub fn {fn_name}(old: {}V{}) -> {}V{} {{",
entity.name, from_ver.version, entity.name, to_ver.version
)
.unwrap();
writeln!(buf, " {}V{} {{", entity.name, to_ver.version).unwrap();
let from_names: HashSet<&str> =
from_ver.fields.iter().map(|f| f.name.as_str()).collect();
for field in &to_ver.fields {
if from_names.contains(field.name.as_str()) {
writeln!(buf, " {}: old.{},", field.name, field.name).unwrap();
} else {
let default = if let Some(d) = &field.default {
d.clone()
} else if let Some(crdt_name) = &field.crdt {
lookup_crdt(crdt_name)
.map(|c| c.default_expr.to_string())
.unwrap_or_else(|| "Default::default()".into())
} else {
"Default::default()".into()
};
writeln!(buf, " {}: {default},", field.name).unwrap();
}
}
writeln!(buf, " }}").unwrap();
writeln!(buf, "}}").unwrap();
} else {
writeln!(
buf,
"/// Migration: {} v{} -> v{}",
entity.name, from_ver.version, to_ver.version
)
.unwrap();
writeln!(buf, "///").unwrap();
writeln!(
buf,
"/// WARNING: This migration requires manual implementation."
)
.unwrap();
writeln!(
buf,
"#[migration(from = {}, to = {})]",
from_ver.version, to_ver.version
)
.unwrap();
writeln!(
buf,
"pub fn {fn_name}(old: {}V{}) -> {}V{} {{",
entity.name, from_ver.version, entity.name, to_ver.version
)
.unwrap();
writeln!(
buf,
" todo!(\"Implement migration from {} v{} to v{}\")",
entity.name, from_ver.version, to_ver.version
)
.unwrap();
writeln!(buf, "}}").unwrap();
}
writeln!(buf).unwrap();
}
(filename, buf)
}
pub fn generate_helpers_file(entities: &[Entity]) -> String {
let mut buf = String::new();
writeln!(buf, "{HEADER}").unwrap();
writeln!(buf, "use crdt_store::{{CrdtDb, MemoryStore, StateStore}};").unwrap();
let mut register_fns: Vec<String> = Vec::new();
for entity in entities {
if entity.versions.len() <= 1 {
continue;
}
let snake = to_snake_case(&entity.name);
for window in entity.versions.windows(2) {
let fn_name = format!(
"register_migrate_{snake}_v{}_to_v{}",
window[0].version, window[1].version
);
register_fns.push(format!("super::{snake}_migrations::{fn_name}"));
}
}
writeln!(buf).unwrap();
let max_version = entities
.iter()
.flat_map(|e| e.versions.iter().map(|v| v.version))
.max()
.unwrap_or(1);
writeln!(
buf,
"/// Create a [`CrdtDb`] with all generated migrations registered."
)
.unwrap();
writeln!(buf, "///").unwrap();
writeln!(
buf,
"/// The database is configured for schema version {max_version} (the latest"
)
.unwrap();
writeln!(buf, "/// version defined in the schema).").unwrap();
writeln!(
buf,
"pub fn create_db<S: StateStore>(store: S) -> CrdtDb<S> {{"
)
.unwrap();
if register_fns.is_empty() {
writeln!(buf, " CrdtDb::with_store(store)").unwrap();
} else {
writeln!(buf, " CrdtDb::builder(store, {max_version})").unwrap();
for reg in ®ister_fns {
writeln!(buf, " .register_migration({reg}())").unwrap();
}
writeln!(buf, " .build()").unwrap();
}
writeln!(buf, "}}").unwrap();
writeln!(buf).unwrap();
writeln!(
buf,
"/// Create a [`CrdtDb`] with [`MemoryStore`] for testing."
)
.unwrap();
writeln!(buf, "pub fn create_memory_db() -> CrdtDb<MemoryStore> {{").unwrap();
writeln!(buf, " create_db(MemoryStore::new())").unwrap();
writeln!(buf, "}}").unwrap();
buf
}
pub fn generate_models_mod_file(entities: &[Entity]) -> String {
let mut buf = String::new();
writeln!(buf, "{HEADER}").unwrap();
writeln!(buf).unwrap();
for entity in entities {
let snake = to_snake_case(&entity.name);
writeln!(buf, "mod {snake};").unwrap();
}
writeln!(buf).unwrap();
for entity in entities {
let snake = to_snake_case(&entity.name);
writeln!(buf, "pub use {snake}::*;").unwrap();
}
buf
}
pub fn generate_migrations_mod_file(entities: &[Entity]) -> String {
let mut buf = String::new();
writeln!(buf, "{HEADER}").unwrap();
writeln!(buf).unwrap();
writeln!(buf, "mod helpers;").unwrap();
for entity in entities {
if entity.versions.len() > 1 {
let snake = to_snake_case(&entity.name);
writeln!(buf, "mod {snake}_migrations;").unwrap();
}
}
writeln!(buf).unwrap();
writeln!(buf, "pub use helpers::*;").unwrap();
for entity in entities {
if entity.versions.len() > 1 {
let snake = to_snake_case(&entity.name);
writeln!(buf, "pub use {snake}_migrations::*;").unwrap();
}
}
buf
}
pub fn generate_repositories_mod_file(entities: &[Entity]) -> String {
let mut buf = String::new();
writeln!(buf, "{HEADER}").unwrap();
writeln!(buf).unwrap();
writeln!(buf, "pub mod traits;").unwrap();
for entity in entities {
let snake = to_snake_case(&entity.name);
writeln!(buf, "pub mod {snake}_repo;").unwrap();
}
writeln!(buf).unwrap();
writeln!(buf, "pub use traits::*;").unwrap();
for entity in entities {
let snake = to_snake_case(&entity.name);
writeln!(buf, "pub use {snake}_repo::*;").unwrap();
}
buf
}
pub fn generate_events_mod_file(entities: &[Entity]) -> String {
let mut buf = String::new();
writeln!(buf, "{HEADER}").unwrap();
writeln!(buf).unwrap();
writeln!(buf, "mod policies;").unwrap();
for entity in entities {
let snake = to_snake_case(&entity.name);
writeln!(buf, "mod {snake}_events;").unwrap();
}
writeln!(buf).unwrap();
writeln!(buf, "pub use policies::*;").unwrap();
for entity in entities {
let snake = to_snake_case(&entity.name);
writeln!(buf, "pub use {snake}_events::*;").unwrap();
}
buf
}
pub fn generate_sync_mod_file(entities: &[Entity]) -> String {
let mut buf = String::new();
writeln!(buf, "{HEADER}").unwrap();
writeln!(buf).unwrap();
let crdt_entities: Vec<&Entity> = entities.iter().filter(|e| entity_uses_crdt(e)).collect();
for entity in &crdt_entities {
let snake = to_snake_case(&entity.name);
writeln!(buf, "mod {snake}_sync;").unwrap();
}
writeln!(buf).unwrap();
for entity in &crdt_entities {
let snake = to_snake_case(&entity.name);
writeln!(buf, "pub use {snake}_sync::*;").unwrap();
}
buf
}
pub fn generate_repository_traits_file(entities: &[Entity]) -> String {
let mut buf = String::new();
writeln!(buf, "{HEADER}").unwrap();
writeln!(buf, "use std::fmt;").unwrap();
writeln!(buf).unwrap();
let imports: Vec<String> = entities.iter().map(|e| e.name.clone()).collect();
writeln!(buf, "use super::super::models::{{{}}};", imports.join(", ")).unwrap();
writeln!(buf).unwrap();
for entity in entities {
let name = &entity.name;
let relation_fields = entity_relation_fields(entity);
writeln!(
buf,
"/// Repository trait for {name} entities (port in hexagonal architecture)."
)
.unwrap();
writeln!(buf, "///").unwrap();
writeln!(
buf,
"/// Implement this trait to provide persistence for {name} entities."
)
.unwrap();
writeln!(
buf,
"/// The default adapter uses `CrdtDb<S>` (see `{name}RepositoryAccess`)."
)
.unwrap();
writeln!(buf, "pub trait {name}Repository {{").unwrap();
writeln!(buf, " /// Error type returned by repository operations.").unwrap();
writeln!(buf, " type Error: fmt::Debug + fmt::Display;").unwrap();
writeln!(buf).unwrap();
writeln!(buf, " /// Load a {name} by its identifier.").unwrap();
writeln!(
buf,
" fn get(&mut self, id: &str) -> Result<Option<{name}>, Self::Error>;"
)
.unwrap();
writeln!(buf).unwrap();
writeln!(buf, " /// Persist a {name} with the given identifier.").unwrap();
writeln!(
buf,
" fn save(&mut self, id: &str, entity: &{name}) -> Result<(), Self::Error>;"
)
.unwrap();
writeln!(buf).unwrap();
writeln!(buf, " /// Delete a {name} by its identifier.").unwrap();
writeln!(
buf,
" fn delete(&mut self, id: &str) -> Result<(), Self::Error>;"
)
.unwrap();
writeln!(buf).unwrap();
writeln!(
buf,
" /// List all {name} entities as `(id, entity)` pairs."
)
.unwrap();
writeln!(
buf,
" fn list(&mut self) -> Result<Vec<(String, {name})>, Self::Error>;"
)
.unwrap();
writeln!(buf).unwrap();
writeln!(
buf,
" /// Check whether a {name} with the given identifier exists."
)
.unwrap();
writeln!(
buf,
" fn exists(&self, id: &str) -> Result<bool, Self::Error>;"
)
.unwrap();
for field in &relation_fields {
let param_type = relation_param_type(&field.field_type);
let rel_name = field.relation.as_ref().unwrap();
writeln!(buf).unwrap();
writeln!(
buf,
" /// Find all {name} entities related to a {rel_name} by `{}`.",
field.name
)
.unwrap();
writeln!(
buf,
" fn find_by_{}(&mut self, val: {param_type}) -> Result<Vec<(String, {name})>, Self::Error>;",
field.name
)
.unwrap();
}
writeln!(buf, "}}").unwrap();
writeln!(buf).unwrap();
}
buf
}
pub fn generate_repository_impl_file(entity: &Entity) -> (String, String) {
let name = &entity.name;
let snake = to_snake_case(name);
let filename = format!("{snake}_repo.rs");
let table = &entity.table;
let mut buf = String::new();
writeln!(buf, "{HEADER}").unwrap();
writeln!(buf, "use crdt_store::{{CrdtDb, DbError, StateStore}};").unwrap();
writeln!(buf).unwrap();
writeln!(buf, "use super::super::models::{name};").unwrap();
writeln!(buf, "use super::traits::{name}Repository;").unwrap();
writeln!(buf).unwrap();
writeln!(
buf,
"/// Repository adapter for {name} entities backed by `CrdtDb<S>`."
)
.unwrap();
writeln!(buf, "///").unwrap();
writeln!(
buf,
"/// This is the \"adapter\" in hexagonal architecture. It implements"
)
.unwrap();
writeln!(
buf,
"/// `{name}Repository` using the CRDT-aware versioned store."
)
.unwrap();
writeln!(
buf,
"pub struct {name}RepositoryAccess<'a, S: StateStore> {{"
)
.unwrap();
writeln!(buf, " db: &'a mut CrdtDb<S>,").unwrap();
writeln!(buf, "}}").unwrap();
writeln!(buf).unwrap();
writeln!(
buf,
"impl<'a, S: StateStore> {name}RepositoryAccess<'a, S> {{"
)
.unwrap();
writeln!(
buf,
" /// Create a new repository accessor with a mutable reference to the database."
)
.unwrap();
writeln!(buf, " pub fn new(db: &'a mut CrdtDb<S>) -> Self {{").unwrap();
writeln!(buf, " Self {{ db }}").unwrap();
writeln!(buf, " }}").unwrap();
writeln!(buf, "}}").unwrap();
writeln!(buf).unwrap();
writeln!(
buf,
"impl<S: StateStore> {name}Repository for {name}RepositoryAccess<'_, S> {{"
)
.unwrap();
writeln!(buf, " type Error = DbError<S::Error>;").unwrap();
writeln!(buf).unwrap();
writeln!(
buf,
" fn get(&mut self, id: &str) -> Result<Option<{name}>, Self::Error> {{"
)
.unwrap();
writeln!(buf, " self.db.load_ns(\"{table}\", id)").unwrap();
writeln!(buf, " }}").unwrap();
writeln!(buf).unwrap();
writeln!(
buf,
" fn save(&mut self, id: &str, entity: &{name}) -> Result<(), Self::Error> {{"
)
.unwrap();
writeln!(buf, " self.db.save_ns(\"{table}\", id, entity)").unwrap();
writeln!(buf, " }}").unwrap();
writeln!(buf).unwrap();
writeln!(
buf,
" fn delete(&mut self, id: &str) -> Result<(), Self::Error> {{"
)
.unwrap();
writeln!(buf, " self.db.delete_ns(\"{table}\", id)").unwrap();
writeln!(buf, " }}").unwrap();
writeln!(buf).unwrap();
writeln!(
buf,
" fn list(&mut self) -> Result<Vec<(String, {name})>, Self::Error> {{"
)
.unwrap();
writeln!(
buf,
" let keys = self.db.list_keys_ns(\"{table}\")?;"
)
.unwrap();
writeln!(buf, " let mut result = Vec::new();").unwrap();
writeln!(buf, " for key in keys {{").unwrap();
writeln!(
buf,
" if let Some(entity) = self.db.load_ns(\"{table}\", &key)? {{"
)
.unwrap();
writeln!(buf, " result.push((key, entity));").unwrap();
writeln!(buf, " }}").unwrap();
writeln!(buf, " }}").unwrap();
writeln!(buf, " Ok(result)").unwrap();
writeln!(buf, " }}").unwrap();
writeln!(buf).unwrap();
writeln!(
buf,
" fn exists(&self, id: &str) -> Result<bool, Self::Error> {{"
)
.unwrap();
writeln!(buf, " self.db.exists_ns(\"{table}\", id)").unwrap();
writeln!(buf, " }}").unwrap();
let relation_fields = entity_relation_fields(entity);
for field in &relation_fields {
let param_type = relation_param_type(&field.field_type);
writeln!(buf).unwrap();
writeln!(
buf,
" fn find_by_{}(&mut self, val: {param_type}) -> Result<Vec<(String, {name})>, Self::Error> {{",
field.name
)
.unwrap();
writeln!(buf, " let all = self.list()?;").unwrap();
writeln!(
buf,
" Ok(all.into_iter().filter(|(_, e)| e.{} == val).collect())",
field.name
)
.unwrap();
writeln!(buf, " }}").unwrap();
}
writeln!(buf, "}}").unwrap();
(filename, buf)
}
pub fn generate_store_file(entities: &[Entity]) -> String {
let mut buf = String::new();
writeln!(buf, "{HEADER}").unwrap();
writeln!(buf, "use crdt_store::{{CrdtDb, MemoryStore, StateStore}};").unwrap();
writeln!(buf).unwrap();
writeln!(buf, "use super::migrations::create_db;").unwrap();
for entity in entities {
let name = &entity.name;
let snake = to_snake_case(name);
writeln!(
buf,
"use super::repositories::{snake}_repo::{name}RepositoryAccess;"
)
.unwrap();
}
writeln!(buf).unwrap();
writeln!(
buf,
"/// Unified persistence layer providing repository access for all entities."
)
.unwrap();
writeln!(buf, "///").unwrap();
writeln!(
buf,
"/// Owns a single `CrdtDb<S>` and provides scoped, typed access"
)
.unwrap();
writeln!(
buf,
"/// to each entity's repository through borrow-checked accessor methods."
)
.unwrap();
writeln!(buf, "///").unwrap();
writeln!(buf, "/// # Example").unwrap();
writeln!(buf, "///").unwrap();
writeln!(buf, "/// ```ignore").unwrap();
writeln!(
buf,
"/// let mut persistence = create_memory_persistence();"
)
.unwrap();
if let Some(first) = entities.first() {
let snake = to_snake_case(&first.name);
writeln!(buf, "/// let mut repo = persistence.{snake}s();").unwrap();
writeln!(buf, "/// repo.save(\"id-1\", &entity)?;").unwrap();
}
writeln!(buf, "/// ```").unwrap();
writeln!(buf, "pub struct Persistence<S: StateStore> {{").unwrap();
writeln!(buf, " db: CrdtDb<S>,").unwrap();
writeln!(buf, "}}").unwrap();
writeln!(buf).unwrap();
writeln!(buf, "impl<S: StateStore> Persistence<S> {{").unwrap();
writeln!(
buf,
" /// Create a new persistence layer with the given store."
)
.unwrap();
writeln!(buf, " ///").unwrap();
writeln!(buf, " /// All migrations are automatically registered.").unwrap();
writeln!(buf, " pub fn new(store: S) -> Self {{").unwrap();
writeln!(buf, " Persistence {{ db: create_db(store) }}").unwrap();
writeln!(buf, " }}").unwrap();
writeln!(buf).unwrap();
for entity in entities {
let name = &entity.name;
let snake = to_snake_case(name);
writeln!(buf, " /// Access the {name} repository.").unwrap();
writeln!(
buf,
" pub fn {snake}s(&mut self) -> {name}RepositoryAccess<'_, S> {{"
)
.unwrap();
writeln!(buf, " {name}RepositoryAccess::new(&mut self.db)").unwrap();
writeln!(buf, " }}").unwrap();
writeln!(buf).unwrap();
}
writeln!(buf, " /// Access the underlying `CrdtDb` (read-only).").unwrap();
writeln!(buf, " pub fn db(&self) -> &CrdtDb<S> {{").unwrap();
writeln!(buf, " &self.db").unwrap();
writeln!(buf, " }}").unwrap();
writeln!(buf).unwrap();
writeln!(buf, " /// Access the underlying `CrdtDb` (mutable).").unwrap();
writeln!(buf, " pub fn db_mut(&mut self) -> &mut CrdtDb<S> {{").unwrap();
writeln!(buf, " &mut self.db").unwrap();
writeln!(buf, " }}").unwrap();
writeln!(buf, "}}").unwrap();
writeln!(buf).unwrap();
writeln!(
buf,
"/// Create a persistence layer backed by an in-memory store."
)
.unwrap();
writeln!(buf, "///").unwrap();
writeln!(buf, "/// Useful for testing and prototyping.").unwrap();
writeln!(
buf,
"pub fn create_memory_persistence() -> Persistence<MemoryStore> {{"
)
.unwrap();
writeln!(buf, " Persistence::new(MemoryStore::new())").unwrap();
writeln!(buf, "}}").unwrap();
buf
}
pub fn generate_event_types_file(entity: &Entity) -> (String, String) {
let name = &entity.name;
let snake = to_snake_case(name);
let filename = format!("{snake}_events.rs");
let latest_fields = &entity
.versions
.last()
.expect("entity must have at least one version")
.fields;
let mut buf = String::new();
writeln!(buf, "{HEADER}").unwrap();
writeln!(buf, "use serde::{{Deserialize, Serialize}};").unwrap();
writeln!(buf).unwrap();
writeln!(
buf,
"/// Events representing state changes for {name} entities."
)
.unwrap();
writeln!(buf, "///").unwrap();
writeln!(
buf,
"/// Used for event sourcing — each event captures a logical mutation"
)
.unwrap();
writeln!(
buf,
"/// independent of the underlying CRDT representation."
)
.unwrap();
writeln!(
buf,
"#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]"
)
.unwrap();
writeln!(buf, "pub enum {name}Event {{").unwrap();
writeln!(
buf,
" /// The entity was created with this initial state."
)
.unwrap();
writeln!(buf, " Created({name}Snapshot),").unwrap();
writeln!(buf, " /// A field was updated.").unwrap();
writeln!(buf, " FieldUpdated({name}FieldUpdate),").unwrap();
writeln!(buf, " /// The entity was deleted.").unwrap();
writeln!(buf, " Deleted,").unwrap();
writeln!(buf, "}}").unwrap();
writeln!(buf).unwrap();
writeln!(
buf,
"/// Complete snapshot of a {name} entity's logical state."
)
.unwrap();
writeln!(buf, "///").unwrap();
writeln!(
buf,
"/// Field types represent logical values (not CRDT wrappers)."
)
.unwrap();
writeln!(
buf,
"#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]"
)
.unwrap();
writeln!(buf, "pub struct {name}Snapshot {{").unwrap();
for field in latest_fields {
let snapshot_type = field_snapshot_type(field);
writeln!(buf, " pub {}: {snapshot_type},", field.name).unwrap();
}
writeln!(buf, "}}").unwrap();
writeln!(buf).unwrap();
writeln!(buf, "/// Individual field updates for {name} entities.").unwrap();
writeln!(
buf,
"#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]"
)
.unwrap();
writeln!(buf, "pub enum {name}FieldUpdate {{").unwrap();
for field in latest_fields {
let variant = to_pascal_case(&field.name);
let snapshot_type = field_snapshot_type(field);
writeln!(buf, " /// Update to the `{}` field.", field.name).unwrap();
writeln!(buf, " {variant}({snapshot_type}),").unwrap();
}
writeln!(buf, "}}").unwrap();
(filename, buf)
}
pub fn generate_snapshot_policy_file(threshold: u64) -> String {
let mut buf = String::new();
writeln!(buf, "{HEADER}").unwrap();
writeln!(buf, "use serde::{{Deserialize, Serialize}};").unwrap();
writeln!(buf).unwrap();
writeln!(
buf,
"/// Policy for determining when to create entity snapshots."
)
.unwrap();
writeln!(buf, "///").unwrap();
writeln!(
buf,
"/// When event count reaches the threshold, a snapshot should be"
)
.unwrap();
writeln!(
buf,
"/// taken to compact the event log and speed up entity reconstruction."
)
.unwrap();
writeln!(buf, "#[derive(Debug, Clone, Serialize, Deserialize)]").unwrap();
writeln!(buf, "pub struct SnapshotPolicy {{").unwrap();
writeln!(
buf,
" /// Number of events before a snapshot is recommended."
)
.unwrap();
writeln!(buf, " pub event_threshold: u64,").unwrap();
writeln!(buf, "}}").unwrap();
writeln!(buf).unwrap();
writeln!(buf, "impl SnapshotPolicy {{").unwrap();
writeln!(
buf,
" /// Check whether a snapshot should be created based on the event count."
)
.unwrap();
writeln!(
buf,
" pub fn should_snapshot(&self, event_count: u64) -> bool {{"
)
.unwrap();
writeln!(buf, " event_count >= self.event_threshold").unwrap();
writeln!(buf, " }}").unwrap();
writeln!(buf, "}}").unwrap();
writeln!(buf).unwrap();
writeln!(
buf,
"/// Default snapshot policy (threshold = {threshold})."
)
.unwrap();
writeln!(
buf,
"pub const DEFAULT_POLICY: SnapshotPolicy = SnapshotPolicy {{"
)
.unwrap();
writeln!(buf, " event_threshold: {threshold},").unwrap();
writeln!(buf, "}};").unwrap();
buf
}
pub fn generate_sync_file(entity: &Entity) -> (String, String) {
let name = &entity.name;
let snake = to_snake_case(name);
let filename = format!("{snake}_sync.rs");
let crdt_fields = entity_crdt_fields(entity);
let delta_fields = entity_delta_fields(entity);
let has_deltas = !delta_fields.is_empty();
let mut buf = String::new();
writeln!(buf, "{HEADER}").unwrap();
writeln!(buf, "use crdt_kit::prelude::*;").unwrap();
writeln!(buf).unwrap();
writeln!(buf, "use super::super::models::{name};").unwrap();
writeln!(buf).unwrap();
if has_deltas {
writeln!(
buf,
"/// Delta state for incremental sync of {name} entities."
)
.unwrap();
writeln!(buf, "///").unwrap();
writeln!(
buf,
"/// Only includes fields backed by `DeltaCrdt`-capable types."
)
.unwrap();
writeln!(
buf,
"/// Fields using state-based-only CRDTs (e.g., LWWRegister) are"
)
.unwrap();
writeln!(buf, "/// synced via full-state merge instead.").unwrap();
writeln!(buf, "#[derive(Debug, Clone)]").unwrap();
writeln!(buf, "pub struct {name}Delta {{").unwrap();
for field in &delta_fields {
let delta_type = delta_field_type(field).unwrap();
writeln!(buf, " /// Delta for the `{}` field.", field.name).unwrap();
writeln!(buf, " pub {}: Option<{delta_type}>,", field.name).unwrap();
}
writeln!(buf, "}}").unwrap();
writeln!(buf).unwrap();
writeln!(
buf,
"/// Compute the delta that `local` needs to catch up with `remote`."
)
.unwrap();
writeln!(buf, "///").unwrap();
writeln!(buf, "/// Returns what `remote` has that `local` doesn't.").unwrap();
writeln!(
buf,
"pub fn compute_{snake}_delta(local: &{name}, remote: &{name}) -> {name}Delta {{"
)
.unwrap();
writeln!(buf, " {name}Delta {{").unwrap();
for field in &delta_fields {
writeln!(
buf,
" {}: Some(remote.{}.delta(&local.{})),",
field.name, field.name, field.name
)
.unwrap();
}
writeln!(buf, " }}").unwrap();
writeln!(buf, "}}").unwrap();
writeln!(buf).unwrap();
writeln!(buf, "/// Apply a delta to a {name} entity.").unwrap();
writeln!(
buf,
"pub fn apply_{snake}_delta(entity: &mut {name}, delta: &{name}Delta) {{"
)
.unwrap();
for field in &delta_fields {
writeln!(buf, " if let Some(d) = &delta.{} {{", field.name).unwrap();
writeln!(buf, " entity.{}.apply_delta(d);", field.name).unwrap();
writeln!(buf, " }}").unwrap();
}
writeln!(buf, "}}").unwrap();
writeln!(buf).unwrap();
}
writeln!(buf, "/// Full-state merge for {name} entities.").unwrap();
writeln!(buf, "///").unwrap();
writeln!(
buf,
"/// Merges all CRDT fields using the `Crdt::merge` trait method."
)
.unwrap();
writeln!(
buf,
"pub fn merge_{snake}(local: &mut {name}, remote: &{name}) {{"
)
.unwrap();
for field in &crdt_fields {
writeln!(
buf,
" local.{}.merge(&remote.{});",
field.name, field.name
)
.unwrap();
}
writeln!(buf, "}}").unwrap();
(filename, buf)
}
pub fn generate_persistence_mod_file(entities: &[Entity], config: &SchemaConfig) -> String {
let mut buf = String::new();
writeln!(buf, "{HEADER}").unwrap();
writeln!(buf).unwrap();
writeln!(buf, "pub mod models;").unwrap();
writeln!(buf, "pub mod migrations;").unwrap();
writeln!(buf, "pub mod repositories;").unwrap();
writeln!(buf, "pub mod store;").unwrap();
let events_enabled = config.events.as_ref().map(|e| e.enabled).unwrap_or(false);
let sync_enabled = config.sync.as_ref().map(|s| s.enabled).unwrap_or(false);
if events_enabled {
writeln!(buf, "pub mod events;").unwrap();
}
if sync_enabled {
writeln!(buf, "pub mod sync;").unwrap();
}
writeln!(buf).unwrap();
writeln!(buf, "pub use models::*;").unwrap();
writeln!(buf, "pub use migrations::{{create_db, create_memory_db}};").unwrap();
writeln!(buf, "pub use repositories::traits::*;").unwrap();
writeln!(buf, "pub use store::*;").unwrap();
if events_enabled {
writeln!(buf, "pub use events::*;").unwrap();
}
if sync_enabled {
writeln!(buf, "pub use sync::*;").unwrap();
}
writeln!(buf).unwrap();
writeln!(buf, "// Entities defined in this persistence layer:").unwrap();
for entity in entities {
let latest = entity.versions.iter().map(|v| v.version).max().unwrap_or(1);
writeln!(
buf,
"// {} (table: \"{}\", latest version: v{latest})",
entity.name, entity.table
)
.unwrap();
}
buf
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::*;
fn make_field(name: &str, ty: &str, default: Option<&str>) -> Field {
Field {
name: name.into(),
field_type: ty.into(),
default: default.map(|s| s.into()),
crdt: None,
relation: None,
}
}
fn make_crdt_field(name: &str, ty: &str, crdt: &str) -> Field {
Field {
name: name.into(),
field_type: ty.into(),
default: None,
crdt: Some(crdt.into()),
relation: None,
}
}
fn make_relation_field(name: &str, ty: &str, relation: &str) -> Field {
Field {
name: name.into(),
field_type: ty.into(),
default: None,
crdt: None,
relation: Some(relation.into()),
}
}
fn make_entity_v1() -> Entity {
Entity {
name: "Task".into(),
table: "tasks".into(),
versions: vec![EntityVersion {
version: 1,
fields: vec![
make_field("title", "String", None),
make_field("done", "bool", None),
],
}],
}
}
fn make_entity_v2() -> Entity {
Entity {
name: "Task".into(),
table: "tasks".into(),
versions: vec![
EntityVersion {
version: 1,
fields: vec![
make_field("title", "String", None),
make_field("done", "bool", None),
],
},
EntityVersion {
version: 2,
fields: vec![
make_field("title", "String", None),
make_field("done", "bool", None),
make_field("priority", "Option<u8>", Some("None")),
],
},
],
}
}
fn make_crdt_entity() -> Entity {
Entity {
name: "Project".into(),
table: "projects".into(),
versions: vec![EntityVersion {
version: 1,
fields: vec![
make_crdt_field("name", "String", "LWWRegister"),
make_crdt_field("members", "String", "ORSet"),
],
}],
}
}
fn make_entity_with_relation() -> Entity {
Entity {
name: "Task".into(),
table: "tasks".into(),
versions: vec![EntityVersion {
version: 1,
fields: vec![
make_field("title", "String", None),
make_relation_field("project_id", "String", "Project"),
],
}],
}
}
fn make_config() -> SchemaConfig {
SchemaConfig {
output: "src/persistence".into(),
events: None,
sync: None,
}
}
#[test]
fn to_snake_case_works() {
assert_eq!(to_snake_case("Task"), "task");
assert_eq!(to_snake_case("SensorReading"), "sensor_reading");
assert_eq!(to_snake_case("IODevice"), "i_o_device");
}
#[test]
fn to_pascal_case_works() {
assert_eq!(to_pascal_case("title"), "Title");
assert_eq!(to_pascal_case("project_id"), "ProjectId");
assert_eq!(to_pascal_case("done"), "Done");
assert_eq!(to_pascal_case("created_at"), "CreatedAt");
}
#[test]
fn entity_file_single_version() {
let entity = make_entity_v1();
let (filename, content) = generate_entity_file(&entity);
assert_eq!(filename, "task.rs");
assert!(content.contains("AUTO-GENERATED"));
assert!(content.contains("pub struct TaskV1"));
assert!(content.contains("pub type Task = TaskV1;"));
assert!(content.contains("#[crdt_schema(version = 1, table = \"tasks\")]"));
assert!(content.contains("pub title: String,"));
assert!(content.contains("pub done: bool,"));
}
#[test]
fn entity_file_two_versions() {
let entity = make_entity_v2();
let (filename, content) = generate_entity_file(&entity);
assert_eq!(filename, "task.rs");
assert!(content.contains("pub struct TaskV1"));
assert!(content.contains("pub struct TaskV2"));
assert!(content.contains("pub type Task = TaskV2;"));
assert!(content.contains("pub priority: Option<u8>,"));
}
#[test]
fn crdt_field_wraps_type() {
let entity = Entity {
name: "Counter".into(),
table: "counters".into(),
versions: vec![EntityVersion {
version: 1,
fields: vec![
make_crdt_field("title", "String", "LWWRegister"),
make_crdt_field("views", "u64", "GCounter"),
make_crdt_field("tags", "String", "ORSet"),
],
}],
};
let (_filename, content) = generate_entity_file(&entity);
assert!(content.contains("use crdt_kit::prelude::*;"));
assert!(content.contains("pub title: LWWRegister<String>,"));
assert!(content.contains("pub views: GCounter,"));
assert!(content.contains("pub tags: ORSet<String>,"));
}
#[test]
fn no_crdt_import_without_crdt_fields() {
let entity = make_entity_v1();
let (_filename, content) = generate_entity_file(&entity);
assert!(!content.contains("crdt_kit"));
}
#[test]
fn relation_field_plain_type() {
let entity = make_entity_with_relation();
let (_filename, content) = generate_entity_file(&entity);
assert!(content.contains("pub project_id: String,"));
}
#[test]
fn migration_auto_generated() {
let entity = make_entity_v2();
let (filename, content) = generate_migration_file(&entity);
assert_eq!(filename, "task_migrations.rs");
assert!(content.contains("AUTO-GENERATED"));
assert!(content.contains("#[migration(from = 1, to = 2)]"));
assert!(content.contains("pub fn migrate_task_v1_to_v2"));
assert!(content.contains("title: old.title,"));
assert!(content.contains("done: old.done,"));
assert!(content.contains("priority: None,"));
assert!(content.contains("use super::super::models::{TaskV1, TaskV2};"));
}
#[test]
fn migration_todo_for_removed_field() {
let entity = Entity {
name: "Task".into(),
table: "tasks".into(),
versions: vec![
EntityVersion {
version: 1,
fields: vec![
make_field("title", "String", None),
make_field("legacy", "String", None),
],
},
EntityVersion {
version: 2,
fields: vec![make_field("title", "String", None)],
},
],
};
let (_filename, content) = generate_migration_file(&entity);
assert!(content.contains("todo!"));
assert!(content.contains("WARNING"));
}
#[test]
fn crdt_field_auto_default_in_migration() {
let entity = Entity {
name: "Task".into(),
table: "tasks".into(),
versions: vec![
EntityVersion {
version: 1,
fields: vec![make_field("title", "String", None)],
},
EntityVersion {
version: 2,
fields: vec![
make_field("title", "String", None),
make_crdt_field("views", "u64", "GCounter"),
],
},
],
};
let (_filename, content) = generate_migration_file(&entity);
assert!(!content.contains("todo!"));
assert!(content.contains("views: GCounter::new(\"_migrated\"),"));
assert!(content.contains("use crdt_kit::prelude::*;"));
}
#[test]
fn helpers_file_with_migrations() {
let entity = make_entity_v2();
let content = generate_helpers_file(&[entity]);
assert!(content.contains("AUTO-GENERATED"));
assert!(content.contains("fn create_db"));
assert!(content.contains("fn create_memory_db"));
assert!(content.contains("register_migrate_task_v1_to_v2"));
assert!(content.contains("CrdtDb::builder(store, 2)"));
}
#[test]
fn helpers_file_no_migrations() {
let entity = make_entity_v1();
let content = generate_helpers_file(&[entity]);
assert!(content.contains("CrdtDb::with_store(store)"));
assert!(!content.contains("register_migrate"));
}
#[test]
fn models_mod_file() {
let entities = vec![make_entity_v1(), make_crdt_entity()];
let content = generate_models_mod_file(&entities);
assert!(content.contains("mod task;"));
assert!(content.contains("mod project;"));
assert!(content.contains("pub use task::*;"));
assert!(content.contains("pub use project::*;"));
}
#[test]
fn migrations_mod_file() {
let entities = vec![make_entity_v2(), make_crdt_entity()];
let content = generate_migrations_mod_file(&entities);
assert!(content.contains("mod helpers;"));
assert!(content.contains("mod task_migrations;"));
assert!(content.contains("pub use helpers::*;"));
assert!(content.contains("pub use task_migrations::*;"));
assert!(!content.contains("mod project_migrations;"));
}
#[test]
fn repositories_mod_file() {
let entities = vec![make_entity_v1(), make_crdt_entity()];
let content = generate_repositories_mod_file(&entities);
assert!(content.contains("pub mod traits;"));
assert!(content.contains("pub mod task_repo;"));
assert!(content.contains("pub mod project_repo;"));
assert!(content.contains("pub use traits::*;"));
}
#[test]
fn repository_traits_basic() {
let entities = vec![make_entity_v1()];
let content = generate_repository_traits_file(&entities);
assert!(content.contains("pub trait TaskRepository"));
assert!(content.contains("type Error: fmt::Debug + fmt::Display;"));
assert!(
content.contains("fn get(&mut self, id: &str) -> Result<Option<Task>, Self::Error>;")
);
assert!(content
.contains("fn save(&mut self, id: &str, entity: &Task) -> Result<(), Self::Error>;"));
assert!(content.contains("fn delete(&mut self, id: &str) -> Result<(), Self::Error>;"));
assert!(content.contains("fn list(&mut self) -> Result<Vec<(String, Task)>, Self::Error>;"));
assert!(content.contains("fn exists(&self, id: &str) -> Result<bool, Self::Error>;"));
}
#[test]
fn repository_traits_with_relations() {
let entities = vec![make_entity_with_relation()];
let content = generate_repository_traits_file(&entities);
assert!(content.contains("fn find_by_project_id(&mut self, val: &str) -> Result<Vec<(String, Task)>, Self::Error>;"));
}
#[test]
fn repository_impl_file() {
let entity = make_entity_with_relation();
let (filename, content) = generate_repository_impl_file(&entity);
assert_eq!(filename, "task_repo.rs");
assert!(content.contains("pub struct TaskRepositoryAccess<'a, S: StateStore>"));
assert!(
content.contains("impl<S: StateStore> TaskRepository for TaskRepositoryAccess<'_, S>")
);
assert!(content.contains("type Error = DbError<S::Error>;"));
assert!(content.contains("self.db.load_ns(\"tasks\", id)"));
assert!(content.contains("self.db.save_ns(\"tasks\", id, entity)"));
assert!(content.contains("self.db.delete_ns(\"tasks\", id)"));
assert!(content.contains("self.db.exists_ns(\"tasks\", id)"));
assert!(content.contains("fn find_by_project_id"));
assert!(content.contains("e.project_id == val"));
}
#[test]
fn store_file() {
let entities = vec![make_entity_v1(), make_crdt_entity()];
let content = generate_store_file(&entities);
assert!(content.contains("pub struct Persistence<S: StateStore>"));
assert!(content.contains("pub fn tasks(&mut self) -> TaskRepositoryAccess<'_, S>"));
assert!(content.contains("pub fn projects(&mut self) -> ProjectRepositoryAccess<'_, S>"));
assert!(content.contains("pub fn db(&self) -> &CrdtDb<S>"));
assert!(content.contains("pub fn db_mut(&mut self) -> &mut CrdtDb<S>"));
assert!(content.contains("pub fn create_memory_persistence()"));
}
#[test]
fn event_types_file() {
let entity = make_entity_v1();
let (filename, content) = generate_event_types_file(&entity);
assert_eq!(filename, "task_events.rs");
assert!(content.contains("pub enum TaskEvent"));
assert!(content.contains("Created(TaskSnapshot)"));
assert!(content.contains("FieldUpdated(TaskFieldUpdate)"));
assert!(content.contains("Deleted"));
assert!(content.contains("pub struct TaskSnapshot"));
assert!(content.contains("pub title: String,"));
assert!(content.contains("pub done: bool,"));
assert!(content.contains("pub enum TaskFieldUpdate"));
assert!(content.contains("Title(String)"));
assert!(content.contains("Done(bool)"));
}
#[test]
fn event_types_crdt_entity_uses_inner_types() {
let entity = make_crdt_entity();
let (_, content) = generate_event_types_file(&entity);
assert!(content.contains("pub name: String,")); assert!(content.contains("pub members: Vec<String>,")); }
#[test]
fn snapshot_policy_file() {
let content = generate_snapshot_policy_file(200);
assert!(content.contains("pub struct SnapshotPolicy"));
assert!(content.contains("pub fn should_snapshot"));
assert!(content.contains("event_threshold: 200,"));
}
#[test]
fn sync_file_with_delta_fields() {
let entity = make_crdt_entity();
let (filename, content) = generate_sync_file(&entity);
assert_eq!(filename, "project_sync.rs");
assert!(content.contains("pub struct ProjectDelta"));
assert!(content.contains("pub members: Option<ORSetDelta<String>>,"));
assert!(!content.contains("name: Option<")); assert!(content.contains("pub fn compute_project_delta"));
assert!(content.contains("pub fn apply_project_delta"));
assert!(content.contains("pub fn merge_project"));
assert!(content.contains("local.name.merge(&remote.name);"));
assert!(content.contains("local.members.merge(&remote.members);"));
}
#[test]
fn sync_file_state_only_crdts() {
let entity = Entity {
name: "Config".into(),
table: "configs".into(),
versions: vec![EntityVersion {
version: 1,
fields: vec![make_crdt_field("value", "String", "LWWRegister")],
}],
};
let (_, content) = generate_sync_file(&entity);
assert!(!content.contains("pub struct ConfigDelta"));
assert!(!content.contains("compute_config_delta"));
assert!(content.contains("pub fn merge_config"));
assert!(content.contains("local.value.merge(&remote.value);"));
}
#[test]
fn persistence_mod_file_basic() {
let entities = vec![make_entity_v1()];
let config = make_config();
let content = generate_persistence_mod_file(&entities, &config);
assert!(content.contains("pub mod models;"));
assert!(content.contains("pub mod migrations;"));
assert!(content.contains("pub mod repositories;"));
assert!(content.contains("pub mod store;"));
assert!(!content.contains("pub mod events;"));
assert!(!content.contains("pub mod sync;"));
assert!(content.contains("pub use models::*;"));
assert!(content.contains("pub use store::*;"));
}
#[test]
fn persistence_mod_file_with_events_sync() {
let entities = vec![make_entity_v1()];
let config = SchemaConfig {
output: "src/persistence".into(),
events: Some(EventsConfig {
enabled: true,
snapshot_threshold: 100,
}),
sync: Some(SyncConfig { enabled: true }),
};
let content = generate_persistence_mod_file(&entities, &config);
assert!(content.contains("pub mod events;"));
assert!(content.contains("pub mod sync;"));
assert!(content.contains("pub use events::*;"));
assert!(content.contains("pub use sync::*;"));
}
}