Skip to main content

lintel_validate/
registry.rs

1use lintel_schema_cache::SchemaCache;
2use schema_catalog::Catalog;
3
4/// The default Lintel catalog registry (always fetched unless `--no-catalog`).
5pub const DEFAULT_REGISTRY: &str = "https://catalog.lintel.tools/catalog.json";
6
7/// Resolve a registry URL, expanding shorthand notations.
8///
9/// Supported shorthands:
10/// - `github:org/repo`        → tries `main` then `master` branch
11/// - `github:org/repo/branch` → uses the specified branch
12///
13/// Plain `http://` and `https://` URLs are returned as-is.
14///
15/// Returns one or more URLs to try in order.
16pub fn resolve_urls(url: &str) -> Vec<String> {
17    if let Some(rest) = url.strip_prefix("github:") {
18        // github:org/repo/branch — explicit branch
19        let parts: Vec<&str> = rest.splitn(3, '/').collect();
20        if parts.len() == 3 {
21            vec![format!(
22                "https://raw.githubusercontent.com/{}/{}/{}/catalog.json",
23                parts[0], parts[1], parts[2]
24            )]
25        } else {
26            // github:org/repo — try main first, then master
27            vec![
28                format!("https://raw.githubusercontent.com/{rest}/main/catalog.json"),
29                format!("https://raw.githubusercontent.com/{rest}/master/catalog.json"),
30            ]
31        }
32    } else {
33        vec![url.to_string()]
34    }
35}
36
37/// Fetch a schema registry catalog by URL.
38///
39/// The URL is first resolved via [`resolve_urls`] to expand shorthand
40/// notations like `github:org/repo`. For GitHub shorthands without an
41/// explicit branch, both `main` and `master` are tried.
42///
43/// # Errors
44///
45/// Returns an error if none of the resolved URLs can be fetched or parsed.
46pub async fn fetch(
47    cache: &SchemaCache,
48    url: &str,
49) -> Result<Catalog, Box<dyn core::error::Error + Send + Sync>> {
50    let urls = resolve_urls(url);
51    let mut last_err: Option<Box<dyn core::error::Error + Send + Sync>> = None;
52    for resolved in &urls {
53        match cache.fetch(resolved).await {
54            Ok((value, _status)) => {
55                let catalog = schema_catalog::parse_catalog_value(value)?;
56                return Ok(catalog);
57            }
58            Err(e) => last_err = Some(e),
59        }
60    }
61    Err(last_err.unwrap_or_else(|| "no URLs to try".into()))
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn github_shorthand_tries_main_then_master() {
70        let urls = resolve_urls("github:my-org/my-schemas");
71        assert_eq!(urls.len(), 2);
72        assert_eq!(
73            urls[0],
74            "https://raw.githubusercontent.com/my-org/my-schemas/main/catalog.json"
75        );
76        assert_eq!(
77            urls[1],
78            "https://raw.githubusercontent.com/my-org/my-schemas/master/catalog.json"
79        );
80    }
81
82    #[test]
83    fn github_shorthand_with_explicit_branch() {
84        let urls = resolve_urls("github:lintel-rs/lintel/master");
85        assert_eq!(urls.len(), 1);
86        assert_eq!(
87            urls[0],
88            "https://raw.githubusercontent.com/lintel-rs/lintel/master/catalog.json"
89        );
90    }
91
92    #[test]
93    fn plain_url_unchanged() {
94        let url = "https://example.com/catalog.json";
95        assert_eq!(resolve_urls(url), vec![url]);
96    }
97
98    #[test]
99    fn default_registry_is_plain_url() {
100        let urls = resolve_urls(DEFAULT_REGISTRY);
101        assert_eq!(urls.len(), 1);
102        assert_eq!(urls[0], "https://catalog.lintel.tools/catalog.json");
103    }
104}