Skip to main content

rust_ef/migration/
snapshot_io.rs

1//! Model snapshot JSON serialization / deserialization.
2
3use super::types::{ModelSnapshot, SnapshotColumn, SnapshotEntityType};
4use crate::error::{EFError, EFResult};
5
6/// Parses a model snapshot JSON file (same format as [`super::history::MigrationStore::save_snapshot`]).
7pub fn parse_model_snapshot_json(text: &str) -> EFResult<Option<ModelSnapshot>> {
8    snapshot_from_json(text)
9}
10
11pub(super) fn migration_io_err(e: std::io::Error) -> EFError {
12    EFError::migration(e.to_string())
13}
14
15pub(super) fn snapshot_to_json(snapshot: &ModelSnapshot) -> String {
16    let mut out = String::from("{\n  \"migration_id\": \"");
17    out.push_str(&snapshot.migration_id.replace('"', "\\\""));
18    out.push_str("\",\n  \"entity_types\": [\n");
19    for (i, et) in snapshot.entity_types.iter().enumerate() {
20        if i > 0 {
21            out.push(',');
22        }
23        out.push_str("    {\n");
24        out.push_str(&format!(
25            "      \"type_name\": \"{}\",\n",
26            et.type_name.replace('"', "\\\"")
27        ));
28        out.push_str(&format!(
29            "      \"table_name\": \"{}\",\n",
30            et.table_name.replace('"', "\\\"")
31        ));
32        out.push_str("      \"columns\": [\n");
33        for (j, col) in et.columns.iter().enumerate() {
34            if j > 0 {
35                out.push(',');
36            }
37            out.push_str(&format!(
38                "        {{\"field_name\":\"{}\",\"column_name\":\"{}\",\"type_name\":\"{}\",\"is_primary_key\":{},\"is_required\":{},\"is_foreign_key\":{},\"max_length\":{},\"is_auto_increment\":{},\"is_sequence\":{},\"sequence_name\":{},\"fk_referenced_table\":{},\"fk_referenced_column\":{},\"has_index\":{},\"is_unique\":{},\"fk_on_delete\":{}}}\n",
39                col.field_name.replace('"', "\\\""),
40                col.column_name.replace('"', "\\\""),
41                col.type_name.replace('"', "\\\""),
42                col.is_primary_key,
43                col.is_required,
44                col.is_foreign_key,
45                col.max_length.map(|n| n.to_string()).unwrap_or_else(|| "null".into()),
46                col.is_auto_increment,
47                col.is_sequence,
48                col.sequence_name
49                    .as_ref()
50                    .map(|s| format!("\"{}\"", s.replace('"', "\\\"")))
51                    .unwrap_or_else(|| "null".into()),
52                col.fk_referenced_table
53                    .as_ref()
54                    .map(|s| format!("\"{}\"", s.replace('"', "\\\"")))
55                    .unwrap_or_else(|| "null".into()),
56                col.fk_referenced_column
57                    .as_ref()
58                    .map(|s| format!("\"{}\"", s.replace('"', "\\\"")))
59                    .unwrap_or_else(|| "null".into()),
60                col.has_index,
61                col.is_unique,
62                col.fk_on_delete
63                    .as_ref()
64                    .map(|s| format!("\"{}\"", s.replace('"', "\\\"")))
65                    .unwrap_or_else(|| "null".into())
66            ));
67        }
68        out.push_str("      ]\n    }\n");
69    }
70    out.push_str("  ]\n}\n");
71    out
72}
73
74fn snapshot_from_json(text: &str) -> EFResult<Option<ModelSnapshot>> {
75    // Minimal JSON parser for snapshot files written by save_snapshot.
76    let migration_id =
77        extract_json_string(text, "migration_id").unwrap_or_else(|| "__snapshot__".to_string());
78    let mut entity_types = Vec::new();
79    if let Some(arr_start) = text.find("\"entity_types\"") {
80        let slice = &text[arr_start..];
81        for table_block in slice.split("\"table_name\"").skip(1) {
82            let table_name = extract_quoted_after_colon(table_block).unwrap_or_default();
83            let _type_name = entity_types.len().to_string(); // fallback
84            let columns_start = table_block.find("\"columns\"");
85            let mut columns = Vec::new();
86            if let Some(cs) = columns_start {
87                let col_slice = &table_block[cs..];
88                for col_chunk in col_slice.split("\"field_name\"").skip(1) {
89                    if let Some(field_name) = extract_quoted_after_colon(col_chunk) {
90                        let column_name = extract_json_string(col_chunk, "column_name")
91                            .unwrap_or_else(|| field_name.clone());
92                        let type_name_col = extract_json_string(col_chunk, "type_name")
93                            .unwrap_or_else(|| "String".to_string());
94                        columns.push(SnapshotColumn {
95                            field_name,
96                            column_name,
97                            type_name: type_name_col,
98                            is_primary_key: col_chunk.contains("\"is_primary_key\":true"),
99                            is_required: col_chunk.contains("\"is_required\":true"),
100                            is_foreign_key: col_chunk.contains("\"is_foreign_key\":true"),
101                            max_length: None,
102                            is_auto_increment: col_chunk.contains("\"is_auto_increment\":true"),
103                            is_sequence: col_chunk.contains("\"is_sequence\":true"),
104                            sequence_name: extract_json_string(col_chunk, "sequence_name"),
105                            fk_referenced_table: extract_json_string(
106                                col_chunk,
107                                "fk_referenced_table",
108                            ),
109                            fk_referenced_column: extract_json_string(
110                                col_chunk,
111                                "fk_referenced_column",
112                            ),
113                            has_index: col_chunk.contains("\"has_index\":true"),
114                            is_unique: col_chunk.contains("\"is_unique\":true"),
115                            fk_on_delete: extract_json_string(col_chunk, "fk_on_delete"),
116                        });
117                    }
118                }
119            }
120            if !table_name.is_empty() {
121                entity_types.push(SnapshotEntityType {
122                    type_name: table_name.clone(),
123                    table_name,
124                    columns,
125                });
126            }
127        }
128    }
129    if entity_types.is_empty() {
130        return Ok(None);
131    }
132    Ok(Some(ModelSnapshot {
133        migration_id,
134        entity_types,
135    }))
136}
137
138fn extract_json_string(haystack: &str, key: &str) -> Option<String> {
139    let needle = format!("\"{key}\"");
140    let pos = haystack.find(&needle)?;
141    extract_quoted_after_colon(&haystack[pos + needle.len()..])
142}
143
144fn extract_quoted_after_colon(s: &str) -> Option<String> {
145    let colon = s.find(':')?;
146    let rest = s[colon + 1..].trim_start();
147    if !rest.starts_with('"') {
148        return None;
149    }
150    let inner = &rest[1..];
151    let end = inner.find('"')?;
152    Some(inner[..end].to_string())
153}