I encountered a problem while creating a config parser for a CLI utility. Due to Rust's trait implementation rules, I can only implement a trait for a type in the crate where the trait is defined or in the crate where the type is defined. However, I want to parse some termcolor::Color
s from a config file.
The code compiles without any problems when I parse the colors to a String
:
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Config {
colors: HashMap<String, String>,
}
const TOML_DATA: &str = r#"
[colors]
color1 = "Cyan"
color2 = "Green"
not_valid_color = "Qwerty"
"#;
fn main() {
let cfg = toml::from_str::<Config>(TOML_DATA);
println!("{:#?}", cfg);
}
I found a working solution for parsing a single field:
use serde::Deserialize;
use termcolor::Color;
#[derive(Debug, Deserialize)]
#[serde(remote = "Color")]
enum ColorWrapper {
Black,
Blue,
Green,
Red,
Cyan,
Magenta,
Yellow,
White,
Ansi256(u8),
Rgb(u8, u8, u8),
__Nonexhaustive,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Config {
#[serde(with = "ColorWrapper")]
color: Color,
}
const TOML_DATA: &str = r#"
color = "Cyan"
"#;
fn main() {
let cfg = toml::from_str::<Config>(TOML_DATA);
println!("{:#?}", cfg);
}
But this approach has some constraints, such as requiring the original enum or struct to be fully public, or preventing me from parsing all colors into a hash table like this:
use serde::Deserialize;
use std::collections::HashMap;
use termcolor::Color;
#[derive(Debug, Deserialize)]
struct Config {
#[serde(with = "ColorWrapper")]
colors: HashMap<String, Color>,
}
I can also imagine the following solution, but it seems too verbose:
use serde::Deserialize;
use std::collections::HashMap;
use std::str::FromStr;
use termcolor::Color;
use thiserror::Error;
#[derive(Debug, Deserialize)]
struct RawConfig {
colors: HashMap<String, String>,
}
#[derive(Debug)]
#[allow(dead_code)]
struct Config {
colors: HashMap<String, Color>,
}
#[derive(Debug, Error)]
#[error("cannot parse config")]
struct ConfigParseError;
impl TryInto<Config> for RawConfig {
type Error = ConfigParseError;
fn try_into(self) -> Result<Config, Self::Error> {
let colors = self
.colors
.iter()
.try_fold(
HashMap::new(),
|mut acc, (key, value)| match Color::from_str(value) {
Ok(val) => {
acc.insert(String::clone(key), val);
Ok(acc)
}
Err(_) => Err(ConfigParseError),
},
)?;
Ok(Config { colors })
}
}
const TOML_DATA: &str = r#"
[colors]
color1 = "Cyan"
color2 = "Green"
#not_valid_color = "Qwerty"
"#;
fn main() {
let raw_cfg: RawConfig = toml::from_str(TOML_DATA).expect("cannot parse toml data");
println!("{:#?}", <RawConfig as TryInto<Config>>::try_into(raw_cfg));
}
How can I parse types which don't provide a Deserialize
implementation from third-party crates with serde?