用 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