Rust实现JSON解析器:从枚举类型到解析逻辑详解

本文详细介绍了如何使用Rust语言实现一个JSON解析器,包括JSON类型的Rust枚举表示、解析错误处理以及对象和数组的具体解析逻辑。通过递归解析方法和剩余字符串切片技术,完整展现了JSON解析的实现过程。

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中。

欢迎发送建议、错误报告和改进想法🙂

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计