-
Notifications
You must be signed in to change notification settings - Fork 1
refactor: use struct and impl method for link check #241
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
Merged
Merged
Changes from 5 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
c74370c
refact: change module name
reddevilmidzy 9af3bb8
refactor: use struct and impl method for link check
reddevilmidzy c04ec47
feat: add default impl for LinkChecker
reddevilmidzy d9ad8f1
refactor: change LinckChecker::new() method return type into Result<>
reddevilmidzy 38c3433
fix: fix typo in error message
reddevilmidzy 74f3455
refactor: use error propagation
reddevilmidzy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,67 @@ | ||
| use crate::{GitHubUrl, RepoManager}; | ||
| use url::Url; | ||
|
|
||
| pub struct LinkChecker { | ||
| client: reqwest::Client, | ||
| } | ||
|
|
||
| impl LinkChecker { | ||
| pub fn new() -> Result<Self, String> { | ||
| let client = reqwest::Client::builder() | ||
| .timeout(std::time::Duration::from_secs(5)) | ||
| .redirect(reqwest::redirect::Policy::none()) | ||
| .build(); | ||
|
|
||
| if let Ok(client) = client { | ||
| Ok(LinkChecker { client }) | ||
| } else { | ||
| Err("failed to create Client".to_string()) | ||
| } | ||
| } | ||
|
|
||
| pub async fn check_link(&self, url: &str) -> LinkCheckResult { | ||
| let mut attempts = 3; | ||
| while attempts > 0 { | ||
| match self.client.get(url).send().await { | ||
| Ok(res) => { | ||
| let status = res.status(); | ||
| if status.is_success() { | ||
| return LinkCheckResult::Valid; | ||
| } else if status.is_redirection() { | ||
| if let Some(redirect_url) = res.headers().get("location") | ||
| && let Ok(redirect_str) = redirect_url.to_str() | ||
| { | ||
| if is_trivial_redirect(url, redirect_str) { | ||
| return LinkCheckResult::Valid; | ||
| } | ||
| return LinkCheckResult::Redirect(redirect_str.to_string()); | ||
| } | ||
| return LinkCheckResult::Valid; | ||
| } else if status.as_u16() == 404 && url.contains("github.com") { | ||
| return handle_github_404(url); | ||
|
Comment on lines
+36
to
+37
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. URL host check is fragile. Using -} else if status.as_u16() == 404 && url.contains("github.com") {
+} else if status.as_u16() == 404 && is_github_url(url) {
return handle_github_404(url);Add a helper function: fn is_github_url(url: &str) -> bool {
Url::parse(url)
.ok()
.and_then(|u| u.host_str())
.map(|h| h == "github.com" || h.ends_with(".github.com"))
.unwrap_or(false)
}🤖 Prompt for AI Agents |
||
| } else { | ||
| return LinkCheckResult::Invalid(format!("HTTP status code: {status}")); | ||
| } | ||
| } | ||
| Err(e) => { | ||
| if attempts == 1 { | ||
| return LinkCheckResult::Invalid(format!("Request error: {e}")); | ||
| } | ||
| } | ||
| } | ||
| attempts -= 1; | ||
| tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; | ||
| } | ||
| LinkCheckResult::Invalid("Max retries exceeded".to_string()) | ||
| } | ||
| } | ||
|
|
||
| impl Default for LinkChecker { | ||
| fn default() -> Self { | ||
| Self::new().expect("failed to create LinkChecker") | ||
reddevilmidzy marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
reddevilmidzy marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
|
|
||
| #[derive(Debug, Eq, PartialEq)] | ||
| pub enum LinkCheckResult { | ||
| Valid, | ||
|
|
@@ -32,48 +93,6 @@ fn handle_github_404(url: &str) -> LinkCheckResult { | |
| } | ||
| } | ||
|
|
||
| pub async fn check_link(url: &str) -> LinkCheckResult { | ||
| let client = reqwest::Client::builder() | ||
| .timeout(std::time::Duration::from_secs(5)) | ||
| .redirect(reqwest::redirect::Policy::none()) | ||
| .build() | ||
| .unwrap(); | ||
|
|
||
| let mut attempts = 3; | ||
| while attempts > 0 { | ||
| match client.get(url).send().await { | ||
| Ok(res) => { | ||
| let status = res.status(); | ||
| if status.is_success() { | ||
| return LinkCheckResult::Valid; | ||
| } else if status.is_redirection() { | ||
| if let Some(redirect_url) = res.headers().get("location") | ||
| && let Ok(redirect_str) = redirect_url.to_str() | ||
| { | ||
| if is_trivial_redirect(url, redirect_str) { | ||
| return LinkCheckResult::Valid; | ||
| } | ||
| return LinkCheckResult::Redirect(redirect_str.to_string()); | ||
| } | ||
| return LinkCheckResult::Valid; | ||
| } else if status.as_u16() == 404 && url.contains("github.com") { | ||
| return handle_github_404(url); | ||
| } else { | ||
| return LinkCheckResult::Invalid(format!("HTTP status code: {status}")); | ||
| } | ||
| } | ||
| Err(e) => { | ||
| if attempts == 1 { | ||
| return LinkCheckResult::Invalid(format!("Request error: {e}")); | ||
| } | ||
| } | ||
| } | ||
| attempts -= 1; | ||
| tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; | ||
| } | ||
| LinkCheckResult::Invalid("Max retries exceeded".to_string()) | ||
| } | ||
|
|
||
| fn is_trivial_redirect(original: &str, redirect: &str) -> bool { | ||
| let orig_url = match Url::parse(original) { | ||
| Ok(url) => url, | ||
|
|
@@ -109,29 +128,32 @@ mod tests { | |
|
|
||
| #[tokio::test] | ||
| async fn validate_link() { | ||
| let link_checker = LinkChecker::default(); | ||
| let link = "https://redddy.ai"; | ||
| assert!(matches!( | ||
| check_link(link).await, | ||
| link_checker.check_link(link).await, | ||
| LinkCheckResult::Invalid(_) | ||
| )); | ||
| let link = "https://lazypazy.tistory.com"; | ||
| assert_eq!(check_link(link).await, LinkCheckResult::Valid); | ||
| assert_eq!(link_checker.check_link(link).await, LinkCheckResult::Valid); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn change_organization_name() { | ||
| let link_checker = LinkChecker::default(); | ||
| let link = "https://github.com/Bibimbap-Team/git-playground"; | ||
| assert_eq!( | ||
| check_link(link).await, | ||
| link_checker.check_link(link).await, | ||
| LinkCheckResult::Redirect("https://github.com/Coduck-Team/git-playground".to_string()) | ||
| ); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn change_branch_name() { | ||
| let link_checker = LinkChecker::default(); | ||
| let link = "https://github.com/reddevilmidzy/kingsac/tree/forever"; | ||
| assert_eq!( | ||
| check_link(link).await, | ||
| link_checker.check_link(link).await, | ||
| LinkCheckResult::Redirect( | ||
| "https://github.com/reddevilmidzy/kingsac/tree/lie".to_string() | ||
| ) | ||
|
|
@@ -140,24 +162,26 @@ mod tests { | |
|
|
||
| #[tokio::test] | ||
| async fn change_repository_name() { | ||
| let link_checker = LinkChecker::default(); | ||
| let link = "https://github.com/reddevilmidzy/test-queensac"; | ||
| assert_eq!( | ||
| check_link(link).await, | ||
| link_checker.check_link(link).await, | ||
| LinkCheckResult::Redirect("https://github.com/reddevilmidzy/kingsac".to_string()) | ||
| ); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn check_redirect_url() { | ||
| let link_checker = LinkChecker::default(); | ||
| let link = "https://gluesql.org/docs"; | ||
| assert_eq!( | ||
| check_link(link).await, | ||
| link_checker.check_link(link).await, | ||
| LinkCheckResult::Valid, | ||
| "check trivial redirect" | ||
| ); | ||
| let link = "https://gluesql.org/docs/"; | ||
| assert_eq!( | ||
| check_link(link).await, | ||
| link_checker.check_link(link).await, | ||
| LinkCheckResult::Valid, | ||
| "check trivial redirect" | ||
| ); | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| mod link; | ||
| mod checker; | ||
| mod service; | ||
|
|
||
| pub use link::{LinkCheckResult, check_link}; | ||
| pub use checker::{LinkCheckResult, LinkChecker}; | ||
| pub use service::{InvalidLinkInfo, LinkCheckEvent, check_links}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.