Rust: JSON解析器
GitHub: 源代码文件在GitHub上
距离我上次编写Rust代码已经有一段时间了,所以我想为什么不写一个小型的JSON解析器呢?于是我打开了我的VSCode。
JSON包含与JavaScript相同的类型(毕竟是JavaScript对象表示法)。
JSON类型
Rust表示
那么如何在Rust中表示这些类型呢?枚举有一个非常酷的特性,每个条目可以包装一个或多个类型。我们开始吧:
1
2
3
4
5
6
7
8
9
|
#[derive(Debug, PartialEq)]
pub enum JsonType {
Object(HashMap<String, JsonType>),
Array(Vec<JsonType>),
String(String),
Number(i64),
Decimal(f64),
Boolean(bool)
}
|
对象的属性存储在一个HashMap中,其中JsonType可以是枚举中的其他表示形式。对于数组,我使用Vector。这是一个可增长的数组类型,也保存我们的JsonType。我为数字需要两个条目,因为浮点数和整数有不同的类型,而字符串/布尔值由相应的内置类型表示。
由于意外的/缺失的标记或不支持的功能(例如嵌套数组),可能会出现解析错误。我用ParserError类型表示它们:
1
2
3
4
5
6
7
8
|
#[derive(Debug, PartialEq)]
pub enum ParserError {
UnexpectedToken(String),
InvalidSyntax(String),
MissingToken(String),
EmptyInput,
NotSupported(String)
}
|
解析逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
pub fn parse_json(mut input: &str) -> Result<JsonType, ParserError> {
if input.trim().is_empty() {
return Err(ParserError::EmptyInput);
}
input = &input.trim_start();
match input.chars().nth(0).unwrap() {
'{' => {
// 解析JSON对象
match parse_object(&input) {
Ok(obj) => Ok(JsonType::Object(obj.0)),
Err(e) => Err(e)
}
},
'[' => {
// 解析JSON数组
match parse_array(&input) {
Ok(arr) => Ok(JsonType::Array(arr.0)),
Err(e) => Err(e)
}
},
_ => return Err(ParserError::UnexpectedToken(format!("Unexpected token: {}", input.chars().nth(0).unwrap())))
}
}
|
这里有什么?这个方法是我们的起点,它接受一个字符串切片作为参数,并返回一个Result<JsonType, ParserError>
。Result表示OK或Err。
JSON的顶级类型要么是数组,要么是对象。所以我们查看第一个字符,根据符号我们解析数组/对象,或者返回一个ParserError。
现在来看对象解析函数的实现代码(如果你想看完整的代码,请访问上面包含的链接):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
|
fn parse_object(mut input: &str) -> Result<(HashMap<String, JsonType>, &str), ParserError> {
let mut result = HashMap::new();
if input.chars().nth(0).unwrap() != '{' {
return Err(ParserError::InvalidSyntax("Object must start with '{'".to_string()));
}
input = &input[1..].trim_start();
loop {
// 解析每个键值对
if (input.chars().nth(0).unwrap()) == '}' {
return Ok((result, &input[1..])); // 空对象
}
match parse_string(&input) {
Ok(key) => {
// 期望一个冒号
input = key.1;
if input.chars().nth(0).unwrap() != ':' {
return Err(ParserError::MissingToken("Expected ':' after key".to_string()));
}
input = &input[1..].trim_start();
let value = if input.chars().nth(0).unwrap() == '{' {
match parse_object(&input) {
Ok(obj) => {
input = obj.1;
JsonType::Object(obj.0)
},
Err(e) => return Err(e)
}
} else if input.chars().nth(0).unwrap() == '[' {
match parse_array(&input) {
Ok(arr) => {
input = arr.1;
JsonType::Array(arr.0)
},
Err(e) => return Err(e)
}
} else if input.chars().nth(0).unwrap() == '"' {
match parse_string(input) {
Ok(s) => {
input = s.1;
JsonType::String(s.0)
},
Err(e) => return Err(e)
}
} else if input.chars().nth(0).unwrap() == 't' || input.chars().nth(0).unwrap() == 'f' {
match parse_boolean(input) {
Ok(b) => {
input = b.1;
JsonType::Boolean(b.0)
},
Err(e) => return Err(e)
}
} else if input.chars().nth(0).unwrap().is_digit(10) || input.chars().nth(0).unwrap() == '-' {
match parse_number(input) {
Ok(n) => {
input = n.1;
n.0
},
Err(e) => return Err(e)
}
} else {
return Err(ParserError::UnexpectedToken(format!("Unexpected token in object value: {}", input.chars().nth(0).unwrap())));
};
result.insert(key.0, value);
input = input.trim_start();
// 检查逗号或对象结束
if input.chars().nth(0).unwrap() == ',' {
// 跳过逗号
input = &input[1..].trim_start();
} else if input.chars().nth(0).unwrap() == '}' {
input = &input[1..].trim_start();
break; // 对象结束
} else {
return Err(ParserError::UnexpectedToken(format!("Expected ',' or '}}' in object, found: {}", input.chars().nth(0).unwrap())));
}
},
Err(e) => return Err(e)
}
}
Ok((result, &input))
}
|
我想解释一下这些方法遵循的概念:每个方法在成功情况下返回一个元组,包含解析的元素以及一个字符串切片。该切片是解析逻辑后剩余的json。这将在调用解析方法中用作它应该继续的点。
为了读取JSON对象,我们需要一个循环,持续读取属性,直到遇到}
。每个属性包含一个名称:值对。名称始终是一个字符串,所以我们可以重用我们的字符串解析方法。如果成功,我们读取:
,然后检查下一个字符。根据这个符号,我们进入适当的读取方法。当这个操作成功时,我们设置剩余的切片并创建正确的JsonType。最后一步是将键值对添加到我们的hashmap中。
欢迎发送建议、错误报告和改进想法🙂