用 Rust 解析 JSON
在我學習 Rust 的過程中,我一直在尋找簡單而實用的項目來讓我的頭腦掌握基本概念,並增加我的語法肌肉記憶。在大多數語言中,我發現自己每天都在做的一件事就是從不同的源加載數據,如文本文件,或者更結構化的源,比如 JSON 或 YAML。
在本文中,我將介紹如何使用 serde 和 serde-json 將 JSON 讀取、解析並序列化爲 Rust structs 以在 Rust 應用程序中使用。
創建新項目
讓我們創建一個新的 Rust 應用程序:
cargo new json-parser
進入 json-parser 目錄後,使用如下命令安裝 serde 和 serde_json,併爲 serde 添加派生特性,我們將在後面使用它:
cargo add serde --features derive
cargo add serde_json
現在讓我們創建解析器!在 src 中創建一個 src/parser.rs 新文件,這將包含我們所有的解析邏輯。
如果你查看 serde_json 的 README 文件,可以找到一些關於如何開箱即用的出色示例。讓我們嘗試其中一個,看看我們是否正確地安裝了所有內容:
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}
現在我們可以在 src/main.rs 中使用這個函數:
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}
執行 cargo run,執行結果:
Please call "John Doe" at the number "+44 1234567"
從這個示例中可以看出,庫 API 非常容易使用。公開了 from_str()方法,該方法從字符串中解析 JSON,也可以從本地或遠程文件加載該方法 (我們將在後面進行此操作)。一旦 JSON 被解析,就可以通過 JSON 中的屬性或鍵來訪問數據(例如 v["name"] 從{"name": "John Doe"}獲取值)。
現在我們已經把它集成到我們的應用程序中了,讓我們看看我們還能對這個庫做些什麼。
從本地文件加載 JSON
爲了加載 JSON,我們需要一個 JSON 文件。我們來創建一個。在項目的根目錄下創建一個名爲 data 的文件夾。在它裏面,創建一個 test.json 文件。
{
"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}
爲了加載數據,我們使用 Rust 標準庫中的 FileSystem API。我們將在 main.rs 中這樣做,並將數據 (也就是 JSON 的長字符串) 傳遞給解析器:
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}
運行 cargo run,應該得到與以前相同的結果 (“Please call…” 消息)。
類型化數據
如果我們在解析數據之前知道數據的結構呢?這將允許我們使用嚴格類型的結構來訪問數據——因此,我們不用 v["name"] 來訪問名稱,當我們鍵入 v 時,我們可以在 IDE 中實現自動補全:v.name,這是一種更好的開發者體驗。
serde_json 的 README 也爲處理類型化數據提供了一個很好的例子,我們可以完全複製粘貼到 parser.rs 中:
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}
然後用 main 中調用的 typed_example() 函數:
1pub mod parser;
2
3fn main() {
4 parser::typed_example();
5}
如何處理對象類型?
在看完這些例子後,我想到的第一個問題是——如何處理具有鍵和屬性的對象?似乎可以使用 HashMap<>類型併爲對象鍵 (通常是 String) 和對象值 (任何類型) 提供類型。
假設我有一個 JSON,看起來像這樣:
{
"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 表示任何浮點數,也就是帶小數的數字。
當 JSON 被解析時,返回一個 HashMap,所以你可以使用 get() 方法訪問裏面的數據,或者使用 for 循環遍歷所有值:
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 中,這相當於枚舉。
經過一番研究,我發現 serde 支持 Enum 類型,如果你傳遞未標記的宏給他們:
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);
在 JSON 中添加以下屬性:
{
"test": 200
}
serde 會從 Number(i32) Enum 屬性中抓取並返回它 - 你需要做一個 match 語句來找出它是什麼:
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}
這也很有效! 您可以輕鬆地創建一些 “動態” 類型,並基於它們的類型對它們進行相當嚴格的處理。
本文翻譯自:
https://dev.to/whoisryosuke/parsing-json-with-rust-5eg8
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/fuP1oOwc82sVrxG88B6nUA