diff --git a/src/commands/apt.rs b/src/commands/apt.rs index ae16047..2cb94b4 100644 --- a/src/commands/apt.rs +++ b/src/commands/apt.rs @@ -7,7 +7,7 @@ pub fn query_apt(ctx: Context, msg: Message, args: Vec<&str>) { let query = args.join(" "); let response: Response = search( &format!("https://sources.debian.org/api/src/{}/", &query), - EMPTY_RESULT, + |_e| EMPTY_RESULT, ); if response.versions.len() == 0 { send(msg.channel_id, "No results", &ctx); diff --git a/src/commands/mod.rs b/src/commands/mod.rs index d8c2055..97c631e 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -5,6 +5,7 @@ use serde::de::DeserializeOwned; use serenity::prelude::*; mod apt; mod pacman; +mod nix; extern crate reqwest; pub struct Handler; @@ -49,6 +50,7 @@ lazy_static! { let mut command_list = Vec::new(); command_list.push(Command::new("pacman", pacman::query_pacman)); command_list.push(Command::new("apt", apt::query_apt)); + command_list.push(Command::new("nix", nix::query_nix)); command_list }; } @@ -85,8 +87,8 @@ pub fn respond_with_results(target: ChannelId, results: &Vec ); } -pub fn search(url: &str, fallback: T) -> T { - return search_inner(url).unwrap_or(fallback); +pub fn search(url: &str, fallback: impl Fn(reqwest::Error) -> T) -> T { + return search_inner(url).unwrap_or_else(fallback); } fn search_inner(url: &str) -> Result { diff --git a/src/commands/nix.rs b/src/commands/nix.rs new file mode 100644 index 0000000..b895787 --- /dev/null +++ b/src/commands/nix.rs @@ -0,0 +1,106 @@ +use crate::commands::*; +use serde::de; +use serde::{Deserialize, Deserializer}; +use serenity::model::channel::Message; +use std::collections::HashMap; +use std::fmt; +use std::marker::PhantomData; + +pub fn query_nix(ctx: Context, msg: Message, args: Vec<&str>) { + // we know for sure that there’s at least one element here + let query = args[0]; + match NIX_PACKAGES.packages.get(query) { + Some(result) => respond_with_results(msg.channel_id, &vec![result], &ctx), + None => send(msg.channel_id, "No results", &ctx), + } +} + +lazy_static! { + #[derive(Debug)] + static ref NIX_PACKAGES: Response = { + search( + &"https://nixos.org/nixpkgs/packages-nixos-19.09.json", + |e| { panic!("{}", e) + }, + ) + }; +} + +#[derive(Deserialize, Debug)] +struct Response { + pub commit: String, + pub packages: HashMap, +} + +#[derive(Deserialize, Debug)] +struct Package { + pname: String, + version: String, + meta: PackageMeta, +} + +#[derive(Deserialize, Debug)] +struct PackageMeta { + description: Option, + /** + * It appears as though the Nix repositories allow either no homepage, one homepage, or a list. + * Definitely no unnecessary complexity because of that. + */ + #[serde(default)] + #[serde(deserialize_with = "string_or_seq_string")] + homepage: String, + //homepage: Option, +} + +impl fmt::Display for Package { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + // This is an en-dash, not a minus sign. + // Putting a minus here removes the first line from output, + // but only for some packages. en-dash works fine. + "{}–{}\n {}\n Homepage: {}", + self.pname, + self.version, + match &self.meta.description { + Some(description) => description, + None => "No description provided", + }, + self.meta.homepage + ) + } +} + +// Custom deserializer to handle the string/vec homepage field. +// Heavily inspired by +// https://stackoverflow.com/questions/41151080/deserialize-a-json-string-or-array-of-strings-into-a-vec +fn string_or_seq_string<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + struct StringOrVec(PhantomData); + + impl<'de> de::Visitor<'de> for StringOrVec { + type Value = String; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("string or list of strings") + } + + fn visit_str(self, value: &str) -> Result { + Ok(value.to_owned()) + } + + fn visit_none(self) -> Result { + Ok("unspecified".to_owned()) + } + + fn visit_seq>(self, visitor: S) -> Result { + let strings: Vec = + Deserialize::deserialize(de::value::SeqAccessDeserializer::new(visitor))?; + Ok(strings.into_iter().next().unwrap()) + } + } + + deserializer.deserialize_any(StringOrVec(PhantomData)) +} diff --git a/src/commands/pacman.rs b/src/commands/pacman.rs index 9cc6293..0d9a458 100644 --- a/src/commands/pacman.rs +++ b/src/commands/pacman.rs @@ -11,7 +11,7 @@ pub fn query_pacman(ctx: Context, msg: Message, args: Vec<&str>) { "https://www.archlinux.org/packages/search/json/?name={}", &args[0] ), - EMPTY_RESULT, + |_e| EMPTY_RESULT, ); // this is 1 for most packages and 2 if there’s a second version in testing if response.results.len() != 0 { @@ -25,7 +25,7 @@ pub fn query_pacman(ctx: Context, msg: Message, args: Vec<&str>) { "https://www.archlinux.org/packages/search/json/?q={}", &query ), - EMPTY_RESULT, + |_e| EMPTY_RESULT, ); if response.results.len() == 0 { send(msg.channel_id, "No results", &ctx);