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 Path { dir: PathBuf, name: String },
24}
25
26pub fn is_path_like(input: &str) -> bool {
36 input == "."
37 || input == ".."
38 || input.starts_with("./")
39 || input.starts_with("../")
40 || input.starts_with('/')
41 || input.starts_with('~')
42}
43
44pub fn path_ref(dir: &Path) -> Result<ServiceRef> {
48 let svc = registry::load_project_service(dir)?;
49 let abs = dir.canonicalize().unwrap_or_else(|_| dir.to_path_buf());
50 Ok(ServiceRef::Path {
51 dir: abs,
52 name: svc.def.service.name,
53 })
54}
55
56impl ServiceRef {
57 pub fn parse(input: &str) -> Result<Self> {
63 let parts: Vec<&str> = input.split('/').collect();
64 match parts.as_slice() {
65 [""] => Err(Error::InvalidServiceRef(
66 "service reference cannot be empty".to_string(),
67 )),
68 [name] => {
69 if name.is_empty() {
70 Err(Error::InvalidServiceRef(
71 "service reference cannot be empty".to_string(),
72 ))
73 } else {
74 Ok(ServiceRef::Default((*name).to_string()))
75 }
76 }
77 [registry, service] => {
78 if registry.is_empty() {
79 return Err(Error::InvalidServiceRef(format!(
80 "registry name cannot be empty in reference '{input}'"
81 )));
82 }
83 if service.is_empty() {
84 return Err(Error::InvalidServiceRef(format!(
85 "service name cannot be empty in reference '{input}'"
86 )));
87 }
88 Ok(ServiceRef::Custom {
89 registry: (*registry).to_string(),
90 service: (*service).to_string(),
91 })
92 }
93 _ => Err(Error::InvalidServiceRef(format!(
94 "invalid service reference '{input}': expected 'service' or 'registry/service'"
95 ))),
96 }
97 }
98
99 pub fn service_name(&self) -> &str {
101 match self {
102 ServiceRef::Default(name) => name,
103 ServiceRef::Custom { service, .. } => service,
104 ServiceRef::Path { name, .. } => name,
105 }
106 }
107
108 pub fn registry_name(&self) -> &str {
114 match self {
115 ServiceRef::Default(_) => crate::paths::REGISTRY_DEFAULT,
116 ServiceRef::Custom { registry, .. } => registry,
117 ServiceRef::Path { dir, .. } => dir.to_str().unwrap_or("local"),
118 }
119 }
120}
121
122pub fn cached_default_registry_dir(cache_dir: &Path) -> Option<PathBuf> {
134 if let Ok(override_path) = std::env::var(REGISTRY_DIR_ENV) {
135 let path = PathBuf::from(override_path);
136 if path.is_dir() {
137 return Some(path);
138 }
139 }
140 let dest = cache_dir.join("default");
141 dest.is_dir().then_some(dest)
142}
143
144pub async fn resolve_default_registry_dir(cache_dir: &Path) -> Result<PathBuf> {
145 if let Ok(override_path) = std::env::var(REGISTRY_DIR_ENV) {
146 let path = PathBuf::from(override_path);
147 if path.is_dir() {
148 return Ok(path);
149 }
150 }
151
152 let dest = cache_dir.join("default");
153 registry::fetch::clone_or_pull(DEFAULT_REGISTRY_URL, &dest).await?;
154 Ok(dest)
155}
156
157pub async fn resolve_registry_dir(
162 service_ref: &ServiceRef,
163 config: &Config,
164 cache_dir: &Path,
165) -> Result<PathBuf> {
166 match service_ref {
167 ServiceRef::Path { dir, .. } => Ok(dir.clone()),
169 ServiceRef::Default(_) => resolve_default_registry_dir(cache_dir).await,
170 ServiceRef::Custom { registry, .. } => {
171 let entry = config
172 .registries
173 .iter()
174 .find(|r| r.name == *registry)
175 .ok_or_else(|| Error::RegistryNotFound(registry.clone()))?;
176
177 let dest = cache_dir.join("registries").join(registry);
178 registry::fetch::clone_or_pull(&entry.url, &dest).await?;
179 Ok(dest)
180 }
181 }
182}
183
184pub async fn resolve_service(
189 service_ref: &ServiceRef,
190 config: &Config,
191 cache_dir: &Path,
192) -> Result<registry::RegistryService> {
193 let repo_dir = resolve_registry_dir(service_ref, config, cache_dir).await?;
194 registry::find_service(&repo_dir, service_ref.service_name())
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn parse_default_service() {
203 let r = ServiceRef::parse("forgejo").expect("should parse");
204 assert_eq!(r, ServiceRef::Default("forgejo".to_string()));
205 assert_eq!(r.service_name(), "forgejo");
206 assert_eq!(r.registry_name(), "default");
207 }
208
209 #[test]
210 fn path_detection_is_syntactic_only() {
211 for p in [".", "..", "./app", "../app", "/abs/app", "~/app"] {
213 assert!(is_path_like(p), "{p} should be treated as a path");
214 }
215 for name in ["forgejo", "acme/forgejo", "caddy", "my-app", "a/b"] {
219 assert!(!is_path_like(name), "{name} must stay a registry ref");
220 }
221 }
222
223 #[test]
224 fn parse_custom_service() {
225 let r = ServiceRef::parse("acme/forgejo").expect("should parse");
226 assert_eq!(
227 r,
228 ServiceRef::Custom {
229 registry: "acme".to_string(),
230 service: "forgejo".to_string(),
231 }
232 );
233 assert_eq!(r.service_name(), "forgejo");
234 assert_eq!(r.registry_name(), "acme");
235 }
236
237 #[test]
238 fn parse_empty_fails() {
239 let err = ServiceRef::parse("").expect_err("empty input should fail");
240 let msg = err.to_string();
241 assert!(
242 msg.contains("empty"),
243 "expected 'empty' in error message, got: {msg}"
244 );
245 }
246
247 #[test]
248 fn parse_empty_parts_fails() {
249 let err = ServiceRef::parse("/forgejo").expect_err("leading slash should fail");
250 let msg = err.to_string();
251 assert!(
252 msg.contains("empty"),
253 "expected 'empty' in error for '/forgejo', got: {msg}"
254 );
255
256 let err = ServiceRef::parse("acme/").expect_err("trailing slash should fail");
257 let msg = err.to_string();
258 assert!(
259 msg.contains("empty"),
260 "expected 'empty' in error for 'acme/', got: {msg}"
261 );
262 }
263
264 #[test]
265 fn parse_too_many_slashes_fails() {
266 let err = ServiceRef::parse("acme/sub/forgejo").expect_err("too many slashes should fail");
267 let msg = err.to_string();
268 assert!(
269 msg.contains("invalid"),
270 "expected 'invalid' in error message, got: {msg}"
271 );
272 }
273
274 #[test]
275 fn env_override_returns_path_directly() {
276 let tmp = tempfile::TempDir::new().expect("tempdir");
281 let cache = tempfile::TempDir::new().expect("cache tempdir");
282 unsafe { std::env::set_var(REGISTRY_DIR_ENV, tmp.path()) };
285
286 let rt = tokio::runtime::Runtime::new().expect("runtime");
287 let resolved = rt
288 .block_on(resolve_default_registry_dir(cache.path()))
289 .expect("resolve");
290 assert_eq!(resolved, tmp.path());
291
292 unsafe { std::env::remove_var(REGISTRY_DIR_ENV) };
293 }
294}