wasm_pkg_common/
metadata.rs1use std::{
2 borrow::Cow,
3 collections::{BTreeSet, HashMap},
4};
5
6use serde::{de::DeserializeOwned, Deserialize, Serialize};
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 #[serde(skip_serializing)]
36 warg_url: Option<String>,
37}
38
39const OCI_PROTOCOL: &str = "oci";
40const WARG_PROTOCOL: &str = "warg";
41
42impl RegistryMetadata {
43 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 pub fn configured_protocols(&self) -> impl Iterator<Item = Cow<'_, str>> {
67 let mut protos: BTreeSet<String> = self.protocol_configs.keys().cloned().collect();
68 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 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 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 #[cfg(feature = "oci_extras")]
117 pub fn set_oci_registry(&mut self, registry: Option<String>) {
118 self.oci_registry = registry;
119 }
120
121 #[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}