1use std::path::Path;
11
12use algocline_engine::card;
13use serde::Deserialize;
14
15use super::hub;
16use super::AppService;
17
18#[derive(Debug, Deserialize)]
21pub struct SinkBackfillParams {
22 pub sink: String,
23 #[serde(default)]
24 pub dry_run: bool,
25}
26
27impl AppService {
28 pub fn card_list(&self, pkg: Option<&str>) -> Result<String, String> {
30 let rows = self.card_store.list(pkg)?;
31 Ok(card::summaries_to_json(&rows).to_string())
32 }
33
34 pub fn card_get(&self, card_id: &str) -> Result<String, String> {
36 match self.card_store.get(card_id)? {
37 Some(v) => Ok(v.to_string()),
38 None => Err(format!("card '{card_id}' not found")),
39 }
40 }
41
42 pub fn card_find(
44 &self,
45 pkg: Option<String>,
46 where_: Option<serde_json::Value>,
47 order_by: Option<serde_json::Value>,
48 limit: Option<usize>,
49 offset: Option<usize>,
50 ) -> Result<String, String> {
51 let where_parsed = match where_ {
52 Some(v) => Some(card::parse_where(&v)?),
53 None => None,
54 };
55 let order_parsed = match order_by {
56 Some(v) => card::parse_order_by(&v)?,
57 None => Vec::new(),
58 };
59 let q = card::FindQuery {
60 pkg,
61 where_: where_parsed,
62 order_by: order_parsed,
63 limit,
64 offset,
65 };
66 let rows = self.card_store.find(q)?;
67 Ok(card::summaries_to_json(&rows).to_string())
68 }
69
70 pub fn card_get_by_alias(&self, name: &str) -> Result<String, String> {
72 match self.card_store.get_by_alias(name)? {
73 Some(v) => Ok(v.to_string()),
74 None => Err(format!("alias '{name}' not found")),
75 }
76 }
77
78 pub fn card_alias_list(&self, pkg: Option<&str>) -> Result<String, String> {
80 let rows = self.card_store.alias_list(pkg)?;
81 Ok(card::aliases_to_json(&rows).to_string())
82 }
83
84 pub fn card_alias_set(
86 &self,
87 name: &str,
88 card_id: &str,
89 pkg: Option<&str>,
90 note: Option<&str>,
91 ) -> Result<String, String> {
92 let alias = self.card_store.alias_set(name, card_id, pkg, note)?;
93 let arr = card::aliases_to_json(std::slice::from_ref(&alias));
94 let single = arr
95 .as_array()
96 .and_then(|a| a.first().cloned())
97 .unwrap_or(serde_json::Value::Null);
98 Ok(single.to_string())
99 }
100
101 pub fn card_append(&self, card_id: &str, fields: serde_json::Value) -> Result<String, String> {
103 let merged = self.card_store.append(card_id, fields)?;
104 Ok(merged.to_string())
105 }
106
107 pub async fn card_install(&self, url: String) -> Result<String, String> {
113 let local_path = Path::new(&url);
115 if local_path.is_absolute() && local_path.is_dir() {
116 return self.card_install_from_dir(local_path, &url);
117 }
118
119 let git_url = if url.starts_with("http://")
121 || url.starts_with("https://")
122 || url.starts_with("file://")
123 || url.starts_with("git@")
124 {
125 url.clone()
126 } else {
127 format!("https://{url}")
128 };
129
130 let staging = tempfile::tempdir().map_err(|e| format!("Failed to create temp dir: {e}"))?;
132
133 let output = tokio::process::Command::new("git")
134 .args([
135 "clone",
136 "--depth",
137 "1",
138 &git_url,
139 &staging.path().to_string_lossy(),
140 ])
141 .output()
142 .await
143 .map_err(|e| format!("Failed to run git: {e}"))?;
144
145 if !output.status.success() {
146 let stderr = String::from_utf8_lossy(&output.stderr);
147 return Err(format!("git clone failed: {stderr}"));
148 }
149
150 self.card_install_from_dir(staging.path(), &url)
151 }
152
153 fn card_install_from_dir(&self, root: &Path, source: &str) -> Result<String, String> {
155 let manifest_path = root.join("alc_cards.toml");
157 if !manifest_path.exists() {
158 return Err("Not a Card Collection: alc_cards.toml not found at root. \
159 Card Collections must have an alc_cards.toml manifest."
160 .into());
161 }
162
163 let mut all_imported: Vec<String> = Vec::new();
164 let mut all_skipped: Vec<String> = Vec::new();
165 let mut packages: Vec<String> = Vec::new();
166
167 let entries =
168 std::fs::read_dir(root).map_err(|e| format!("Failed to read source dir: {e}"))?;
169
170 for entry in entries.flatten() {
171 let path = entry.path();
172 if !path.is_dir() {
173 continue;
174 }
175 let pkg_name = match entry.file_name().to_str() {
176 Some(n) if !n.starts_with('_') && !n.starts_with('.') => n.to_string(),
177 _ => continue,
178 };
179
180 let has_toml = std::fs::read_dir(&path)
182 .map(|entries| {
183 entries
184 .flatten()
185 .any(|e| e.path().extension().is_some_and(|ext| ext == "toml"))
186 })
187 .unwrap_or(false);
188
189 if !has_toml {
190 continue;
191 }
192
193 let (imported, skipped) =
194 card::import_from_dir_with_store(&*self.card_store, &path, &pkg_name)?;
195 if !imported.is_empty() || !skipped.is_empty() {
196 packages.push(pkg_name);
197 }
198 all_imported.extend(imported);
199 all_skipped.extend(skipped);
200 }
201
202 if all_imported.is_empty() && all_skipped.is_empty() {
203 return Err("No Card files found in any subdirectory.".into());
204 }
205
206 let mut storage_warnings: Vec<String> = Vec::new();
210 if let Err(e) = hub::register_source(&self.log_config.app_dir(), source, "card_install") {
211 storage_warnings.push(format!("hub register_source: {e}"));
212 }
213
214 let mut response = serde_json::json!({
215 "installed_cards": all_imported,
216 "skipped_cards": all_skipped,
217 "packages": packages,
218 "source": source,
219 "mode": "card_collection",
220 });
221 if !storage_warnings.is_empty() {
222 response["storage_warnings"] = serde_json::json!(storage_warnings);
223 }
224 Ok(response.to_string())
225 }
226
227 pub(crate) fn import_pkg_bundled_cards(&self, pkg_name: &str, cards_dir: &Path) -> Vec<String> {
232 match card::import_from_dir_with_store(&*self.card_store, cards_dir, pkg_name) {
233 Ok((imported, _)) => imported,
234 Err(e) => {
235 tracing::warn!("Failed to import bundled cards for '{pkg_name}': {e}");
236 Vec::new()
237 }
238 }
239 }
240
241 pub fn card_samples(
243 &self,
244 card_id: &str,
245 offset: usize,
246 limit: Option<usize>,
247 where_: Option<serde_json::Value>,
248 ) -> Result<String, String> {
249 let where_parsed = match where_ {
250 Some(v) => Some(card::parse_where(&v)?),
251 None => None,
252 };
253 let q = card::SamplesQuery {
254 offset,
255 limit,
256 where_: where_parsed,
257 };
258 let rows = self.card_store.read_samples(card_id, q)?;
259 Ok(serde_json::Value::Array(rows).to_string())
260 }
261
262 pub fn card_lineage(
264 &self,
265 card_id: &str,
266 direction: Option<&str>,
267 depth: Option<usize>,
268 include_stats: Option<bool>,
269 relation_filter: Option<Vec<String>>,
270 ) -> Result<String, String> {
271 let dir = match direction {
272 Some(s) => card::LineageDirection::parse(s)?,
273 None => card::LineageDirection::Up,
274 };
275 let q = card::LineageQuery {
276 card_id: card_id.to_string(),
277 direction: dir,
278 depth,
279 include_stats: include_stats.unwrap_or(true),
280 relation_filter,
281 };
282 match self.card_store.lineage(q)? {
283 Some(res) => Ok(card::lineage_to_json(&res).to_string()),
284 None => Err(format!("card '{card_id}' not found")),
285 }
286 }
287
288 pub fn card_sink_backfill(&self, params: SinkBackfillParams) -> Result<String, String> {
294 let report = self
295 .card_store
296 .card_sink_backfill(¶ms.sink, params.dry_run)?;
297 serde_json::to_string(&report)
298 .map_err(|e| format!("failed to serialize SinkBackfillReport: {e}"))
299 }
300}