Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#69] Improves error message when the tag is not found #127

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ available [on GitHub][2].
* [#111](/~https://github.com/chshersh/tool-sync/issues/111):
Adds repo URLs to the output of `default-config` and `install` commands
(by [@crudiedo][crudiedo])
* [#69](/~https://github.com/chshersh/tool-sync/issues/69):
Adds improved error message with suggesstion when trying to prefetch non-existing tag
(by [@crudiedo][crudiedo])

### Fixed

Expand Down
23 changes: 23 additions & 0 deletions src/infra/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::error::Error;
use std::io::Read;

use crate::model::release::{Asset, Release};
use crate::model::tag::Tag;

/// GitHub API client to handle all API requests
pub struct Client {
Expand Down Expand Up @@ -30,6 +31,14 @@ impl Client {
)
}

fn tags_url(&self) -> String {
format!(
"https://api.github.com/repos/{owner}/{repo}/tags",
owner = self.owner,
repo = self.repo,
)
}

pub fn fetch_release_info(&self) -> Result<Release, Box<dyn Error>> {
let release_url = self.release_url();

Expand All @@ -44,6 +53,20 @@ impl Client {
Ok(release)
}

pub fn fetch_available_tags(&self) -> Result<Vec<Tag>, Box<dyn Error>> {
let tags_url = self.tags_url();

let req = add_auth_header(
ureq::get(&tags_url)
.set("Accept", "application/vnd.github+json")
.set("User-Agent", "chshersh/tool-sync-0.2.0"),
);

let release: Vec<Tag> = req.call()?.into_json()?;

Ok(release)
}

pub fn get_asset_stream(
&self,
asset: &Asset,
Expand Down
1 change: 1 addition & 0 deletions src/model.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod asset_name;
pub mod release;
pub mod tag;
pub mod tool;
38 changes: 38 additions & 0 deletions src/model/tag.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use serde::Deserialize;
use std::error::Error;
use std::fmt::{Display, Formatter};

#[derive(Deserialize, Debug)]
pub struct Tag {
pub name: String,
}

#[derive(Debug, PartialEq, Eq)]
pub enum TagError {
/// Tag is not in the available tags
NotFound(String, Option<String>),
}

impl Display for Tag {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
// Use `self.number` to refer to each positional data point.
write!(f, "{}", self.name)
}
}

impl Display for TagError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotFound(tag, possible_tag) => match possible_tag {
Some(closest_tag) => write!(
f,
"There's no tag '{}'. Perhaps you meant '{}'?",
tag, closest_tag
),
None => write!(f, "There's no tag '{}'", tag),
},
}
}
}

impl Error for TagError {}
1 change: 1 addition & 0 deletions src/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod archive;
mod configure;
pub mod db;
mod download;
mod edit_distance;
mod install;
mod prefetch;
mod progress;
Expand Down
103 changes: 103 additions & 0 deletions src/sync/edit_distance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/// The maximum allowed threshold edit distance
/// All the values above that will be dropped
///
/// WARNING: Updating the constant would probably break the tests below,
/// so please change them accordingly
pub const EDIT_DISTANCE_THRESHOLD: usize = 4;

/// Calculate the Levenshtein \ Edit distance between two strings
///
/// The implementation is a copy from
/// [edit-distance] https://crates.io/crates/edit-distance
pub fn edit_distance(a: &str, b: &str) -> usize {
let len_a = a.chars().count();
let len_b = b.chars().count();
if len_a < len_b {
return edit_distance(b, a);
}
// handle special case of 0 length
if len_a == 0 {
return len_b;
} else if len_b == 0 {
return len_a;
}

let len_b = len_b + 1;

let mut pre;
let mut tmp;
let mut cur = vec![0; len_b];

// initialize string b
#[allow(clippy::all)]
for i in 1..len_b {
cur[i] = i;
}

// calculate edit distance
for (i, ca) in a.chars().enumerate() {
// get first column for this row
pre = cur[0];
cur[0] = i + 1;
for (j, cb) in b.chars().enumerate() {
tmp = cur[j + 1];
cur[j + 1] = std::cmp::min(
// deletion
tmp + 1,
std::cmp::min(
// insertion
cur[j] + 1,
// match or substitution
pre + usize::from(ca != cb),
),
);
pre = tmp;
}
}
cur[len_b - 1]
}

/// Find and return closest string based on the lowest edit distance between source and every possible value
pub fn closest_string(source: String, possible_values: Vec<String>) -> Option<String> {
possible_values
.iter()
.filter(|value| source.len().abs_diff(value.len()) <= EDIT_DISTANCE_THRESHOLD)
.map(|value| (value, edit_distance(&source, value)))
.filter(|item| item.1 <= EDIT_DISTANCE_THRESHOLD)
.min_by(|a, b| a.1.cmp(&b.1))
.map(|(k, _v)| k.to_string())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_closest_string_result() {
let possible_string = closest_string(
"v13.0.0".to_string(),
vec![
"13.0.1".to_string(),
"13.0.0".to_string(),
"13.0.2".to_string(),
"12.0.0".to_string(),
],
);

assert_eq!(Some("13.0.0").as_deref(), possible_string.as_deref());
}

#[test]
fn test_closest_string_none() {
let possible_string = closest_string(
"v13.0.10".to_string(),
vec![
"v13".to_string(),
"v24.1.22".to_string(),
"23.1.21".to_string(),
],
);

assert_eq!(None::<String>.as_deref(), possible_string.as_deref());
}
}
23 changes: 22 additions & 1 deletion src/sync/prefetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ use super::configure::configure_tool;
use crate::config::schema::ConfigAsset;
use crate::infra::client::Client;
use crate::model::release::AssetError;
use crate::model::tag::TagError;
use crate::model::tool::{Tool, ToolAsset};
use crate::sync::edit_distance::closest_string;

const PREFETCH: Emoji<'_, '_> = Emoji("🔄 ", "-> ");
const ERROR: Emoji<'_, '_> = Emoji("❌ ", "x ");
Expand Down Expand Up @@ -123,8 +125,27 @@ fn prefetch_tool(
};

match client.fetch_release_info() {
Err(e) => {
Err(mut e) => {
if let Some(ureq::Error::Status(404, _)) = e.downcast_ref::<ureq::Error>() {
if let Ok(available_tags) = client.fetch_available_tags() {
let raw_tag = tool_info.tag.to_str_version().replace("tags/", "");

e = TagError::NotFound(
raw_tag.clone(),
closest_string(
raw_tag,
available_tags
.iter()
.map(|tag| tag.name.to_string())
.collect(),
),
)
.into();
}
}

prefetch_progress.unexpected_err_msg(tool_name, e);

// do some other processing
prefetch_progress.update_message(already_completed);
None
Expand Down