ninja-core 1.14.1

A powerful, cross-platform package manager and runtime for managing tools and plugins (shurikens)
Documentation
use std::path::Path;

use futures_util::future::join_all;
use reqwest;
use serde::{Deserialize, Serialize};
use tokio::fs;

#[derive(Debug, Serialize, Deserialize)]
pub struct Registry {
    pub name: String,
    pub description: Option<String>,
    pub shurikens: Vec<ArmoryItem>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum ArmoryItem {
    #[serde(rename = "shuriken")]
    Shuriken {
        name: String,
        version: String,
        description: String,
        author: String,
        license: String,
        platforms: Vec<String>,
        url: String,
    },

    #[serde(rename = "bundle")]
    Bundle {
        name: String,
        version: String,
        description: String,
        author: String,
        license: String,
        shurikens: Vec<String>,
    },
}

impl ArmoryItem {
    pub fn resolve(self) -> Self {
        match self {
            ArmoryItem::Shuriken {
                name,
                version,
                description,
                author,
                license,
                platforms,
                url,
            } => {
                let resolved_partial_url = url.replace("{{ os }}", std::env::consts::OS);
                let resolved_url =
                    resolved_partial_url.replace("{{ arch }}", std::env::consts::ARCH);

                ArmoryItem::Shuriken {
                    name,
                    version,
                    description,
                    author,
                    license,
                    platforms,
                    url: resolved_url,
                }
            }

            ArmoryItem::Bundle { .. } => self,
        }
    }
}

pub fn is_absolute_url(url: &str) -> bool {
    url.starts_with("http://") || url.starts_with("https://")
}

pub async fn fetch_registry(url: &str) -> Result<Registry, anyhow::Error> {
    let response = reqwest::get(url).await?;
    let status = response.status();
    let text = response.text().await?;

    if !status.is_success() {
        return Err(anyhow::anyhow!("Failed to fetch registry: HTTP {}", status));
    }

    let registry: Registry = serde_yaml::from_str(&text)?;
    Ok(registry)
}

pub async fn download_shuriken(path: &Path, url: &str) -> Result<(), anyhow::Error> {
    let response = reqwest::get(url).await?;
    let bytes = response.bytes().await?.to_vec();
    fs::write(path, &bytes).await?;
    Ok(())
}

pub async fn get_shurikens_from_registries(urls: &[String]) -> Vec<ArmoryItem> {
    let futures = urls.iter().map(|url| fetch_registry(url));
    let results = join_all(futures).await;

    let mut all_shurikens = Vec::new();

    for result in results {
        match result {
            Ok(registry) => {
                let shurikens = registry
                    .shurikens
                    .into_iter()
                    .filter(|item| matches!(item, ArmoryItem::Shuriken { .. }));

                all_shurikens.extend(shurikens);
            }
            Err(e) => {
                eprintln!("Failed to fetch registry: {}", e);
            }
        }
    }

    all_shurikens
}

pub async fn get_shuriken_from_registries(
    name: String,
    registries: &[String],
) -> Option<ArmoryItem> {
    let shurikens = get_shurikens_from_registries(registries).await;

    shurikens.into_iter().find(|item| match item {
        ArmoryItem::Shuriken {
            name: item_name, ..
        } => item_name == &name,
        _ => false,
    })
}