diff options
author | Matthias Beyer <mail@beyermatthias.de> | 2021-04-06 12:04:17 +0200 |
---|---|---|
committer | Matthias Beyer <mail@beyermatthias.de> | 2021-04-06 12:04:17 +0200 |
commit | c8580bc601330d3d00bfe492a00464f5c403ccb5 (patch) | |
tree | 788b10e8473a70c9f1aab8a4022ccdd440802386 | |
parent | 32b24f4e03d0dc48db7f7d9927501b07b4821c33 (diff) |
Remove CLI
Signed-off-by: Matthias Beyer <mail@beyermatthias.de>
-rw-r--r-- | src/backend.rs | 56 | ||||
-rw-r--r-- | src/cli.rs | 150 | ||||
-rw-r--r-- | src/config.rs | 28 | ||||
-rw-r--r-- | src/filter.rs | 49 | ||||
-rw-r--r-- | src/frontend/json.rs | 41 | ||||
-rw-r--r-- | src/frontend/list.rs | 85 | ||||
-rw-r--r-- | src/frontend/mod.rs | 41 | ||||
-rw-r--r-- | src/frontend/table.rs | 94 | ||||
-rw-r--r-- | src/main.rs | 261 |
9 files changed, 1 insertions, 804 deletions
diff --git a/src/backend.rs b/src/backend.rs deleted file mode 100644 index e260e8c..0000000 --- a/src/backend.rs +++ /dev/null @@ -1,56 +0,0 @@ -use clap::ArgMatches; - -use librepology::v1::api::Api; -use librepology::v1::buffer::BufferApi; -use librepology::v1::error::Result; -use librepology::v1::restapi::RestApi; -use librepology::v1::types::*; - -use crate::config::Configuration; - -/// Helper type for cli implementation -/// for being transparent in what backend we use -pub enum Backend { - Buffer(BufferApi), - RepologyOrg(RestApi), -} - -/// Implement Api for Backend -/// -/// With this, we can use the `Backend` object and do not have to care whether we have a librepology:: -impl Api for Backend { - fn project<N: AsRef<str>>(&self, name: N) -> Result<Vec<Package>> { - match self { - Backend::Buffer(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::Buffer(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::Buffer(inner) => inner.problems_for_maintainer(maintainer), - Backend::RepologyOrg(inner) => inner.problems_for_maintainer(maintainer), - } - } -} - -pub fn new_backend(app: &ArgMatches, config: &Configuration) -> anyhow::Result<Backend> { - if app.is_present("input_stdin") { - trace!("Building new STDIN backend"); - BufferApi::read_from(std::io::stdin()) - .map(Backend::Buffer) - .map_err(anyhow::Error::from) - } else { - trace!("Building new remote 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 deleted file mode 100644 index 871f941..0000000 --- a/src/cli.rs +++ /dev/null @@ -1,150 +0,0 @@ -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") - ) - - .arg(Arg::with_name("sort-version") - .long("sort-version") - .required(false) - .multiple(false) - .takes_value(false) - .help("Sort output by version") - .conflicts_with("sort-repo") - ) - .arg(Arg::with_name("sort-repo") - .long("sort-repo") - .required(false) - .multiple(false) - .takes_value(false) - .help("Sort output by repository") - .conflicts_with("sort-version") - ) - .arg(Arg::with_name("latest") - .long("latest") - .required(false) - .multiple(false) - .takes_value(false) - .help("Try to find the lastest version (version is string-compared if not used with --semver)") - .conflicts_with("sort-version") - .conflicts_with("sort-repo") - ) - .arg(Arg::with_name("semver") - .long("semver") - .required(false) - .multiple(false) - .takes_value(false) - .requires("latest") - .help("Try to find latest version using semver. If semver could not be parsed, equality is assumed, which might yield bogus results.") - ) - ) - - .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)) - - - .arg(Arg::with_name("sort-maintainer") - .long("sort-maintainer") - .required(false) - .multiple(false) - .takes_value(false) - .help("Sort output by maintainer") - .conflicts_with("sort-repo") - ) - .arg(Arg::with_name("sort-repo") - .long("sort-repo") - .required(false) - .multiple(false) - .takes_value(false) - .help("Sort output by repository") - .conflicts_with("sort-maintainer") - ) - ) - - .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!). - "#) -} diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index 240b13f..0000000 --- a/src/config.rs +++ /dev/null @@ -1,28 +0,0 @@ -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>, -} - -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 - } -} diff --git a/src/filter.rs b/src/filter.rs deleted file mode 100644 index 334ccbd..0000000 --- a/src/filter.rs +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index 2a7f40a..0000000 --- a/src/frontend/json.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::io::Stdout; -use std::io::Write; - -use anyhow::Error; -use anyhow::Result; -use librepology::v1::types::Package; -use librepology::v1::types::Problem; - -use crate::frontend::Frontend; - -pub struct JsonFrontend(Stdout); - -/// A Frontend that serializes the data to JSON -/// -/// Useful for piping the data as structured data to another program. -/// -/// # Warning -/// -/// This frontend does _not_ maintain compatibility with repolocli itself. That means that piping -/// output from repolocli to repolocli is _NOT_ supported by this frontend. -/// -impl JsonFrontend { - pub fn new(stdout: Stdout) -> Self { - JsonFrontend(stdout) - } - - fn write(&self, output: String) -> Result<()> { - let mut outlock = self.0.lock(); - writeln!(outlock, "{}", output).map_err(Error::from) - } -} - -impl Frontend for JsonFrontend { - fn list_packages(&self, packages: Vec<Package>) -> Result<()> { - self.write(serde_json::ser::to_string_pretty(&packages).map_err(Error::from)?) - } - - fn list_problems(&self, problems: Vec<Problem>) -> Result<()> { - self.write(serde_json::ser::to_string_pretty(&problems).map_err(Error::from)?) - } -} diff --git a/src/frontend/list.rs b/src/frontend/list.rs deleted file mode 100644 index 9e06828..0000000 --- a/src/frontend/list.rs +++ /dev/null @@ -1,85 +0,0 @@ -use std::io::Stdout; -use std::io::Write; -use std::ops::Deref; - -use anyhow::Error; -use anyhow::Result; -use librepology::v1::types::Name; -use librepology::v1::types::Package; -use librepology::v1::types::Problem; - -use crate::frontend::Frontend; - -pub struct ListFrontend(Stdout); - -/// A Frontend that prints the data in a human-readable way but without ASCII-art. -/// -/// It seperates the values with dashes ("-") for a slightly better reading experience. -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() { - stat.deref().to_string() - } 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() { - url.deref().to_string() - } 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 - .any_name() - .map(Name::deref) - .map(String::deref) - .unwrap_or_else(|| "<unknown>"), - 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 deleted file mode 100644 index f1d5001..0000000 --- a/src/frontend/mod.rs +++ /dev/null @@ -1,41 +0,0 @@ -use anyhow::Result; -use clap::ArgMatches; - -use librepology::v1::types::*; - -use crate::config::Configuration; -use crate::frontend::json::JsonFrontend; -use crate::frontend::list::ListFrontend; -use crate::frontend::table::TableFrontend; - -/// A Frontend represents a way to show the data to the user -pub trait Frontend { - fn list_packages(&self, packages: Vec<Package>) -> Result<()>; - fn list_problems(&self, problems: Vec<Problem>) -> Result<()>; -} - -pub mod json; -pub mod list; -pub mod table; - -/// Helper function for building a new Frontend object based on the commandline parameters -pub fn new_frontend(app: &ArgMatches, _config: &Configuration) -> Result<Box<dyn 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 deleted file mode 100644 index 6a18af3..0000000 --- a/src/frontend/table.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::io::Stdout; -use std::ops::Deref; - -use anyhow::Result; -use librepology::v1::types::Name; -use librepology::v1::types::Package; -use librepology::v1::types::Problem; -use prettytable::format; -use prettytable::Table; - -use crate::frontend::Frontend; - -/// A Frontend that formats the output in a nice ASCII-art table -pub struct TableFrontend(Stdout); - -impl TableFrontend { - pub fn new(stdout: Stdout) -> Self { - TableFrontend(stdout) - } - - fn mktable(&self) -> Table { - 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"]); - table - } - - fn print(&self, table: Table) -> Result<()> { - let mut outlock = self.0.lock(); - table.print(&mut outlock)?; - Ok(()) - } -} - -impl Frontend for TableFrontend { - fn list_packages(&self, packages: Vec<Package>) -> Result<()> { - let mut table = self.mktable(); - 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 - - let name = package - .any_name() - .map(Name::deref) - .map(String::clone) - .unwrap_or_else(|| String::from("<unknown>")); - - table.add_row(row![name, package.version(), package.repo(), status, url]); - }); - self.print(table) - } - - fn list_problems(&self, problems: Vec<Problem>) -> Result<()> { - let mut table = self.mktable(); - 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() - ]); - }); - self.print(table) - } - -} diff --git a/src/main.rs b/src/main.rs index 838f4a2..f6320bc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,262 +1,3 @@ -extern crate boolinator; -extern crate filters; -extern crate flexi_logger; -extern crate itertools; -extern crate semver; -extern crate serde; -extern crate serde_json; -extern crate toml; -extern crate url; -extern crate xdg; - -#[macro_use] -extern crate serde_derive; -#[macro_use] -extern crate log; -#[macro_use] -extern crate anyhow; -#[macro_use] -extern crate prettytable; - -mod backend; -mod cli; -mod config; -mod frontend; - -use std::cmp::Ordering; -use std::path::PathBuf; - -use anyhow::Context; -use anyhow::Error; -use anyhow::Result; -use boolinator::Boolinator; -use clap::ArgMatches; -use filters::filter::Filter; -use itertools::Itertools; -use semver::Version as SemverVersion; - -use config::Configuration; -use librepology::v1::api::Api; -use librepology::v1::types::Package; -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 app() -> 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) { - debug!("Found passed configuration file at {}", path.display()); - Ok(path) - } else { - debug!("Searching for configuration in XDG"); - xdg::BaseDirectories::new()? - .find_config_file("repolocli.toml") - .ok_or_else(|| anyhow!("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) - .context("Configuration file parsing") - }?; - trace!("Config deserialized"); - - debug!("Initializing Backend"); - let backend = crate::backend::new_backend(&app, &config)?; - debug!("Backend initialized"); - - debug!("Initializing Frontend"); - let frontend = crate::frontend::new_frontend(&app, &config)?; - debug!("Frontend initialized"); - - let repository_filter = { - let blacklist_filter = |repo: &Repo| -> bool { - if config.blacklist().contains(repo) { - trace!("In Blacklist: {:?} -> false", repo); - false - } else { - trace!("Not in Blacklist: {:?} -> true", repo); - true - } - }; - - let whitelist_filter = |repo: &Repo| -> bool { - if config.whitelist().contains(repo) { - trace!("In Whitelist: {:?} -> true", repo); - true - } else { - trace!("Not in Whitelist: {:?} -> false", repo); - false - } - }; - - blacklist_filter.or(whitelist_filter) - }; - debug!("Repository filter constructed successfully"); - - match app.subcommand() { - ("project", Some(mtch)) => { - debug!("Subcommand: 'project'"); - trace!("sort-versions: {}", mtch.is_present("sort-version")); - trace!("sort-repository: {}", mtch.is_present("sort-repo")); - - 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 mut packages: Vec<Package> = { - debug!("Fetching packages"); - let iter = backend - .project(&name)? - .into_iter() - .filter(|package| repository_filter.filter(package.repo())); - - if mtch.is_present("sort-version") { - trace!("Sorting by version"); - iter.sorted_by(|a, b| Ord::cmp(a.version(), b.version())) - .collect() - } else if mtch.is_present("sort-repo") { - trace!("Sorting by repository"); - iter.sorted_by(|a, b| Ord::cmp(a.repo(), b.repo())) - .collect() - } else { - trace!("Not sorting"); - iter.collect() - } - }; - - let packages = if mtch.is_present("latest") { - if mtch.is_present("semver") { - let comp = |a: &Package, b: &Package| { - let av = SemverVersion::parse(a.version()); - let bv = SemverVersion::parse(b.version()); - - if let (Ok(av), Ok(bv)) = (av, bv) { - av.partial_cmp(&bv).unwrap_or(Ordering::Equal) - } else { - Ordering::Equal - } - }; - - packages.sort_by(comp); - } else { - packages.sort_by(|a, b| { - a.version() - .partial_cmp(b.version()) - .unwrap_or(Ordering::Equal) - }); - } - packages.pop().into_iter().collect::<Vec<_>>() - } else { - packages - }; - - debug!("Listing packages in frontend"); - frontend.list_packages(packages) - } - - ("problems", Some(mtch)) => { - debug!("Subcommand: 'problems'"); - - let repo = mtch.value_of("repo"); - let maintainer = mtch.value_of("maintainer"); - - trace!("repo = {:?}", repo); - trace!("maintainer = {:?}", maintainer); - - let problems = { - debug!("Finding problems..."); - let iter = 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())); - - if mtch.is_present("sort-maintainer") { - trace!("Sorting problems by maintainer"); - iter.sorted_by(|a, b| Ord::cmp(a.maintainer(), b.maintainer())) - .collect() - } else if mtch.is_present("sort-repo") { - trace!("Sorting problems by repo"); - iter.sorted_by(|a, b| Ord::cmp(a.repo(), b.repo())) - .collect() - } else { - trace!("Not sorting problems"); - iter.collect() - } - }; - - debug!("Listing problems in frontend"); - frontend.list_problems(problems) - } - - (other, _mtch) => { - debug!("Subcommand: {}", other); - app.is_present("input_stdin") - .as_result((), format_err!("Input not from stdin")) - .and_then(|_| { - // 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(); - - debug!("Listing packages"); - frontend.list_packages(packages) - }) - .map_err(|_| format_err!("Unknown command: {}", other)) - } - } -} - -fn print_error(e: Error) { - error!("Error: {}", e); - e.chain().for_each(|cause| error!("Caused by: {}", cause)); -} - fn main() { - let _ = app().map_err(print_error); + println!("Hello World"); } |