Cleanup some client structure

This commit is contained in:
Daniel Flanagan 2024-03-19 16:47:50 -05:00
parent b0078dd224
commit 1bcdcda955
8 changed files with 176 additions and 61 deletions

13
.gitignore vendored
View file

@ -1,10 +1,11 @@
# cargo build output
/target /target
# direnv cache
/.direnv /.direnv
# pre-commit config (setup by nix flake)
/.pre-commit-config.yaml /.pre-commit-config.yaml
# data
# Added by cargo /data
#
# already existing elements were commented out
#/target

66
Cargo.lock generated
View file

@ -103,6 +103,12 @@ version = "3.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.5.0" version = "1.5.0"
@ -176,6 +182,30 @@ version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "crc32fast"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
[[package]] [[package]]
name = "either" name = "either"
version = "1.9.0" version = "1.9.0"
@ -253,6 +283,16 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "fs2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
dependencies = [
"libc",
"winapi",
]
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.3.30" version = "0.3.30"
@ -342,6 +382,15 @@ dependencies = [
"slab", "slab",
] ]
[[package]]
name = "fxhash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
dependencies = [
"byteorder",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.12" version = "0.2.12"
@ -1191,6 +1240,22 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "sled"
version = "0.34.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935"
dependencies = [
"crc32fast",
"crossbeam-epoch",
"crossbeam-utils",
"fs2",
"fxhash",
"libc",
"log",
"parking_lot 0.11.2",
]
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.13.1" version = "1.13.1"
@ -1266,6 +1331,7 @@ dependencies = [
"reqwest-tracing", "reqwest-tracing",
"serde", "serde",
"serde_json", "serde_json",
"sled",
"tokio", "tokio",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",

View file

@ -14,6 +14,7 @@ reqwest-retry = "0.4.0"
reqwest-tracing = "0.4.8" reqwest-tracing = "0.4.8"
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.114" serde_json = "1.0.114"
sled = "0.34.7"
tokio = { version = "1.36.0", features = ["full"] } tokio = { version = "1.36.0", features = ["full"] }
tracing = "0.1.40" tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }

View file

@ -1,13 +1,68 @@
use reqwest::Client; use crate::result::Result;
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; use reqwest::{Client as RClient, Method, Url};
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware, RequestBuilder};
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware}; use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
use reqwest_tracing::TracingMiddleware; use reqwest_tracing::TracingMiddleware;
use serde::de;
use tracing::debug;
pub fn wrap_with_middleware(client: Client) -> ClientWithMiddleware { pub struct Client {
client: ClientWithMiddleware,
base_url: Url,
}
pub trait ResourceRequest {
async fn res<T: de::DeserializeOwned>(self) -> Result<T>;
}
impl ResourceRequest for RequestBuilder {
async fn res<T: de::DeserializeOwned>(self) -> Result<T> {
/*
let body = self.send().await?.text().await?;
debug!(body);
serde_json::from_str(&body).map_err(|e| e.into())
*/
self.send().await?.json().await.map_err(|e| e.into())
}
}
impl Client {
pub fn try_new(client: RClient, base_url: &str) -> Result<Self> {
let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3); let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3);
ClientBuilder::new(client) let client = ClientBuilder::new(client)
.with(TracingMiddleware::default()) .with(TracingMiddleware::default())
.with(RetryTransientMiddleware::new_with_policy(retry_policy)) .with(RetryTransientMiddleware::new_with_policy(retry_policy))
.build() .build();
// force trailing slash
let mut base_url = Url::parse(base_url)?;
base_url.set_path(&format!("{}{}", base_url.path().trim_end_matches('/'), "/"));
Ok(Self { client, base_url })
}
/// Takes a path segment and properly joins it relative to `base_url`
pub fn url(&self, path: &str) -> Result<Url> {
self.base_url
.join(path.trim_start_matches('/'))
.map_err(|e| e.into())
}
pub fn build(&self, method: Method, rel_url_path: &str) -> Result<RequestBuilder> {
Ok(self.client.request(method, self.url(rel_url_path)?))
}
pub async fn request<T: de::DeserializeOwned>(
&self,
method: Method,
rel_url_path: &str,
) -> Result<T> {
self.build(method, rel_url_path)?.res().await
}
pub async fn get<T: de::DeserializeOwned>(&self, rel_url_path: &str) -> Result<T> {
self.request::<T>(Method::GET, rel_url_path).await
}
} }

View file

@ -1,14 +1,13 @@
use crate::result::Result; use crate::{client::Client, result::Result};
use reqwest::{ use reqwest::{
header::{HeaderMap, HeaderValue}, header::{HeaderMap, HeaderValue},
Client, Url, Client as RClient,
}; };
use reqwest_middleware::ClientWithMiddleware;
use serde::Deserialize; use serde::Deserialize;
pub struct GitLab { pub struct GitLab {
url: Url, client: Client,
client: ClientWithMiddleware,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -25,24 +24,19 @@ pub struct User {
impl GitLab { impl GitLab {
pub fn try_new(url: &str, token: &str) -> Result<Self> { pub fn try_new(url: &str, token: &str) -> Result<Self> {
let url = Url::parse(&format!("{}{}", url.trim_end_matches('/'), "/"))?;
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
headers.insert("PRIVATE-TOKEN", HeaderValue::from_str(token)?); headers.insert("PRIVATE-TOKEN", HeaderValue::from_str(token)?);
headers.insert("content-type", HeaderValue::from_str("application/json")?); headers.insert("content-type", HeaderValue::from_str("application/json")?);
headers.insert("accepts", HeaderValue::from_str("application/json")?); headers.insert("accepts", HeaderValue::from_str("application/json")?);
let base_client = Client::builder().default_headers(headers).build()?; let base_client = RClient::builder().default_headers(headers).build()?;
let client = crate::client::wrap_with_middleware(base_client);
Ok(Self { url, client }) let client = Client::try_new(base_client, url)?;
}
pub fn url(&self, path: &str) -> Result<Url> { Ok(Self { client })
Ok(self.url.join(path.trim_start_matches('/'))?)
} }
pub async fn me(&self) -> Result<User> { pub async fn me(&self) -> Result<User> {
let res = self.client.get(self.url("/user")?).send().await?; self.client.get("/user").await
Ok(res.json().await?)
} }
} }

View file

@ -1,16 +1,18 @@
use crate::result::Result; use crate::{
client::{Client, ResourceRequest},
result::Result,
};
use reqwest::{ use reqwest::{
header::{HeaderMap, HeaderValue}, header::{HeaderMap, HeaderValue},
Client, Url, Client as RClient, Method,
}; };
use reqwest_middleware::ClientWithMiddleware;
use serde::Deserialize; use serde::Deserialize;
use tokio::spawn; use tokio::spawn;
use tracing::debug; use tracing::debug;
pub struct Jira { pub struct Jira {
url: Url, client: Client,
client: ClientWithMiddleware,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -43,7 +45,6 @@ pub struct IssueSearch {
impl Jira { impl Jira {
pub fn try_new(url: &str, token: &str) -> Result<Self> { pub fn try_new(url: &str, token: &str) -> Result<Self> {
let url = Url::parse(&format!("{}{}", url.trim_end_matches('/'), "/"))?;
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
headers.insert( headers.insert(
"Authorization", "Authorization",
@ -51,39 +52,27 @@ impl Jira {
); );
headers.insert("content-type", HeaderValue::from_str("application/json")?); headers.insert("content-type", HeaderValue::from_str("application/json")?);
headers.insert("accepts", HeaderValue::from_str("application/json")?); headers.insert("accepts", HeaderValue::from_str("application/json")?);
let base_client = RClient::builder().default_headers(headers).build()?;
let base_client = Client::builder().default_headers(headers).build()?; let client = Client::try_new(base_client, url)?;
let client = crate::client::wrap_with_middleware(base_client);
Ok(Self { url, client }) Ok(Self { client })
}
pub fn url(&self, path: &str) -> Result<Url> {
Ok(self.url.join(path.trim_start_matches('/'))?)
} }
pub async fn me(&self) -> Result<User> { pub async fn me(&self) -> Result<User> {
let res = self self.client.get("/rest/api/3/myself").await
.client
.get(self.url("/rest/api/3/myself")?)
.send()
.await?;
Ok(res.json().await?)
} }
pub async fn jql(&self, jql: &str) -> Result<Vec<Issue>> { pub async fn jql(&self, jql: &str) -> Result<Vec<Issue>> {
debug!("Fetching issues for jql {}", jql); debug!("Fetching issues for jql {}", jql);
let mut results = self let mut results: IssueSearch = self
.client .client
.get(self.url("/rest/api/3/search")?) .build(Method::GET, "/rest/api/3/search")?
.query(&[("jql", jql)]) .query(&[("jql", jql)])
.send() .res()
.await?
.json::<IssueSearch>()
.await?; .await?;
// TODO: parallelize after first page // TODO: are there rate limits to be concerned about?
// TODO: are there rate limits to be concerned about
let mut issues = Vec::with_capacity(results.total); let mut issues = Vec::with_capacity(results.total);
issues.append(&mut results.issues); issues.append(&mut results.issues);
@ -95,14 +84,14 @@ impl Jira {
debug!("Fetching issues for jql {} (page {})", jql, i); debug!("Fetching issues for jql {} (page {})", jql, i);
futures.push(spawn( futures.push(spawn(
self.client self.client
.get(self.url("/rest/api/3/search")?) .build(Method::GET, "/rest/api/3/search")?
.query(&[("jql", jql), ("startAt", &start_at.to_string())]) .query(&[("jql", jql), ("startAt", &start_at.to_string())])
.send(), .res::<IssueSearch>(),
)); ));
} }
for task in futures { for task in futures {
let mut result = task.await.unwrap()?.json::<IssueSearch>().await?; let mut result = task.await??;
issues.append(&mut result.issues); issues.append(&mut result.issues);
} }

View file

@ -10,6 +10,7 @@ use tasks::Tasks;
use tracing::{error, info}; use tracing::{error, info};
use tracing_subscriber::{filter::LevelFilter, EnvFilter}; use tracing_subscriber::{filter::LevelFilter, EnvFilter};
#[allow(dead_code)]
const ANSI_CLEAR: &'static str = "\x1b[2J\x1b[1;1H"; const ANSI_CLEAR: &'static str = "\x1b[2J\x1b[1;1H";
#[tokio::main] #[tokio::main]
@ -31,7 +32,7 @@ async fn main() -> Result<()> {
} }
async fn run() -> Result<()> { async fn run() -> Result<()> {
print!("{ANSI_CLEAR}"); // print!("{ANSI_CLEAR}");
let tasks = Tasks::try_new()?; let tasks = Tasks::try_new()?;
let gitlab_user = tasks.gitlab.me().await?; let gitlab_user = tasks.gitlab.me().await?;
info!("{gitlab_user:#?}"); info!("{gitlab_user:#?}");

View file

@ -1,5 +1,7 @@
use std::{collections::HashSet, env, process::Command}; use std::{collections::HashSet, env, process::Command};
use serde::{Deserialize, Serialize};
use crate::{gitlab::GitLab, jira::Jira, result::Result}; use crate::{gitlab::GitLab, jira::Jira, result::Result};
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -22,6 +24,8 @@ pub struct Task {
pub struct Tasks { pub struct Tasks {
pub gitlab: GitLab, pub gitlab: GitLab,
pub jira: Jira, pub jira: Jira,
db: sled::Db,
} }
impl Tasks { impl Tasks {
@ -44,10 +48,14 @@ impl Tasks {
})?; })?;
let jira = Jira::try_new("https://billcom.atlassian.net", &jira_token)?; let jira = Jira::try_new("https://billcom.atlassian.net", &jira_token)?;
Ok(Self { gitlab, jira }) let db = sled::open("data/tasks")?;
Ok(Self { gitlab, jira, db })
} }
pub async fn all(&self) -> Result<Vec<Task>> {} pub async fn all(&self) -> Result<Vec<Task>> {
Ok(vec![])
}
/// fetches jira issues and compares to known local tasks /// fetches jira issues and compares to known local tasks
/// for use when sync'ing local tasks to remote state (jira, gitlab) /// for use when sync'ing local tasks to remote state (jira, gitlab)