Cleanup some client structure
This commit is contained in:
parent
b0078dd224
commit
1bcdcda955
8 changed files with 176 additions and 61 deletions
13
.gitignore
vendored
13
.gitignore
vendored
|
@ -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
66
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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?)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
47
src/jira.rs
47
src/jira.rs
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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:#?}");
|
||||||
|
|
12
src/tasks.rs
12
src/tasks.rs
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue