wasm_pkg_common/
metadata.rs1use std::{
2 borrow::Cow,
3 collections::{BTreeSet, HashMap},
4};
5
6use serde::{Deserialize, Serialize, de::DeserializeOwned};
7
8use crate::Error;
9
10pub 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 pub preferred_protocol: Option<String>,
20
21 #[serde(flatten)]
23 pub protocol_configs: HashMap<String, JsonObject>,
24
25 #[serde(skip_serializing)]
28 oci_registry: Option<String>,
29
30 #[serde(skip_serializing)]
32 oci_namespace_prefix: Option<String>,
33}
34
35pub const OCI_PROTOCOL: &str = "oci";
37pub const LOCAL_PROTOCOL: &str = "local";
39
40impl RegistryMetadata {
41 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 pub fn configured_protocols(&self) -> impl Iterator<Item = Cow<'_, str>> {
61 let mut protos: BTreeSet<String> = self.protocol_configs.keys().cloned().collect();
62 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 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 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 #[cfg(feature = "oci_extras")]
102 pub fn set_oci_registry(&mut self, registry: Option<String>) {
103 self.oci_registry = registry;
104 }
105
106 #[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}