Как правильно парсить сложные данные JSON в Rust?

Я пишу свою собственную реализацию сбора Magic на Rust в качестве упражнения (бесполезности и) изучения языка, и я пытаюсь проанализировать данные json в структуры данных, используя serde и serde_json. Проблема, с которой я сталкиваюсь, заключается в том, что некоторые свойства структур и перечислений в моей структуре данных ПОДРАЗУМЕВАЮТСЯ данными, заданными в формате json, поэтому мне нужно, чтобы некоторые свойства выполнялись через некоторые функции, когда serde анализирует json, и я не знаю, как правильно это сделать. Я пробовал использовать #[serde(default = "parse_costs")], но мне нужно иметь возможность передавать аргументы функции, вызываемой по умолчанию.

Вот как выглядят данные в формате JSON:

{
  "library" : {
    "+2 Mace": [{
      "colorIdentity": [
        "W"
      ],
      "colors": [
        "W"
      ],
      "convertedManaCost": 2,
      "keywords": [
        "Equip"
      ],
      "layout": "normal",
      "manaCost": "{1}{W}",
      "manaValue": 2,
      "name": "+2 Mace",
      "subtypes": [
        "Equipment"
      ],
      "supertypes": [ ],
      "text": "Equipped creature gets +2/+2.\nEquip {3} ({3}: Attach to target creature you control. Equip only as a sorcery.)",
      "type": "Artifact — Equipment",
      "types": [
        "Artifact"
      ]
    }],
    // ... about 30,000 more cards that look more or less like the above.
  }
}

Вот отрывок того, как выглядит моя реализация ржавчины:

enum Color {
    #[strum(
        serialize = "black",
        serialize = "b",
        serialize = "{black}",
        ascii_case_insensitive
    )] // all colors have these strum serialize derives, i'm leaving them out for brevity.
    B,
    U,
    C,
    G,
    R,
    W,
    None,
}

// all structs have these derives, i'm leaving them out for brevity.
#[derive(Debug, Deserialize)]
struct Payment {
    color: Color,
    quantity: u8,
}

struct Cost {
    cost: HashMap<Color, u8>,
}

impl Cost {
    fn new(payments: Vec<Payment>) -> Self{
        let mut cost =  HashMap::new();
        if payments.len() == 0 {
            cost.insert(Color::None, 0);
        }
        payments.iter().for_each(|payment|{
            let key = &payment.color;
            if cost.contains_key(key) {
                let mut val = cost.get_mut(key).unwrap();
                *val += &payment.quantity;
            } else {
                cost.insert(payment.color.clone(), payment.quantity);
            }
        });
        return Self {cost}
    }
}

fn parse_costs(mana_cost: &str) -> Cost{
    let re = Regex::new(r"\{(\w+)}").unwrap();
    let haystack = mana_cost;
    let mut payments_vec:Vec<Payment> = vec!();

    for (_, [color]) in re.captures_iter(haystack).map(|c| c.extract()){
        if color.parse::<u8>().is_ok(){
            payments_vec.push(Payment{ color: Color::C, quantity: color.parse().unwrap() })
        } else {
            payments_vec.push(Payment { color: Color::from_str(color).unwrap(), quantity: 1 });
        }
    }
    Cost::new(payments_vec)
}

struct Card {
    // ... other properties that work fine and arent complicated
    #[serde(rename(deserialize = "text"), default)]
    description: String,
    #[serde(default)]
    keywords: Vec<String>,
    layout: String,
    #[serde(rename(deserialize = "manaCost"), default)]
    mana_cost: String,
    #[serde(rename(deserialize = "manaValue"), default)]
    mana_value: u8,
    name: String,

    // THE PROBLEM:

    // this is what I'm trying to do but this doesn't work because you cant pass arguments
    // to the default function
    #[serde(default = "parse_costs(manaCost)")]
    cost: Cost,
}

Итак, во-первых, есть ли способ сделать то, что я делаю, без реализации Deserialize для структуры вручную? Это выглядит СУПЕР сложным, и я хочу спуститься в эту кроличью нору только в случае крайней необходимости.

И во-вторых, есть ли лучший способ сделать то, что я пытаюсь здесь сделать? Потому что, если бы это был javascript, я бы решил эту проблему, просто преобразовав объект JSON в объект javascript, а затем просто перебирая каждое из свойств в библиотеке, попутно сопоставляя и преобразуя каждое свойство в свою структуру данных. Но Serde ОЧЕНЬ приближает меня к тому, что мне нужно, без необходимости дублировать кучу вещей в памяти, но... я просто не знаю, как заставить его делать последний бит.

Чего бы вы хотели, если бы cost было поставлено раньше mana_cost?

MeetTitan 26.06.2024 01:25

Вам действительно нужно поле mana_cost или вы можете просто использовать cost для хранения этой информации?

drewtato 26.06.2024 02:40

В идеале этого бы не произошло — mana_cost исходит из данных json, cost исходит от меня; но в случае, если mana_cost пуста, программа должна просто предположить, что стоимость равна нулю.

Andrew Luhring 26.06.2024 02:40

@drewtato mana_cost — это строка, которую мы анализируем, чтобы получить стоимость. я имею в виду, что после того, как мы его используем, нам вообще не нужно хранить его в объекте Card

Andrew Luhring 26.06.2024 02:41

Я только что обновил функцию parse_costs, чтобы это не был просто комментарий, если это вообще поможет.

Andrew Luhring 26.06.2024 02:45

У @drewtato, по моему мнению, лучшая идея. По возможности анализируйте поля во время десериализации, чтобы избежать сохранения как проанализированных, так и непроанализированных полей. Вы также можете десериализовать в промежуточную структуру со всеми неразобранными полями и преобразовать в структуру со всеми проанализированными полями, используя обычную старую функцию. Я склонен десериализовать свои данные в том виде, в каком они мне даны, дословно, а затем приводить оттуда к необходимым типам.

MeetTitan 26.06.2024 23:04
Как сделать HTTP-запрос в Javascript?
Как сделать HTTP-запрос в Javascript?
В JavaScript вы можете сделать HTTP-запрос, используя объект XMLHttpRequest или более новый API fetch. Вот пример для обоих методов:
3
6
96
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

При использовании Serde отдельные поля в одной структуре не знают друг о друге, поэтому такие преобразования должны происходить со структурой в целом. Что действительно позволяет Serde, так это выполнять столько обработки, сколько вы хотите, для одного значения.

Действительно, реализация Deserialize может быть очень сложной, но большей части этого можно избежать, используя реализацию Deserialize другого типа.

Вот как это можно сделать для Cost:

impl<'de> Deserialize<'de> for Cost {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        // `&str` can't deserialize JSON strings with escapes, and `String`
        // is not optimally efficient when there are no escapes, so we use
        // `Cow`. `Cow`'s deserialization uses `str` when it can, otherwise
        // it falls back to `String`.
        let cow = Cow::<str>::deserialize(deserializer)?;
        let s: &str = cow.as_ref();
        Ok(parse_costs(s))
    }
}

Тогда поставьте это mana_cost вместо String.

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Card {
    #[serde(rename = "text", default)]
    description: String,
    #[serde(default)]
    keywords: Vec<String>,
    layout: String,
    #[serde(default)]
    mana_cost: Cost,
    mana_value: u8,
    name: String,
}

Вся игровая площадка, с некоторыми другими исправлениями стиля.

Обратите внимание: если вы хотите десериализовать в стандартный тип (например, непосредственно в HashMap<Color, u8> без промежуточной Cost структуры), вы можете использовать deserialize_with, чтобы указать функцию десериализации для одного поля.

Jmb 26.06.2024 08:51

Другие вопросы по теме