Skip to main content

ryra_core/registry/
resolve.rs

1use std::path::{Path, PathBuf};
2
3use crate::config::schema::Config;
4use crate::error::{Error, Result};
5use crate::registry;
6
7/// A reference to a service in a registry.
8///
9/// - `Bundled("forgejo")` — refers to a service in the embedded bundled registry
10/// - `Custom { registry: "acme", service: "forgejo" }` — refers to a service in a named custom registry
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum ServiceRef {
13    /// A service from the embedded bundled registry. E.g., `forgejo`.
14    Bundled(String),
15    /// A service from a named custom registry. E.g., `acme/forgejo`.
16    Custom { registry: String, service: String },
17}
18
19impl ServiceRef {
20    /// Parse a service reference from a string.
21    ///
22    /// - `"forgejo"` → `Bundled("forgejo")`
23    /// - `"acme/forgejo"` → `Custom { registry: "acme", service: "forgejo" }`
24    /// - `""`, `"/forgejo"`, `"acme/"`, `"acme/sub/forgejo"` → error
25    pub fn parse(input: &str) -> Result<Self> {
26        let parts: Vec<&str> = input.split('/').collect();
27        match parts.as_slice() {
28            [""] => Err(Error::InvalidServiceRef(
29                "service reference cannot be empty".to_string(),
30            )),
31            [name] => {
32                if name.is_empty() {
33                    Err(Error::InvalidServiceRef(
34                        "service reference cannot be empty".to_string(),
35                    ))
36                } else {
37                    Ok(ServiceRef::Bundled(name.to_string()))
38                }
39            }
40            [registry, service] => {
41                if registry.is_empty() {
42                    return Err(Error::InvalidServiceRef(format!(
43                        "registry name cannot be empty in reference '{input}'"
44                    )));
45                }
46                if service.is_empty() {
47                    return Err(Error::InvalidServiceRef(format!(
48                        "service name cannot be empty in reference '{input}'"
49                    )));
50                }
51                Ok(ServiceRef::Custom {
52                    registry: registry.to_string(),
53                    service: service.to_string(),
54                })
55            }
56            _ => Err(Error::InvalidServiceRef(format!(
57                "invalid service reference '{input}': expected 'service' or 'registry/service'"
58            ))),
59        }
60    }
61
62    /// Returns the service name part of this reference.
63    pub fn service_name(&self) -> &str {
64        match self {
65            ServiceRef::Bundled(name) => name,
66            ServiceRef::Custom { service, .. } => service,
67        }
68    }
69
70    /// Returns the registry name for this reference.
71    ///
72    /// Returns `"bundled"` for bundled services.
73    pub fn registry_name(&self) -> &str {
74        match self {
75            ServiceRef::Bundled(_) => "bundled",
76            ServiceRef::Custom { registry, .. } => registry,
77        }
78    }
79}
80
81/// Resolve the registry directory for a service reference.
82///
83/// - For `Bundled`: extracts the bundled registry to `<cache_dir>/bundled/` if needed.
84/// - For `Custom`: looks up the registry name in `config.registries` and clones/pulls it.
85pub async fn resolve_registry_dir(
86    service_ref: &ServiceRef,
87    config: &Config,
88    cache_dir: &Path,
89) -> Result<PathBuf> {
90    match service_ref {
91        ServiceRef::Bundled(_) => registry::bundled::ensure_bundled(cache_dir),
92        ServiceRef::Custom { registry, .. } => {
93            let entry = config
94                .registries
95                .iter()
96                .find(|r| r.name == *registry)
97                .ok_or_else(|| Error::RegistryNotFound(registry.clone()))?;
98
99            let dest = cache_dir.join("registries").join(registry);
100            registry::fetch::clone_or_pull(&entry.url, &dest).await?;
101            Ok(dest)
102        }
103    }
104}
105
106/// Resolve a service from a registry, returning its definition and directory.
107///
108/// - For `Bundled`: finds the service in the embedded bundled registry.
109/// - For `Custom`: finds the service in the named custom registry.
110pub async fn resolve_service(
111    service_ref: &ServiceRef,
112    config: &Config,
113    cache_dir: &Path,
114) -> Result<registry::RegistryService> {
115    let repo_dir = resolve_registry_dir(service_ref, config, cache_dir).await?;
116    registry::find_service(&repo_dir, service_ref.service_name())
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn parse_bundled_service() {
125        let r = ServiceRef::parse("forgejo").expect("should parse");
126        assert_eq!(r, ServiceRef::Bundled("forgejo".to_string()));
127        assert_eq!(r.service_name(), "forgejo");
128        assert_eq!(r.registry_name(), "bundled");
129    }
130
131    #[test]
132    fn parse_custom_service() {
133        let r = ServiceRef::parse("acme/forgejo").expect("should parse");
134        assert_eq!(
135            r,
136            ServiceRef::Custom {
137                registry: "acme".to_string(),
138                service: "forgejo".to_string(),
139            }
140        );
141        assert_eq!(r.service_name(), "forgejo");
142        assert_eq!(r.registry_name(), "acme");
143    }
144
145    #[test]
146    fn parse_empty_fails() {
147        let err = ServiceRef::parse("").expect_err("empty input should fail");
148        let msg = err.to_string();
149        assert!(
150            msg.contains("empty"),
151            "expected 'empty' in error message, got: {msg}"
152        );
153    }
154
155    #[test]
156    fn parse_empty_parts_fails() {
157        let err = ServiceRef::parse("/forgejo").expect_err("leading slash should fail");
158        let msg = err.to_string();
159        assert!(
160            msg.contains("empty"),
161            "expected 'empty' in error for '/forgejo', got: {msg}"
162        );
163
164        let err = ServiceRef::parse("acme/").expect_err("trailing slash should fail");
165        let msg = err.to_string();
166        assert!(
167            msg.contains("empty"),
168            "expected 'empty' in error for 'acme/', got: {msg}"
169        );
170    }
171
172    #[test]
173    fn parse_too_many_slashes_fails() {
174        let err = ServiceRef::parse("acme/sub/forgejo").expect_err("too many slashes should fail");
175        let msg = err.to_string();
176        assert!(
177            msg.contains("invalid"),
178            "expected 'invalid' in error message, got: {msg}"
179        );
180    }
181}