Skip to main content

sz_rust_cli/
field_parser.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2024-2026 SZ-Rust Team
3//
4//! 字段定义解析器
5//!
6//! 对应 design.md 第 1.1.3.7 节,解析 `"name:Type,age:i32"` 格式的字段定义,
7//! 并提供 Rust 类型 → SQL 类型映射。
8
9use crate::error::CliError;
10
11/// 字段定义
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Field {
14    /// 字段名(Rust 标识符)
15    pub name: String,
16    /// Rust 类型名(如 `String`、`i32`、`Option<String>`)
17    pub rust_type: String,
18    /// SQL 类型(如 `VARCHAR(255)`、`INT`)
19    pub sql_type: String,
20    /// 是否可空
21    pub is_nullable: bool,
22    /// 是否主键
23    pub is_primary_key: bool,
24    /// 是否索引
25    pub is_indexed: bool,
26}
27
28/// 字段定义解析器
29pub struct FieldParser;
30
31/// Rust 类型 → SQL 类型映射
32const TYPE_MAP: &[(&str, &str)] = &[
33    ("String", "VARCHAR(255)"),
34    ("i32", "INT"),
35    ("i64", "BIGINT"),
36    ("f64", "DOUBLE"),
37    ("bool", "BOOLEAN"),
38    ("DateTime", "DATETIME"),
39    ("Json", "JSON"),
40];
41
42/// 已知的 Rust 类型列表
43const KNOWN_TYPES: &[&str] = &["String", "i32", "i64", "f64", "bool", "DateTime", "Json"];
44
45/// 字段修饰符
46const MODIFIER_PK: &str = "pk";
47const MODIFIER_INDEX: &str = "index";
48
49impl FieldParser {
50    /// 解析字段定义字符串
51    ///
52    /// 格式:`"name:Type,name2:Type2,..."`
53    ///
54    /// 支持修饰符:
55    /// - `name:Type:pk` — 标记为主键
56    /// - `name:Type:index` — 标记为索引
57    /// - `name:Type?` — 标记为可空(语法糖,等价于 `Option<Type>`)
58    ///
59    /// # 错误
60    ///
61    /// - `CliError::FieldParseError`:格式错误、未知类型、非法标识符、注入字符
62    pub fn parse(input: &str) -> Result<Vec<Field>, CliError> {
63        let input = input.trim();
64
65        if input.is_empty() {
66            return Err(CliError::FieldParseError(
67                "field definition is empty".to_string(),
68            ));
69        }
70
71        if input.ends_with(',') {
72            return Err(CliError::FieldParseError(format!(
73                "trailing comma at end of field definition: '{input}'"
74            )));
75        }
76
77        let mut fields = Vec::new();
78        for (idx, part) in input.split(',').enumerate() {
79            let part = part.trim();
80            if part.is_empty() {
81                return Err(CliError::FieldParseError(format!(
82                    "empty field at position {idx}"
83                )));
84            }
85            let field = Self::parse_single(part, idx)?;
86            fields.push(field);
87        }
88
89        Ok(fields)
90    }
91
92    /// 解析单个字段定义
93    fn parse_single(part: &str, idx: usize) -> Result<Field, CliError> {
94        let tokens: Vec<&str> = part.split(':').collect();
95        if tokens.len() < 2 {
96            return Err(CliError::FieldParseError(format!(
97                "missing ':' in field definition '{part}' at position {idx}"
98            )));
99        }
100
101        let name = tokens[0].trim();
102        let mut rust_type = tokens[1].trim().to_string();
103        let mut is_nullable = false;
104        let mut is_primary_key = false;
105        let mut is_indexed = false;
106
107        if rust_type.ends_with('?') {
108            is_nullable = true;
109            rust_type = rust_type[..rust_type.len() - 1].to_string();
110        }
111
112        for modifier in tokens.iter().skip(2) {
113            let modifier = modifier.trim();
114            match modifier {
115                MODIFIER_PK => is_primary_key = true,
116                MODIFIER_INDEX => is_indexed = true,
117                other => {
118                    return Err(CliError::FieldParseError(format!(
119                        "unknown modifier '{other}' in field definition '{part}' at position {idx}"
120                    )));
121                }
122            }
123        }
124
125        Self::validate_name(name, idx)?;
126        Self::validate_type(&rust_type, idx)?;
127
128        let sql_type = Self::rust_type_to_sql(&rust_type)?;
129
130        Ok(Field {
131            name: name.to_string(),
132            rust_type,
133            sql_type,
134            is_nullable,
135            is_primary_key,
136            is_indexed,
137        })
138    }
139
140    /// 校验字段名合法性
141    fn validate_name(name: &str, idx: usize) -> Result<(), CliError> {
142        if name.is_empty() {
143            return Err(CliError::FieldParseError(format!(
144                "empty field name at position {idx}"
145            )));
146        }
147
148        if name
149            .chars()
150            .next()
151            .map(|c| c.is_ascii_digit())
152            .unwrap_or(false)
153        {
154            return Err(CliError::FieldParseError(format!(
155                "field name '{name}' at position {idx} starts with a digit"
156            )));
157        }
158
159        if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
160            return Err(CliError::FieldParseError(format!(
161                "field name '{name}' at position {idx} contains invalid characters (only letters, digits, and underscores are allowed)"
162            )));
163        }
164
165        Ok(())
166    }
167
168    /// 校验类型合法性
169    fn validate_type(rust_type: &str, idx: usize) -> Result<(), CliError> {
170        if !KNOWN_TYPES.contains(&rust_type) {
171            return Err(CliError::FieldParseError(format!(
172                "unknown type '{rust_type}' at position {idx}. Known types: {}",
173                KNOWN_TYPES.join(", ")
174            )));
175        }
176        Ok(())
177    }
178
179    /// Rust 类型 → SQL 类型映射
180    pub fn rust_type_to_sql(rust_type: &str) -> Result<String, CliError> {
181        for (rust, sql) in TYPE_MAP {
182            if *rust == rust_type {
183                return Ok(sql.to_string());
184            }
185        }
186        Err(CliError::FieldParseError(format!(
187            "unknown Rust type '{rust_type}'. Known types: {}",
188            KNOWN_TYPES.join(", ")
189        )))
190    }
191
192    /// 返回全部已知 Rust 类型
193    pub fn known_types() -> &'static [&'static str] {
194        KNOWN_TYPES
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn test_parse_basic() {
204        let fields = FieldParser::parse("id:i32,name:String,age:i32").unwrap();
205        assert_eq!(fields.len(), 3);
206        assert_eq!(fields[0].name, "id");
207        assert_eq!(fields[0].rust_type, "i32");
208        assert_eq!(fields[0].sql_type, "INT");
209        assert_eq!(fields[1].name, "name");
210        assert_eq!(fields[1].rust_type, "String");
211        assert_eq!(fields[1].sql_type, "VARCHAR(255)");
212    }
213
214    #[test]
215    fn test_parse_with_datetime() {
216        let fields = FieldParser::parse("id:i32,name:String,created_at:DateTime").unwrap();
217        assert_eq!(fields.len(), 3);
218        assert_eq!(fields[2].name, "created_at");
219        assert_eq!(fields[2].rust_type, "DateTime");
220        assert_eq!(fields[2].sql_type, "DATETIME");
221    }
222
223    #[test]
224    fn test_parse_all_types() {
225        let fields =
226            FieldParser::parse("a:String,b:i32,c:i64,d:f64,e:bool,f:DateTime,g:Json").unwrap();
227        assert_eq!(fields.len(), 7);
228        assert_eq!(fields[0].sql_type, "VARCHAR(255)");
229        assert_eq!(fields[1].sql_type, "INT");
230        assert_eq!(fields[2].sql_type, "BIGINT");
231        assert_eq!(fields[3].sql_type, "DOUBLE");
232        assert_eq!(fields[4].sql_type, "BOOLEAN");
233        assert_eq!(fields[5].sql_type, "DATETIME");
234        assert_eq!(fields[6].sql_type, "JSON");
235    }
236
237    #[test]
238    fn test_parse_nullable() {
239        let fields = FieldParser::parse("id:i32,name:String?").unwrap();
240        assert!(!fields[0].is_nullable);
241        assert!(fields[1].is_nullable);
242        assert_eq!(fields[1].rust_type, "String");
243    }
244
245    #[test]
246    fn test_parse_primary_key() {
247        let fields = FieldParser::parse("id:i32:pk,name:String").unwrap();
248        assert!(fields[0].is_primary_key);
249        assert!(!fields[1].is_primary_key);
250    }
251
252    #[test]
253    fn test_parse_indexed() {
254        let fields = FieldParser::parse("id:i32:pk,email:String:index").unwrap();
255        assert!(fields[0].is_primary_key);
256        assert!(fields[1].is_indexed);
257    }
258
259    #[test]
260    fn test_parse_empty_string() {
261        let result = FieldParser::parse("");
262        assert!(result.is_err());
263        assert!(matches!(result.unwrap_err(), CliError::FieldParseError(_)));
264    }
265
266    #[test]
267    fn test_parse_trailing_comma() {
268        let result = FieldParser::parse("id:i32,name:String,");
269        assert!(result.is_err());
270        let err = result.unwrap_err();
271        assert!(matches!(err, CliError::FieldParseError(_)));
272        assert!(err.to_string().contains("trailing comma"));
273    }
274
275    #[test]
276    fn test_parse_unknown_type() {
277        let result = FieldParser::parse("id:UnknownType");
278        assert!(result.is_err());
279        let err = result.unwrap_err();
280        assert!(matches!(err, CliError::FieldParseError(_)));
281        assert!(err.to_string().contains("unknown type"));
282    }
283
284    #[test]
285    fn test_parse_missing_colon() {
286        let result = FieldParser::parse("id_i32");
287        assert!(result.is_err());
288        let err = result.unwrap_err();
289        assert!(err.to_string().contains("missing ':'"));
290    }
291
292    #[test]
293    fn test_parse_name_starts_with_digit() {
294        let result = FieldParser::parse("1id:i32");
295        assert!(result.is_err());
296        let err = result.unwrap_err();
297        assert!(err.to_string().contains("starts with a digit"));
298    }
299
300    #[test]
301    fn test_parse_name_with_special_chars() {
302        let result = FieldParser::parse("na;me:i32");
303        assert!(result.is_err());
304        let err = result.unwrap_err();
305        assert!(err.to_string().contains("invalid characters"));
306    }
307
308    #[test]
309    fn test_parse_injection_semicolon() {
310        let result = FieldParser::parse("name:String;rm -rf /");
311        assert!(result.is_err());
312    }
313
314    #[test]
315    fn test_parse_injection_pipe() {
316        let result = FieldParser::parse("name:String|cat /etc/passwd");
317        assert!(result.is_err());
318    }
319
320    #[test]
321    fn test_parse_unknown_modifier() {
322        let result = FieldParser::parse("id:i32:foobar");
323        assert!(result.is_err());
324        let err = result.unwrap_err();
325        assert!(err.to_string().contains("unknown modifier"));
326    }
327
328    #[test]
329    fn test_parse_whitespace_trimming() {
330        let fields = FieldParser::parse(" id : i32 , name : String ").unwrap();
331        assert_eq!(fields.len(), 2);
332        assert_eq!(fields[0].name, "id");
333        assert_eq!(fields[1].name, "name");
334    }
335
336    #[test]
337    fn test_rust_type_to_sql() {
338        assert_eq!(
339            FieldParser::rust_type_to_sql("String").unwrap(),
340            "VARCHAR(255)"
341        );
342        assert_eq!(FieldParser::rust_type_to_sql("i32").unwrap(), "INT");
343        assert_eq!(FieldParser::rust_type_to_sql("i64").unwrap(), "BIGINT");
344        assert_eq!(FieldParser::rust_type_to_sql("f64").unwrap(), "DOUBLE");
345        assert_eq!(FieldParser::rust_type_to_sql("bool").unwrap(), "BOOLEAN");
346        assert_eq!(
347            FieldParser::rust_type_to_sql("DateTime").unwrap(),
348            "DATETIME"
349        );
350        assert_eq!(FieldParser::rust_type_to_sql("Json").unwrap(), "JSON");
351    }
352
353    #[test]
354    fn test_rust_type_to_sql_unknown() {
355        let result = FieldParser::rust_type_to_sql("Unknown");
356        assert!(result.is_err());
357    }
358
359    #[test]
360    fn test_known_types() {
361        let types = FieldParser::known_types();
362        assert_eq!(types.len(), 7);
363        assert!(types.contains(&"String"));
364        assert!(types.contains(&"Json"));
365    }
366}