diff --git a/Cargo.lock b/Cargo.lock index cfe440e..faf9a0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -329,9 +329,11 @@ dependencies = [ "configparser", "contextswitch-types", "dotenv", + "lazy_static", "listenfd", "mktemp", "once_cell", + "regex", "reqwest", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index bc262f3..814ef51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,9 @@ tracing = { version = "0.1.0", features = ["log"] } tracing-subscriber = { version = "0.3.0", features = ["std", "env-filter", "fmt", "json"] } tracing-log = "0.1.0" tracing-actix-web = "=0.5.0-beta.9" +regex = "1.5.0" +lazy_static = "1.4.0" [dev-dependencies] once_cell = "1.0" -reqwest = "0.11.0" +reqwest = { version = "0.11.0", features = ["json"] } diff --git a/src/contextswitch.rs b/src/contextswitch.rs index 629892a..25f4ff7 100644 --- a/src/contextswitch.rs +++ b/src/contextswitch.rs @@ -43,3 +43,7 @@ pub fn export(filters: Vec<&str>) -> Result, Error> { .collect(); tasks } + +pub fn add(add_args: Vec<&str>) -> Result { + taskwarrior::add(add_args) +} diff --git a/src/lib.rs b/src/lib.rs index 2e282c0..2b1b411 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,16 @@ -use actix_web::{dev::Server, middleware, web, App, HttpResponse, HttpServer}; +use actix_web::{dev::Server, http, middleware, web, App, HttpResponse, HttpServer}; +use contextswitch_types::TaskDefinition; use listenfd::ListenFd; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; +use serde_json::json; use std::env; use std::io::Error; use std::net::TcpListener; use tracing_actix_web::TracingLogger; +#[macro_use] +extern crate lazy_static; + pub mod contextswitch; pub mod observability; pub mod taskwarrior; @@ -15,7 +20,7 @@ struct TaskQuery { filter: Option, } -#[tracing::instrument(level = "debug", skip(task_query))] +#[tracing::instrument(level = "debug", skip_all, fields(filter = %task_query.filter.as_ref().unwrap_or(&"".to_string())))] async fn list_tasks(task_query: web::Query) -> Result { let filter = task_query .filter @@ -28,6 +33,19 @@ async fn list_tasks(task_query: web::Query) -> Result) -> Result { + let task_id = contextswitch::add(task_definition.definition.split(' ').collect())?; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .body(json!({ "id": task_id }).to_string())) +} + +async fn option_task() -> HttpResponse { + HttpResponse::Ok().finish() +} + async fn health_check() -> HttpResponse { HttpResponse::Ok().finish() } @@ -41,10 +59,17 @@ pub fn run(listener: TcpListener) -> Result { .wrap(middleware::Compress::default()) .wrap( middleware::DefaultHeaders::new() - .add(("Access-Control-Allow-Origin", cs_front_base_url.as_bytes())), + .add(("Access-Control-Allow-Origin", cs_front_base_url.as_bytes())) + .add(( + "Access-Control-Allow-Methods", + "POST, GET, OPTIONS".as_bytes(), + )) + .add(("Access-Control-Allow-Headers", "content-type".as_bytes())), ) .route("/ping", web::get().to(health_check)) .route("/tasks", web::get().to(list_tasks)) + .route("/tasks", web::post().to(add_task)) + .route("/tasks", web::method(http::Method::OPTIONS).to(option_task)) }) .keep_alive(60) .shutdown_timeout(60); diff --git a/src/taskwarrior.rs b/src/taskwarrior.rs index 914a456..ae1d455 100644 --- a/src/taskwarrior.rs +++ b/src/taskwarrior.rs @@ -1,9 +1,10 @@ use chrono::{DateTime, Utc}; use configparser::ini::Ini; +use regex::Regex; use serde::{Deserialize, Serialize}; use serde_json; use std::env; -use std::io::Error; +use std::io::{Error, ErrorKind}; use std::path::Path; use std::process::Command; use std::str; @@ -13,7 +14,7 @@ use uuid::Uuid; #[derive(Debug, Serialize, Deserialize, PartialEq)] pub struct Task { pub uuid: Uuid, - pub id: u32, + pub id: u64, #[serde(with = "contextswitch_types::tw_date_format")] pub entry: DateTime, #[serde(with = "contextswitch_types::tw_date_format")] @@ -99,11 +100,8 @@ pub fn load_config(task_data_location: Option<&str>) -> String { #[tracing::instrument(level = "debug")] pub fn export(filters: Vec<&str>) -> Result, Error> { - let mut args = vec!["export"]; - args.extend(filters); + let args = [filters, vec!["export"]].concat(); let export_output = Command::new("task").args(args).output()?; - let output = String::from_utf8(export_output.stdout.clone()).unwrap(); - debug!("export output: {}", output); let tasks: Vec = serde_json::from_slice(&export_output.stdout)?; @@ -111,12 +109,31 @@ pub fn export(filters: Vec<&str>) -> Result, Error> { } #[tracing::instrument(level = "debug")] -pub fn add(add_args: Vec<&str>) -> Result<(), Error> { +pub fn add(add_args: Vec<&str>) -> Result { let mut args = vec!["add"]; args.extend(add_args); let add_output = Command::new("task").args(args).output()?; let output = String::from_utf8(add_output.stdout).unwrap(); - debug!("add output: {}", output); + lazy_static! { + static ref RE: Regex = Regex::new(r"Created task (?P\d+).").unwrap(); + } + let task_id_capture = RE.captures(&output).ok_or_else(|| { + Error::new( + ErrorKind::Other, + format!("Cannot extract task ID from: {}", &output), + ) + })?; + let task_id_str = task_id_capture + .name("id") + .ok_or_else(|| { + Error::new( + ErrorKind::Other, + format!("Cannot extract task ID value from: {}", &output), + ) + })? + .as_str(); - Ok(()) + task_id_str + .parse::() + .map_err(|_| Error::new(ErrorKind::Other, "Cannot parse task ID value")) } diff --git a/tests/task.rs b/tests/task.rs index 9212647..3a0f822 100644 --- a/tests/task.rs +++ b/tests/task.rs @@ -1,27 +1,53 @@ pub mod test_helper; -use contextswitch_api::taskwarrior; +use contextswitch_api::{taskwarrior, TaskDefinition}; use contextswitch_types::Task; #[tokio::test] async fn list_tasks() { let address = test_helper::spawn_app(); - let task_data_path = test_helper::setup_tasks(); - let client = reqwest::Client::new(); - taskwarrior::add(vec!["test1", "contextswitch:'{\"test\": 1}'"]).unwrap(); + let task_id = + taskwarrior::add(vec!["test", "list_tasks", "contextswitch:'{\"test\": 1}'"]).unwrap(); - let response: reqwest::Response = client - .get(&format!("{}/tasks", &address)) + let tasks: Vec = reqwest::Client::new() + .get(&format!("{}/tasks?filter={}", &address, task_id)) .send() .await - .expect("Failed to execute request."); + .expect("Failed to execute request") + .json() + .await + .expect("Cannot parse JSON result"); - test_helper::clear_tasks(task_data_path); - let text_body = response.text_with_charset("utf-8").await.unwrap(); - let tasks: Vec = serde_json::from_str(&text_body).unwrap(); assert_eq!(tasks.len(), 1); - assert_eq!(tasks[0].description, "test1"); - + assert_eq!(tasks[0].description, "test list_tasks"); let cs_metadata = tasks[0].contextswitch.as_ref().unwrap(); assert_eq!(cs_metadata.test, 1); } + +#[tokio::test] +async fn add_task() { + let address = test_helper::spawn_app(); + println!("add_task address: {}", address); + + let response: serde_json::Value = reqwest::Client::new() + .post(&format!("{}/tasks", &address)) + .json(&TaskDefinition { + definition: "test add_task contextswitch:{\"test\":1}".to_string(), + }) + .send() + .await + .expect("Failed to execute request") + .json() + .await + .expect("Cannot parse JSON result"); + let new_task_id = response["id"].as_u64().unwrap(); + let tasks = taskwarrior::export(vec![&new_task_id.to_string()]).unwrap(); + + assert_eq!(tasks.len(), 1); + assert_eq!(tasks[0].id, new_task_id); + assert_eq!(tasks[0].description, "test add_task"); + assert_eq!( + tasks[0].contextswitch.as_ref().unwrap(), + &"{\"test\":1}".to_string() + ); +} diff --git a/tests/test_helper.rs b/tests/test_helper.rs index f932428..3004e9b 100644 --- a/tests/test_helper.rs +++ b/tests/test_helper.rs @@ -10,23 +10,31 @@ static TRACING: Lazy<()> = Lazy::new(|| { init_subscriber(subscriber); }); -pub fn spawn_app() -> String { - Lazy::force(&TRACING); - +static SERVER_ADDRESS: Lazy = Lazy::new(|| { let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind random port"); let port = listener.local_addr().unwrap().port(); let server = contextswitch_api::run(listener).expect("Failed to bind address"); let _ = tokio::spawn(server); format!("http://127.0.0.1:{}", port) -} +}); -pub fn setup_tasks() -> String { +static TASK_DATA_LOCATION: Lazy = Lazy::new(|| { let tmp_dir = Temp::new_dir().unwrap(); let task_data_location = taskwarrior::load_config(tmp_dir.to_str()); tmp_dir.release(); - return task_data_location; + task_data_location +}); + +pub fn spawn_app() -> String { + Lazy::force(&TRACING); + setup_tasks(); + Lazy::force(&SERVER_ADDRESS).to_string() +} + +pub fn setup_tasks() -> String { + Lazy::force(&TASK_DATA_LOCATION).to_string() } pub fn clear_tasks(task_data_location: String) {