Я пишу свою собственную реализацию сбора 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 ОЧЕНЬ приближает меня к тому, что мне нужно, без необходимости дублировать кучу вещей в памяти, но... я просто не знаю, как заставить его делать последний бит.
Вам действительно нужно поле mana_cost
или вы можете просто использовать cost
для хранения этой информации?
В идеале этого бы не произошло — mana_cost
исходит из данных json, cost
исходит от меня; но в случае, если mana_cost пуста, программа должна просто предположить, что стоимость равна нулю.
@drewtato mana_cost — это строка, которую мы анализируем, чтобы получить стоимость. я имею в виду, что после того, как мы его используем, нам вообще не нужно хранить его в объекте Card
Я только что обновил функцию parse_costs, чтобы это не был просто комментарий, если это вообще поможет.
У @drewtato, по моему мнению, лучшая идея. По возможности анализируйте поля во время десериализации, чтобы избежать сохранения как проанализированных, так и непроанализированных полей. Вы также можете десериализовать в промежуточную структуру со всеми неразобранными полями и преобразовать в структуру со всеми проанализированными полями, используя обычную старую функцию. Я склонен десериализовать свои данные в том виде, в каком они мне даны, дословно, а затем приводить оттуда к необходимым типам.
При использовании 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, чтобы указать функцию десериализации для одного поля.
Чего бы вы хотели, если бы
cost
было поставлено раньшеmana_cost
?