use std::{
fmt::{self, Formatter},
path::PathBuf,
};
use serde::{
de::{self, MapAccess},
Deserialize, Deserializer, Serialize,
};
use crate::{ExtensionKey, Extensions, Identifier};
#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
pub struct CredentialSpec {
#[serde(flatten)]
pub kind: Kind,
#[serde(flatten)]
pub extensions: Extensions,
}
impl<'de> Deserialize<'de> for CredentialSpec {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_map(Visitor)
}
}
#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Kind {
Config(Identifier),
File(PathBuf),
Registry(String),
}
struct Visitor;
impl<'de> de::Visitor<'de> for Visitor {
type Value = CredentialSpec;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
formatter.write_str("struct CredentialSpec")
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
let mut kind = None;
let mut extensions = Extensions::new();
while let Some(key) = map.next_key()? {
match key {
Field::Config => set_kind(&mut kind, || map.next_value().map(Kind::Config))?,
Field::File => set_kind(&mut kind, || map.next_value().map(Kind::File))?,
Field::Registry => set_kind(&mut kind, || map.next_value().map(Kind::Registry))?,
Field::Extension(key) => {
if extensions.insert(key, map.next_value()?).is_some() {
return Err(de::Error::custom("duplicate extension key"));
}
}
}
}
let kind = kind.ok_or_else(|| de::Error::missing_field("config, file, or registry"))?;
Ok(CredentialSpec { kind, extensions })
}
}
fn set_kind<E, F>(kind: &mut Option<Kind>, f: F) -> Result<(), E>
where
E: de::Error,
F: FnOnce() -> Result<Kind, E>,
{
if kind.is_none() {
*kind = Some(f()?);
Ok(())
} else {
Err(E::custom(
"only one of `config`, `file`, or `registry` may be set",
))
}
}
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "snake_case")]
enum Field {
Config,
File,
Registry,
Extension(ExtensionKey),
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn round_trip() {
let test = CredentialSpec {
kind: Kind::File("test.json".into()),
extensions: Extensions::from([(ExtensionKey::new("x-test").unwrap(), "test".into())]),
};
let string = serde_yaml::to_string(&test).unwrap();
assert_eq!(string, "file: test.json\nx-test: test\n");
let test2 = serde_yaml::from_str(&string).unwrap();
assert_eq!(test, test2);
}
}