Skip to main content

ninja/common/
config.rs

1use anyhow::Result;
2use log::{self, error, info};
3use serde::{Deserialize, Serialize};
4use std::{collections::HashMap, path::Path};
5use tokio::fs;
6use url::Url;
7
8use crate::common::registry::{ArmoryItem, fetch_registry, is_absolute_url};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ShurikenReference {
12    pub registry: String,
13    pub shuriken: String,
14}
15
16impl ShurikenReference {
17    /// Parse a "registry:shuriken" format string
18    pub fn parse(input: &str) -> Result<Self, anyhow::Error> {
19        let parts: Vec<&str> = input.splitn(2, ':').collect();
20        if parts.len() != 2 {
21            return Err(anyhow::anyhow!(
22                "Invalid shuriken reference format. Expected 'registry:shuriken', got '{}'",
23                input
24            ));
25        }
26
27        Ok(ShurikenReference {
28            registry: parts[0].to_string(),
29            shuriken: parts[1].to_string(),
30        })
31    }
32}
33
34#[derive(Clone, Debug, Serialize, Deserialize)]
35pub struct NinjaConfig {
36    pub registries: HashMap<String, String>,
37    pub check_updates: bool,
38    pub dev_mode: bool,
39}
40
41impl NinjaConfig {
42    pub fn new() -> Self {
43        Self {
44            registries: HashMap::from([(
45                "ninja".to_string(),
46                "https://raw.githubusercontent.com/tunafysh/ninja-packages/main/registry.yml"
47                    .to_string(),
48            )]),
49            check_updates: true,
50            dev_mode: false,
51        }
52    }
53
54    pub async fn generate_default_config(&self, root_dir: &Path) -> Result<()> {
55        let config_path = root_dir.join("config.toml");
56        let serialized = toml::ser::to_string_pretty(&self)?;
57
58        fs::write(config_path, serialized).await?;
59
60        Ok(())
61    }
62
63    pub fn add_registry(&mut self, name: String, url: String) {
64        self.registries.insert(name, url);
65    }
66
67    pub fn set_check_updates(&mut self, check: bool) {
68        self.check_updates = check;
69    }
70
71    pub fn set_dev_mode(&mut self, dev_mode: bool) {
72        self.dev_mode = dev_mode;
73    }
74
75    pub fn remove_registry(&mut self, registry: &str) {
76        self.registries.remove(registry);
77    }
78}
79
80impl Default for NinjaConfig {
81    fn default() -> Self {
82        Self::new()
83    }
84}
85
86pub async fn fetch_registries(
87    config: &NinjaConfig,
88) -> HashMap<String, crate::common::registry::Registry> {
89    let mut registries = HashMap::new();
90
91    for (name, url) in &config.registries {
92        info!("Fetching registry '{}' from {}", name, url);
93        match crate::common::registry::fetch_registry(url).await {
94            Ok(reg) => {
95                info!(
96                    "Successfully fetched registry '{}' with {} items",
97                    name,
98                    reg.shurikens.len()
99                );
100                registries.insert(name.clone(), reg);
101            }
102            Err(e) => error!("Failed to fetch registry '{}': {}", name, e),
103        }
104    }
105
106    registries
107}
108
109pub fn resolve_shuriken_url(
110    registry_url: &str,
111    shuriken_url: &str,
112) -> Result<String, anyhow::Error> {
113    if is_absolute_url(shuriken_url) {
114        return Ok(shuriken_url.to_string());
115    }
116
117    let base = Url::parse(registry_url)?;
118    let resolved = base.join(shuriken_url)?;
119
120    // Inject OS and ARCH in placeholders if present
121    let os = std::env::consts::OS;
122    let arch = std::env::consts::ARCH;
123    let resolved_str = resolved
124        .as_str()
125        .replace("{{os}}", os)
126        .replace("{{arch}}", arch);
127
128    Ok(resolved_str)
129}
130
131/// Find a shuriken in the fetched registries by its reference (e.g., "official:my-shuriken")
132pub async fn find_shuriken_in_registries(
133    registries: &HashMap<String, String>,
134    reference: &ShurikenReference,
135) -> Result<(ArmoryItem, String), anyhow::Error> {
136    let registry_url = registries.get(&reference.registry).ok_or_else(|| {
137        anyhow::anyhow!(
138            "Registry {} does not exist in the config.",
139            &reference.registry
140        )
141    })?;
142
143    let registry = fetch_registry(registry_url).await?;
144
145    let shuriken = registry
146        .shurikens
147        .iter()
148        .find(|s| match s {
149            ArmoryItem::Shuriken { name, .. } => name == &reference.shuriken,
150            ArmoryItem::Bundle { name, .. } => name == &reference.shuriken,
151        })
152        .ok_or_else(|| {
153            anyhow::anyhow!(
154                "Shuriken '{}' not found in registry '{}'",
155                reference.shuriken,
156                reference.registry
157            )
158        })?;
159
160    Ok((shuriken.clone(), reference.registry.clone()))
161}
162
163/// Get information about a shuriken from registries as JSON
164pub async fn get_shuriken_info(
165    registries: &HashMap<String, String>,
166    reference: &ShurikenReference,
167) -> Result<serde_json::Value, anyhow::Error> {
168    let (shuriken, registry_name) = find_shuriken_in_registries(registries, reference).await?;
169
170    let info = match shuriken {
171        ArmoryItem::Shuriken {
172            name,
173            version,
174            description,
175            author,
176            license,
177            platforms,
178            url,
179        } => {
180            serde_json::json!({
181                "type": "shuriken",
182                "name": name,
183                "version": version,
184                "registry": registry_name,
185                "author": author,
186                "license": license,
187                "description": description,
188                "platforms": platforms,
189                "url": url
190            })
191        }
192        ArmoryItem::Bundle {
193            name,
194            version,
195            description,
196            author,
197            license,
198            shurikens,
199        } => {
200            serde_json::json!({
201                "type": "bundle",
202                "name": name,
203                "version": version,
204                "registry": registry_name,
205                "author": author,
206                "license": license,
207                "description": description,
208                "contains": shurikens
209            })
210        }
211    };
212
213    Ok(info)
214}
215
216/// Resolve the download URL for a shuriken from registries
217pub async fn resolve_download_url(
218    registries: &HashMap<String, String>,
219    reference: &ShurikenReference,
220) -> Result<String, anyhow::Error> {
221    if !registries.contains_key(&reference.registry) {
222        return Err(anyhow::anyhow!(
223            "Registry {} not found in config.",
224            &reference.registry
225        ));
226    }
227
228    let registry = fetch_registry(&reference.registry).await?;
229
230    let shuriken = registry
231        .shurikens
232        .iter()
233        .find(|s| match s {
234            ArmoryItem::Shuriken { name, .. } => name == &reference.shuriken,
235            ArmoryItem::Bundle { name, .. } => name == &reference.shuriken,
236        })
237        .ok_or_else(|| {
238            anyhow::anyhow!(
239                "Shuriken '{}' not found in registry '{}'",
240                reference.shuriken,
241                reference.registry
242            )
243        })?
244        .to_owned()
245        .resolve();
246
247    let shuriken_url = match shuriken {
248        ArmoryItem::Shuriken { url, .. } => url,
249        _ => return Err(anyhow::anyhow!("Bundles do not have direct download URLs")),
250    };
251
252    resolve_shuriken_url(&reference.registry, &shuriken_url)
253}