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    /// OCI Namespace Prefix
30    #[serde(skip_serializing)]
31    oci_namespace_prefix: Option<String>,
32    /// Warg URL
33    #[serde(skip_serializing)]
34    warg_url: Option<String>,
35}
36
37const OCI_PROTOCOL: &str = "oci";
38const WARG_PROTOCOL: &str = "warg";
39
40impl RegistryMetadata {
41    /// Returns the registry's preferred protocol.
42    ///
43    /// The preferred protocol is:
44    /// - the `preferredProtocol` metadata field, if given
45    /// - the protocol configuration key, if only one configuration is given
46    /// - the protocol backward-compatible aliases configuration, if only one configuration is given
47    pub fn preferred_protocol(&self) -> Option<&str> {
48        if let Some(protocol) = self.preferred_protocol.as_deref() {
49            return Some(protocol);
50        }
51        if self.protocol_configs.len() == 1 {
52            return self.protocol_configs.keys().next().map(|x| x.as_str());
53        } else if self.protocol_configs.is_empty() {
54            match (self.oci_registry.is_some(), self.warg_url.is_some()) {
55                (true, false) => return Some(OCI_PROTOCOL),
56                (false, true) => return Some(WARG_PROTOCOL),
57                _ => {}
58            }
59        }
60        None
61    }
62
63    /// Returns an iterator of protocols configured by the registry.
64    pub fn configured_protocols(&self) -> impl Iterator<Item = Cow<str>> {
65        let mut protos: BTreeSet<String> = self.protocol_configs.keys().cloned().collect();
66        // Backward-compatibility aliases
67        if self.oci_registry.is_some() || self.oci_namespace_prefix.is_some() {
68            protos.insert(OCI_PROTOCOL.into());
69        }
70        if self.warg_url.is_some() {
71            protos.insert(WARG_PROTOCOL.into());
72        }
73        protos.into_iter().map(Into::into)
74    }
75
76    /// Deserializes protocol config for the given protocol.
77    ///
78    /// Returns `Ok(None)` if no configuration is available for the given
79    /// protocol.
80    /// Returns `Err` if configuration is available for the given protocol but
81    /// deserialization fails.
82    pub fn protocol_config<T: DeserializeOwned>(&self, protocol: &str) -> Result<Option<T>, Error> {
83        let mut config = self.protocol_configs.get(protocol).cloned();
84
85        // Backward-compatibility aliases
86        let mut maybe_set = |key: &str, val: &Option<String>| {
87            if let Some(value) = val {
88                config
89                    .get_or_insert_with(Default::default)
90                    .insert(key.into(), value.clone().into());
91            }
92        };
93        match protocol {
94            OCI_PROTOCOL => {
95                maybe_set("registry", &self.oci_registry);
96                maybe_set("namespacePrefix", &self.oci_namespace_prefix);
97            }
98            WARG_PROTOCOL => {
99                maybe_set("url", &self.warg_url);
100            }
101            _ => {}
102        }
103
104        if config.is_none() {
105            return Ok(None);
106        }
107        Ok(Some(
108            serde_json::from_value(config.unwrap().into())
109                .map_err(|err| Error::InvalidRegistryMetadata(err.into()))?,
110        ))
111    }
112}
113
114#[cfg(feature = "metadata-client")]
115mod client {
116    use anyhow::Context;
117    use http::StatusCode;
118
119    use super::REGISTRY_METADATA_PATH;
120    use crate::{registry::Registry, Error};
121
122    impl super::RegistryMetadata {
123        pub async fn fetch_or_default(registry: &Registry) -> Self {
124            match Self::fetch(registry).await {
125                Ok(Some(meta)) => {
126                    tracing::debug!(?meta, "Got registry metadata");
127                    meta
128                }
129                Ok(None) => {
130                    tracing::debug!("Metadata not found");
131                    Default::default()
132                }
133                Err(err) => {
134                    tracing::warn!(error = ?err, "Error fetching registry metadata");
135                    Default::default()
136                }
137            }
138        }
139
140        pub async fn fetch(registry: &Registry) -> Result<Option<Self>, Error> {
141            let scheme = if registry.host() == "localhost" {
142                "http"
143            } else {
144                "https"
145            };
146            let url = format!("{scheme}://{registry}{REGISTRY_METADATA_PATH}");
147            Self::fetch_url(&url)
148                .await
149                .with_context(|| format!("error fetching registry metadata from {url:?}"))
150                .map_err(Error::RegistryMetadataError)
151        }
152
153        async fn fetch_url(url: &str) -> anyhow::Result<Option<Self>> {
154            tracing::debug!(?url, "Fetching registry metadata");
155
156            let resp = reqwest::get(url).await?;
157            if resp.status() == StatusCode::NOT_FOUND {
158                return Ok(None);
159            }
160            let resp = resp.error_for_status()?;
161            Ok(Some(resp.json().await?))
162        }
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use serde_json::json;
169
170    use super::*;
171
172    #[derive(Deserialize, Debug, PartialEq)]
173    #[serde(rename_all = "camelCase")]
174    struct OtherProtocolConfig {
175        key: String,
176    }
177
178    #[test]
179    fn smoke_test() {
180        let meta: RegistryMetadata = serde_json::from_value(json!({
181            "oci": {"registry": "oci.example.com"},
182            "warg": {"url": "https://warg.example.com"},
183        }))
184        .unwrap();
185        assert_eq!(meta.preferred_protocol(), None);
186        assert_eq!(
187            meta.configured_protocols().collect::<Vec<_>>(),
188            ["oci", "warg"]
189        );
190        let oci_config: JsonObject = meta.protocol_config("oci").unwrap().unwrap();
191        assert_eq!(oci_config["registry"], "oci.example.com");
192        let warg_config: JsonObject = meta.protocol_config("warg").unwrap().unwrap();
193        assert_eq!(warg_config["url"], "https://warg.example.com");
194        let other_config: Option<OtherProtocolConfig> = meta.protocol_config("other").unwrap();
195        assert_eq!(other_config, None);
196    }
197
198    #[test]
199    fn preferred_protocol_explicit() {
200        let meta: RegistryMetadata = serde_json::from_value(json!({
201            "preferredProtocol": "warg",
202            "oci": {"registry": "oci.example.com"},
203            "warg": {"url": "https://warg.example.com"},
204        }))
205        .unwrap();
206        assert_eq!(meta.preferred_protocol(), Some("warg"));
207    }
208
209    #[test]
210    fn preferred_protocol_implicit_oci() {
211        let meta: RegistryMetadata = serde_json::from_value(json!({
212            "oci": {"registry": "oci.example.com"},
213        }))
214        .unwrap();
215        assert_eq!(meta.preferred_protocol(), Some("oci"));
216    }
217
218    #[test]
219    fn preferred_protocol_implicit_warg() {
220        let meta: RegistryMetadata = serde_json::from_value(json!({
221            "warg": {"url": "https://warg.example.com"},
222        }))
223        .unwrap();
224        assert_eq!(meta.preferred_protocol(), Some("warg"));
225    }
226
227    #[test]
228    fn backward_compat_preferred_protocol_implicit_oci() {
229        let meta: RegistryMetadata = serde_json::from_value(json!({
230            "ociRegistry": "oci.example.com",
231            "ociNamespacePrefix": "prefix/",
232        }))
233        .unwrap();
234        assert_eq!(meta.preferred_protocol(), Some("oci"));
235    }
236
237    #[test]
238    fn backward_compat_preferred_protocol_implicit_warg() {
239        let meta: RegistryMetadata = serde_json::from_value(json!({
240            "wargUrl": "https://warg.example.com",
241        }))
242        .unwrap();
243        assert_eq!(meta.preferred_protocol(), Some("warg"));
244    }
245
246    #[test]
247    fn basic_backward_compat_test() {
248        let meta: RegistryMetadata = serde_json::from_value(json!({
249            "ociRegistry": "oci.example.com",
250            "ociNamespacePrefix": "prefix/",
251            "wargUrl": "https://warg.example.com",
252        }))
253        .unwrap();
254        assert_eq!(
255            meta.configured_protocols().collect::<Vec<_>>(),
256            ["oci", "warg"]
257        );
258        let oci_config: JsonObject = meta.protocol_config("oci").unwrap().unwrap();
259        assert_eq!(oci_config["registry"], "oci.example.com");
260        assert_eq!(oci_config["namespacePrefix"], "prefix/");
261        let warg_config: JsonObject = meta.protocol_config("warg").unwrap().unwrap();
262        assert_eq!(warg_config["url"], "https://warg.example.com");
263    }
264
265    #[test]
266    fn merged_backward_compat_test() {
267        let meta: RegistryMetadata = serde_json::from_value(json!({
268            "wargUrl": "https://warg.example.com",
269            "other": {"key": "value"}
270        }))
271        .unwrap();
272        assert_eq!(
273            meta.configured_protocols().collect::<Vec<_>>(),
274            ["other", "warg"]
275        );
276        let warg_config: JsonObject = meta.protocol_config("warg").unwrap().unwrap();
277        assert_eq!(warg_config["url"], "https://warg.example.com");
278        let other_config: OtherProtocolConfig = meta.protocol_config("other").unwrap().unwrap();
279        assert_eq!(other_config.key, "value");
280    }
281
282    #[test]
283    fn bad_protocol_config() {
284        let meta: RegistryMetadata = serde_json::from_value(json!({
285            "other": {"bad": "config"}
286        }))
287        .unwrap();
288        assert_eq!(meta.configured_protocols().collect::<Vec<_>>(), ["other"]);
289        let res = meta.protocol_config::<OtherProtocolConfig>("other");
290        assert!(res.is_err(), "{res:?}");
291    }
292}