Skip to main content

crdt_codegen/
templates.rs

1use crate::schema::{lookup_crdt, lookup_delta_type, Entity, EntityVersion, Field, SchemaConfig};
2use std::collections::HashSet;
3use std::fmt::Write;
4
5/// Header prepended to every generated file.
6///
7/// Suppresses `dead_code` and `unused_imports` warnings since generated code
8/// provides a full API surface that users may not fully consume in every project.
9const 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
23// ── Helpers ────────────────────────────────────────────────────────
24
25/// Convert PascalCase to snake_case.
26pub(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
41/// Convert snake_case to PascalCase (for enum variant names).
42pub(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
58/// Compute the Rust type for a field, wrapping with CRDT type if specified.
59///
60/// - `{ type = "String", crdt = "LWWRegister" }` → `LWWRegister<String>`
61/// - `{ type = "u64", crdt = "GCounter" }` → `GCounter` (non-generic)
62/// - `{ type = "String" }` → `String` (plain)
63fn 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
76/// Check if any field across all versions of an entity uses a CRDT type.
77fn 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
84/// Get fields with `relation` annotations from the latest version of an entity.
85fn 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
93/// Get fields with CRDT annotations from the latest version of an entity.
94fn 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
102/// Get fields with delta-capable CRDT types from the latest version.
103fn 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
116/// Compute the snapshot (logical) type for a field, considering CRDT semantics.
117///
118/// - `GCounter` → `u64` (counter value)
119/// - `PNCounter` → `i64` (counter value)
120/// - `LWWRegister<T>` → `T` (single value)
121/// - `MVRegister<T>` → `Vec<T>` (multiple concurrent values)
122/// - `GSet<T>` / `TwoPSet<T>` / `ORSet<T>` → `Vec<T>` (set elements)
123/// - Plain field → field_type as-is
124fn 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
135/// Compute the delta type for a CRDT field (e.g., `ORSetDelta<String>`).
136///
137/// Returns `None` for CRDTs that don't support `DeltaCrdt`.
138fn 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
149/// Return the parameter type for `find_by_*` methods based on the field type.
150fn relation_param_type(field_type: &str) -> String {
151    if field_type == "String" {
152        "&str".into()
153    } else {
154        field_type.into()
155    }
156}
157
158// ── Struct generation ──────────────────────────────────────────────
159
160/// Generate the entity structs file for a single entity.
161///
162/// Produces `{snake_name}.rs` with `{Name}V1`, `{Name}V2`, ...,
163/// and `pub type {Name} = {Name}V{latest};`.
164pub 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    // Type alias for the current version.
209    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
215// ── Migration generation ───────────────────────────────────────────
216
217/// Determine if a migration between two consecutive versions can be
218/// auto-generated (only field additions with defaults).
219fn 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            // New or changed field — must have a default (or a CRDT auto-default).
230            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    // Also check that no fields were removed (all from-fields exist in to).
239    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
249/// Find new fields added between two versions.
250fn 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
258/// Generate the migration file for a single entity.
259///
260/// Contains one `#[migration]` function per version transition.
261/// Import path uses `super::super::models::` since migration files live
262/// in `migrations/` while model files live in `models/`.
263pub 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    // Import all versioned structs used in migrations.
275    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            // Carry over existing fields.
341            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                    // New field — use explicit default, CRDT auto-default, or fallback.
348                    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            // Complex migration — emit skeleton with todo!().
365            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
403// ── Helpers generation ─────────────────────────────────────────────
404
405/// Generate the `helpers.rs` file with `create_db` and `create_memory_db`.
406///
407/// This file lives in `migrations/helpers.rs` and registers all
408/// auto-generated migration functions with the `CrdtDb` builder.
409pub 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    // Collect all migration register functions.
415    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    // Find the maximum version across all entities.
435    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 &register_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
484// ── Sub-module mod.rs generators ──────────────────────────────────
485
486/// Generate `models/mod.rs` — declares and re-exports all entity model modules.
487pub 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
506/// Generate `migrations/mod.rs` — declares migration modules and helpers.
507pub 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
532/// Generate `repositories/mod.rs` — declares trait and implementation modules.
533pub 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
554/// Generate `events/mod.rs` — declares event type modules and policies.
555pub 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
576/// Generate `sync/mod.rs` — declares sync/delta modules for entities with CRDTs.
577pub 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
598// ── Repository generation ─────────────────────────────────────────
599
600/// Generate `repositories/traits.rs` — repository trait (port) per entity.
601///
602/// Each trait defines the standard CRUD operations plus relation-based
603/// finder methods. This is the "port" in hexagonal architecture — the
604/// domain/application layer depends on these traits, not on concrete stores.
605pub 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    // Import the current version type alias for each entity.
612    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        // get
642        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        // save
651        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        // delete
660        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        // list
669        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        // exists
682        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        // find_by_* for relation fields
694        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
719/// Generate a repository implementation file for a single entity.
720///
721/// Produces `repositories/{snake}_repo.rs` with `{Name}RepositoryAccess<S>`
722/// — the "adapter" backed by `CrdtDb<S>`.
723pub 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    // Struct definition.
738    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    // Constructor.
764    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    // Trait implementation.
781    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    // get
790    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    // save
800    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    // delete
810    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    // list
820    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    // exists
845    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    // find_by_* for relation fields
854    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
879// ── Store generation ──────────────────────────────────────────────
880
881/// Generate `store.rs` — the unified `Persistence<S>` entry point.
882///
883/// Provides scoped repository access for each entity while owning
884/// a single `CrdtDb<S>` instance. This is the main API for the
885/// persistence layer.
886pub 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    // Import repository access types.
894    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    // Persistence struct.
906    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    // Constructor.
947    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    // Entity accessor methods.
960    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    // Raw db access.
975    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    // Convenience factory.
990    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
1008// ── Event generation ──────────────────────────────────────────────
1009
1010/// Generate `events/{snake}_events.rs` — event types for a single entity.
1011///
1012/// Produces an event enum, a snapshot struct (using logical/inner types),
1013/// and a field update enum for fine-grained change tracking.
1014pub 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    // Use fields from the latest version.
1020    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    // Event enum.
1032    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    // Snapshot struct (uses logical/inner types, not CRDT-wrapped).
1068    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    // Field update enum.
1093    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
1111/// Generate `events/policies.rs` — snapshot policy with configurable threshold.
1112pub 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
1177// ── Sync generation ───────────────────────────────────────────────
1178
1179/// Generate `sync/{snake}_sync.rs` — delta sync and merge helpers for a CRDT entity.
1180///
1181/// Only generated for entities that have at least one CRDT field.
1182/// The delta struct includes fields backed by `DeltaCrdt` (GCounter, PNCounter, ORSet).
1183/// The merge function handles all CRDT fields via state-based merge.
1184pub 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    // Delta struct (only if entity has delta-capable fields).
1201    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        // compute_*_delta function.
1230        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        // apply_*_delta function.
1256        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    // merge_* function (all CRDT fields, state-based).
1272    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
1297// ── Top-level mod.rs generation ───────────────────────────────────
1298
1299/// Generate the top-level `mod.rs` for the persistence module.
1300///
1301/// Conditionally includes `events` and `sync` modules based on config.
1302pub 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    // Core modules (always present).
1308    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    // Conditional modules.
1314    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    // Re-exports for convenience.
1326    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    // List all entities for documentation.
1339    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    // ── Helper tests ──────────────────────────────────────────────
1464
1465    #[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    // ── Entity file tests ─────────────────────────────────────────
1481
1482    #[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    // ── Migration tests ───────────────────────────────────────────
1542
1543    #[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        // Import path uses nested module structure.
1555        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    // ── Helpers tests ─────────────────────────────────────────────
1608
1609    #[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    // ── Sub-module mod.rs tests ───────────────────────────────────
1629
1630    #[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        // Single-version entity has no migration module.
1649        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    // ── Repository tests ──────────────────────────────────────────
1663
1664    #[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    // ── Store tests ───────────────────────────────────────────────
1706
1707    #[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    // ── Event tests ───────────────────────────────────────────────
1720
1721    #[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        // Snapshot uses logical types, not CRDT wrappers.
1743        assert!(content.contains("pub name: String,")); // LWWRegister<String> → String
1744        assert!(content.contains("pub members: Vec<String>,")); // ORSet<String> → Vec<String>
1745    }
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    // ── Sync tests ────────────────────────────────────────────────
1756
1757    #[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        // Delta struct only includes ORSet (delta-capable), not LWWRegister.
1763        assert!(content.contains("pub struct ProjectDelta"));
1764        assert!(content.contains("pub members: Option<ORSetDelta<String>>,"));
1765        assert!(!content.contains("name: Option<")); // LWWRegister is state-only
1766                                                     // Delta functions.
1767        assert!(content.contains("pub fn compute_project_delta"));
1768        assert!(content.contains("pub fn apply_project_delta"));
1769        // Merge includes ALL CRDT fields.
1770        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        // Entity with only LWWRegister (no DeltaCrdt support).
1778        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        // No delta struct since no delta-capable fields.
1788        assert!(!content.contains("pub struct ConfigDelta"));
1789        assert!(!content.contains("compute_config_delta"));
1790        // But merge still works.
1791        assert!(content.contains("pub fn merge_config"));
1792        assert!(content.contains("local.value.merge(&remote.value);"));
1793    }
1794
1795    // ── Persistence mod.rs tests ──────────────────────────────────
1796
1797    #[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}