Skip to main content

lex_syntax/
registry.rs

1//! Mapping a **tenant-qualified registry** string to the hub's public,
2//! unauthenticated read URLs (#893, #917).
3//!
4//! A registry dependency names its source as `host/tenant[/store]`:
5//!
6//! ```toml
7//! [dependencies]
8//! lex-nt = { registry = "vcs.lexlang.org/lex-official/lex-nt", version = "^1.2" }
9//! ```
10//!
11//! The public package surface the hub serves is
12//! `GET https://<host>/v1/public/<tenant>/<name>/…` (a named store is
13//! selected with `?store=<store>`), and it needs no credentials for a
14//! **public** package. The resolver (`lex pkg lock`/`install`) reads that
15//! surface, so it must turn the `registry` string into those URLs rather
16//! than hitting the authenticated, tenant-from-key `/v1/pkg/<name>/…` routes
17//! (which return `401` to an anonymous resolver).
18//!
19//! A bare-host registry (`"vcs.lexlang.org"`, no tenant) can't address the
20//! public surface — there's no tenant to scope to — so [`public`] returns
21//! `None` and the caller falls back to the legacy `{registry}/v1/pkg/…`
22//! form (which only ever worked for a single-tenant host anyway).
23
24/// A registry resolved to its public read surface.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct PublicRegistry {
27    /// `https://<host>/v1/public/<tenant>` — the base every public read
28    /// hangs off.
29    pub base: String,
30    /// The named store under the tenant, if the registry named one. Passed
31    /// as `?store=<store>`; `None` reads the tenant's default (flat) store.
32    pub store: Option<String>,
33}
34
35impl PublicRegistry {
36    /// `…/v1/public/<tenant>/<name>/versions[?store=…]` — the release list.
37    pub fn versions_url(&self, name: &str) -> String {
38        self.with_store(format!("{}/{}/versions", self.base, name))
39    }
40
41    /// `…/v1/public/<tenant>/<name>/<version>/archive[?store=…]` — the
42    /// package archive for a resolved version.
43    pub fn archive_url(&self, name: &str, version: &str) -> String {
44        self.with_store(format!("{}/{}/{}/archive", self.base, name, version))
45    }
46
47    /// `…/v1/public/<tenant>/<name>/<version>/contract[?store=…]` — the
48    /// signed capability contract, when the surface serves one. (The public
49    /// surface may not; callers treat a `404` as "unsigned".)
50    pub fn contract_url(&self, name: &str, version: &str) -> String {
51        self.with_store(format!("{}/{}/{}/contract", self.base, name, version))
52    }
53
54    fn with_store(&self, url: String) -> String {
55        match &self.store {
56            Some(s) => format!("{url}?store={s}"),
57            None => url,
58        }
59    }
60}
61
62/// Parse a `host/tenant[/store]` registry (scheme optional, trailing slash
63/// tolerated) into its public read surface. Returns `None` for a bare-host
64/// registry with no tenant segment — the caller then uses the legacy
65/// `{registry}/v1/pkg/…` form.
66pub fn public(registry: &str) -> Option<PublicRegistry> {
67    let trimmed = registry.trim().trim_end_matches('/');
68    let (scheme, rest) = match trimmed.split_once("://") {
69        Some((s, r)) => (s, r),
70        None => ("https", trimmed),
71    };
72    let mut segs = rest.split('/').filter(|s| !s.is_empty());
73    let host = segs.next()?;
74    let tenant = segs.next()?; // no tenant → not a public-addressable registry
75    if host.is_empty() || tenant.is_empty() {
76        return None;
77    }
78    let store = segs.next().map(str::to_string);
79    Some(PublicRegistry {
80        base: format!("{scheme}://{host}/v1/public/{tenant}"),
81        store,
82    })
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn flat_store_registry() {
91        let r = public("vcs.lexlang.org/alpibrusl").unwrap();
92        assert_eq!(r.base, "https://vcs.lexlang.org/v1/public/alpibrusl");
93        assert_eq!(r.store, None);
94        assert_eq!(
95            r.versions_url("lex-agent"),
96            "https://vcs.lexlang.org/v1/public/alpibrusl/lex-agent/versions"
97        );
98        assert_eq!(
99            r.archive_url("lex-agent", "0.1.0"),
100            "https://vcs.lexlang.org/v1/public/alpibrusl/lex-agent/0.1.0/archive"
101        );
102    }
103
104    #[test]
105    fn named_store_registry_adds_store_query() {
106        let r = public("vcs.lexlang.org/lex-official/lex-nt").unwrap();
107        assert_eq!(r.base, "https://vcs.lexlang.org/v1/public/lex-official");
108        assert_eq!(r.store.as_deref(), Some("lex-nt"));
109        assert_eq!(
110            r.versions_url("lex-nt"),
111            "https://vcs.lexlang.org/v1/public/lex-official/lex-nt/versions?store=lex-nt"
112        );
113        assert_eq!(
114            r.contract_url("lex-nt", "1.2.0"),
115            "https://vcs.lexlang.org/v1/public/lex-official/lex-nt/1.2.0/contract?store=lex-nt"
116        );
117    }
118
119    #[test]
120    fn explicit_scheme_is_preserved() {
121        let r = public("http://localhost:4040/acme").unwrap();
122        assert_eq!(r.base, "http://localhost:4040/v1/public/acme");
123    }
124
125    #[test]
126    fn bare_host_has_no_public_surface() {
127        assert_eq!(public("vcs.lexlang.org"), None);
128        assert_eq!(public("https://vcs.lexlang.org"), None);
129        assert_eq!(public(""), None);
130    }
131}