ryra_core/registry/
resolve.rs1use std::path::{Path, PathBuf};
2
3use crate::config::schema::Config;
4use crate::error::{Error, Result};
5use crate::paths::{DEFAULT_REGISTRY_URL, REGISTRY_DIR_ENV};
6use crate::registry;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum ServiceRef {
16 Default(String),
18 Custom { registry: String, service: String },
20}
21
22impl ServiceRef {
23 pub fn parse(input: &str) -> Result<Self> {
29 let parts: Vec<&str> = input.split('/').collect();
30 match parts.as_slice() {
31 [""] => Err(Error::InvalidServiceRef(
32 "service reference cannot be empty".to_string(),
33 )),
34 [name] => {
35 if name.is_empty() {
36 Err(Error::InvalidServiceRef(
37 "service reference cannot be empty".to_string(),
38 ))
39 } else {
40 Ok(ServiceRef::Default((*name).to_string()))
41 }
42 }
43 [registry, service] => {
44 if registry.is_empty() {
45 return Err(Error::InvalidServiceRef(format!(
46 "registry name cannot be empty in reference '{input}'"
47 )));
48 }
49 if service.is_empty() {
50 return Err(Error::InvalidServiceRef(format!(
51 "service name cannot be empty in reference '{input}'"
52 )));
53 }
54 Ok(ServiceRef::Custom {
55 registry: (*registry).to_string(),
56 service: (*service).to_string(),
57 })
58 }
59 _ => Err(Error::InvalidServiceRef(format!(
60 "invalid service reference '{input}': expected 'service' or 'registry/service'"
61 ))),
62 }
63 }
64
65 pub fn service_name(&self) -> &str {
67 match self {
68 ServiceRef::Default(name) => name,
69 ServiceRef::Custom { service, .. } => service,
70 }
71 }
72
73 pub fn registry_name(&self) -> &str {
77 match self {
78 ServiceRef::Default(_) => crate::paths::REGISTRY_DEFAULT,
79 ServiceRef::Custom { registry, .. } => registry,
80 }
81 }
82}
83
84pub async fn resolve_default_registry_dir(cache_dir: &Path) -> Result<PathBuf> {
92 if let Ok(override_path) = std::env::var(REGISTRY_DIR_ENV) {
93 let path = PathBuf::from(override_path);
94 if path.is_dir() {
95 return Ok(path);
96 }
97 }
98
99 let dest = cache_dir.join("default");
100 registry::fetch::clone_or_pull(DEFAULT_REGISTRY_URL, &dest).await?;
101 Ok(dest)
102}
103
104pub async fn resolve_registry_dir(
109 service_ref: &ServiceRef,
110 config: &Config,
111 cache_dir: &Path,
112) -> Result<PathBuf> {
113 match service_ref {
114 ServiceRef::Default(_) => resolve_default_registry_dir(cache_dir).await,
115 ServiceRef::Custom { registry, .. } => {
116 let entry = config
117 .registries
118 .iter()
119 .find(|r| r.name == *registry)
120 .ok_or_else(|| Error::RegistryNotFound(registry.clone()))?;
121
122 let dest = cache_dir.join("registries").join(registry);
123 registry::fetch::clone_or_pull(&entry.url, &dest).await?;
124 Ok(dest)
125 }
126 }
127}
128
129pub async fn resolve_service(
134 service_ref: &ServiceRef,
135 config: &Config,
136 cache_dir: &Path,
137) -> Result<registry::RegistryService> {
138 let repo_dir = resolve_registry_dir(service_ref, config, cache_dir).await?;
139 registry::find_service(&repo_dir, service_ref.service_name())
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn parse_default_service() {
148 let r = ServiceRef::parse("forgejo").expect("should parse");
149 assert_eq!(r, ServiceRef::Default("forgejo".to_string()));
150 assert_eq!(r.service_name(), "forgejo");
151 assert_eq!(r.registry_name(), "default");
152 }
153
154 #[test]
155 fn parse_custom_service() {
156 let r = ServiceRef::parse("acme/forgejo").expect("should parse");
157 assert_eq!(
158 r,
159 ServiceRef::Custom {
160 registry: "acme".to_string(),
161 service: "forgejo".to_string(),
162 }
163 );
164 assert_eq!(r.service_name(), "forgejo");
165 assert_eq!(r.registry_name(), "acme");
166 }
167
168 #[test]
169 fn parse_empty_fails() {
170 let err = ServiceRef::parse("").expect_err("empty input should fail");
171 let msg = err.to_string();
172 assert!(
173 msg.contains("empty"),
174 "expected 'empty' in error message, got: {msg}"
175 );
176 }
177
178 #[test]
179 fn parse_empty_parts_fails() {
180 let err = ServiceRef::parse("/forgejo").expect_err("leading slash should fail");
181 let msg = err.to_string();
182 assert!(
183 msg.contains("empty"),
184 "expected 'empty' in error for '/forgejo', got: {msg}"
185 );
186
187 let err = ServiceRef::parse("acme/").expect_err("trailing slash should fail");
188 let msg = err.to_string();
189 assert!(
190 msg.contains("empty"),
191 "expected 'empty' in error for 'acme/', got: {msg}"
192 );
193 }
194
195 #[test]
196 fn parse_too_many_slashes_fails() {
197 let err = ServiceRef::parse("acme/sub/forgejo").expect_err("too many slashes should fail");
198 let msg = err.to_string();
199 assert!(
200 msg.contains("invalid"),
201 "expected 'invalid' in error message, got: {msg}"
202 );
203 }
204
205 #[test]
206 fn env_override_returns_path_directly() {
207 let tmp = tempfile::TempDir::new().expect("tempdir");
212 let cache = tempfile::TempDir::new().expect("cache tempdir");
213 unsafe { std::env::set_var(REGISTRY_DIR_ENV, tmp.path()) };
216
217 let rt = tokio::runtime::Runtime::new().expect("runtime");
218 let resolved = rt
219 .block_on(resolve_default_registry_dir(cache.path()))
220 .expect("resolve");
221 assert_eq!(resolved, tmp.path());
222
223 unsafe { std::env::remove_var(REGISTRY_DIR_ENV) };
224 }
225}