Как "десериализовать с" для контейнера, используя serde в Rust
MVCE:
use std::collections::HashMap;
use std::fmt;
use std::marker::PhantomData;
use std::str::FromStr;
use serde; // 1.0.85
use serde::de::{self, MapAccess, Visitor}; // 1.0.85
use serde_derive::Deserialize; // 1.0.85
use toml; // 0.4.10
use void::Void; // 1.0.2
// See: https://serde.rs/string-or-struct.html
fn string_or_struct<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: serde::Deserialize<'de> + FromStr<Err = Void>,
D: serde::Deserializer<'de>,
{
// This is a Visitor that forwards string types to T's `FromStr` impl and
// forwards map types to T's `Deserialize` impl. The `PhantomData` is to
// keep the compiler from complaining about T being an unused generic type
// parameter. We need T in order to know the Value type for the Visitor
// impl.
struct StringOrStruct<T>(PhantomData<fn() -> T>);
impl<'de, T> Visitor<'de> for StringOrStruct<T>
where
T: serde::Deserialize<'de> + FromStr<Err = Void>,
{
type Value = T;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("string or map")
}
fn visit_str<E>(self, value: &str) -> Result<T, E>
where
E: de::Error,
{
Ok(FromStr::from_str(value).unwrap())
}
fn visit_map<M>(self, visitor: M) -> Result<T, M::Error>
where
M: MapAccess<'de>,
{
// `MapAccessDeserializer` is a wrapper that turns a `MapAccess`
// into a `Deserializer`, allowing it to be used as the input to T's
// `Deserialize` implementation. T then deserializes itself using
// the entries from the map visitor.
serde::Deserialize::deserialize(de::value::MapAccessDeserializer::new(visitor))
}
}
deserializer.deserialize_any(StringOrStruct(PhantomData))
}
impl FromStr for Obj {
type Err = Void;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Obj {
x: 0,
y: s.to_owned(),
})
}
}
// ----------------------------------------------------------------------------
#[derive(Debug, Deserialize)]
struct Obj {
x: isize,
y: String,
}
#[derive(Debug, Deserialize)]
struct Simple {
#[serde(deserialize_with = "string_or_struct")]
obj: Obj,
}
#[derive(Debug, Deserialize)]
struct InsideHashMap {
objs: HashMap<String, Obj>,
}
fn main() {
// Basic deserialization of Obj
let toml = r#"
x = 5
y = "hello"
"#;
let obj: Obj = toml::from_str(toml).unwrap();
println!("{:?}", obj);
// Basic deserialization of Obj as a field in a struct
let toml = r#"
[obj]
x = 5
y = "hello"
"#;
let simple: Simple = toml::from_str(toml).unwrap();
println!("{:?}", simple);
// Basic deserialization of Obj as a field in a struct as a string or struct
let toml = r#"
obj = "hello"
"#;
let simple: Simple = toml::from_str(toml).unwrap();
println!("{:?}", simple);
// Deserialization of an Obj inside a HashMap
let toml = r#"
[objs]
a = { x = 5, y = "hello" }
"#;
let working: InsideHashMap = toml::from_str(toml).unwrap();
println!("{:?}", working);
// Deserialization of Obj inside a HashMap field as a string or struct
let toml = r#"
[objs]
a = "hello"
"#;
let not_working: InsideHashMap = toml::from_str(toml).unwrap();
println!("{:?}", not_working);
}
Я хочу использовать serde для десериализации формата TOML, где структура может быть указана в виде строки или обычной спецификации структуры
a = "a string"
b = { x = 5, y = "another string" }
В этом примере я бы закончил с HashMap, который выглядит как
{
"a": Obj { x: 0, y: "a string" },
"b": Obj { x: 5, y: "another string" }
}
Я прочитал https://serde.rs/string-or-struct.html о том, как использовать атрибут "deserialize_with" в поле структуры. Но как мне это сделать, когда структура находится внутри контейнера, такого как HashMap?
#[derive(Debug, Deserialize)]
struct Obj {
x: isize,
y: String
}
#[derive(Debug, Deserialize)]
struct Simple {
#[serde(deserialize_with = "string_or_struct")]
obj: Obj
}
#[derive(Debug, Deserialize)]
struct InsideHashMap {
objs: HashMap<String, Obj> // <-- how can I use "deserialize_with" on Obj here
}
1 ответ
Во-первых, нам нужна другая структура, чтобы использовать deserialize_with
для нашего HashMap:
#[derive(Debug, Deserialize)]
struct Flatten {
#[serde(deserialize_with = "string_or_struct", flatten)]
obj: Obj,
}
Итак, мы можем написать:
#[derive(Debug, Deserialize)]
struct InsideHashMap {
objs: HashMap<String, Flatten>,
}
Это должно сработать, но это не потому, что (пока я не знаю, почему deserialize_with
не работает вместе, кажется, что он не использует deserialize_with
реализация)
Итак, мы должны использовать сложный путь, давайте реализуем это:
use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
use serde; // 1.0.85
use serde::de::{self, Deserialize, MapAccess, Visitor}; // 1.0.85
use serde::Deserializer;
use serde_derive::Deserialize; // 1.0.85
use toml; // 0.4.10
use void::Void; // 1.0.2
#[derive(Debug)]
struct Obj {
x: isize,
y: String,
}
struct ObjVisitor;
// OjbAux is here to avoid implement the deserialiser of the map by hand we can't use
// Obj cause it will cause infinite recursion
#[derive(Debug, Deserialize)]
struct ObjAux {
x: isize,
y: String,
}
impl<'de> Visitor<'de> for ObjVisitor {
type Value = Obj;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("string or map")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(FromStr::from_str(value).unwrap())
}
fn visit_map<M>(self, visitor: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let aux: ObjAux = Deserialize::deserialize(de::value::MapAccessDeserializer::new(visitor))?;
Ok(Obj { x: aux.x, y: aux.y })
}
}
impl<'de> Deserialize<'de> for Obj {
fn deserialize<D>(deserializer: D) -> Result<Obj, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(ObjVisitor)
}
}
impl FromStr for Obj {
type Err = Void;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Obj {
x: 0,
y: s.to_owned(),
})
}
}
#[derive(Debug, Deserialize)]
struct Simple {
obj: Obj,
}
#[derive(Debug, Deserialize)]
struct InsideHashMap {
objs: HashMap<String, Obj>,
}
fn main() {
// Basic deserialization of Obj
let toml = r#"
x = 5
y = "hello"
"#;
let obj: Obj = toml::from_str(toml).unwrap();
println!("{:?}", obj);
// Basic deserialization of Obj as a field in a struct
let toml = r#"
[obj]
x = 5
y = "hello"
"#;
let simple: Simple = toml::from_str(toml).unwrap();
println!("{:?}", simple);
// Basic deserialization of Obj as a field in a struct as a string or struct
let toml = r#"
obj = "hello"
"#;
let simple: Simple = toml::from_str(toml).unwrap();
println!("{:?}", simple);
// Deserialization of an Obj inside a HashMap
let toml = r#"
[objs]
a = { x = 5, y = "hello" }
"#;
let working: InsideHashMap = toml::from_str(toml).unwrap();
println!("{:?}", working);
// Deserialization of Obj inside a HashMap field as a string or struct
let toml = r#"
[objs]
a = "hello"
"#;
let not_working: InsideHashMap = toml::from_str(toml).unwrap();
println!("{:?}", not_working);
}
Эта работа, как и ожидалось.