ryra_core/registry/
mod.rs1pub mod fetch;
2pub mod manage;
3pub mod resolve;
4pub mod service_def;
5pub mod test_def;
6
7use std::path::{Path, PathBuf};
8
9use crate::error::{Error, Result};
10use service_def::ServiceDef;
11
12pub struct RegistryService {
14 pub def: ServiceDef,
15 pub service_dir: PathBuf,
17}
18
19pub fn find_service(repo_dir: &Path, name: &str) -> Result<RegistryService> {
21 let svc_dir = repo_dir.join(name);
22 let service_toml = svc_dir.join("service.toml");
23
24 if !service_toml.exists() {
25 return Err(Error::ServiceNotFound {
26 name: name.to_string(),
27 suggestions: suggest_close_names(repo_dir, name),
28 });
29 }
30
31 let contents = std::fs::read_to_string(&service_toml).map_err(|source| Error::FileRead {
32 path: service_toml.clone(),
33 source,
34 })?;
35 let def: ServiceDef = toml::from_str(&contents).map_err(|source| Error::TomlParse {
36 path: service_toml,
37 source,
38 })?;
39
40 if let Err(msg) = def.validate() {
41 return Err(Error::ConfigValidation(msg));
42 }
43
44 Ok(RegistryService {
45 def,
46 service_dir: svc_dir,
47 })
48}
49
50pub fn list_available(repo_dir: &Path) -> Result<Vec<RegistryService>> {
52 if !repo_dir.exists() {
53 return Ok(Vec::new());
54 }
55
56 let entries = std::fs::read_dir(repo_dir).map_err(|source| Error::FileRead {
57 path: repo_dir.to_path_buf(),
58 source,
59 })?;
60
61 let mut services = Vec::new();
62 for entry in entries {
63 let entry = entry.map_err(|source| Error::FileRead {
64 path: repo_dir.to_path_buf(),
65 source,
66 })?;
67 let svc_dir = entry.path();
68 let service_toml = svc_dir.join("service.toml");
69 if service_toml.exists() {
70 let contents =
71 std::fs::read_to_string(&service_toml).map_err(|source| Error::FileRead {
72 path: service_toml.clone(),
73 source,
74 })?;
75 let def: ServiceDef = toml::from_str(&contents).map_err(|source| Error::TomlParse {
76 path: service_toml,
77 source,
78 })?;
79 services.push(RegistryService {
80 def,
81 service_dir: svc_dir,
82 });
83 }
84 }
85
86 services.sort_by(|a, b| a.def.service.name.cmp(&b.def.service.name));
87 Ok(services)
88}
89
90fn suggest_close_names(repo_dir: &Path, name: &str) -> Vec<String> {
101 let Ok(entries) = std::fs::read_dir(repo_dir) else {
102 return Vec::new();
103 };
104 let candidates: Vec<String> = entries
105 .filter_map(|e| e.ok())
106 .filter(|e| e.path().join("service.toml").exists())
107 .filter_map(|e| e.file_name().into_string().ok())
108 .collect();
109 let max_dist = (name.len() / 3 + 1).min(3);
110 let mut scored: Vec<(usize, String)> = candidates
111 .into_iter()
112 .map(|c| (levenshtein(name, &c), c))
113 .filter(|(d, _)| *d <= max_dist)
114 .collect();
115 scored.sort_by_key(|(d, _)| *d);
116 scored.into_iter().take(3).map(|(_, n)| n).collect()
117}
118
119fn levenshtein(a: &str, b: &str) -> usize {
123 let a: Vec<char> = a.chars().flat_map(char::to_lowercase).collect();
124 let b: Vec<char> = b.chars().flat_map(char::to_lowercase).collect();
125 if a.is_empty() {
126 return b.len();
127 }
128 if b.is_empty() {
129 return a.len();
130 }
131 let mut dp: Vec<usize> = (0..=b.len()).collect();
132 for i in 1..=a.len() {
133 let mut prev = dp[0];
134 dp[0] = i;
135 for j in 1..=b.len() {
136 let temp = dp[j];
137 dp[j] = if a[i - 1] == b[j - 1] {
138 prev
139 } else {
140 1 + prev.min(dp[j].min(dp[j - 1]))
141 };
142 prev = temp;
143 }
144 }
145 dp[b.len()]
146}
147
148pub fn format_service_suggestions(suggestions: &[String]) -> String {
152 match suggestions {
153 [] => String::new(),
154 [one] => format!(" — did you mean '{one}'?"),
155 many => format!(" — did you mean one of: {}?", many.join(", ")),
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn levenshtein_basics() {
165 assert_eq!(levenshtein("seafile", "seafile"), 0);
166 assert_eq!(levenshtein("seafule", "seafile"), 1); assert_eq!(levenshtein("seafil", "seafile"), 1); assert_eq!(levenshtein("seafiles", "seafile"), 1); assert_eq!(levenshtein("SEAFILE", "seafile"), 0); }
171
172 #[test]
173 fn format_suggestions_shapes() {
174 assert_eq!(format_service_suggestions(&[]), "");
175 assert_eq!(
176 format_service_suggestions(&["seafile".into()]),
177 " — did you mean 'seafile'?"
178 );
179 assert_eq!(
180 format_service_suggestions(&["seafile".into(), "vikunja".into()]),
181 " — did you mean one of: seafile, vikunja?"
182 );
183 }
184}