A good place?

This commit is contained in:
Daniel Flanagan 2024-03-19 12:07:37 -05:00
parent 9cdbc9e2db
commit 4833cb7b0e
10 changed files with 236 additions and 90 deletions

5
.cargo/config.toml Normal file
View File

@ -0,0 +1,5 @@
[target.x86_64-unknown-linux-gnu]
linker = "clang"
# TODO: cannot use mold due to libssl
# rustflags = ["-C", "link-arg=-fuse-ld=/nix/store/qcwcg3q98cm6pvyfnpn4apfh5w3xvy1d-mold-2.4.1/bin/mold"]

16
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug",
"program": "${workspaceFolder}/target/debug/tasks",
"args": [],
"cwd": "${workspaceFolder}"
}
]
}

View File

@ -17,3 +17,16 @@ serde_json = "1.0.114"
tokio = { version = "1.36.0", features = ["full"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
[profile.dev]
opt-level = 1
[profile.dev.package."*"]
opt-level = 3
[profile.release]
strip = true
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"

View File

@ -41,6 +41,14 @@
rustPackages.clippy
rust-analyzer
# linker
clang
mold
# debugger
lldb
# libs
openssl
pkg-config
];

13
src/client.rs Normal file
View File

@ -0,0 +1,13 @@
use reqwest::Client;
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
use reqwest_tracing::TracingMiddleware;
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()
}

View File

@ -1,59 +1,16 @@
use color_eyre::owo_colors::OwoColorize;
use crate::result::Result;
use reqwest::{
header::{HeaderMap, HeaderValue},
Client, Url,
};
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
use reqwest_tracing::TracingMiddleware;
use reqwest_middleware::ClientWithMiddleware;
use serde::Deserialize;
use tracing::trace;
pub struct GitLab {
url: Url,
client: ClientWithMiddleware,
}
// ---
/* "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg",
// "web_url": "http://localhost:3000/john_smith",
// "created_at": "2012-05-23T08:00:58Z",
// "bio": "",
// "location": null,
// "skype": "",
// "linkedin": "",
// "twitter": "",
// "discord": "",
// "website_url": "",
// "organization": "",
// "job_title": "",
// "pronouns": "he/him",
// "bot": false,
// "work_information": null,
// "followers": 0,
// "following": 0,
// "local_time": "3:38 PM",
// "last_sign_in_at": "2012-06-01T11:41:01Z",
// "confirmed_at": "2012-05-23T09:05:22Z",
// "theme_id": 1,
// "last_activity_on": "2012-05-23",
// "color_scheme_id": 2,
// "projects_limit": 100,
// "current_sign_in_at": "2012-06-02T06:36:55Z",
// "identities": [
// {"provider": "github", "extern_uid": "2435223452345"},
// {"provider": "bitbucket", "extern_uid": "john_smith"},
// {"provider": "google_oauth2", "extern_uid": "8776128412476123468721346"}
// ],
// "can_create_group": true,
// "can_create_project": true,
// "two_factor_enabled": true,
// "external": false,
// "private_profile": false,
// "commit_email": "admin@example.com",
// }
*/
#[derive(Deserialize, Debug)]
pub struct User {
pub id: u64,
@ -67,35 +24,25 @@ pub struct User {
}
impl GitLab {
pub fn try_new(url: &str, token: &str) -> Result<Self, anyhow::Error> {
let url = Url::parse(url)?;
pub fn try_new(url: &str, token: &str) -> Result<Self> {
let url = Url::parse(&format!("{}{}", url.trim_end_matches('/'), "/"))?;
let mut headers = HeaderMap::new();
headers.insert("PRIVATE-TOKEN", HeaderValue::from_str(token)?);
let proxy = reqwest::Proxy::all("socks5h://localhost:9982")?;
headers.insert("content-type", HeaderValue::from_str("application/json")?);
headers.insert("accepts", HeaderValue::from_str("application/json")?);
let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3);
let base_client = Client::builder()
.default_headers(headers)
.proxy(proxy)
.build()?;
let client = ClientBuilder::new(base_client)
.with(TracingMiddleware::default())
.with(RetryTransientMiddleware::new_with_policy(retry_policy))
.build();
let base_client = Client::builder().default_headers(headers).build()?;
let client = crate::client::wrap_with_middleware(base_client);
Ok(Self { url, client })
}
pub fn url(&self, path: &str) -> Result<Url, anyhow::Error> {
Ok(self.url.join(path)?)
pub fn url(&self, path: &str) -> Result<Url> {
Ok(self.url.join(path.trim_start_matches('/'))?)
}
pub async fn me(&self) -> Result<User, anyhow::Error> {
pub async fn me(&self) -> Result<User> {
let res = self.client.get(self.url("/user")?).send().await?;
trace!("{res:?}");
let body = res.text().await?;
trace!("{body:?}");
Ok(serde_json::from_str::<User>(&body)?)
// Ok(res.json::<User>().await?)
Ok(res.json().await?)
}
}

View File

@ -1 +1,117 @@
use crate::result::Result;
use reqwest::{
header::{HeaderMap, HeaderValue},
Client, Url,
};
use reqwest_middleware::ClientWithMiddleware;
use serde::Deserialize;
use tokio::spawn;
use tracing::debug;
pub struct Jira {
url: Url,
client: ClientWithMiddleware,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct User {
pub account_id: String,
pub email_address: String,
pub account_type: String,
pub display_name: String,
pub active: bool,
pub time_zone: String,
pub locale: String,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Issue {
pub id: String,
pub key: String,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct IssueSearch {
issues: Vec<Issue>,
max_results: usize,
pub start_at: usize,
total: usize,
}
impl Jira {
pub fn try_new(url: &str, token: &str) -> Result<Self> {
let url = Url::parse(&format!("{}{}", url.trim_end_matches('/'), "/"))?;
let mut headers = HeaderMap::new();
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Basic {}", token.trim()))?,
);
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);
Ok(Self { url, client })
}
pub fn url(&self, path: &str) -> Result<Url> {
Ok(self.url.join(path.trim_start_matches('/'))?)
}
pub async fn me(&self) -> Result<User> {
let res = self
.client
.get(self.url("/rest/api/3/myself")?)
.send()
.await?;
Ok(res.json().await?)
}
pub async fn jql(&self, jql: &str) -> Result<Vec<Issue>> {
debug!("Fetching issues for jql {}", jql);
let mut results = self
.client
.get(self.url("/rest/api/3/search")?)
.query(&[("jql", jql)])
.send()
.await?
.json::<IssueSearch>()
.await?;
// TODO: parallelize after first page
// TODO: are there rate limits to be concerned about
let mut issues = Vec::with_capacity(results.total);
issues.append(&mut results.issues);
let mut futures = Vec::with_capacity((results.total / results.max_results) + 1);
for i in 1..futures.capacity() {
let start_at = i * results.max_results;
debug!("Fetching issues for jql {} (page {})", jql, i);
futures.push(spawn(
self.client
.get(self.url("/rest/api/3/search")?)
.query(&[("jql", jql), ("startAt", &start_at.to_string())])
.send(),
));
}
for task in futures {
let mut result = task.await.unwrap()?.json::<IssueSearch>().await?;
issues.append(&mut result.issues);
}
Ok(issues)
}
pub async fn assigned_open_issues(&self) -> Result<Vec<Issue>> {
let me = self.me().await?;
let jql = format!("assignee = {0} and statusCategory != Done", me.account_id);
self.jql(&jql).await
}
}

View File

@ -1,37 +1,44 @@
mod client;
mod config;
mod gitlab;
mod jira;
mod result;
mod tasks;
use crate::result::Result;
use tasks::Tasks;
use tracing::{info, instrument};
use tracing::{error, info};
use tracing_subscriber::{filter::LevelFilter, EnvFilter};
const ANSI_CLEAR: &'static str = "\x1b[2J\x1b[1;1H";
mod config;
mod gitlab;
mod jira;
mod tasks;
#[instrument]
pub fn init() {
#[tokio::main]
async fn main() -> Result<()> {
color_eyre::install().expect("Failed to install color_eyre");
setup_trace_logger();
info!("Instrumentation initialized.");
}
#[instrument]
pub fn setup_trace_logger() {
let filter = EnvFilter::builder()
.with_default_directive(LevelFilter::TRACE.into())
.parse_lossy("trace,tasks=trace");
.parse_lossy("info,tasks=trace");
tracing_subscriber::fmt().with_env_filter(filter).init();
match run().await {
Ok(()) => Ok(()),
Err(err) => {
error!("{err}");
Err(err)
}
}
}
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
init();
tracing::info!("{ANSI_CLEAR}Hallo, mate!");
async fn run() -> Result<()> {
print!("{ANSI_CLEAR}");
let tasks = Tasks::try_new()?;
let gitlab_user = tasks.gitlab.me().await?;
info!("{gitlab_user:#?}");
let jira_user = tasks.jira.me().await?;
info!("{jira_user:#?}");
let issues = tasks.jira.assigned_open_issues().await?;
info!("{issues:#?}");
info!("{}", issues.len());
Ok(())
}

3
src/result.rs Normal file
View File

@ -0,0 +1,3 @@
use anyhow::Error;
pub type Result<T> = std::result::Result<T, Error>;

View File

@ -1,14 +1,32 @@
use std::env;
use std::{env, process::Command};
use crate::gitlab::GitLab;
use crate::{gitlab::GitLab, jira::Jira, result::Result};
pub struct Tasks {
pub gitlab: GitLab,
pub jira: Jira,
}
impl Tasks {
pub fn try_new() -> Result<Self, anyhow::Error> {
let gitlab = GitLab::try_new("https://git.hq.bill.com/api/v4", &env::var("GITLAB_TOKEN")?)?;
Ok(Self { gitlab })
pub fn try_new() -> Result<Self> {
let gl_token = env::var("GITLAB_TOKEN").or_else(|_| -> Result<String> {
let output = Command::new("pass")
.arg("client/divvy/gitlab-glpat")
.output()?;
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
})?;
let gitlab = GitLab::try_new("https://git.hq.bill.com/api/v4", &gl_token)?;
let jira_token = env::var("JIRA_TOKEN").or_else(|_| -> Result<String> {
let output = Command::new("pass")
.arg("client/divvy/jira-api-token-with-email")
.output()?;
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
})?;
let jira = Jira::try_new("https://billcom.atlassian.net", &jira_token)?;
Ok(Self { gitlab, jira })
}
}