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 #[serde(skip_serializing)]
31 oci_namespace_prefix: Option<String>,
32 #[serde(skip_serializing)]
34 warg_url: Option<String>,
35}
36
37const OCI_PROTOCOL: &str = "oci";
38const WARG_PROTOCOL: &str = "warg";
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() {
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 pub fn configured_protocols(&self) -> impl Iterator<Item = Cow<str>> {
65 let mut protos: BTreeSet<String> = self.protocol_configs.keys().cloned().collect();
66 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 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 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}