aur-rs 0.1.1

Library for interacting with the Arch User Repository's RPC interface
Documentation
//! This is a library for interacting with the Arch User Repository (AUR).
//! It is a work in progress and is not ready for production use.
//! The goal is to provide a simple interface for interacting with the AUR.
//!
//! For a basic name search call `search_package` which takes a package name
//! and returns a `SearchResponse` struct.  This struct contains a `results`
//! field which is a vector of `Package` structs.  These structs contain
//! information about the package.
//!
//! # Example
//! ```rust
//! #[tokio::main]
//! async fn main() {
//!     let request = aur_rs::Request::default();
//!     let response = request.search_package_by_name("yay-bin").await.unwrap();
//!     println!("#{:#?}", response);
//! }
//! ```
//!
//! Another way to get information about a package is to use the `package_info`
//! function.  This function takes a package name and returns `ReturnData`.
//!
//! # Example
//! ```rust
//! #[tokio::main]
//! async fn main() {
//! let request = aur_rs::Request::default();
//!     let response = request.search_info_by_name("yay-bin").await.unwrap();
//!
//!     if let Some(keywords) = &response.results[0].keywords {
//!        println!("Keywords: {:?}", keywords);
//!    }
//! }
//! ```

#![warn(missing_docs)]

use serde::{Deserialize, Serialize};
use url::Url;

// If we are running tests then use a local mock of the AUR RPC.
const AUR_RPC_URL: &str = "https://aur.archlinux.org/rpc/v5";

/// This is a request to the AUR RPC
#[derive(Serialize, Deserialize, Debug)]
pub struct Request {
    /// URL of the AUR RPC endpoint.  This is s    assert_eq!(response.results[1].name, "yay-git");t to the value of
    /// https://aur.archlinux.org/rpc/v5 by default; but for testing,
    /// or should there be some change this could prove useful.
    pub endpoint: String,
}

impl Default for Request {
    fn default() -> Self {
        Self {
            endpoint: AUR_RPC_URL.to_string(),
        }
    }
}

/// This is a a response to a search request.
#[derive(Serialize, Deserialize, Debug)]
pub struct ReturnData {
    /// API version
    version: u32,
    #[serde(rename = "type")]
    /// Response type one of `error`, `search`, `multiinfo``
    type_: String,
    /// Number of packages found
    pub resultcount: u32,
    /// Vector of packages.  If this is empty then no packages were found,
    /// or there was an error.
    pub results: Vec<Package>,
    /// Error message if there was an error. (probably handle this internally
    /// for now)
    error: Option<String>,
}

/// This is a package.  TODO(2020-05-03): Add more documentation.
#[derive(Serialize, Deserialize, Debug)]
pub struct Package {
    /// Package name
    #[serde(rename = "Name")]
    pub name: String,
    /// Package version
    #[serde(rename = "Version")]
    pub version: String,
    /// Package description (nullable)
    #[serde(rename = "Description")]
    pub description: Option<String>,
    /// Package maintainer (nullable)
    #[serde(rename = "Maintainer")]
    pub maintainer: Option<String>,
    /// Package URL pointing to the package source (nullable)
    #[serde(rename = "URL")]
    pub url: Option<String>,
    /// Number of votes
    #[serde(rename = "NumVotes")]
    pub num_votes: u32,
    /// Popularity of the package from 0 to 10
    #[serde(rename = "Popularity")]
    pub popularity: f32,
    /// Out of date flag.  Could be a date or null.
    #[serde(rename = "OutOfDate")]
    pub out_of_date: Option<u32>,
    /// Package base name.  Usually the same as the package name.
    #[serde(rename = "PackageBase")]
    pub package_base: String,
    /// ID of the package base for this specific package.
    #[serde(rename = "PackageBaseID")]
    pub package_base_id: u32,
    /// First submitted date in unix time.
    #[serde(rename = "FirstSubmitted")]
    pub first_submitted: u32,
    /// Last modified date in unix time.
    #[serde(rename = "LastModified")]
    pub last_modified: u32,
    /// URL path to the package snapshot. (null)
    #[serde(rename = "URLPath")]
    pub url_path: Option<String>,
    /// Unique ID of the package.
    #[serde(rename = "ID")]
    pub id: u32,

    // The following fields are optional and only returned when using the
    // `multiinfo` method.
    /// Vector of dependencies
    #[serde(rename = "Depends")]
    pub depends: Option<Vec<String>>,
    /// Vector of make dependencies
    #[serde(rename = "MakeDepends")]
    pub make_depends: Option<Vec<String>>,
    /// Vector of optional dependencies
    #[serde(rename = "OptDepends")]
    pub opt_depends: Option<Vec<String>>,
    /// Vector of check dependencies
    #[serde(rename = "CheckDepends")]
    pub check_depends: Option<Vec<String>>,
    /// Vector of conflicts
    #[serde(rename = "Conflicts")]
    pub conflicts: Option<Vec<String>>,
    /// Vector of provides
    #[serde(rename = "Provides")]
    pub provides: Option<Vec<String>>,
    /// Vector of packages replaced by this package
    #[serde(rename = "Replaces")]
    pub replaces: Option<Vec<String>>,
    /// Vector of groups
    #[serde(rename = "Groups")]
    pub groups: Option<Vec<String>>,
    /// Vector of licenses defind in the PKGBUILD
    #[serde(rename = "License")]
    pub license: Option<Vec<String>>,
    /// Vector of keywords
    #[serde(rename = "Keywords")]
    pub keywords: Option<Vec<String>>,
}

impl Request {
    // Private stuff
    async fn search(&self, search_term: &str) -> Result<ReturnData, Box<dyn std::error::Error>> {
        let resp = reqwest::get(search_term).await?.text().await?;
        let search_response: ReturnData = serde_json::from_str(&resp)?;
        Ok(search_response)
    }

    async fn starts_with(&self, url: &str) -> Result<Vec<String>, Box<dyn std::error::Error>> {
        let resp = reqwest::get(url).await?.text().await?;
        let search_response: Vec<String> = serde_json::from_str(&resp)?;

        Ok(search_response)
    }

    /// Package names are case insensitive.
    /// They can be searched by a few attributes:
    /// name, name-desc, depends, checkdepends, optdepends, makedepends,
    /// maintainer, submitter, provides, conflicts, replaces, keywords,
    /// groups, and comaintainers

    /// Search for package by package name.
    pub async fn search_package_by_name(
        &self,
        package: &str,
    ) -> Result<ReturnData, Box<dyn std::error::Error>> {
        let url = format!("{}/search/{}?by=name", self.endpoint, package);
        self.search(&url).await
    }

    /// Search for package by package name and description.
    pub async fn search_package_by_name_desc(
        &self,
        package: &str,
    ) -> Result<ReturnData, Box<dyn std::error::Error>> {
        let url = format!("{}/search/{}?by=name-desc", self.endpoint, package);
        self.search(&url).await
    }

    /// Search for package that depend on package.
    pub async fn search_package_by_depends(
        &self,
        package: &str,
    ) -> Result<ReturnData, Box<dyn std::error::Error>> {
        let url = format!("{}/search/{}?by=depends", self.endpoint, package);
        self.search(&url).await
    }

    /// Search for package that check depend on package.
    pub async fn search_package_by_checkdepends(
        &self,
        package: &str,
    ) -> Result<ReturnData, Box<dyn std::error::Error>> {
        let url = format!("{}/search/{}?by=checkdepends", self.endpoint, package);
        self.search(&url).await
    }

    /// Search for package that optionally depend on package.
    pub async fn search_package_by_optdepends(
        &self,
        package: &str,
    ) -> Result<ReturnData, Box<dyn std::error::Error>> {
        let url = format!("{}/search/{}?by=optdepends", self.endpoint, package);
        self.search(&url).await
    }

    /// Search for packages that need package to build.
    pub async fn search_package_by_makedepends(
        &self,
        package: &str,
    ) -> Result<ReturnData, Box<dyn std::error::Error>> {
        let url = format!("{}/search/{}?by=optdepends", self.endpoint, package);
        self.search(&url).await
    }

    /// Search for package by package maintainer.
    pub async fn search_package_by_maintainer(
        &self,
        package: &str,
    ) -> Result<ReturnData, Box<dyn std::error::Error>> {
        let url = format!("{}/search/{}?by=maintainer", self.endpoint, package);
        self.search(&url).await
    }

    /// Search for package by package submitter.
    pub async fn search_package_by_submitter(
        &self,
        package: &str,
    ) -> Result<ReturnData, Box<dyn std::error::Error>> {
        let url = format!("{}/search/{}?by=submitter", self.endpoint, package);
        self.search(&url).await
    }

    /// Search for packages that provide the same functionality.
    pub async fn search_package_by_provides(
        &self,
        package: &str,
    ) -> Result<ReturnData, Box<dyn std::error::Error>> {
        let url = format!("{}/search/{}?by=provides", self.endpoint, package);
        self.search(&url).await
    }

    /// Search for packages that conflict with package.
    pub async fn search_package_by_conflicts(
        &self,
        package: &str,
    ) -> Result<ReturnData, Box<dyn std::error::Error>> {
        let url = format!("{}/search/{}?by=conflicts", self.endpoint, package);
        self.search(&url).await
    }

    /// Search for packages that this package replaces.
    pub async fn search_package_by_replaces(
        &self,
        package: &str,
    ) -> Result<ReturnData, Box<dyn std::error::Error>> {
        let url = format!("{}/search/{}?by=replaces", self.endpoint, package);
        self.search(&url).await
    }

    /// Search for packages by keywords.
    pub async fn search_package_by_keywords(
        &self,
        package: &str,
    ) -> Result<ReturnData, Box<dyn std::error::Error>> {
        let url = format!("{}/search/{}?by=keywords", self.endpoint, package);
        self.search(&url).await
    }

    /// Search packages' groups.
    pub async fn search_package_by_groups(
        &self,
        package: &str,
    ) -> Result<ReturnData, Box<dyn std::error::Error>> {
        let url = format!("{}/search/{}?by=groups", self.endpoint, package);
        self.search(&url).await
    }

    /// Search for packages by comaintainers.
    pub async fn search_package_by_comaintainers(
        &self,
        package: &str,
    ) -> Result<ReturnData, Box<dyn std::error::Error>> {
        let url = format!("{}/search/{}?by=comaintainers", self.endpoint, package);
        self.search(&url).await
    }

    /// Search for a packages' info by name.
    pub async fn search_info_by_name(
        &self,
        package: &str,
    ) -> Result<ReturnData, Box<dyn std::error::Error>> {
        let url = format!("{}/info/{}", self.endpoint, package);
        self.search(&url).await
    }

    /// Search for information on multiple packages at once.
    pub async fn search_multi_info_by_names(
        &self,
        packages: &[&str],
    ) -> Result<ReturnData, Box<dyn std::error::Error>> {
        let url_str = format!("{}/info", self.endpoint);

        let mut args: Vec<(&str, &str)> = vec![];

        for package in packages {
            // Might need to change this to argv%5B%5D rather than arg[] in the
            // future?
            args.push(("arg[]", package))
        }

        let url = Url::parse_with_params(&url_str, args)?;

        return self.search(url.as_str()).await;
    }

    /// Search for a package that starts with the given string.
    /// Note: Limited to 20 results.
    pub async fn starts_with_name(
        &self,
        starts_with: &str,
    ) -> Result<Vec<String>, Box<dyn std::error::Error>> {
        let url = format!("{}/suggest/{}", self.endpoint, starts_with);
        self.starts_with(&url).await
    }

    /// Search for a package base that starts with the given string.
    /// Note: Limited to 20 results.
    pub async fn starts_with_basename(
        &self,
        base_start: &str,
    ) -> Result<Vec<String>, Box<dyn std::error::Error>> {
        let url = format!("{}/suggest-pkgbase/{}", self.endpoint, base_start);
        self.starts_with(&url).await
    }
}