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