compose_spec/service/
credential_spec.rs

1//! Provides [`CredentialSpec`] for the `credential_spec` field of [`Service`](super::Service).
2
3use 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/// Credential spec for a managed service account.
16///
17/// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/05-services.md#credential_spec)
18#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
19pub struct CredentialSpec {
20    /// One of [`config`](Kind::Config), [`file`](Kind::File), or [`registry`](Kind::Registry).
21    ///
22    /// (De)serialized via flattening.
23    #[serde(flatten)]
24    pub kind: Kind,
25
26    /// Extension values, which are (de)serialized via flattening.
27    ///
28    /// [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/11-extension.md)
29    #[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/// Kind of [`CredentialSpec`].
40#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
41#[serde(rename_all = "snake_case")]
42pub enum Kind {
43    /// [`CredentialSpec`] set in top-level `configs` field of [`Compose`](crate::Compose).
44    Config(Identifier),
45
46    /// Read from file.
47    File(PathBuf),
48
49    /// Read from the Windows registry on the daemon's host.
50    Registry(String),
51}
52
53/// [`de::Visitor`] for deserializing [`CredentialSpec`].
54struct 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
85/// Set the `kind` field with the result of `f` if it is [`None`], otherwise error.
86fn 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/// Fields of [`CredentialSpec`].
102#[derive(Deserialize)]
103#[serde(field_identifier, rename_all = "snake_case")]
104enum Field {
105    /// [`config`](Kind::Config)
106    Config,
107
108    /// [`file`](Kind::File)
109    File,
110
111    /// [`registry`](Kind::Registry)
112    Registry,
113
114    /// Extension key.
115    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}