Skip to content

Commit

Permalink
refactor(*): stricter linting
Browse files Browse the repository at this point in the history
Also consume all past events in `Terminal::read_input`
  • Loading branch information
Lioness100 committed Aug 16, 2022
1 parent de869d7 commit d2982b4
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 112 deletions.
9 changes: 1 addition & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
[package]
name = "guess-that-lang"
version = "1.0.16"
version = "1.0.17"
rust-version = "1.63"
authors = ["Lioness100 <[email protected]>"]
edition = "2021"
description = "CLI game to see how fast you can guess the language of a code block!"
repository = "https://github.com/Lioness100/guess-that-lang"
license = "Apache-2.0"
keywords = ["cli", "game", "cli-game", "fun"]
categories = ["command-line-utilities", "games"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
ansi_colours = "1.1.1"
ansi_term = "0.12.1"
anyhow = "1.0.59"
argh = "0.1.8"
confy = "0.4.0"
crossterm = "0.25.0"
Expand Down
52 changes: 25 additions & 27 deletions src/game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use rand::{seq::SliceRandom, thread_rng};
use crate::{
github::{GistData, Github},
terminal::Terminal,
Config, CONFIG,
Config, Result, CONFIG,
};

/// The prompt to be shown before the options in [`Terminal::print_round_info`].
Expand Down Expand Up @@ -99,7 +99,7 @@ impl Drop for Game {

impl Game {
/// Create new game.
pub fn new(client: Github) -> anyhow::Result<Self> {
pub fn new(client: Github) -> Result<Self> {
let terminal = Terminal::new()?;

Ok(Self {
Expand All @@ -113,34 +113,32 @@ impl Game {
/// Get the language options for a round. This will choose 3 random unique
/// languages, push them to a vec along with the correct language, and
/// shuffle the vec.
pub fn get_options(correct_language: &str) -> Vec<&str> {
#[must_use]
pub fn get_options(correct_language: &str) -> Option<Vec<&str>> {
let mut options = Vec::<&str>::with_capacity(4);
options.push(correct_language);

let mut thread_rng = thread_rng();
while options.len() < 4 {
let random_language = LANGUAGES.choose(&mut thread_rng).unwrap();
let random_language = LANGUAGES.choose(&mut thread_rng)?;
if !options.contains(random_language) {
options.push(random_language);
}
}

options.shuffle(&mut thread_rng);
options
Some(options)
}

/// Start a new round, which is called in the main function with a for loop.
pub fn start_new_round(
&mut self,
preloader: Option<Receiver<()>>,
) -> anyhow::Result<ControlFlow<()>> {
pub fn start_new_round(&mut self, preloader: Option<Receiver<()>>) -> Result<ControlFlow<()>> {
if self.gist_data.is_empty() {
self.gist_data = self.client.get_gists(&self.terminal.syntaxes)?;
}

let gist = self.gist_data.pop().unwrap();
let gist = self.gist_data.pop().ok_or("empty gist vec")?;
let text = self.client.get_gist(&gist.url)?;
let width = Terminal::width();
let width = Terminal::width()?;

let highlighter = self.terminal.get_highlighter(&gist.extension);
let code = match self.terminal.parse_code(&text, highlighter, &width) {
Expand All @@ -149,14 +147,14 @@ impl Game {
None => return self.start_new_round(preloader),
};

let options = Self::get_options(&gist.language);
let options = Self::get_options(&gist.language).ok_or("failed to get options")?;

if let Some(preloader) = preloader {
preloader.recv().unwrap();
let _ = preloader.recv();
}

self.terminal
.print_round_info(&options, &code, &width, self.points);
.print_round_info(&options, &code, &width, self.points)?;

let available_points = Mutex::new(100.0);

Expand All @@ -167,47 +165,47 @@ impl Game {
thread::scope(|s| {
let display = s.spawn(|| {
self.terminal
.start_showing_code(code, &available_points, receiver);
.start_showing_code(code, &available_points, receiver)
});

let input = s.spawn(|| {
let input = Terminal::read_input_char();
let input = Terminal::read_input_char()?;

// Notifies [`Terminal::start_showing_code`] to not show the
// next line.
let sender = sender;
let _sent = sender.send(());
let _ = sender.send(());

if input == 'q' || input == 'c' {
Ok(ControlFlow::Break(()))
} else {
let result = self.terminal.process_input(
input.to_digit(10).unwrap(),
input.to_digit(10).ok_or("invalid input")?,
&options,
&gist.language,
&available_points,
&mut self.points,
)?;
);

// Let the user visually process the result. If they got it
// correct, the timer is set after a thread is spawned to
// preload the next round's gist.
if result == ControlFlow::Break(()) {
if let Ok(ControlFlow::Break(())) = result {
thread::sleep(Duration::from_millis(1500));
}

Ok(result)
result
}
});

display.join().unwrap();
display.join().unwrap()?;
input.join().unwrap()
})
}

// Wait 1.5 seconds for the user to visually process they got the right
// answer while the next round is preloading, then start the next round.
pub fn start_next_round(&mut self) -> anyhow::Result<ControlFlow<()>> {
/// Wait 1.5 seconds for the user to visually process they got the right
/// answer while the next round is preloading, then start the next round.
pub fn start_next_round(&mut self) -> Result<ControlFlow<()>> {
let (sender, receiver) = mpsc::channel();

thread::scope(|s| {
Expand All @@ -218,8 +216,8 @@ impl Game {
// Clear the screen and move to the top right corner. This is not
// a method of [`Terminal`] because it would take a lot of work to
// let the borrow checker let me use `self` again.
execute!(stdout().lock(), Clear(ClearType::All), MoveTo(0, 0)).unwrap();
sender.send(()).unwrap();
let _clear = execute!(stdout().lock(), Clear(ClearType::All), MoveTo(0, 0));
let _ = sender.send(());

handle.join().unwrap()
})
Expand Down
43 changes: 21 additions & 22 deletions src/github.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
use std::{collections::BTreeMap, path::Path};

use anyhow::{bail, Context};
use rand::{seq::SliceRandom, thread_rng, Rng};
use regex::RegexBuilder;
use serde::Deserialize;
use syntect::parsing::SyntaxSet;
use ureq::{Agent, AgentBuilder, Response};

use crate::{game::LANGUAGES, Config, ARGS, CONFIG};
use crate::{game::LANGUAGES, Config, Result, ARGS, CONFIG};

pub const GITHUB_BASE_URL: &str = "https://api.github.com";

Expand All @@ -33,9 +32,9 @@ pub struct GistData {
}

impl GistData {
/// Create a new GistData struct from a [`Gist`]. This will return [`None`]
/// Create a new [`GistData`] struct from a [`Gist`]. This will return [`None`]
/// if none of the gist files use one of the supported languages.
pub fn from(gist: Gist, syntaxes: &SyntaxSet) -> Option<GistData> {
pub fn from(gist: Gist, syntaxes: &SyntaxSet) -> Option<Self> {
let file = gist.files.into_values().find(|file| {
matches!(file.language.as_ref(), Some(language) if LANGUAGES.contains(&language.as_str()))
})?;
Expand All @@ -46,7 +45,7 @@ impl GistData {
Some(Self {
url: file.raw_url.to_string(),
extension: extension.to_string(),
language: file.language.unwrap(),
language: file.language?,
})
}
}
Expand All @@ -70,18 +69,20 @@ impl Default for Github {
}

impl Github {
pub fn new() -> anyhow::Result<Self> {
pub fn new() -> Result<Self> {
let mut github = Self::default();
github.token = github.apply_token()?;

Ok(github)
}

pub fn apply_token(&mut self) -> anyhow::Result<Option<String>> {
/// If a token is found from arguments or the config: validate it and return
/// it. If it wasn't found from the config, store it in the config.
pub fn apply_token(&mut self) -> Result<Option<String>> {
if let Some(token) = &ARGS.token {
Github::test_token_structure(token)?;
Self::test_token_structure(token)?;
self.validate_token(token)
.context("Invalid personal access token")?;
.expect("Invalid personal access token");

confy::store(
"guess-that-lang",
Expand All @@ -92,7 +93,9 @@ impl Github {
)?;

return Ok(Some(token.to_string()));
} else if !CONFIG.token.is_empty() {
}

if !CONFIG.token.is_empty() {
let result = self.validate_token(&CONFIG.token);
if result.is_err() {
confy::store(
Expand All @@ -103,7 +106,7 @@ impl Github {
},
)?;

result.context("The token found in the config is invalid, so it has been removed. Please try again.")?;
panic!("The token found in the config is invalid, so it has been removed. Please try again.")
} else {
return Ok(Some(CONFIG.token.clone()));
}
Expand All @@ -115,23 +118,19 @@ impl Github {
/// Test a Github personal access token via regex and return it if valid. The
/// second step of validation is [`validate_token`] which requires querying the
/// Github API asynchronously and thus can not be used with [`clap::value_parser`].
pub fn test_token_structure(token: &str) -> anyhow::Result<String> {
pub fn test_token_structure(token: &str) -> Result<String> {
let re = RegexBuilder::new(r"[\da-f]{40}|ghp_\w{36,251}")
// This is an expensive regex, so the size limit needs to be increased.
.size_limit(1 << 25)
.build()
.unwrap();
.build()?;

if re.is_match(token) {
Ok(token.to_string())
} else {
bail!("Invalid token format")
}
assert!(re.is_match(token), "Invalid token format");
Ok(token.to_string())
}

/// Queries the Github ratelimit API using the provided token to make sure it's
/// valid. The ratelimit data itself isn't used.
pub fn validate_token(&self, token: &str) -> anyhow::Result<Response> {
pub fn validate_token(&self, token: &str) -> Result<Response> {
self.agent
.get(&format!("{GITHUB_BASE_URL}/rate_limit"))
.set("Authorization", &format!("Bearer {token}"))
Expand All @@ -141,7 +140,7 @@ impl Github {

/// Get a vec of random valid gists on Github. This is used with the assumption
/// that at least one valid gist will be found.
pub fn get_gists(&self, syntaxes: &SyntaxSet) -> anyhow::Result<Vec<GistData>> {
pub fn get_gists(&self, syntaxes: &SyntaxSet) -> Result<Vec<GistData>> {
let mut request = ureq::get(&format!("{GITHUB_BASE_URL}/gists/public"))
.query("page", &thread_rng().gen_range(0..=100).to_string());

Expand All @@ -162,7 +161,7 @@ impl Github {
}

/// Get single gist content.
pub fn get_gist(&self, url: &str) -> anyhow::Result<String> {
pub fn get_gist(&self, url: &str) -> Result<String> {
let mut request = ureq::get(url);

if let Some(token) = &self.token {
Expand Down
24 changes: 6 additions & 18 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,13 @@
#![forbid(unsafe_code)]
#![warn(clippy::pedantic)]
#![warn(clippy::pedantic, clippy::cargo)]
#![allow(
// Allowed to avoid breaking changes.
clippy::module_name_repetitions,
clippy::struct_excessive_bools,
clippy::unused_self,
// Allowed as they are too pedantic
clippy::cast_possible_truncation,
clippy::unreadable_literal,
clippy::cast_possible_wrap,
clippy::wildcard_imports,
clippy::cast_sign_loss,
clippy::too_many_lines,
clippy::doc_markdown,
clippy::cast_lossless,
clippy::must_use_candidate,
clippy::needless_pass_by_value,
// Document this later
clippy::missing_errors_doc,
clippy::missing_panics_doc,
clippy::missing_panics_doc
)]

use std::ops::ControlFlow;
use std::{error::Error, ops::ControlFlow, result};

use argh::FromArgs;
use lazy_static::lazy_static;
Expand All @@ -33,6 +19,8 @@ pub mod terminal;

use crate::{game::Game, github::Github, terminal::ThemeStyle};

pub type Result<T> = result::Result<T, Box<dyn Error + Send + Sync>>;

/// CLI game to see how fast you can guess the language of a code block!
#[derive(FromArgs)]
pub struct Args {
Expand Down Expand Up @@ -66,7 +54,7 @@ lazy_static! {
pub static ref CONFIG: Config = confy::load("guess-that-lang").unwrap();
}

pub fn main() -> anyhow::Result<()> {
pub fn main() -> Result<()> {
let client = Github::new()?;
let mut game = Game::new(client)?;

Expand Down
Loading

0 comments on commit d2982b4

Please sign in to comment.