facto-core 0.1.0-alpha.1

Core models, registry/forge/runtime clients for facto: real-time package ecosystem data for AI agents
Documentation
use std::future::Future;
use std::pin::Pin;

use crate::models::*;
use crate::registries::{Registry, RegistryError, RegistryResult};

pub struct Packagist {
    client: reqwest::Client,
}

impl Packagist {
    pub fn new(client: reqwest::Client) -> Self {
        Self { client }
    }
}

#[derive(serde::Deserialize)]
struct PackagistSearchResponse {
    results: Vec<PackagistSearchItem>,
}

#[derive(serde::Deserialize)]
struct PackagistSearchItem {
    name: String,
    description: Option<String>,
    #[serde(default)]
    downloads: u64,
}

impl Registry for Packagist {
    fn id(&self) -> &str {
        "packagist"
    }

    fn display_name(&self) -> &str {
        "Packagist"
    }

    fn get_package<'a>(
        &'a self,
        name: &'a str,
    ) -> Pin<Box<dyn Future<Output = RegistryResult<PackageInfo>> + Send + 'a>> {
        Box::pin(async move {
            let url = format!(
                "https://repo.packagist.org/p2/{}.json",
                urlencoding::encode(name)
            );
            let resp = self.client.get(&url).send().await?;

            if resp.status().as_u16() == 404 {
                return Err(RegistryError::NotFound);
            }

            let data: serde_json::Value = crate::registries::bounded_json(resp).await?;

            let versions = data["packages"][name]
                .as_array()
                .ok_or_else(|| RegistryError::Parse("invalid response".into()))?;

            let latest = versions.first();

            let keywords: Vec<String> = latest
                .and_then(|v| v["keywords"].as_array())
                .map(|arr| {
                    arr.iter()
                        .filter_map(|v| v.as_str().map(String::from))
                        .collect()
                })
                .unwrap_or_default();

            Ok(PackageInfo {
                name: name.to_string(),
                registry: "packagist".into(),
                latest_version: latest
                    .and_then(|v| v["version"].as_str())
                    .map(|s| s.to_string()),
                description: latest
                    .and_then(|v| v["description"].as_str())
                    .map(|s| s.to_string()),
                license: latest
                    .and_then(|v| v["license"].as_array())
                    .and_then(|l| l.first())
                    .and_then(|v| v.as_str())
                    .map(|s| s.to_string()),
                homepage: latest
                    .and_then(|v| v["homepage"].as_str())
                    .map(|s| s.to_string()),
                repository: latest
                    .and_then(|v| v["source"]["url"].as_str())
                    .map(|s| s.to_string()),
                authors: Vec::new(),
                updated_at: None,
                keywords,
                classifiers: Vec::new(),
                requires_python: None,
            })
        })
    }

    fn get_versions<'a>(
        &'a self,
        name: &'a str,
    ) -> Pin<Box<dyn Future<Output = RegistryResult<Vec<VersionInfo>>> + Send + 'a>> {
        Box::pin(async move {
            let url = format!(
                "https://repo.packagist.org/p2/{}.json",
                urlencoding::encode(name)
            );
            let resp = self.client.get(&url).send().await?;

            if resp.status().as_u16() == 404 {
                return Err(RegistryError::NotFound);
            }

            let data: serde_json::Value = crate::registries::bounded_json(resp).await?;

            let versions = data["packages"][name]
                .as_array()
                .ok_or_else(|| RegistryError::Parse("invalid response".into()))?;

            let mut result: Vec<VersionInfo> = versions
                .iter()
                .filter_map(|v| {
                    let version = v["version"].as_str()?.to_string();
                    let prerelease = version.contains("-dev")
                        || version.contains("-alpha")
                        || version.contains("-beta")
                        || version.contains("-RC");
                    Some(VersionInfo {
                        version,
                        released_at: None,
                        prerelease,
                    })
                })
                .collect();

            crate::registries::sort_versions_semver(&mut result);
            Ok(result)
        })
    }

    fn search<'a>(
        &'a self,
        query: &'a str,
        limit: usize,
    ) -> Pin<Box<dyn Future<Output = RegistryResult<Vec<SearchResult>>> + Send + 'a>> {
        Box::pin(async move {
            let url = format!(
                "https://packagist.org/search.json?q={}&per_page={}",
                urlencoding::encode(query),
                limit
            );
            let resp = self.client.get(&url).send().await?;

            let resp = crate::registries::ensure_status(resp)?;

            let data: PackagistSearchResponse = crate::registries::bounded_json(resp).await?;

            Ok(data
                .results
                .into_iter()
                .map(|item| SearchResult {
                    name: item.name,
                    description: item.description,
                    latest_version: None,
                    downloads: Some(item.downloads),
                    keywords: Vec::new(),
                })
                .collect())
        })
    }
}