use std::env::args; use anyhow::{bail, Context}; use futures::StreamExt; use reqwest::Client; use serde_json::{json, Map, Value}; pub mod types; macro_rules! get_json_value { ($key:expr, $json_value:ident, $type:ident, $get:ident) => { match $json_value.get($key) { Some(resp) => { let resp = resp.to_owned(); if resp.$type() { resp.$get().expect( "The should have been checked in the if guard, so unpacking here is fine", ).to_owned() } else { bail!( "Value {} => \n{}\n is not of type: {}", $key, resp, stringify!($type) ); } } None => { bail!( "There seems to be no '{}' in your json data (json value: '{}')\n Has the api changend?", $key, serde_json::to_string_pretty(&$json_value).expect("Will always work") ); } } }; } use futures::stream::futures_unordered::FuturesUnordered; use types::{Extension, InputExtension}; #[tokio::main] async fn main() -> anyhow::Result<()> { let mut extensions: Vec = vec![]; for input_extension in args() .skip(1) .map(|str| InputExtension::try_from(str)) .collect::>>() { extensions.push(input_extension?); } let resulting_extensions = process_extensions(extensions).await?; let mut output = Map::new(); for extension in resulting_extensions { output.insert(extension.pname.clone(), json!(extension)); } println!( "{}", serde_json::to_string_pretty(&serde_json::Value::Object(output)).expect( "This is constructed from json, it should also be possible to serialize it again" ) ); Ok(()) } async fn process_extensions(extensions: Vec) -> anyhow::Result> { let mut output = Vec::with_capacity(extensions.len()); let client = Client::new(); for extension in extensions .iter() .map(|ext| { let local_client = &client; index_extension(ext, local_client) }) .collect::>() .collect::>() .await { output.push(extension?); } Ok(output) } async fn index_extension(extension: &InputExtension, client: &Client) -> anyhow::Result { let response = client .get(format!( "https://addons.mozilla.org/api/v5/addons/addon/{}", extension, )) .send() .await .context("Accessing the mozzila extenios api failed with error: {e}")?; eprintln!("Indexing {} ({})...", extension, response.status()); let response: Value = serde_json::from_str( &response .text() .await .context("Turning the response to text fail with error: {e}")?, ) .context("Deserializing the response failed! Error: {e}")?; if let Some(detail) = response.get("detail") { if detail == "Not found." { bail!("Your extension ('{}') was not found!", extension); } }; let release = { get_json_value!("current_version", response, is_object, as_object) }; #[allow(non_snake_case)] let addonId = { get_json_value!("guid", response, is_string, as_str) }; let version = { get_json_value!("version", release, is_string, as_str) }; let file = { get_json_value!("file", release, is_object, as_object) }; let url = { get_json_value!("url", file, is_string, as_str) }; let sha256 = { let hash = get_json_value!("hash", file, is_string, as_str); if hash.starts_with("sha256:") { hash } else { bail!("This hash type is unhandled: {}", hash); } }; Ok(Extension { pname: extension.moz_name.clone(), default_area: extension.default_area, version, addonId, url, sha256, }) }