Skip to main content

wasm_pkg_common/
metadata.rs

1use std::{
2    borrow::Cow,
3    collections::{BTreeSet, HashMap},
4};
5
6use serde::{Deserialize, Serialize, de::DeserializeOwned};
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
35/// OCI registry try
36pub const OCI_PROTOCOL: &str = "oci";
37/// Local filesystem key
38pub const LOCAL_PROTOCOL: &str = "local";
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() && self.oci_registry.is_some() {
54            return Some(OCI_PROTOCOL);
55        }
56        None
57    }
58
59    /// Returns an iterator of protocols configured by the registry.
60    pub fn configured_protocols(&self) -> impl Iterator<Item = Cow<'_, str>> {
61        let mut protos: BTreeSet<String> = self.protocol_configs.keys().cloned().collect();
62        // Backward-compatibility aliases
63        if self.oci_registry.is_some() || self.oci_namespace_prefix.is_some() {
64            protos.insert(OCI_PROTOCOL.into());
65        }
66        protos.into_iter().map(Into::into)
67    }
68
69    /// Deserializes protocol config for the given protocol.
70    ///
71    /// Returns `Ok(None)` if no configuration is available for the given
72    /// protocol.
73    /// Returns `Err` if configuration is available for the given protocol but
74    /// deserialization fails.
75    pub fn protocol_config<T: DeserializeOwned>(&self, protocol: &str) -> Result<Option<T>, Error> {
76        let mut config = self.protocol_configs.get(protocol).cloned();
77
78        // Backward-compatibility aliases
79        let mut maybe_set = |key: &str, val: &Option<String>| {
80            if let Some(value) = val {
81                config
82                    .get_or_insert_with(Default::default)
83                    .insert(key.into(), value.clone().into());
84            }
85        };
86        if protocol == OCI_PROTOCOL {
87            maybe_set("registry", &self.oci_registry);
88            maybe_set("namespacePrefix", &self.oci_namespace_prefix);
89        }
90
91        if config.is_none() {
92            return Ok(None);
93        }
94        Ok(Some(
95            serde_json::from_value(config.unwrap().into())
96                .map_err(|err| Error::InvalidRegistryMetadata(err.into()))?,
97        ))
98    }
99
100    /// Set the OCI registry
101    #[cfg(feature = "oci_extras")]
102    pub fn set_oci_registry(&mut self, registry: Option<String>) {
103        self.oci_registry = registry;
104    }
105
106    /// Set the OCI namespace prefix
107    #[cfg(feature = "oci_extras")]
108    pub fn set_oci_namespace_prefix(&mut self, ns_prefix: Option<String>) {
109        self.oci_namespace_prefix = ns_prefix;
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use serde_json::json;
116
117    use super::*;
118
119    #[derive(Deserialize, Debug, PartialEq)]
120    #[serde(rename_all = "camelCase")]
121    struct OtherProtocolConfig {
122        key: String,
123    }
124
125    #[test]
126    fn smoke_test() {
127        let meta: RegistryMetadata = serde_json::from_value(json!({
128            "oci": {"registry": "oci.example.com"},
129            "other": {"key": "value"}
130        }))
131        .unwrap();
132        assert_eq!(meta.preferred_protocol(), None);
133        assert_eq!(
134            meta.configured_protocols().collect::<Vec<_>>(),
135            ["oci", "other"]
136        );
137        let oci_config: JsonObject = meta.protocol_config("oci").unwrap().unwrap();
138        assert_eq!(oci_config["registry"], "oci.example.com");
139        let other_config: OtherProtocolConfig = meta.protocol_config("other").unwrap().unwrap();
140        assert_eq!(other_config.key, "value");
141    }
142
143    #[test]
144    fn preferred_protocol_explicit() {
145        let meta: RegistryMetadata = serde_json::from_value(json!({
146            "preferredProtocol": "oci",
147            "oci": {"registry": "oci.example.com"},
148            "other": {"key": "value"},
149        }))
150        .unwrap();
151        assert_eq!(meta.preferred_protocol(), Some("oci"));
152    }
153
154    #[test]
155    fn preferred_protocol_implicit_oci() {
156        let meta: RegistryMetadata = serde_json::from_value(json!({
157            "oci": {"registry": "oci.example.com"},
158        }))
159        .unwrap();
160        assert_eq!(meta.preferred_protocol(), Some("oci"));
161    }
162
163    #[test]
164    fn preferred_protocol_implicit_other() {
165        let meta: RegistryMetadata = serde_json::from_value(json!({
166            "other": {"key": "value"},
167        }))
168        .unwrap();
169        assert_eq!(meta.preferred_protocol(), Some("other"));
170    }
171
172    #[test]
173    fn backward_compat_preferred_protocol_implicit_oci() {
174        let meta: RegistryMetadata = serde_json::from_value(json!({
175            "ociRegistry": "oci.example.com",
176            "ociNamespacePrefix": "prefix/",
177        }))
178        .unwrap();
179        assert_eq!(meta.preferred_protocol(), Some("oci"));
180    }
181
182    #[test]
183    fn basic_backward_compat_test() {
184        let meta: RegistryMetadata = serde_json::from_value(json!({
185            "ociRegistry": "oci.example.com",
186            "ociNamespacePrefix": "prefix/",
187        }))
188        .unwrap();
189        assert_eq!(meta.configured_protocols().collect::<Vec<_>>(), ["oci"]);
190        let oci_config: JsonObject = meta.protocol_config("oci").unwrap().unwrap();
191        assert_eq!(oci_config["registry"], "oci.example.com");
192        assert_eq!(oci_config["namespacePrefix"], "prefix/");
193    }
194
195    #[test]
196    fn merged_backward_compat_test() {
197        let meta: RegistryMetadata = serde_json::from_value(json!({
198            "ociRegistry": "oci.example.com",
199            "other": {"key": "value"}
200        }))
201        .unwrap();
202        assert_eq!(
203            meta.configured_protocols().collect::<Vec<_>>(),
204            ["oci", "other"]
205        );
206        let oci_config: JsonObject = meta.protocol_config("oci").unwrap().unwrap();
207        assert_eq!(oci_config["registry"], "oci.example.com");
208        let other_config: OtherProtocolConfig = meta.protocol_config("other").unwrap().unwrap();
209        assert_eq!(other_config.key, "value");
210    }
211
212    #[test]
213    fn bad_protocol_config() {
214        let meta: RegistryMetadata = serde_json::from_value(json!({
215            "other": {"bad": "config"}
216        }))
217        .unwrap();
218        assert_eq!(meta.configured_protocols().collect::<Vec<_>>(), ["other"]);
219        let res = meta.protocol_config::<OtherProtocolConfig>("other");
220        assert!(res.is_err(), "{res:?}");
221    }
222}