在我学习Rust的过程中,我一直在寻找简单而实用的项目来让我的头脑掌握基本概念,并增加我的语法肌肉记忆。在大多数语言中,我发现自己每天都在做的一件事就是从不同的源加载数据,如文本文件,或者更结构化的源,比如JSON或YAML。
cargo new json-parser
cargo add serde --features derive
cargo add serde_json
现在让我们创建解析器!在src中创建一个src/parser.rs新文件,这将包含我们所有的解析逻辑。
1use serde_json::{Result, Value};
2
3pub fn untyped_example() -> Result<()> {
4 // Some JSON input data as a &str. Maybe this comes from the user.
5 let data = r#"
6 {
7 "name": "John Doe",
8 "age": 43,
9 "phones": [
10 "+44 1234567",
11 "+44 2345678"
12 ]
13 }"#;
14
15 // Parse the string of data into serde_json::Value.
16 let v: Value = serde_json::from_str(data)?;
17
18 // Access parts of the data by indexing with square brackets.
19 println!("Please call {} at the number {}", v["name"], v["phones"][0]);
20
21 Ok(())
22}
1pub mod parser;
2
3fn main() {
4 // Parse the JSON
5 let result = parser::untyped_example();
6
7 // Handle errors from the parser if any
8 match result {
9 Ok(result) => (),
10 Err(error) => print!("{}", error),
11 }
12}
Please call "John Doe" at the number "+44 1234567"
从这个示例中可以看出,库API非常容易使用。公开了from_str()方法,该方法从字符串中解析JSON,也可以从本地或远程文件加载该方法(我们将在后面进行此操作)。一旦JSON被解析,就可以通过JSON中的属性或键来访问数据(例如v["name"]从{"name": "John Doe"}获取值)。
{
"name": "John Doe",
"age": 43,
"phones": [
"+44 1234567",
"+44 2345678"
]
}
1pub fn untyped_example(json_data: &str) -> Result<()> {
2 let v: Value = serde_json::from_str(json_data)?;
3
4 // Access parts of the data by indexing with square brackets.
5 println!("Please call {} at the number {}", v["name"], v["phones"][0]);
6
7 Ok(())
8}
1use std::fs;
2
3pub mod parser;
4
5fn main() {
6 // Grab JSON file
7 let file_path = "data/test.json".to_owned();
8 let contents = fs::read_to_string(file_path).expect("Couldn't find or load that file.");
9
10 parser::untyped_example(&contents);
11}
如果我们在解析数据之前知道数据的结构呢?这将允许我们使用严格类型的结构来访问数据——因此,我们不用v["name"]来访问名称,当我们键入v时,我们可以在IDE中实现自动补全:v.name,这是一种更好的开发者体验。
1use serde::{Serialize, Deserialize};
2use serde_json::{Result, Value};
3
4#[derive(Serialize, Deserialize)]
5struct Person {
6 name: String,
7 age: u8,
8 phones: Vec<String>,
9}
10
11pub fn typed_example() -> Result<()> {
12 // Some JSON input data as a &str. Maybe this comes from the user.
13 let data = r#"
14 {
15 "name": "John Doe",
16 "age": 43,
17 "phones": [
18 "+44 1234567",
19 "+44 2345678"
20 ]
21 }"#;
22
23 // Parse the string of data into a Person object. This is exactly the
24 // same function as the one that produced serde_json::Value above, but
25 // now we are asking it for a Person as output.
26 let p: Person = serde_json::from_str(data)?;
27
28 // Do things just like with any other Rust data structure.
29 println!("Please call {} at the number {}", p.name, p.phones[0]);
30
31 Ok(())
32}
1pub mod parser;
2
3fn main() {
4 parser::typed_example();
5}
在看完这些例子后,我想到的第一个问题是——如何处理具有键和属性的对象?似乎可以使用HashMap<>类型并为对象键(通常是String)和对象值(任何类型)提供类型。
{
"animation": {
"default": "400ms ease-in",
"fast": "300ms ease-in"
},
"breakpoints": {
"mobile": "320px",
"tablet": "768px",
"computer": "992px",
"desktop": "1200px",
"widescreen": "1920px"
},
"colors": {
"text": "#111212",
"background": "#fff",
"primary": "#005CDD",
"secondary": "#6D59F0",
"muted": "#f6f6f9",
"gray": "#D3D7DA",
"highlight": "hsla(205, 100%, 40%, 0.125)",
"white": "#FFF",
"black": "#111212"
},
"fonts": {
"body": "Roboto, Helvetiva Neue, Helvetica, Aria, sans-serif",
"heading": "Archivo, Helvetiva Neue, Helvetica, Aria, sans-serif",
"monospace": "Menlo, monospace"
},
"font_sizes": [12, 14, 16, 20, 24, 32, 48, 64, 96],
"font_weights": {
"body": 400,
"heading": 500,
"bold": 700
},
"line_heights": {
"body": 1.5,
"heading": 1.25
},
"space": [0, 4, 8, 16, 32, 64, 128, 256, 512],
"sizes": {
"avatar": 48
},
"radii": {
"default": 0,
"circle": 99999
},
"shadows": {
"card": {
"light": "15px 15px 35px rgba(0, 127, 255, 0.5)"
}
},
"gradients": {
"primary": "linear-gradient()"
}
}
1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Result;
5
6#[derive(Serialize, Deserialize)]
7struct Theme {
8 colors: HashMap<String, String>,
9 space: Vec<i32>,
10 font_sizes: Vec<i32>,
11 fonts: HashMap<String, String>,
12 font_weights: HashMap<String, i32>,
13 line_heights: HashMap<String, f32>,
14 breakpoints: HashMap<String, String>,
15 animation: HashMap<String, String>,
16 gradients: HashMap<String, String>,
17}
你可以看到,我用String表示任何基于字符串的值,用i32表示数字,特别是用f32表示任何浮点数,也就是带小数的数字。
1// Get a single value
2println!("Black: {}", p.colors.get("black"));
3
4// Loop over all the colors
5for (key, color) in p.colors {
6 // Create the custom property
7 let custom_property = format!("--colors-{}", key);
8 let css_rule = format!("{}: {};", &custom_property, color);
9
10 // @TODO: Export a CSS stylesheet file (or return CSS)
11 println!("{}", css_rule);
12 stylesheet.push(css_rule);
13
14 // Add the custom property
15 theme_tokens.colors.insert(key, custom_property);
16}
但如果我们的属性是可选类型(比如“大小”单位可以是数字10或字符串10px)?在Typescript中,我们可以创建这样的类型Size = string | number。在Rust中,这相当于枚举。
1#[derive(Serialize, Deserialize, Debug)]
2#[serde(untagged)]
3enum Colors {
4 Name(String),
5 Number(i32),
6}
7
8#[derive(Serialize, Deserialize)]
9struct Theme {
10 test: Colors,
11}
12
13// ... after parsing
14let p: Theme = serde_json::from_str(json_data)?;
15println!("{:#?}", p.test);
{
"test": 200
}
1match p.test {
2 // We don't want the name so we do nothing by passing empty tuple
3 Name(val) -> (),
4 Number(num) -> println!("Theme Color is number: {}", num),
5}