summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorMatthias Beyer <mail@beyermatthias.de>2019-04-19 17:54:52 +0200
committerMatthias Beyer <mail@beyermatthias.de>2019-04-19 17:58:33 +0200
commit7cc25e9cf2dda28f1f8dea43caac7bd6291ed1f1 (patch)
tree9fa1b948ac6ff7179fb311b54e60f1e41a97087c /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.rs50
-rw-r--r--src/cli.rs99
-rw-r--r--src/config.rs60
-rw-r--r--src/filter.rs49
-rw-r--r--src/frontend/json.rs73
-rw-r--r--src/frontend/list.rs71
-rw-r--r--src/frontend/mod.rs40
-rw-r--r--src/frontend/table.rs94
-rw-r--r--src/main.rs170
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(())
+}