use crate::domain::action::identity::{ActionId, CommitDate, CommitSha, Version};
use crate::domain::action::resolved::Commit;
use crate::domain::action::spec::Spec as ActionSpec;
use crate::domain::action::specifier::Specifier;
use crate::domain::action::uses_ref::RefType;
use crate::domain::resolution::{Error as ResolutionError, ShaDescription, VersionRegistry};
use std::time::Duration;
use thiserror::Error;
const USER_AGENT: &str = "gx-cli";
const REQUEST_TIMEOUT_SECS: u64 = 30;
#[derive(Debug, Error)]
pub enum Error {
#[error("failed to create HTTP client")]
ClientInit(#[source] reqwest::Error),
#[error("failed to fetch {operation} from {url}")]
Request {
operation: &'static str,
url: String,
#[source]
source: reqwest::Error,
},
#[error("GitHub API rate limit exceeded for {url}")]
RateLimited { url: String },
#[error("GitHub API unauthorized for {url}")]
Unauthorized { url: String },
#[error("GitHub API not found: {url}")]
NotFound { url: String },
#[error("GitHub API returned status {status} for {url}")]
ApiError { status: u16, url: String },
#[error("failed to parse response from {url}")]
ParseResponse {
url: String,
#[source]
source: reqwest::Error,
},
}
#[derive(Clone)]
pub struct Registry {
pub client: reqwest::blocking::Client,
pub token: Option<crate::config::GitHubToken>,
}
impl Registry {
pub fn new(token: Option<crate::config::GitHubToken>) -> Result<Self, Error> {
let client = reqwest::blocking::Client::builder()
.user_agent(USER_AGENT)
.timeout(Duration::from_secs(REQUEST_TIMEOUT_SECS))
.build()
.map_err(Error::ClientInit)?;
Ok(Self { client, token })
}
pub(super) fn authenticated_get(&self, url: &str) -> reqwest::blocking::RequestBuilder {
let req = self.client.get(url);
match &self.token {
Some(token) => req.header("Authorization", format!("Bearer {}", token.as_str())),
None => req,
}
}
pub(super) fn check_status(response: &reqwest::blocking::Response, url: &str) -> Error {
let status = response.status();
if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
return Error::RateLimited {
url: url.to_owned(),
};
}
if status == reqwest::StatusCode::FORBIDDEN {
let remaining = response
.headers()
.get("x-ratelimit-remaining")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(1);
if remaining == 0 {
return Error::RateLimited {
url: url.to_owned(),
};
}
return Error::Unauthorized {
url: url.to_owned(),
};
}
if status == reqwest::StatusCode::UNAUTHORIZED {
return Error::Unauthorized {
url: url.to_owned(),
};
}
if status == reqwest::StatusCode::NOT_FOUND {
return Error::NotFound {
url: url.to_owned(),
};
}
Error::ApiError {
status: status.as_u16(),
url: url.to_owned(),
}
}
}
impl VersionRegistry for Registry {
fn lookup_sha(&self, id: &ActionId, version: &Version) -> Result<Commit, ResolutionError> {
let (sha, ref_type) =
self.resolve_ref(id.as_str(), version.as_str())
.map_err(|e| match e {
Error::RateLimited { .. } => ResolutionError::RateLimited,
Error::Unauthorized { .. } => ResolutionError::AuthRequired,
Error::ClientInit(_)
| Error::Request { .. }
| Error::NotFound { .. }
| Error::ApiError { .. }
| Error::ParseResponse { .. } => ResolutionError::ResolveFailed {
spec: ActionSpec::new(id.clone(), Specifier::from_v1(version.as_str())),
reason: e.to_string(),
},
})?;
let base_repo = id.base_repo();
let base_repo_str = base_repo.as_str();
let date = if ref_type == Some(RefType::Tag) {
self.fetch_release_date(base_repo_str, version.as_str())
.ok()
.flatten()
.or_else(|| self.fetch_tag_date(base_repo_str, &sha).ok().flatten())
.or_else(|| self.fetch_commit_date(base_repo_str, &sha).ok().flatten())
.unwrap_or_default()
} else if ref_type == Some(RefType::Release) {
self.fetch_release_date(base_repo_str, version.as_str())
.ok()
.flatten()
.or_else(|| self.fetch_commit_date(base_repo_str, &sha).ok().flatten())
.unwrap_or_default()
} else {
self.fetch_commit_date(base_repo_str, &sha)
.ok()
.flatten()
.unwrap_or_default()
};
Ok(Commit {
sha: CommitSha::from(sha),
repository: base_repo,
ref_type,
date: CommitDate::from(date),
})
}
fn tags_for_sha(
&self,
id: &ActionId,
sha: &CommitSha,
) -> Result<Vec<Version>, ResolutionError> {
self.get_tags_for_sha(id.as_str(), sha.as_str())
.map(|tags| tags.into_iter().map(Version::from).collect())
.map_err(|e| match e {
Error::RateLimited { .. } => ResolutionError::RateLimited,
Error::Unauthorized { .. } => ResolutionError::AuthRequired,
Error::ClientInit(_)
| Error::Request { .. }
| Error::NotFound { .. }
| Error::ApiError { .. }
| Error::ParseResponse { .. } => ResolutionError::NoTagsForSha {
action: id.clone(),
sha: sha.clone(),
},
})
}
fn all_tags(&self, id: &ActionId) -> Result<Vec<Version>, ResolutionError> {
self.get_version_tags(id.as_str())
.map(|tags| tags.into_iter().map(Version::from).collect())
.map_err(|e| match e {
Error::RateLimited { .. } => ResolutionError::RateLimited,
Error::Unauthorized { .. } => ResolutionError::AuthRequired,
Error::ClientInit(_)
| Error::Request { .. }
| Error::NotFound { .. }
| Error::ApiError { .. }
| Error::ParseResponse { .. } => ResolutionError::ResolveFailed {
spec: ActionSpec::new(id.clone(), Specifier::Ref(String::new())),
reason: e.to_string(),
},
})
}
fn describe_sha(
&self,
id: &ActionId,
sha: &CommitSha,
) -> Result<ShaDescription, ResolutionError> {
let base_repo = id.base_repo();
let date = self
.fetch_commit_date(base_repo.as_str(), sha.as_str())
.map_err(|e| match e {
Error::RateLimited { .. } => ResolutionError::RateLimited,
Error::Unauthorized { .. } => ResolutionError::AuthRequired,
Error::ClientInit(_)
| Error::Request { .. }
| Error::NotFound { .. }
| Error::ApiError { .. }
| Error::ParseResponse { .. } => ResolutionError::ResolveFailed {
spec: ActionSpec::new(id.clone(), Specifier::Sha(sha.as_str().to_owned())),
reason: e.to_string(),
},
})?
.unwrap_or_default();
let tags = self
.get_tags_for_sha(id.as_str(), sha.as_str())
.unwrap_or_default()
.into_iter()
.map(Version::from)
.collect();
Ok(ShaDescription {
tags,
repository: base_repo,
date: CommitDate::from(date),
})
}
}