diff options
author | Matthias Beyer <mail@beyermatthias.de> | 2019-04-19 17:54:52 +0200 |
---|---|---|
committer | Matthias Beyer <mail@beyermatthias.de> | 2019-04-19 17:58:33 +0200 |
commit | 7cc25e9cf2dda28f1f8dea43caac7bd6291ed1f1 (patch) | |
tree | 9fa1b948ac6ff7179fb311b54e60f1e41a97087c /src |
Initial import
In the beginning there was darkness.
So I spoke "git init".
And there was a git repository.
Diffstat (limited to 'src')
-rw-r--r-- | src/backend.rs | 50 | ||||
-rw-r--r-- | src/cli.rs | 99 | ||||
-rw-r--r-- | src/config.rs | 60 | ||||
-rw-r--r-- | src/filter.rs | 49 | ||||
-rw-r--r-- | src/frontend/json.rs | 73 | ||||
-rw-r--r-- | src/frontend/list.rs | 71 | ||||
-rw-r--r-- | src/frontend/mod.rs | 40 | ||||
-rw-r--r-- | src/frontend/table.rs | 94 | ||||
-rw-r--r-- | src/main.rs | 170 |
9 files changed, 706 insertions, 0 deletions
diff --git a/src/backend.rs b/src/backend.rs new file mode 100644 index 0000000..57afd10 --- /dev/null +++ b/src/backend.rs @@ -0,0 +1,50 @@ +use clap::ArgMatches; +use failure::Fallible as Result; + +use librepology::v1::api::Api; +use librepology::v1::restapi::RestApi; +use librepology::v1::types::*; +use librepology::v1::api::StdinWrapper; + +use crate::config::Configuration; + +/// Helper type for cli implementation +/// for being transparent in what backend we use +pub enum Backend { + Stdin(StdinWrapper), + RepologyOrg(RestApi), +} + +impl Api for Backend { + fn project<N: AsRef<str>>(&self, name: N) -> Result<Vec<Package>> { + match self { + Backend::Stdin(inner) => inner.project(name), + Backend::RepologyOrg(inner) => inner.project(name), + } + } + + fn problems_for_repo<R: AsRef<str>>(&self, repo: R) -> Result<Vec<Problem>> { + match self { + Backend::Stdin(inner) => inner.problems_for_repo(repo), + Backend::RepologyOrg(inner) => inner.problems_for_repo(repo), + } + } + + fn problems_for_maintainer<M: AsRef<str>>(&self, maintainer: M) -> Result<Vec<Problem>> { + match self { + Backend::Stdin(inner) => inner.problems_for_maintainer(maintainer), + Backend::RepologyOrg(inner) => inner.problems_for_maintainer(maintainer), + } + } +} + +pub fn new_backend(app: &ArgMatches, config: &Configuration) -> Result<Backend> { + if app.is_present("input_stdin") { + Ok(Backend::Stdin(StdinWrapper::from(::std::io::stdin()))) + } else { + debug!("Constructing backend"); + let url = config.repology_url().as_str().into(); + trace!("url = {}", url); + Ok(Backend::RepologyOrg(RestApi::new(url))) + } +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..25533cc --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,99 @@ +use clap::{App, Arg, ArgGroup, SubCommand}; + +pub fn build_cli<'a>() -> App<'a, 'a> { + App::new("repolocli") + .version("0.1") + .author("Matthias Beyer <mail@beyermatthias.de>") + .about("Query repology.org and postprocess its output") + + .arg(Arg::with_name("config") + .long("config") + .value_name("PATH") + .required(false) + .multiple(false) + .takes_value(true) + .help("Override default configuration file path") + + ) + + .arg(Arg::with_name("verbose") + .long("verbose") + .short("v") + .required(false) + .multiple(true) + .takes_value(false) + .help("Increase verbosity. Default = Info, -v = Debug, -vv = Trace") + ) + + .arg(Arg::with_name("quiet") + .long("quiet") + .short("q") + .required(false) + .multiple(true) + .takes_value(false) + .help("Decrease verbosity. Default = Info, -q = Warn, -qq = Error") + ) + + .arg(Arg::with_name("output") + .long("output") + .short("o") + .required(false) + .multiple(false) + .takes_value(true) + .possible_values(&["table", "json", "lines"]) + .default_value("lines") + .help("Output format") + ) + + .arg(Arg::with_name("input_stdin") + .long("stdin") + .short("I") + .required(false) + .multiple(false) + .takes_value(false) + .help("Read data (JSON) from stdin.") + ) + + .subcommand(SubCommand::with_name("project") + .arg(Arg::with_name("project_name") + .index(1) + .required(false) // TODO: Make required, is not required currently when --stdin is passed. + .multiple(false) + .takes_value(true) + .help("Query data about a project") + ) + ) + + .subcommand(SubCommand::with_name("problems") + .arg(Arg::with_name("repo") + .short("r") + .long("repo") + .alias("repository") + .required(false) + .multiple(false) + .takes_value(true) + .help("The repository to get problems for") + ) + + .arg(Arg::with_name("maintainer") + .short("m") + .long("maintainer") + .alias("maint") + .required(false) + .multiple(false) + .takes_value(true) + .help("The maintainer to get problems for") + ) + + .group(ArgGroup::with_name("problems-args") + .args(&["repo", "maintainer"]) + .required(true)) + + ) + .after_help(r#" + repolocli can read data from stdin, if you want to postprocess repology.org data you already + fetched from repology.org/api/v1 via curl (or some other method). + In this case, repolocli is only a easier-to-use 'jq' (if you don't know jq, look it up NOW!). + "#) + +}
\ No newline at end of file diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..7d47004 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,60 @@ +use std::ops::Deref; + +use url::Url; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Configuration { + #[serde(with = "url_serde")] + #[serde(rename = "repology_url")] + repology_url: Url, + + #[serde(rename = "whitelist")] + whitelist: Vec<String>, + + #[serde(rename = "blacklist")] + blacklist: Vec<String>, + + #[serde(rename = "local_packages")] + local_packages: Option<Vec<Package>>, +} + +impl Configuration { + pub fn repology_url(&self) -> &Url { + &self.repology_url + } + + pub fn whitelist(&self) -> &Vec<String> { + &self.whitelist + } + + pub fn blacklist(&self) -> &Vec<String> { + &self.blacklist + } + + // unused + //pub fn local_packages(&self) -> Option<&Vec<Package>> { + // self.local_packages.as_ref() + //} + +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Package { + #[serde(rename = "name")] + name: String, + + #[serde(rename = "local_version")] + local_version: Version, +} + +/// Not reusing the librepology type here because it might change +#[derive(Debug, Serialize, Deserialize)] +pub struct Version(String); + +impl Deref for Version { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/src/filter.rs b/src/filter.rs new file mode 100644 index 0000000..334ccbd --- /dev/null +++ b/src/filter.rs @@ -0,0 +1,49 @@ +use filters::filter::Filter; +use filters::ops::and::And; +use filters::ops::bool::Bool; +use filters::ops::not::Not; + +use crate::config::Configuration; + +struct BlackListFilter { + repo_name: String, +} + +impl BlackListFilter { + pub fn new(repo_name: String) -> Self { + BlackListFilter { repo_name } + } +} + +impl Filter<String> for BlackListFilter { + fn filter(&self, element: &String) -> bool { + element != self.repo_name + } +} + +struct WhiteListFilter { + repo_name: String, +} + +impl Filter<String> for WhiteListFilter { + fn filter(&self, element: &String) -> bool { + element == self.repo_name + } +} + +pub fn repo_filter(config: &Configuration) -> Box<Filter<String>> { + let blacklist = config + .blacklist() + .iter() + .cloned() + .map(BlackListFilter::new) + .fold(Box::new(Bool::new(true)), |accu, element| accu.and(element)); + let whitelist = config + .whitelist() + .iter() + .cloned() + .map(WhiteListFilter::new) + .fold(Box::new(Bool::new(true)), |accu, element| accu.and(element)); + + Box::new(blacklist.not().or(whitelist)) +} diff --git a/src/frontend/json.rs b/src/frontend/json.rs new file mode 100644 index 0000000..7430040 --- /dev/null +++ b/src/frontend/json.rs @@ -0,0 +1,73 @@ +use std::io::Stdout; +use std::io::Write; +use std::ops::Deref; + +use librepology::v1::types::Package; +use librepology::v1::types::Problem; +use failure::Fallible as Result; +use failure::Error; + +use crate::frontend::Frontend; + +pub struct JsonFrontend(Stdout); + +impl JsonFrontend { + pub fn new(stdout: Stdout) -> Self { + JsonFrontend(stdout) + } +} + +impl Frontend for JsonFrontend { + /// TODO: Implement to-JSON + fn list_packages(&self, packages: Vec<Package>) -> Result<()> { + let mut outlock = self.0.lock(); + + packages.iter().fold(Ok(()), |accu, package| { + accu.and_then(|_| { + let status = if let Some(stat) = package.status() { + format!("{}", stat) + } else { + String::from("No status") + }; // not optimal, but works for now. + + let url = if let Some(url) = package.www() { + if let Some(url) = url.first() { + format!("{}", url.deref()) + } else { + String::from("") + } + } else { + String::from("") + }; // not optimal, but works for now + + writeln!(outlock, + "{name:10} - {version:8} - {repo:15} - {status:5} - {www}", + name = package.name().deref(), + version = package.version().deref(), + repo = package.repo().deref(), + status = status, + www = url).map(|_| ()).map_err(Error::from) + }) + }) + } + + /// TODO: Implement to-JSON + fn list_problems(&self, problems: Vec<Problem>) -> Result<()> { + let mut outlock = self.0.lock(); + + problems.iter().fold(Ok(()), |accu, problem| { + accu.and_then(|_| { + writeln!(outlock, + "{repo:10} - {name:10} - {effname:10} - {maintainer:15} - {desc}", + repo = problem.repo().deref(), + name = problem.name().deref(), + effname = problem.effname().deref(), + maintainer = problem.maintainer().deref(), + desc = problem.problem_description()) + .map(|_| ()) + .map_err(Error::from) + }) + }) + } +} + diff --git a/src/frontend/list.rs b/src/frontend/list.rs new file mode 100644 index 0000000..cf5a946 --- /dev/null +++ b/src/frontend/list.rs @@ -0,0 +1,71 @@ +use std::io::Stdout; +use std::io::Write; +use std::ops::Deref; + +use librepology::v1::types::Package; +use librepology::v1::types::Problem; +use failure::Fallible as Result; +use failure::Error; + +use crate::frontend::Frontend; + +pub struct ListFrontend(Stdout); + +impl ListFrontend { + pub fn new(stdout: Stdout) -> Self { + ListFrontend(stdout) + } +} + +impl Frontend for ListFrontend { + fn list_packages(&self, packages: Vec<Package>) -> Result<()> { + let mut outlock = self.0.lock(); + + packages.iter().fold(Ok(()), |accu, package| { + accu.and_then(|_| { + let status = if let Some(stat) = package.status() { + format!("{}", stat) + } else { + String::from("No status") + }; // not optimal, but works for now. + + let url = if let Some(url) = package.www() { + if let Some(url) = url.first() { + format!("{}", url.deref()) + } else { + String::from("") + } + } else { + String::from("") + }; // not optimal, but works for now + + writeln!(outlock, + "{name:10} - {version:8} - {repo:15} - {status:5} - {www}", + name = package.name().deref(), + version = package.version().deref(), + repo = package.repo().deref(), + status = status, + www = url).map(|_| ()).map_err(Error::from) + }) + }) + } + + fn list_problems(&self, problems: Vec<Problem>) -> Result<()> { + let mut outlock = self.0.lock(); + + problems.iter().fold(Ok(()), |accu, problem| { + accu.and_then(|_| { + writeln!(outlock, + "{repo:10} - {name:10} - {effname:10} - {maintainer:15} - {desc}", + repo = problem.repo().deref(), + name = problem.name().deref(), + effname = problem.effname().deref(), + maintainer = problem.maintainer().deref(), + desc = problem.problem_description()) + .map(|_| ()) + .map_err(Error::from) + }) + }) + } +} + diff --git a/src/frontend/mod.rs b/src/frontend/mod.rs new file mode 100644 index 0000000..41ab972 --- /dev/null +++ b/src/frontend/mod.rs @@ -0,0 +1,40 @@ +use clap::ArgMatches; +use failure::Fallible as Result; + +use librepology::v1::types::*; + +use crate::config::Configuration; +use crate::frontend::list::ListFrontend; +use crate::frontend::json::JsonFrontend; +use crate::frontend::table::TableFrontend; + +pub trait Frontend { + fn list_packages(&self, packages: Vec<Package>) -> Result<()>; + fn list_problems(&self, problems: Vec<Problem>) -> Result<()>; +} + +pub mod list; +pub mod json; +pub mod table; + +pub fn new_frontend(app: &ArgMatches, _config: &Configuration) -> Result<Box<Frontend>> { + match app.value_of("output") { + None | Some("lines") => { + debug!("No output specified, using default"); + Ok(Box::new(ListFrontend::new(::std::io::stdout()))) + }, + + Some("json") => { + debug!("Using JSON Frontend"); + Ok(Box::new(JsonFrontend::new(::std::io::stdout()))) + }, + + Some("table") => { + debug!("Using table Frontend"); + Ok(Box::new(TableFrontend::new(::std::io::stdout()))) + }, + + Some(other) => Err(format_err!("Unknown Frontend '{}'", other)), + } + +} diff --git a/src/frontend/table.rs b/src/frontend/table.rs new file mode 100644 index 0000000..ba4e77a --- /dev/null +++ b/src/frontend/table.rs @@ -0,0 +1,94 @@ +use std::io::Stdout; +use std::ops::Deref; + +use librepology::v1::types::Package; +use librepology::v1::types::Problem; +use failure::Fallible as Result; +use prettytable::format; +use prettytable::Table; + +use crate::frontend::Frontend; + +pub struct TableFrontend(Stdout); + +impl TableFrontend { + pub fn new(stdout: Stdout) -> Self { + TableFrontend(stdout) + } +} + +impl Frontend for TableFrontend { + fn list_packages(&self, packages: Vec<Package>) -> Result<()> { + let mut table = Table::new(); + let format = format::FormatBuilder::new() + .column_separator('|') + .borders('|') + .separators( + &[format::LinePosition::Title, format::LinePosition::Top, format::LinePosition::Bottom], + format::LineSeparator::new('-', '+', '+', '+') + ) + .padding(1, 1) + .build(); + table.set_format(format); + + table.set_titles(row!["Name", "Version", "Repo", "Status", "URL"]); + + packages.iter().for_each(|package| { + let status = if let Some(stat) = package.status() { + format!("{}", stat) + } else { + String::from("No status") + }; // not optimal, but works for now. + + let url = if let Some(url) = package.www() { + if let Some(url) = url.first() { + format!("{}", url.deref()) + } else { + String::from("") + } + } else { + String::from("") + }; // not optimal, but works for now + + table.add_row(row![package.name(), package.version(), package.repo(), status, url]); + }); + + let mut outlock = self.0.lock(); + table.print(&mut outlock)?; + + Ok(()) + } + + fn list_problems(&self, problems: Vec<Problem>) -> Result<()> { + let mut table = Table::new(); + let format = format::FormatBuilder::new() + .column_separator('|') + .borders('|') + .separators( + &[format::LinePosition::Title, format::LinePosition::Top, format::LinePosition::Bottom], + format::LineSeparator::new('-', '+', '+', '+') + ) + .padding(1, 1) + .build(); + table.set_format(format); + + table.set_titles(row!["Repo", "Name", "EffName", "Maintainer", "Description"]); + + problems.iter().for_each(|problem| { + trace!("Adding row for: {:?}", problem); + table.add_row(row![ + problem.repo(), + problem.name(), + problem.effname(), + problem.maintainer(), + problem.problem_description() + ]); + }); + + let mut outlock = self.0.lock(); + table.print(&mut outlock)?; + + Ok(()) + } +} + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..dddbe42 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,170 @@ +extern crate serde; +extern crate serde_json; +extern crate toml; +extern crate toml_query; +extern crate url; +extern crate xdg; +extern crate flexi_logger; +extern crate reqwest; +extern crate tokio; +extern crate filters; + +#[macro_use] extern crate serde_derive; +#[macro_use] extern crate log; +#[macro_use] extern crate failure; +#[macro_use] extern crate prettytable; + +mod config; +mod backend; +mod frontend; +mod cli; + +use std::path::PathBuf; +use failure::err_msg; +use failure::Error; +use failure::Fallible as Result; +use clap::ArgMatches; +use filters::filter::Filter; + +use config::Configuration; +use librepology::v1::api::Api; +use librepology::v1::types::Repo; + +fn initialize_logging(app: &ArgMatches) -> Result<()> { + let verbosity = app.occurrences_of("verbose"); + let quietness = app.occurrences_of("quiet"); + let sum = verbosity as i64 - quietness as i64; + let mut level_filter = flexi_logger::LevelFilter::Info; + + if sum == 1 { + level_filter = flexi_logger::LevelFilter::Debug; + } else if sum >= 2 { + level_filter = flexi_logger::LevelFilter::Trace; + } else if sum == -1 { + level_filter = flexi_logger::LevelFilter::Warn; + } else if sum <= -2 { + level_filter = flexi_logger::LevelFilter::Error; + } + + let mut builder = flexi_logger::LogSpecBuilder::new(); + builder.default(level_filter); + + flexi_logger::Logger::with(builder.build()) + .start() + .map(|_| { + debug!("Logger initialized!"); + }) + .map_err(Error::from) +} + +fn main() -> Result<()> { + let app = cli::build_cli().get_matches(); + initialize_logging(&app)?; + let config : Configuration = { + let path = if let Some(path) = app + .value_of("config") + .map(PathBuf::from) + { + Ok(path) + } else { + xdg::BaseDirectories::new()? + .find_config_file("repolocli.toml") + .ok_or_else(|| err_msg("Cannot find repolocli.toml")) + }?; + + debug!("Parsing configuration from file: {}", path.display()); + + let buffer = std::fs::read_to_string(path).map_err(Error::from)?; + trace!("Config read into memory"); + toml::de::from_str(&buffer).map_err(Error::from) + }?; + trace!("Config deserialized"); + + let backend = crate::backend::new_backend(&app, &config)?; + let frontend = crate::frontend::new_frontend(&app, &config)?; + + let repository_filter = { + let blacklist_filter = |repo: &Repo| -> bool { + if config.blacklist().contains(repo) { + trace!("In Blacklist: {:?} -> false", repo); + return false; + } else { + trace!("Not in Blacklist: {:?} -> true", repo); + return true; + } + }; + + let whitelist_filter = |repo: &Repo| -> bool { + if config.whitelist().contains(repo) { + trace!("In Whitelist: {:?} -> true", repo); + return true; + } else { + trace!("Not in Whitelist: {:?} -> false", repo); + return false; + } + }; + + blacklist_filter.or(whitelist_filter) + }; + + match app.subcommand() { + ("project", Some(mtch)) => { + trace!("Handling project"); + + let name = if app.is_present("input_stdin") { + // Ugly, but works: + // If we have "--stdin" on CLI, we have a CLI/Stdin backend, which means that we can query + // _any_ "project", and get the stdin anyways. This is really not like it should be, but + // works for now + "" + } else { + mtch.value_of("project_name").unwrap() // safe by clap + }; + + let packages = backend + .project(&name)? + .into_iter() + .filter(|package| repository_filter.filter(package.repo())) + .collect(); + frontend.list_packages(packages)?; + }, + ("problems", Some(mtch)) => { + trace!("Handling problems"); + + let repo = mtch.value_of("repo"); + let maintainer = mtch.value_of("maintainer"); + + let problems = match (repo, maintainer) { + (Some(r), None) => backend.problems_for_repo(&r)?, + (None, Some(m)) => backend.problems_for_maintainer(&m)?, + (None, None) => unimplemented!(), + (Some(_), Some(_)) => unimplemented!(), + } + .into_iter() + .filter(|problem| repository_filter.filter(problem.repo())) + .collect(); + + frontend.list_problems(problems)?; + } + + (other, _mtch) => { + if app.is_present("input_stdin") { + // Ugly, but works: + // If we have "--stdin" on CLI, we have a CLI/Stdin backend, which means that we can query + // _any_ "project", and get the stdin anyways. This is really not like it should be, but + // works for now + let packages = backend + .project("")? + .into_iter() + .filter(|package| repository_filter.filter(package.repo())) + .collect(); + frontend.list_packages(packages)?; + } else { + error!("Unknown command: '{}'", other); + ::std::process::exit(1) + } + } + } + + Ok(()) +} |