diff --git a/.gitignore b/.gitignore index 1aa4767..58e9527 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,11 @@ +# cargo build output /target + +# direnv cache /.direnv + +# pre-commit config (setup by nix flake) /.pre-commit-config.yaml - -# Added by cargo -# -# already existing elements were commented out - -#/target +# data +/data diff --git a/Cargo.lock b/Cargo.lock index 8846b20..d0f5fa1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,6 +103,12 @@ version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.5.0" @@ -176,6 +182,30 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "either" version = "1.9.0" @@ -253,6 +283,16 @@ dependencies = [ "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]] name = "futures" version = "0.3.30" @@ -342,6 +382,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "getrandom" version = "0.2.12" @@ -1191,6 +1240,22 @@ dependencies = [ "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]] name = "smallvec" version = "1.13.1" @@ -1266,6 +1331,7 @@ dependencies = [ "reqwest-tracing", "serde", "serde_json", + "sled", "tokio", "tracing", "tracing-subscriber", diff --git a/Cargo.toml b/Cargo.toml index 0d56490..9ae6b00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ reqwest-retry = "0.4.0" reqwest-tracing = "0.4.8" serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.114" +sled = "0.34.7" tokio = { version = "1.36.0", features = ["full"] } tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } diff --git a/src/client.rs b/src/client.rs index 1a5150e..a5b51d6 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,13 +1,68 @@ -use reqwest::Client; -use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; +use crate::result::Result; +use reqwest::{Client as RClient, Method, Url}; +use reqwest_middleware::{ClientBuilder, ClientWithMiddleware, RequestBuilder}; use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware}; use reqwest_tracing::TracingMiddleware; +use serde::de; +use tracing::debug; -pub fn wrap_with_middleware(client: Client) -> ClientWithMiddleware { - let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3); - - ClientBuilder::new(client) - .with(TracingMiddleware::default()) - .with(RetryTransientMiddleware::new_with_policy(retry_policy)) - .build() +pub struct Client { + client: ClientWithMiddleware, + base_url: Url, +} + +pub trait ResourceRequest { + async fn res(self) -> Result; +} + +impl ResourceRequest for RequestBuilder { + async fn res(self) -> Result { + /* + 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 { + let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3); + + let client = ClientBuilder::new(client) + .with(TracingMiddleware::default()) + .with(RetryTransientMiddleware::new_with_policy(retry_policy)) + .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 { + self.base_url + .join(path.trim_start_matches('/')) + .map_err(|e| e.into()) + } + + pub fn build(&self, method: Method, rel_url_path: &str) -> Result { + Ok(self.client.request(method, self.url(rel_url_path)?)) + } + + pub async fn request( + &self, + method: Method, + rel_url_path: &str, + ) -> Result { + self.build(method, rel_url_path)?.res().await + } + + pub async fn get(&self, rel_url_path: &str) -> Result { + self.request::(Method::GET, rel_url_path).await + } } diff --git a/src/gitlab.rs b/src/gitlab.rs index 5541306..39002b4 100644 --- a/src/gitlab.rs +++ b/src/gitlab.rs @@ -1,14 +1,13 @@ -use crate::result::Result; +use crate::{client::Client, result::Result}; use reqwest::{ header::{HeaderMap, HeaderValue}, - Client, Url, + Client as RClient, }; -use reqwest_middleware::ClientWithMiddleware; + use serde::Deserialize; pub struct GitLab { - url: Url, - client: ClientWithMiddleware, + client: Client, } #[derive(Deserialize, Debug)] @@ -25,24 +24,19 @@ pub struct User { impl GitLab { pub fn try_new(url: &str, token: &str) -> Result { - let url = Url::parse(&format!("{}{}", url.trim_end_matches('/'), "/"))?; let mut headers = HeaderMap::new(); headers.insert("PRIVATE-TOKEN", HeaderValue::from_str(token)?); headers.insert("content-type", HeaderValue::from_str("application/json")?); headers.insert("accepts", HeaderValue::from_str("application/json")?); - let base_client = Client::builder().default_headers(headers).build()?; - let client = crate::client::wrap_with_middleware(base_client); + let base_client = RClient::builder().default_headers(headers).build()?; - Ok(Self { url, client }) - } + let client = Client::try_new(base_client, url)?; - pub fn url(&self, path: &str) -> Result { - Ok(self.url.join(path.trim_start_matches('/'))?) + Ok(Self { client }) } pub async fn me(&self) -> Result { - let res = self.client.get(self.url("/user")?).send().await?; - Ok(res.json().await?) + self.client.get("/user").await } } diff --git a/src/jira.rs b/src/jira.rs index 9a9db67..b8ad29d 100644 --- a/src/jira.rs +++ b/src/jira.rs @@ -1,16 +1,18 @@ -use crate::result::Result; +use crate::{ + client::{Client, ResourceRequest}, + result::Result, +}; use reqwest::{ header::{HeaderMap, HeaderValue}, - Client, Url, + Client as RClient, Method, }; -use reqwest_middleware::ClientWithMiddleware; + use serde::Deserialize; use tokio::spawn; use tracing::debug; pub struct Jira { - url: Url, - client: ClientWithMiddleware, + client: Client, } #[derive(Deserialize, Debug)] @@ -43,7 +45,6 @@ pub struct IssueSearch { impl Jira { pub fn try_new(url: &str, token: &str) -> Result { - let url = Url::parse(&format!("{}{}", url.trim_end_matches('/'), "/"))?; let mut headers = HeaderMap::new(); headers.insert( "Authorization", @@ -51,39 +52,27 @@ impl Jira { ); headers.insert("content-type", 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 = crate::client::wrap_with_middleware(base_client); + let client = Client::try_new(base_client, url)?; - Ok(Self { url, client }) - } - - pub fn url(&self, path: &str) -> Result { - Ok(self.url.join(path.trim_start_matches('/'))?) + Ok(Self { client }) } pub async fn me(&self) -> Result { - let res = self - .client - .get(self.url("/rest/api/3/myself")?) - .send() - .await?; - Ok(res.json().await?) + self.client.get("/rest/api/3/myself").await } pub async fn jql(&self, jql: &str) -> Result> { debug!("Fetching issues for jql {}", jql); - let mut results = self + let mut results: IssueSearch = self .client - .get(self.url("/rest/api/3/search")?) + .build(Method::GET, "/rest/api/3/search")? .query(&[("jql", jql)]) - .send() - .await? - .json::() + .res() .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); issues.append(&mut results.issues); @@ -95,14 +84,14 @@ impl Jira { debug!("Fetching issues for jql {} (page {})", jql, i); futures.push(spawn( self.client - .get(self.url("/rest/api/3/search")?) + .build(Method::GET, "/rest/api/3/search")? .query(&[("jql", jql), ("startAt", &start_at.to_string())]) - .send(), + .res::(), )); } for task in futures { - let mut result = task.await.unwrap()?.json::().await?; + let mut result = task.await??; issues.append(&mut result.issues); } diff --git a/src/main.rs b/src/main.rs index 94bbb87..8b42b03 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ use tasks::Tasks; use tracing::{error, info}; use tracing_subscriber::{filter::LevelFilter, EnvFilter}; +#[allow(dead_code)] const ANSI_CLEAR: &'static str = "\x1b[2J\x1b[1;1H"; #[tokio::main] @@ -31,7 +32,7 @@ async fn main() -> Result<()> { } async fn run() -> Result<()> { - print!("{ANSI_CLEAR}"); + // print!("{ANSI_CLEAR}"); let tasks = Tasks::try_new()?; let gitlab_user = tasks.gitlab.me().await?; info!("{gitlab_user:#?}"); diff --git a/src/tasks.rs b/src/tasks.rs index 4617c3c..e6653cd 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -1,5 +1,7 @@ use std::{collections::HashSet, env, process::Command}; +use serde::{Deserialize, Serialize}; + use crate::{gitlab::GitLab, jira::Jira, result::Result}; #[derive(Serialize, Deserialize, Debug)] @@ -22,6 +24,8 @@ pub struct Task { pub struct Tasks { pub gitlab: GitLab, pub jira: Jira, + + db: sled::Db, } impl Tasks { @@ -44,10 +48,14 @@ impl Tasks { })?; 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> {} + pub async fn all(&self) -> Result> { + Ok(vec![]) + } /// fetches jira issues and compares to known local tasks /// for use when sync'ing local tasks to remote state (jira, gitlab)