aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/check.yaml35
-rw-r--r--.github/workflows/release.yaml50
-rw-r--r--.gitignore7
-rw-r--r--.license_template17
-rw-r--r--Cargo.toml46
-rw-r--r--Makefile.toml38
-rw-r--r--README.rst93
-rw-r--r--build.rs19
-rw-r--r--rust-toolchain.toml2
-rw-r--r--rustfmt.toml30
-rw-r--r--src/export.rs96
-rw-r--r--src/main.rs303
12 files changed, 736 insertions, 0 deletions
diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml
new file mode 100644
index 0000000..47f63a3
--- /dev/null
+++ b/.github/workflows/check.yaml
@@ -0,0 +1,35 @@
+name: Check ✅
+
+on:
+ workflow_dispatch:
+ push:
+ paths:
+ - "*"
+ pull_request:
+ paths:
+ - "*"
+
+env:
+ CARGO_TERM_COLOR: always
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout 🛒
+ uses: actions/checkout@v3
+
+ - name: Toolchain 🧰
+ uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: nightly-2022-02-22
+ components: rustfmt, clippy
+ override: true
+
+ - name: Check ✅
+ uses: actions-rs/cargo@v1
+ continue-on-error: false
+ with:
+ command: check
+ args: --verbose
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
new file mode 100644
index 0000000..43df957
--- /dev/null
+++ b/.github/workflows/release.yaml
@@ -0,0 +1,50 @@
+name: Release ⚾
+
+on:
+ workflow_dispatch:
+ push:
+ tags:
+ - '*'
+
+jobs:
+ release:
+ runs-on: ubuntu-20.04
+ steps:
+ - name: Checkout 🛒
+ uses: actions/checkout@v2
+
+ - name: Toolchain 🧰
+ uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: nightly-2022-02-22
+ components: rustfmt, clippy
+ override: true
+
+ - name: Build 🏗
+ uses: actions-rs/cargo@v1
+ continue-on-error: false
+ with:
+ command: build
+ args: --release
+
+ - name: Create Release 🏉
+ id: create_release
+ uses: actions/create-release@v1
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ with:
+ tag_name: ${{ github.ref }}
+ release_name: ${{ github.ref }}
+ draft: false
+ prerelease: false
+
+ - name: Upload Artifacts to Release 💎
+ uses: actions/upload-release-asset@v1
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ with:
+ upload_url: ${{ steps.create_release.outputs.upload_url }}
+ asset_path: ./target/release/senpy
+ asset_name: senpy
+ asset_content_type: application/x-elf # x-msdownload for Windows
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..59a8c42
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+# Rust
+target/
+Cargo.lock
+**/*.rs.bk
+
+# IDE
+/.idea/
diff --git a/.license_template b/.license_template
new file mode 100644
index 0000000..f2af2b7
--- /dev/null
+++ b/.license_template
@@ -0,0 +1,17 @@
+// This file is part of senpy-cli <https://github.com/senpy-club/cli>.
+// Copyright (C) {20\d{2}(-20\d{2})?} Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) {20\d{2}(-20\d{2})?} Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only \ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..705d216
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,46 @@
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[package]
+name = "senpy-cli"
+version = "0.1.0"
+authors = ["Fuwn <[email protected]>"]
+edition = "2021"
+description = "Command-line interface to The Senpy Club API"
+documentation = "https://docs.rs/senpy-cli/"
+readme = "README.rst"
+homepage = "https://github.com/senpy-club/cli"
+repository = "https://github.com/senpy-club/cli"
+license = "GPL-3.0-only"
+keywords = ["rust", "anime", "senpy"]
+categories = ["web-programming", "web-programming::http-server", "command-line-interface", "command-line-utilities"]
+
+[[bin]]
+name = "senpy"
+path = "src/main.rs"
+
+# Slower builds, faster executables
+[profile.release]
+lto = "fat"
+codegen-units = 1
+# Optimize for size
+opt-level = "s"
+
+[build-dependencies]
+# Build variables
+vergen = "7.0.0"
+
+# `Result`
+anyhow = "1.0.56"
+
+[dependencies]
+# CLI
+clap = "3.1.6"
+
+# The Senpy Club API
+senpy = "0.1.2"
+
+# Serialization
+serde = "1.0.136"
+serde_json = "1.0.79"
+serde_yaml = "0.8.23"
+serde_dhall = "0.11.0"
diff --git a/Makefile.toml b/Makefile.toml
new file mode 100644
index 0000000..f459978
--- /dev/null
+++ b/Makefile.toml
@@ -0,0 +1,38 @@
+# ------------
+# | Wrappers |
+# ------------
+[tasks.fmt]
+command = "cargo"
+args = ["fmt"]
+private = true
+
+[tasks.check]
+command = "cargo"
+args = ["check"]
+private = true
+
+[tasks.clippy]
+command = "cargo"
+args = ["clippy"]
+private = true
+
+# -------------
+# | Executors |
+# -------------
+[tasks.checkf]
+dependencies = ["fmt", "check"]
+
+[tasks.checkfc]
+dependencies = ["fmt", "check", "clippy"]
+
+[tasks.docs]
+workspace = false
+toolchain = "nightly"
+command = "cargo"
+args = ["doc", "--open", "--document-private-items", "--no-deps"]
+
+[tasks.run]
+workspace = false
+dependencies = ["checkfc"]
+command = "cargo"
+args = ["run", "--bin", "senpy", "--", "${@}"]
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..446c710
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,93 @@
+:code:`senpy-cli`
+=========
+
+The Senpy Club CLI is a tool that provides quick access to The Senpy Club API
+from the command-line!
+
+Get data **fast** from The Senpy Club API either as stdout or to a supported
+file format.
+
+Quick links
+^^^^^^^^^^^
+
+.. raw:: html
+
+ <p>
+ <a href="https://discord.com/invite/yWKgRT6">
+ <img src="https://img.shields.io/discord/246524734718738442"
+ alt="Discord" />
+ </a>
+ <a href="https://saythanks.io/to/[email protected]">
+ <img src="https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg"
+ alt="Say Thanks" />
+ </a>
+ <a href="LICENSE">
+ <img src="https://img.shields.io/github/license/senpy-club/cli"
+ alt="License" />
+ </a>
+ <a href="https://crates.io/crates/senpy-cli">
+ <img src="https://img.shields.io/crates/v/senpy-cli.svg"
+ alt="Crate" />
+ </a>
+ <a href="https://docs.rs/senpy-cli">
+ <img src="https://docs.rs/senpy-cli/badge.svg"
+ alt="Documentation" />
+ </a>
+ <a href="https://github.com/senpy-club/lci/actions/workflows/check.yaml">
+ <img src="https://github.com/senpy-club/cli/actions/workflows/check.yaml/badge.svg?branch=main"
+ alt="Build Status" />
+ </a>
+ </p>
+
+Installation
+^^^^^^^^^^^^
+
+Install from crates.io
+----------------------
+
+.. code-block:: shell
+
+ $ cargo install senpy-cli --force
+
+Download from releases
+----------------------
+
+Alternatively, prebuilt binaries for x86_64-based Linux systems are available in
+the `releases <https://github.com/senpy-club/cli/releases/latest>`_. If you are
+using a different operating system or architecture such as macOS or Windows;
+you'll have to build and install The Senpy Club CLI yourself!
+
+Install from self-compile
+-------------------------
+
+.. code-block:: shell
+
+ $ cargo install --git https://github.com/senpy-club/cli --branch main
+
+If you are building and installing yourself; you must have
+`Rust <https://www.rust-lang.org/>`_ installed!
+
+Usage
+^^^^^
+
+The Senpy Club CLI allows you to export to stdout in the form of
+ten-space-seperated columns or to a file in the JSON, YAML, or Dhall formats.
+
+Examples
+--------
+
+.. code-block:: shell
+
+ $ senpy languages # Prints all available languages in a single column
+ $ senpy languages -t # Prints all available languages in one column and the fetch time in another
+ $ senpy languages -f languages.json # Exports all available languages to a JSON file
+ $ senpy languages -f languages.yaml # Exports all available languages to a YAML file
+ $ senpy languages -f languages.dhall # Exports all available languages to a Dhall file
+ $ senpy random | awk '{ print $1 }' # Prints the first column (language) from stdout on *nix-based environments
+
+Feel free to explore the rest of the available commands with :code:`senpy help`!
+
+License
+^^^^^^^
+
+`GNU General Public License v3.0 <https://github.com/senpy-club/api-worker/blob/main/LICENSE>`_
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000..5512a66
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,19 @@
+// This file is part of senpy-cli <https://github.com/senpy-club/cli>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+fn main() -> anyhow::Result<()> { vergen::vergen(vergen::Config::default()) }
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
new file mode 100644
index 0000000..22a9829
--- /dev/null
+++ b/rust-toolchain.toml
@@ -0,0 +1,2 @@
+[toolchain]
+channel = "nightly-2022-02-22"
diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644
index 0000000..1dd4c90
--- /dev/null
+++ b/rustfmt.toml
@@ -0,0 +1,30 @@
+condense_wildcard_suffixes = true
+edition = "2021"
+enum_discrim_align_threshold = 20
+# error_on_line_overflow = true
+# error_on_unformatted = true
+fn_single_line = true
+force_multiline_blocks = true
+format_code_in_doc_comments = true
+format_macro_matchers = true
+format_strings = true
+imports_layout = "HorizontalVertical"
+license_template_path = ".license_template"
+max_width = 80
+match_arm_blocks = false
+imports_granularity = "Crate"
+newline_style = "Unix"
+normalize_comments = true
+normalize_doc_attributes = true
+reorder_impl_items = true
+group_imports = "StdExternalCrate"
+reorder_modules = true
+report_fixme = "Always"
+# report_todo = "Always"
+struct_field_align_threshold = 20
+struct_lit_single_line = false
+tab_spaces = 2
+use_field_init_shorthand = true
+use_try_shorthand = true
+where_single_line = true
+wrap_comments = true
diff --git a/src/export.rs b/src/export.rs
new file mode 100644
index 0000000..6d95b65
--- /dev/null
+++ b/src/export.rs
@@ -0,0 +1,96 @@
+// This file is part of senpy-cli <https://github.com/senpy-club/cli>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+use std::{
+ fs::File,
+ io::{BufWriter, Write},
+};
+
+use senpy::Random;
+
+pub trait Exporter {
+ fn random_to_file(_: &str, _: Random);
+ fn vec_to_file(_: &str, _: Vec<String>);
+}
+
+pub struct JsonExporter;
+pub struct YamlExporter;
+pub struct DhallExporter;
+
+impl Exporter for JsonExporter {
+ fn random_to_file(filename: &str, random: Random) {
+ let writer = BufWriter::new(File::create(filename).unwrap());
+
+ serde_json::to_writer_pretty(
+ writer,
+ &serde_json::json!({
+ "language": random.language,
+ "image": random.image,
+ }),
+ )
+ .unwrap();
+ }
+
+ fn vec_to_file(filename: &str, languages: Vec<String>) {
+ let writer = BufWriter::new(File::create(filename).unwrap());
+
+ serde_json::to_writer_pretty(writer, &languages).unwrap();
+ }
+}
+
+impl Exporter for YamlExporter {
+ fn random_to_file(filename: &str, random: Random) {
+ let writer = BufWriter::new(File::create(filename).unwrap());
+
+ serde_yaml::to_writer(writer, &random).unwrap()
+ }
+
+ fn vec_to_file(filename: &str, languages: Vec<String>) {
+ let writer = BufWriter::new(File::create(filename).unwrap());
+
+ serde_yaml::to_writer(writer, &languages).unwrap();
+ }
+}
+
+impl Exporter for DhallExporter {
+ fn random_to_file(filename: &str, random: Random) {
+ let mut writer = BufWriter::new(File::create(filename).unwrap());
+
+ writer
+ .write_all(
+ serde_dhall::serialize(&random)
+ .to_string()
+ .unwrap()
+ .as_bytes(),
+ )
+ .unwrap();
+ }
+
+ fn vec_to_file(filename: &str, languages: Vec<String>) {
+ let mut writer = BufWriter::new(File::create(filename).unwrap());
+
+ writer
+ .write_all(
+ serde_dhall::serialize(&languages)
+ .to_string()
+ .unwrap()
+ .as_bytes(),
+ )
+ .unwrap();
+ }
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..96513f6
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,303 @@
+// This file is part of senpy-cli <https://github.com/senpy-club/cli>.
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// Copyright (C) 2022-2022 Fuwn <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-only
+
+mod export;
+
+use clap::{Arg, Command};
+
+use crate::export::Exporter;
+
+fn main() {
+ let matches = clap::Command::new(env!("CARGO_PKG_NAME"))
+ .version(&*format!(
+ "{}(1)-{}-({})-{}",
+ env!("CARGO_PKG_VERSION"),
+ env!("VERGEN_CARGO_PROFILE"),
+ env!("VERGEN_CARGO_TARGET_TRIPLE"),
+ env!("VERGEN_GIT_SHA").get(0..7).unwrap(),
+ ))
+ .author(env!("CARGO_PKG_AUTHORS"))
+ .subcommand_required(true)
+ .subcommands(vec![
+ Command::new("languages")
+ .alias("langs")
+ .about("Prints all available languages")
+ .arg(Arg::new("time").long("time").short('t'))
+ .arg(Arg::new("file").long("file").short('f').takes_value(true)),
+ Command::new("language")
+ .alias("lang")
+ .arg_required_else_help(true)
+ .arg(Arg::new("language").required(true))
+ .about("Prints all available images of a given language")
+ .arg(Arg::new("time").long("time").short('t'))
+ .arg(Arg::new("file").long("file").short('f').takes_value(true)),
+ Command::new("random")
+ .alias("rand")
+ .about("Prints a random language and image pair")
+ .arg(Arg::new("time").long("time").short('t'))
+ .arg(Arg::new("file").long("file").short('f').takes_value(true)),
+ ])
+ .get_matches();
+
+ match matches.subcommand() {
+ Some(("languages", matches_languages)) => {
+ let time = std::time::Instant::now();
+ let languages = senpy::languages();
+ let time_taken = time.elapsed();
+
+ match languages {
+ Ok(languages) => {
+ let time_export = std::time::Instant::now();
+ let mut exported = false;
+
+ if matches_languages.is_present("file") {
+ if matches_languages.value_of("file").unwrap().contains("json") {
+ crate::export::JsonExporter::vec_to_file(
+ matches_languages.value_of("file").unwrap(),
+ languages,
+ );
+
+ exported = true;
+ } else if matches_languages
+ .value_of("file")
+ .unwrap()
+ .contains("yaml")
+ || matches_languages.value_of("file").unwrap().contains("yml")
+ {
+ crate::export::YamlExporter::vec_to_file(
+ matches_languages.value_of("file").unwrap(),
+ languages,
+ );
+
+ exported = true;
+ } else if matches_languages
+ .value_of("file")
+ .unwrap()
+ .contains("dhall")
+ {
+ crate::export::DhallExporter::vec_to_file(
+ matches_languages.value_of("file").unwrap(),
+ languages,
+ );
+
+ exported = true;
+ }
+
+ if exported {
+ println!(
+ "time taken to export to file: {} ns",
+ time_export.elapsed().as_nanos(),
+ );
+ }
+ } else {
+ // https://stackoverflow.com/a/30380640/14452787
+ println!(
+ "{0: <10} {1: <10}",
+ "Languages",
+ if matches_languages.is_present("time") {
+ "Time"
+ } else {
+ ""
+ }
+ );
+
+ for (index, language) in languages.into_iter().enumerate() {
+ println!(
+ "{0: <10} {1: <10}",
+ language,
+ if matches_languages.is_present("time") && index == 0 {
+ time_taken.as_millis().to_string()
+ } else {
+ "".to_string()
+ }
+ );
+ }
+ }
+ }
+ Err(e) => println!("{0: <10} {1: <10}", "Error", e),
+ }
+ }
+ Some(("language", matches_language)) => {
+ let time = std::time::Instant::now();
+ let languages = senpy::languages();
+ let time_taken = time.elapsed();
+
+ match languages {
+ Ok(languages) => {
+ let language = &languages
+ .into_iter()
+ .filter(|l| {
+ l.to_lowercase()
+ == matches_language
+ .value_of("language")
+ .unwrap()
+ .to_lowercase()
+ })
+ .collect::<Vec<String>>()[0];
+ let time_2 = std::time::Instant::now();
+ let images = senpy::language(language.to_string().as_str());
+ let time_taken_2 = time_2.elapsed();
+
+ match images {
+ Ok(images) => {
+ let time_export = std::time::Instant::now();
+ let mut exported = false;
+
+ if matches_language.is_present("file") {
+ if matches_language.value_of("file").unwrap().contains("json") {
+ crate::export::JsonExporter::vec_to_file(
+ matches_language.value_of("file").unwrap(),
+ images,
+ );
+
+ exported = true;
+ } else if matches_language
+ .value_of("file")
+ .unwrap()
+ .contains("yaml")
+ || matches_language.value_of("file").unwrap().contains("yml")
+ {
+ crate::export::YamlExporter::vec_to_file(
+ matches_language.value_of("file").unwrap(),
+ images,
+ );
+
+ exported = true;
+ } else if matches_language
+ .value_of("file")
+ .unwrap()
+ .contains("dhall")
+ {
+ crate::export::DhallExporter::vec_to_file(
+ matches_language.value_of("file").unwrap(),
+ images,
+ );
+
+ exported = true;
+ }
+
+ if exported {
+ println!(
+ "time taken to export to file: {} ns",
+ time_export.elapsed().as_nanos(),
+ );
+ }
+ } else {
+ // https://stackoverflow.com/a/30380640/14452787
+ println!(
+ "{0: <10} {1: <10}",
+ "Images",
+ if matches_language.is_present("time") {
+ "Time"
+ } else {
+ ""
+ }
+ );
+
+ for (index, image) in images.into_iter().enumerate() {
+ println!(
+ "{0: <10} {1: <10}",
+ image,
+ if matches_language.is_present("time") && index == 0 {
+ (time_taken + time_taken_2).as_millis().to_string()
+ } else {
+ "".to_string()
+ }
+ );
+ }
+ }
+ }
+ Err(e) => println!("{0: <10} {1: <10}", "Error", e),
+ }
+ }
+ Err(e) => println!("{0: <10} {1: <10}", "Error", e),
+ }
+ }
+ Some(("random", matches_random)) => {
+ let time = std::time::Instant::now();
+ let random = senpy::random();
+ let time_taken = time.elapsed();
+
+ match random {
+ Ok(image) => {
+ let time_export = std::time::Instant::now();
+ let mut exported = false;
+
+ if matches_random.is_present("file") {
+ if matches_random.value_of("file").unwrap().contains("json") {
+ crate::export::JsonExporter::random_to_file(
+ matches_random.value_of("file").unwrap(),
+ image,
+ );
+
+ exported = true;
+ } else if matches_random.value_of("file").unwrap().contains("yaml")
+ || matches_random.value_of("file").unwrap().contains("yml")
+ {
+ crate::export::YamlExporter::random_to_file(
+ matches_random.value_of("file").unwrap(),
+ image,
+ );
+
+ exported = true;
+ } else if matches_random.value_of("file").unwrap().contains("dhall")
+ {
+ crate::export::DhallExporter::random_to_file(
+ matches_random.value_of("file").unwrap(),
+ image,
+ );
+
+ exported = true;
+ }
+
+ if exported {
+ println!(
+ "time taken to export to file: {} ns",
+ time_export.elapsed().as_nanos(),
+ );
+ }
+ } else {
+ // https://stackoverflow.com/a/30380640/14452787
+ println!(
+ "{0: <10} {1: <10} {2: <10}",
+ "Language",
+ "Image",
+ if matches_random.is_present("time") {
+ "Time"
+ } else {
+ ""
+ }
+ );
+ println!(
+ "{0: <10} {1: <10} {2: <10}",
+ image.language,
+ image.image,
+ if matches_random.is_present("time") {
+ time_taken.as_millis().to_string()
+ } else {
+ "".to_string()
+ }
+ );
+ }
+ }
+ Err(e) => println!("{0: <10} {1: <10}", "Error", e),
+ }
+ }
+ _ => unreachable!(),
+ }
+}