compose_spec/service/
credential_spec.rs1use std::{
4 fmt::{self, Formatter},
5 path::PathBuf,
6};
7
8use serde::{
9 de::{self, MapAccess},
10 Deserialize, Deserializer, Serialize,
11};
12
13use crate::{ExtensionKey, Extensions, Identifier};
14
15#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
19pub struct CredentialSpec {
20 #[serde(flatten)]
24 pub kind: Kind,
25
26 #[serde(flatten)]
30 pub extensions: Extensions,
31}
32
33impl<'de> Deserialize<'de> for CredentialSpec {
34 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
35 deserializer.deserialize_map(Visitor)
36 }
37}
38
39#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
41#[serde(rename_all = "snake_case")]
42pub enum Kind {
43 Config(Identifier),
45
46 File(PathBuf),
48
49 Registry(String),
51}
52
53struct Visitor;
55
56impl<'de> de::Visitor<'de> for Visitor {
57 type Value = CredentialSpec;
58
59 fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
60 formatter.write_str("struct CredentialSpec")
61 }
62
63 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
64 let mut kind = None;
65 let mut extensions = Extensions::new();
66
67 while let Some(key) = map.next_key()? {
68 match key {
69 Field::Config => set_kind(&mut kind, || map.next_value().map(Kind::Config))?,
70 Field::File => set_kind(&mut kind, || map.next_value().map(Kind::File))?,
71 Field::Registry => set_kind(&mut kind, || map.next_value().map(Kind::Registry))?,
72 Field::Extension(key) => {
73 if extensions.insert(key, map.next_value()?).is_some() {
74 return Err(de::Error::custom("duplicate extension key"));
75 }
76 }
77 }
78 }
79
80 let kind = kind.ok_or_else(|| de::Error::missing_field("config, file, or registry"))?;
81 Ok(CredentialSpec { kind, extensions })
82 }
83}
84
85fn set_kind<E, F>(kind: &mut Option<Kind>, f: F) -> Result<(), E>
87where
88 E: de::Error,
89 F: FnOnce() -> Result<Kind, E>,
90{
91 if kind.is_none() {
92 *kind = Some(f()?);
93 Ok(())
94 } else {
95 Err(E::custom(
96 "only one of `config`, `file`, or `registry` may be set",
97 ))
98 }
99}
100
101#[derive(Deserialize)]
103#[serde(field_identifier, rename_all = "snake_case")]
104enum Field {
105 Config,
107
108 File,
110
111 Registry,
113
114 Extension(ExtensionKey),
116}
117
118#[cfg(test)]
119#[allow(clippy::unwrap_used)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn round_trip() {
125 let test = CredentialSpec {
126 kind: Kind::File("test.json".into()),
127 extensions: Extensions::from([(ExtensionKey::new("x-test").unwrap(), "test".into())]),
128 };
129
130 let string = serde_yaml::to_string(&test).unwrap();
131 assert_eq!(string, "file: test.json\nx-test: test\n");
132
133 let test2 = serde_yaml::from_str(&string).unwrap();
134 assert_eq!(test, test2);
135 }
136}