Skip to content

Commit

Permalink
feat: project quotas optional
Browse files Browse the repository at this point in the history
  • Loading branch information
chris13524 committed May 27, 2024
1 parent 6103669 commit fc67e8e
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 33 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ once_cell = "1.15"
regex = "1.6"
reqwest = { version = "0.11", features = ["json"] }
thiserror = "1.0"
url = "2.5.0"

[dev-dependencies]
tokio = { version = "1.29.1", features = ["full"] }
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ Check project IDs against the WalletConnect registry
## Dev loop

```bash
just amigood
just devloop
```
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ commit-check:
echo ' ^^^ To install `cargo install --locked cocogitto`, see https://github.com/cocogitto/cocogitto for details'
fi

amigood: lint test test-all
devloop: lint test test-all

# Build project documentation
_build-docs $open="":
Expand Down
17 changes: 7 additions & 10 deletions src/project/types/project_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ pub struct ProjectData {
pub verified_domains: Vec<String>,
pub bundle_ids: Vec<String>,
pub package_names: Vec<String>,
}

#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ProjectDataWithQuota {
#[serde(flatten)]
pub project_data: ProjectData,
pub quota: Quota,
}

Expand Down Expand Up @@ -153,11 +160,6 @@ mod test {
"https://prod.header.example.com".to_owned(),
],
is_enabled: true,
quota: Quota {
max: 100000000,
current: 0,
is_valid: true,
},
bundle_ids: vec![
"com.example.bundle".to_owned(),
"com.example.bundle.dev".to_owned(),
Expand Down Expand Up @@ -251,11 +253,6 @@ mod test {
is_verify_enabled: false,
allowed_origins: vec![],
is_enabled: true,
quota: Quota {
max: 100000000,
current: 0,
is_valid: true,
},
bundle_ids: vec![],
package_names: vec![],
};
Expand Down
138 changes: 118 additions & 20 deletions src/registry/client.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
use {
crate::{project::ProjectData, registry::error::RegistryError},
crate::{
project::{ProjectData, ProjectDataWithQuota},
registry::error::RegistryError,
},
async_trait::async_trait,
reqwest::{
header::{self, HeaderValue},
IntoUrl,
StatusCode,
Url,
},
serde::de::DeserializeOwned,
std::{fmt::Debug, time::Duration},
};

Expand All @@ -15,6 +21,10 @@ pub type RegistryResult<T> = Result<T, RegistryError>;
#[async_trait]
pub trait RegistryClient: 'static + Send + Sync + Debug {
async fn project_data(&self, id: &str) -> RegistryResult<Option<ProjectData>>;
async fn project_data_with_quota(
&self,
id: &str,
) -> RegistryResult<Option<ProjectDataWithQuota>>;
}

/// HTTP client configuration.
Expand Down Expand Up @@ -52,17 +62,17 @@ impl Default for HttpClientConfig {

#[derive(Debug, Clone)]
pub struct RegistryHttpClient {
base_url: String,
base_url: Url,
http_client: reqwest::Client,
}

impl RegistryHttpClient {
pub fn new(base_url: impl Into<String>, auth_token: &str) -> RegistryResult<Self> {
pub fn new(base_url: impl IntoUrl, auth_token: &str) -> RegistryResult<Self> {
Self::with_config(base_url, auth_token, Default::default())
}

pub fn with_config(
base_url: impl Into<String>,
base_url: impl IntoUrl,
auth_token: &str,
config: HttpClientConfig,
) -> RegistryResult<Self> {
Expand All @@ -85,26 +95,55 @@ impl RegistryHttpClient {
}

Ok(Self {
base_url: base_url.into(),
http_client: http_client.build()?,
base_url: base_url.into_url().map_err(RegistryError::BaseUrlIntoUrl)?,
http_client: http_client.build().map_err(RegistryError::BuildClient)?,
})
}
}

#[async_trait]
impl RegistryClient for RegistryHttpClient {
async fn project_data(&self, project_id: &str) -> RegistryResult<Option<ProjectData>> {
async fn project_data_impl<T: DeserializeOwned>(
&self,
project_id: &str,
quota: bool,
) -> RegistryResult<Option<T>> {
if !is_valid_project_id(project_id) {
return Ok(None);
}

let url = format!("{}/internal/project/key/{project_id}", self.base_url);
let resp = self.http_client.get(url).send().await?;
let url = build_url(&self.base_url, project_id, quota).map_err(RegistryError::UrlBuild)?;

let resp = self
.http_client
.get(url)
.send()
.await
.map_err(RegistryError::Transport)?;

parse_http_response(resp).await
}
}

#[async_trait]
impl RegistryClient for RegistryHttpClient {
async fn project_data(&self, project_id: &str) -> RegistryResult<Option<ProjectData>> {
self.project_data_impl(project_id, false).await
}

async fn project_data_with_quota(
&self,
project_id: &str,
) -> RegistryResult<Option<ProjectDataWithQuota>> {
self.project_data_impl(project_id, true).await
}
}

fn build_url(base_url: &Url, project_id: &str, quota: bool) -> Result<Url, url::ParseError> {
let mut url = base_url.join(&format!("/internal/project/key/{project_id}"))?;
if quota {
url.query_pairs_mut().append_pair("quota", "true");
}
Ok(url)
}

/// Checks if the project ID is formatted properly. It must be 32 hex
/// characters.
fn is_valid_project_id(project_id: &str) -> bool {
Expand All @@ -115,10 +154,16 @@ fn is_hex_string(string: &str) -> bool {
string.chars().all(|c| c.is_ascii_hexdigit())
}

async fn parse_http_response(resp: reqwest::Response) -> RegistryResult<Option<ProjectData>> {
async fn parse_http_response<T: DeserializeOwned>(
resp: reqwest::Response,
) -> RegistryResult<Option<T>> {
let status = resp.status();
match status {
code if code.is_success() => Ok(Some(resp.json().await?)),
code if code.is_success() => Ok(Some(
resp.json()
.await
.map_err(RegistryError::ResponseJsonParse)?,
)),
StatusCode::FORBIDDEN => Err(RegistryError::Config(INVALID_TOKEN_ERROR)),
StatusCode::NOT_FOUND => Ok(None),
_ => Err(RegistryError::Response(format!(
Expand All @@ -132,10 +177,10 @@ async fn parse_http_response(resp: reqwest::Response) -> RegistryResult<Option<P
mod test {
use {
super::*,
crate::project,
crate::project::Quota,
wiremock::{
http::Method,
matchers::{method, path},
matchers::{method, path, query_param},
Mock,
MockServer,
ResponseTemplate,
Expand All @@ -156,7 +201,33 @@ mod test {
verified_domains: vec![],
bundle_ids: vec![],
package_names: vec![],
quota: project::Quota {
}
}

#[tokio::test]
async fn project_exists() {
let project_id = "a".repeat(32);

let mock_server = MockServer::start().await;

Mock::given(method(Method::Get))
.and(path(format!("/internal/project/key/{project_id}")))
.respond_with(ResponseTemplate::new(StatusCode::OK).set_body_json(mock_project_data()))
.mount(&mock_server)
.await;

let response = RegistryHttpClient::new(mock_server.uri(), "auth")
.unwrap()
.project_data(&project_id)
.await
.unwrap();
assert!(response.is_some());
}

fn mock_project_data_quota() -> ProjectDataWithQuota {
ProjectDataWithQuota {
project_data: mock_project_data(),
quota: Quota {
max: 42,
current: 1,
is_valid: true,
Expand All @@ -165,20 +236,23 @@ mod test {
}

#[tokio::test]
async fn project_exists() {
async fn project_exists_quota() {
let project_id = "a".repeat(32);

let mock_server = MockServer::start().await;

Mock::given(method(Method::Get))
.and(path(format!("/internal/project/key/{project_id}")))
.respond_with(ResponseTemplate::new(StatusCode::OK).set_body_json(mock_project_data()))
.and(query_param("quota", "true"))
.respond_with(
ResponseTemplate::new(StatusCode::OK).set_body_json(mock_project_data_quota()),
)
.mount(&mock_server)
.await;

let response = RegistryHttpClient::new(mock_server.uri(), "auth")
.unwrap()
.project_data(&project_id)
.project_data_with_quota(&project_id)
.await
.unwrap();
assert!(response.is_some());
Expand Down Expand Up @@ -267,4 +341,28 @@ mod test {
RegistryResult::Err(RegistryError::Config(INVALID_TOKEN_ERROR))
));
}

#[test]
fn test_build_url() {
let base_url = Url::parse("http://example.com").unwrap();
let project_id = "a".repeat(32);

let url = build_url(&base_url, &project_id, false).unwrap();
assert_eq!(
url.as_str(),
"http://example.com/internal/project/key/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
);
}

#[test]
fn test_build_url_quota() {
let base_url = Url::parse("http://example.com").unwrap();
let project_id = "a".repeat(32);

let url = build_url(&base_url, &project_id, true).unwrap();
assert_eq!(
url.as_str(),
"http://example.com/internal/project/key/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?quota=true"
);
}
}
14 changes: 13 additions & 1 deletion src/registry/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,23 @@ use thiserror::Error as ThisError;
#[derive(ThisError, Debug)]
pub enum RegistryError {
#[error("transport error: {0}")]
Transport(#[from] reqwest::Error),
Transport(reqwest::Error),

#[error("invalid config: {0}")]
Config(&'static str),

#[error("json parse error: {0}")]
ResponseJsonParse(reqwest::Error),

#[error("invalid response: {0}")]
Response(String),

#[error("building URL: {0}")]
UrlBuild(url::ParseError),

#[error("BaseUrlIntoUrl: {0}")]
BaseUrlIntoUrl(reqwest::Error),

#[error("building client: {0}")]
BuildClient(reqwest::Error),
}

0 comments on commit fc67e8e

Please sign in to comment.