Когда я изучал Rust, я искал простые и практичные проекты, чтобы разобраться в основных концепциях и улучшить свою синтаксическую мышечную память. Одна из вещей, которые я делаю каждый день на большинстве языков, — это загрузка данных из разных источников, таких как текстовые файлы, или более структурированных источников, таких как JSON или YAML.

Так что это заставило меня задуматься, как будет выглядеть синтаксический анализ JSON в Rust? Однако не с нуля — мы не заинтересованы в построении парсера. В идеале мы бы использовали либо внутренние методы Rust, либо сторонний крейт.

В этой статье я расскажу, как я использовал основной а также serde-json для чтения, анализа и сериализации JSON в структуры Rust для использования в приложениях Rust. Вы можете найти окончательный исходный код на Github.

Давайте создадим новое приложение Rust для кода внутри. Вы также можете клонировать эту ветку и пропустите этот шаг.

cargo new json-parser
Войти в полноэкранный режим

Выйти из полноэкранного режима

Как только вы перейдете внутрь, установите serde а также serde_json. Мы также добавим derive функция для serde, которую мы будем использовать позже.

cargo add serde --features derive
cargo add serde_json
Войти в полноэкранный режим

Выйти из полноэкранного режима

Теперь давайте сделаем наш парсер! Создайте новый файл в src/parser.rs который будет содержать всю нашу логику синтаксического анализа.

Если вы посмотрите на файл serde_json README, вы можете найти несколько фантастических примеров того, как использовать библиотеку из коробки. Давайте попробуем один из них, чтобы увидеть, все ли мы установили правильно:

use serde_json::{Result, Value};

pub fn untyped_example() -> Result<()> {
    // Some JSON input data as a &str. Maybe this comes from the user.
    let data = r#"
        {
            "name": "John Doe",
            "age": 43,
            "phones": [
                "+44 1234567",
                "+44 2345678"
            ]
        }"#;

    // Parse the string of data into serde_json::Value.
    let v: Value = serde_json::from_str(data)?;

    // Access parts of the data by indexing with square brackets.
    println!("Please call {} at the number {}", v["name"], v["phones"][0]);

    Ok(())
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Теперь мы можем использовать эту функцию внутри нашего src/main.rs

// "Import" anything public in the parser module
pub mod parser;

fn main() {
    println!("Hello, world!");

    // Parse the JSON
    let result = parser::untyped_example();

    // Handle errors from the parser if any
    match result {
        Ok(result) => (),
        Err(error) => print!("{}", error),
    }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Попробуйте запустить это с cargo run. Вы должны увидеть текст «Пожалуйста, позвоните Джону Доу по…» в консоли. Если нет, мы также добавили обработку ошибок match который должен возвращать любые ошибки из serde.

Из этого примера видно, что 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"
    ]
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Для загрузки данных мы можем использовать FileSystem API из стандартной библиотеки Rust. Мы сделаем это в нашем main.rs файл и передать данные (также известные как длинная строка JSON) парсеру.

// src/main.rs
use std::fs;

pub mod parser;

fn main() {
    println!("Hello, world!");

    // Grab JSON file
    let file_path = "data/test.json".to_owned();
    let contents = fs::read_to_string(file_path).expect("Couldn't find or load that file.");

    parser::untyped_example(&contents);
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

И нам нужно изменить синтаксический анализатор, чтобы принимать данные сейчас. Вы также можете удалить data переменная со встроенным JSON, так как она нам больше не нужна:

// src/parser.rs
pub fn untyped_example(json_data: &str) -> Result<()> {
    let v: Value = serde_json::from_str(json_data)?;
Войти в полноэкранный режим

Выйти из полноэкранного режима

Попробуйте запустить это с cargo run и вы должны получить тот же результат, что и раньше (сообщение «Пожалуйста, позвоните…»).


Типизированные данные

Но что, если мы узнаем, какова структура наших данных, до того, как проанализируем их? Например, мы можем захотеть разобрать тему, используя Системный интерфейс / Спецификация стилизованной системыкак Пользовательский интерфейс чакры.

Это позволило бы нам получить доступ к нашим данным, используя строго типизированный structs — поэтому вместо использования v["name"] чтобы получить доступ к имени, мы могли бы получить автодополнение в нашей IDE, когда мы набираем v. и это закончилось бы v.name. Это намного удобнее для разработчиков и создает больше страховочных сетей против использования свойств, которых не существует.

serde_json README также предоставляет отличный пример обработки типизированных данных. Мы можем полностью скопировать и вставить это в [parser.rs]( файл.

use serde::{Deserialize, Serialize};
use serde_json::Result;

#[derive(Serialize, Deserialize)]
struct Person {
    name: String,
    age: u8,
    phones: Vec<String>,
}

pub fn typed_example() -> Result<()> {
    // Some JSON input data as a &str. Maybe this comes from the user.
    let data = r#"
        {
            "name": "John Doe",
            "age": 43,
            "phones": [
                "+44 1234567",
                "+44 2345678"
            ]
        }"#;

    // Parse the string of data into a Person object. This is exactly the
    // same function as the one that produced serde_json::Value above, but
    // now we are asking it for a Person as output.
    let p: Person = serde_json::from_str(data)?;

    // Do things just like with any other Rust data structure.
    println!("Please call {} at the number {}", p.name, p.phones[0]);

    Ok(())
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Затем замените untyped_example функция с typed_example функция в вашем main.rs файл.

pub mod parser;

fn main() {
    println!("Hello, world!");

        // You can keep the file system stuff, I removed it for simplicity
        // we'll use it later in the tutorial

    parser::typed_example();
}
Войти в полноэкранный режим

Выйти из полноэкранного режима


Как работать с типами объектов?

Итак, первый вопрос, который пришел в голову после просмотра примеров — как обращаться с объектом с ключами и свойствами? Кажется, вы можете использовать HashMap<> введите и укажите тип ключа объекта (обычно это String) и значение объекта (что угодно — иногда String — может быть i32 для чисел).

Итак, скажем, у меня была тема 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()"
  }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Вы можете структурировать этот тип, чтобы он выглядел следующим образом:

use std::collections::HashMap;

use serde::{Deserialize, Serialize};
use serde_json::Result;

#[derive(Serialize, Deserialize)]
struct Theme {
    colors: HashMap<String, String>,
    space: Vec<i32>,
    font_sizes: Vec<i32>,
    fonts: HashMap<String, String>,
    font_weights: HashMap<String, i32>,
    line_heights: HashMap<String, f32>,
    breakpoints: HashMap<String, String>,
    animation: HashMap<String, String>,
    gradients: HashMap<String, String>,
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Вы можете видеть, что я использую String для любых строковых значений, i32 для цифр и конкретно f32 для любых «поплавков», то есть чисел с десятичными знаками.

Когда JSON анализируется, HashMap возвращается, поэтому вы можете получить доступ к данным внутри с помощью get() метод — или перебрать все значения, используя for петля:

// Get a single value
println!("Black: {}", p.colors.get("black"));

// Loop over all the colors
for (key, color) in p.colors {
    // Create the custom property
    let custom_property = format!("--colors-{}", key);
    let css_rule = format!("{}: {};", &custom_property, color);

    // @TODO: Export a CSS stylesheet file (or return CSS)
    println!("{}", css_rule);
    stylesheet.push(css_rule);

    // Add the custom property
    theme_tokens.colors.insert(key, custom_property);
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Это работает очень хорошо! Здесь вы можете видеть, что мы можем перебирать цвета и даже генерировать пользовательские свойства CSS на основе ключа и значения из HashMap (также известного как имя и значение цвета).


Обработка необязательных / нескольких типов

Но что, если у нас есть необязательные типы? Или несколько типов для одного и того же свойства (например, единица измерения размера, которая может быть числом). 10 или строка типа 10px)? В Typescript мы могли бы просто создать такой тип type Size = string | number. В Rust эквивалентом этого будет Enum.

После немного исследую Я обнаружил, что serde поддерживает типы Enum, если вы передаете untagged макрос к ним:

#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
enum Colors {
    Name(String),
    Number(i32),
}

#[derive(Serialize, Deserialize)]
struct Theme {
    test: Colors,
}

// ... after parsing
let p: Theme = serde_json::from_str(json_data)?;
println!("{:#?}", p.test);
Войти в полноэкранный режим

Выйти из полноэкранного режима

Добавьте в свой JSON следующее свойство:

{
    "test": 200
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

И серде выхватит из Number(i32) Перечислите свойство и верните его — вам нужно будет сделать match заявление, чтобы выяснить, что это такое + вернуть значение:

match p.test {
    // We don't want the name so we do nothing by passing empty tuple
    Name(val) -> (),
    Number(num) -> println!("Theme Color is number: {}", num),
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Это тоже прекрасно работает! Вы можете легко создать несколько «динамических» типов и по-прежнему иметь довольно строгое обращение с ними в зависимости от их типа.

Я надеюсь, что это дало вам хорошее представление о том, как анализировать JSON в Rust с помощью ящика serde и как обрабатывать различные варианты использования и типы данных. Есть много крутых приложений, которые вы можете создать, используя JSON (или другие типы файлов — serde поддерживает такие форматы, как YAML, TOML и другие.).

Как всегда, вы можете найти полный код для этого урока на моем Github.

Если у вас есть какие-либо вопросы или вы хотите поделиться тем, над чем вы работали, не стесняйтесь поразил меня в Твиттере.

Ваше здоровье,
Ре