Skip to main content

gize_generator/
registry.rs

1//! Idempotent edits to "registry" files — the ones `gize make app` must update in place
2//! rather than create: `src/app/mod.rs` (module + route wiring).
3//!
4//! MVP approach: marker-based insertion. The generated `app/mod.rs` carries two markers
5//! (`gize:modules` and `gize:module-routes`); we insert around them and short-circuit if
6//! the module is already registered, which makes re-runs a no-op. ADR-004 earmarks a
7//! `syn`-based editor as a hardening follow-up; the pure-function boundary here keeps that
8//! swap internal.
9
10use anyhow::{Result, bail};
11
12const MODULES_MARKER: &str = "// gize:modules (do not remove this marker)";
13const ROUTES_MARKER: &str = "// gize:module-routes (do not remove this marker)";
14
15/// The result of a registry edit.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct Edit {
18    /// Whether the edit changed anything (false = already registered).
19    pub changed: bool,
20    /// The (possibly unchanged) file content.
21    pub content: String,
22}
23
24/// Register `module` in an `app/mod.rs` source: add `mod <module>;` and merge its routes.
25/// Idempotent — if the module is already present, returns `changed: false`.
26pub fn register_module(source: &str, module: &str) -> Result<Edit> {
27    let mod_decl = format!("mod {module};");
28    if source.contains(&mod_decl) {
29        return Ok(Edit {
30            changed: false,
31            content: source.to_string(),
32        });
33    }
34
35    if !source.contains(MODULES_MARKER) || !source.contains(ROUTES_MARKER) {
36        bail!(
37            "src/app/mod.rs is missing gize markers; cannot register `{module}` automatically. \
38             Re-add the `// gize:modules` and `// gize:module-routes` markers or wire the module by hand."
39        );
40    }
41
42    let with_mod = insert_after_marker(source, MODULES_MARKER, &mod_decl);
43    let merge_call = format!("        .merge({module}::routes())");
44    let content = insert_before_marker(&with_mod, ROUTES_MARKER, &merge_call);
45
46    Ok(Edit {
47        changed: true,
48        content,
49    })
50}
51
52/// Insert `new_line` immediately after the first line containing `marker`.
53fn insert_after_marker(source: &str, marker: &str, new_line: &str) -> String {
54    let mut out: Vec<String> = Vec::new();
55    for line in source.lines() {
56        out.push(line.to_string());
57        if line.contains(marker) {
58            out.push(new_line.to_string());
59        }
60    }
61    finish(out, source)
62}
63
64/// Insert `new_line` immediately before the first line containing `marker`.
65fn insert_before_marker(source: &str, marker: &str, new_line: &str) -> String {
66    let mut out: Vec<String> = Vec::new();
67    for line in source.lines() {
68        if line.contains(marker) {
69            out.push(new_line.to_string());
70        }
71        out.push(line.to_string());
72    }
73    finish(out, source)
74}
75
76/// Re-join lines, preserving a trailing newline if the source had one.
77fn finish(lines: Vec<String>, source: &str) -> String {
78    let mut s = lines.join("\n");
79    if source.ends_with('\n') {
80        s.push('\n');
81    }
82    s
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    const APP_MOD: &str = r#"use axum::Router;
90
91use crate::state::AppState;
92
93// gize:modules (do not remove this marker)
94
95pub fn routes() -> Router<AppState> {
96    Router::new()
97    // gize:module-routes (do not remove this marker)
98}
99"#;
100
101    #[test]
102    fn registers_a_new_module() {
103        let edit = register_module(APP_MOD, "users").unwrap();
104        assert!(edit.changed);
105        assert!(edit.content.contains("mod users;"));
106        assert!(edit.content.contains(".merge(users::routes())"));
107        // markers are preserved for the next registration
108        assert!(edit.content.contains(MODULES_MARKER));
109        assert!(edit.content.contains(ROUTES_MARKER));
110    }
111
112    #[test]
113    fn is_idempotent() {
114        let first = register_module(APP_MOD, "users").unwrap();
115        let second = register_module(&first.content, "users").unwrap();
116        assert!(!second.changed);
117        assert_eq!(first.content, second.content);
118    }
119
120    #[test]
121    fn registers_multiple_modules() {
122        let first = register_module(APP_MOD, "users").unwrap();
123        let second = register_module(&first.content, "products").unwrap();
124        assert!(second.changed);
125        assert!(second.content.contains("mod users;"));
126        assert!(second.content.contains("mod products;"));
127        assert!(second.content.contains(".merge(users::routes())"));
128        assert!(second.content.contains(".merge(products::routes())"));
129    }
130
131    #[test]
132    fn fails_without_markers() {
133        assert!(register_module("fn routes() {}", "users").is_err());
134    }
135}