algocline_app/service/
card.rs1use std::path::Path;
11
12use algocline_engine::card;
13
14use super::hub;
15use super::AppService;
16
17impl AppService {
18 pub fn card_list(&self, pkg: Option<&str>) -> Result<String, String> {
20 let rows = card::list(pkg)?;
21 Ok(card::summaries_to_json(&rows).to_string())
22 }
23
24 pub fn card_get(&self, card_id: &str) -> Result<String, String> {
26 match card::get(card_id)? {
27 Some(v) => Ok(v.to_string()),
28 None => Err(format!("card '{card_id}' not found")),
29 }
30 }
31
32 #[allow(clippy::too_many_arguments)]
34 pub fn card_find(
35 &self,
36 pkg: Option<String>,
37 scenario: Option<String>,
38 model: Option<String>,
39 sort: Option<String>,
40 limit: Option<usize>,
41 min_pass_rate: Option<f64>,
42 ) -> Result<String, String> {
43 let q = card::FindQuery {
44 pkg,
45 scenario,
46 model,
47 sort,
48 limit,
49 min_pass_rate,
50 };
51 let rows = card::find(q)?;
52 Ok(card::summaries_to_json(&rows).to_string())
53 }
54
55 pub fn card_get_by_alias(&self, name: &str) -> Result<String, String> {
57 match card::get_by_alias(name)? {
58 Some(v) => Ok(v.to_string()),
59 None => Err(format!("alias '{name}' not found")),
60 }
61 }
62
63 pub fn card_alias_list(&self, pkg: Option<&str>) -> Result<String, String> {
65 let rows = card::alias_list(pkg)?;
66 Ok(card::aliases_to_json(&rows).to_string())
67 }
68
69 pub fn card_alias_set(
71 &self,
72 name: &str,
73 card_id: &str,
74 pkg: Option<&str>,
75 note: Option<&str>,
76 ) -> Result<String, String> {
77 let alias = card::alias_set(name, card_id, pkg, note)?;
78 let arr = card::aliases_to_json(std::slice::from_ref(&alias));
79 let single = arr
80 .as_array()
81 .and_then(|a| a.first().cloned())
82 .unwrap_or(serde_json::Value::Null);
83 Ok(single.to_string())
84 }
85
86 pub fn card_append(&self, card_id: &str, fields: serde_json::Value) -> Result<String, String> {
88 let merged = card::append(card_id, fields)?;
89 Ok(merged.to_string())
90 }
91
92 pub async fn card_install(&self, url: String) -> Result<String, String> {
98 let local_path = Path::new(&url);
100 if local_path.is_absolute() && local_path.is_dir() {
101 return self.card_install_from_dir(local_path, &url);
102 }
103
104 let git_url = if url.starts_with("http://")
106 || url.starts_with("https://")
107 || url.starts_with("file://")
108 || url.starts_with("git@")
109 {
110 url.clone()
111 } else {
112 format!("https://{url}")
113 };
114
115 let staging = tempfile::tempdir().map_err(|e| format!("Failed to create temp dir: {e}"))?;
117
118 let output = tokio::process::Command::new("git")
119 .args([
120 "clone",
121 "--depth",
122 "1",
123 &git_url,
124 &staging.path().to_string_lossy(),
125 ])
126 .output()
127 .await
128 .map_err(|e| format!("Failed to run git: {e}"))?;
129
130 if !output.status.success() {
131 let stderr = String::from_utf8_lossy(&output.stderr);
132 return Err(format!("git clone failed: {stderr}"));
133 }
134
135 self.card_install_from_dir(staging.path(), &url)
136 }
137
138 fn card_install_from_dir(&self, root: &Path, source: &str) -> Result<String, String> {
140 let manifest_path = root.join("alc_cards.toml");
142 if !manifest_path.exists() {
143 return Err("Not a Card Collection: alc_cards.toml not found at root. \
144 Card Collections must have an alc_cards.toml manifest."
145 .into());
146 }
147
148 let mut all_imported: Vec<String> = Vec::new();
149 let mut all_skipped: Vec<String> = Vec::new();
150 let mut packages: Vec<String> = Vec::new();
151
152 let entries =
153 std::fs::read_dir(root).map_err(|e| format!("Failed to read source dir: {e}"))?;
154
155 for entry in entries.flatten() {
156 let path = entry.path();
157 if !path.is_dir() {
158 continue;
159 }
160 let pkg_name = match entry.file_name().to_str() {
161 Some(n) if !n.starts_with('_') && !n.starts_with('.') => n.to_string(),
162 _ => continue,
163 };
164
165 let has_toml = std::fs::read_dir(&path)
167 .map(|entries| {
168 entries
169 .flatten()
170 .any(|e| e.path().extension().is_some_and(|ext| ext == "toml"))
171 })
172 .unwrap_or(false);
173
174 if !has_toml {
175 continue;
176 }
177
178 let (imported, skipped) = card::import_from_dir(&path, &pkg_name)?;
179 if !imported.is_empty() || !skipped.is_empty() {
180 packages.push(pkg_name);
181 }
182 all_imported.extend(imported);
183 all_skipped.extend(skipped);
184 }
185
186 if all_imported.is_empty() && all_skipped.is_empty() {
187 return Err("No Card files found in any subdirectory.".into());
188 }
189
190 hub::register_source(source, "card_install");
192
193 let response = serde_json::json!({
194 "installed_cards": all_imported,
195 "skipped_cards": all_skipped,
196 "packages": packages,
197 "source": source,
198 "mode": "card_collection",
199 });
200 Ok(response.to_string())
201 }
202
203 pub(crate) fn import_pkg_bundled_cards(pkg_name: &str, cards_dir: &Path) -> Vec<String> {
208 match card::import_from_dir(cards_dir, pkg_name) {
209 Ok((imported, _)) => imported,
210 Err(e) => {
211 tracing::warn!("Failed to import bundled cards for '{pkg_name}': {e}");
212 Vec::new()
213 }
214 }
215 }
216
217 pub fn card_samples(
219 &self,
220 card_id: &str,
221 offset: usize,
222 limit: Option<usize>,
223 ) -> Result<String, String> {
224 let rows = card::read_samples(card_id, offset, limit)?;
225 Ok(serde_json::Value::Array(rows).to_string())
226 }
227}