wasm_pkg_common/
metadata.rs

1use std::{
2    borrow::Cow,
3    collections::{BTreeSet, HashMap},
4};
5
6use serde::{de::DeserializeOwned, Deserialize, Serialize};
7
8use crate::Error;
9
10/// Well-Known URI (RFC 8615) path for registry metadata.
11pub const REGISTRY_METADATA_PATH: &str = "/.well-known/wasm-pkg/registry.json";
12
13type JsonObject = serde_json::Map<String, serde_json::Value>;
14
15#[derive(Debug, Default, Clone, Deserialize, Serialize)]
16#[serde(rename_all = "camelCase")]
17pub struct RegistryMetadata {
18    /// The registry's preferred protocol.
19    pub preferred_protocol: Option<String>,
20
21    /// Protocol-specific configuration.
22    #[serde(flatten)]
23    pub protocol_configs: HashMap<String, JsonObject>,
24
25    // Backward-compatibility aliases:
26    /// OCI Registry
27    #[serde(skip_serializing)]
28    oci_registry: Option<String>,
29
30    /// OCI Namespace Prefix
31    #[serde(skip_serializing)]
32    oci_namespace_prefix: Option<String>,
33
34    /// Warg URL
35    #[serde(skip_serializing)]
36    warg_url: Option<String>,
37}
38
39const OCI_PROTOCOL: &str = "oci";
40const WARG_PROTOCOL: &str = "warg";
41
42impl RegistryMetadata {
43    /// Returns the registry's preferred protocol.
44    ///
45    /// The preferred protocol is:
46    /// - the `preferredProtocol` metadata field, if given
47    /// - the protocol configuration key, if only one configuration is given
48    /// - the protocol backward-compatible aliases configuration, if only one configuration is given
49    pub fn preferred_protocol(&self) -> Option<&str> {
50        if let Some(protocol) = self.preferred_protocol.as_deref() {
51            return Some(protocol);
52        }
53        if self.protocol_configs.len() == 1 {
54            return self.protocol_configs.keys().next().map(|x| x.as_str());
55        } else if self.protocol_configs.is_empty() {
56            match (self.oci_registry.is_some(), self.warg_url.is_some()) {
57                (true, false) => return Some(OCI_PROTOCOL),
58                (false, true) => return Some(WARG_PROTOCOL),
59                _ => {}
60            }
61        }
62        None
63    }
64
65    /// Returns an iterator of protocols configured by the registry.
66    pub fn configured_protocols(&self) -> impl Iterator<Item = Cow<'_, str>> {
67        let mut protos: BTreeSet<String> = self.protocol_configs.keys().cloned().collect();
68        // Backward-compatibility aliases
69        if self.oci_registry.is_some() || self.oci_namespace_prefix.is_some() {
70            protos.insert(OCI_PROTOCOL.into());
71        }
72        if self.warg_url.is_some() {
73            protos.insert(WARG_PROTOCOL.into());
74        }
75        protos.into_iter().map(Into::into)
76    }
77
78    /// Deserializes protocol config for the given protocol.
79    ///
80    /// Returns `Ok(None)` if no configuration is available for the given
81    /// protocol.
82    /// Returns `Err` if configuration is available for the given protocol but
83    /// deserialization fails.
84    pub fn protocol_config<T: DeserializeOwned>(&self, protocol: &str) -> Result<Option<T>, Error> {
85        let mut config = self.protocol_configs.get(protocol).cloned();
86
87        // Backward-compatibility aliases
88        let mut maybe_set = |key: &str, val: &Option<String>| {
89            if let Some(value) = val {
90                config
91                    .get_or_insert_with(Default::default)
92                    .insert(key.into(), value.clone().into());
93            }
94        };
95        match protocol {
96            OCI_PROTOCOL => {
97                maybe_set("registry", &self.oci_registry);
98                maybe_set("namespacePrefix", &self.oci_namespace_prefix);
99            }
100            WARG_PROTOCOL => {
101                maybe_set("url", &self.warg_url);
102            }
103            _ => {}
104        }
105
106        if config.is_none() {
107            return Ok(None);
108        }
109        Ok(Some(
110            serde_json::from_value(config.unwrap().into())
111                .map_err(|err| Error::InvalidRegistryMetadata(err.into()))?,
112        ))
113    }
114
115    /// Set the OCI registry
116    #[cfg(feature = "oci_extras")]
117    pub fn set_oci_registry(&mut self, registry: Option<String>) {
118        self.oci_registry = registry;
119    }
120
121    /// Set the OCI namespace prefix
122    #[cfg(feature = "oci_extras")]
123    pub fn set_oci_namespace_prefix(&mut self, ns_prefix: Option<String>) {
124        self.oci_namespace_prefix = ns_prefix;
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use serde_json::json;
131
132    use super::*;
133
134    #[derive(Deserialize, Debug, PartialEq)]
135    #[serde(rename_all = "camelCase")]
136    struct OtherProtocolConfig {
137        key: String,
138    }
139
140    #[test]
141    fn smoke_test() {
142        let meta: RegistryMetadata = serde_json::from_value(json!({
143            "oci": {"registry": "oci.example.com"},
144            "warg": {"url": "https://warg.example.com"},
145        }))
146        .unwrap();
147        assert_eq!(meta.preferred_protocol(), None);
148        assert_eq!(
149            meta.configured_protocols().collect::<Vec<_>>(),
150            ["oci", "warg"]
151        );
152        let oci_config: JsonObject = meta.protocol_config("oci").unwrap().unwrap();
153        assert_eq!(oci_config["registry"], "oci.example.com");
154        let warg_config: JsonObject = meta.protocol_config("warg").unwrap().unwrap();
155        assert_eq!(warg_config["url"], "https://warg.example.com");
156        let other_config: Option<OtherProtocolConfig> = meta.protocol_config("other").unwrap();
157        assert_eq!(other_config, None);
158    }
159
160    #[test]
161    fn preferred_protocol_explicit() {
162        let meta: RegistryMetadata = serde_json::from_value(json!({
163            "preferredProtocol": "warg",
164            "oci": {"registry": "oci.example.com"},
165            "warg": {"url": "https://warg.example.com"},
166        }))
167        .unwrap();
168        assert_eq!(meta.preferred_protocol(), Some("warg"));
169    }
170
171    #[test]
172    fn preferred_protocol_implicit_oci() {
173        let meta: RegistryMetadata = serde_json::from_value(json!({
174            "oci": {"registry": "oci.example.com"},
175        }))
176        .unwrap();
177        assert_eq!(meta.preferred_protocol(), Some("oci"));
178    }
179
180    #[test]
181    fn preferred_protocol_implicit_warg() {
182        let meta: RegistryMetadata = serde_json::from_value(json!({
183            "warg": {"url": "https://warg.example.com"},
184        }))
185        .unwrap();
186        assert_eq!(meta.preferred_protocol(), Some("warg"));
187    }
188
189    #[test]
190    fn backward_compat_preferred_protocol_implicit_oci() {
191        let meta: RegistryMetadata = serde_json::from_value(json!({
192            "ociRegistry": "oci.example.com",
193            "ociNamespacePrefix": "prefix/",
194        }))
195        .unwrap();
196        assert_eq!(meta.preferred_protocol(), Some("oci"));
197    }
198
199    #[test]
200    fn backward_compat_preferred_protocol_implicit_warg() {
201        let meta: RegistryMetadata = serde_json::from_value(json!({
202            "wargUrl": "https://warg.example.com",
203        }))
204        .unwrap();
205        assert_eq!(meta.preferred_protocol(), Some("warg"));
206    }
207
208    #[test]
209    fn basic_backward_compat_test() {
210        let meta: RegistryMetadata = serde_json::from_value(json!({
211            "ociRegistry": "oci.example.com",
212            "ociNamespacePrefix": "prefix/",
213            "wargUrl": "https://warg.example.com",
214        }))
215        .unwrap();
216        assert_eq!(
217            meta.configured_protocols().collect::<Vec<_>>(),
218            ["oci", "warg"]
219        );
220        let oci_config: JsonObject = meta.protocol_config("oci").unwrap().unwrap();
221        assert_eq!(oci_config["registry"], "oci.example.com");
222        assert_eq!(oci_config["namespacePrefix"], "prefix/");
223        let warg_config: JsonObject = meta.protocol_config("warg").unwrap().unwrap();
224        assert_eq!(warg_config["url"], "https://warg.example.com");
225    }
226
227    #[test]
228    fn merged_backward_compat_test() {
229        let meta: RegistryMetadata = serde_json::from_value(json!({
230            "wargUrl": "https://warg.example.com",
231            "other": {"key": "value"}
232        }))
233        .unwrap();
234        assert_eq!(
235            meta.configured_protocols().collect::<Vec<_>>(),
236            ["other", "warg"]
237        );
238        let warg_config: JsonObject = meta.protocol_config("warg").unwrap().unwrap();
239        assert_eq!(warg_config["url"], "https://warg.example.com");
240        let other_config: OtherProtocolConfig = meta.protocol_config("other").unwrap().unwrap();
241        assert_eq!(other_config.key, "value");
242    }
243
244    #[test]
245    fn bad_protocol_config() {
246        let meta: RegistryMetadata = serde_json::from_value(json!({
247            "other": {"bad": "config"}
248        }))
249        .unwrap();
250        assert_eq!(meta.configured_protocols().collect::<Vec<_>>(), ["other"]);
251        let res = meta.protocol_config::<OtherProtocolConfig>("other");
252        assert!(res.is_err(), "{res:?}");
253    }
254}