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 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 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
131pub 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
163pub 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
216pub 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}