From 26a5db4e64d70c11cf5fca21c9ab06a257e5e527 Mon Sep 17 00:00:00 2001 From: Kent Tristan Yves Sarmiento Date: Mon, 18 Dec 2023 10:55:24 +0000 Subject: [PATCH] feat: POST /v1/links support --- Cargo.lock | 167 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 4 +- src/controller/links.rs | 25 ++++-- src/repository/links.rs | 4 +- src/service/links.rs | 12 +-- src/types.rs | 1 + src/types/links.rs | 13 ++-- src/types/repository.rs | 2 +- src/types/request.rs | 12 +++ src/types/service.rs | 2 +- tests/links.rs | 53 +++++++++++-- 11 files changed, 265 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3c8bda5..84d1f8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,21 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstyle" version = "1.0.4" @@ -157,6 +172,12 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + [[package]] name = "bytes" version = "1.5.0" @@ -181,6 +202,25 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "downcast" version = "0.11.0" @@ -318,6 +358,17 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gimli" version = "0.28.1" @@ -506,6 +557,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -541,6 +615,15 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +[[package]] +name = "js-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lambda_http" version = "0.8.3" @@ -663,6 +746,7 @@ name = "link-for-later" version = "0.1.0" dependencies = [ "axum", + "chrono", "http-body-util", "lambda_http", "lambda_runtime 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)", @@ -673,6 +757,7 @@ dependencies = [ "tower", "tracing", "tracing-subscriber", + "uuid", ] [[package]] @@ -746,6 +831,15 @@ dependencies = [ "syn", ] +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -1184,6 +1278,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "uuid" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +dependencies = [ + "getrandom", + "serde", +] + [[package]] name = "want" version = "0.3.1" @@ -1199,6 +1303,60 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" + [[package]] name = "winapi" version = "0.3.9" @@ -1221,6 +1379,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index d9cb0b5..161df33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,16 +10,18 @@ publish = false [dependencies] axum = "0.7.2" +chrono = { version = "0.4.31", default-features = false, features=["clock", "serde"] } http-body-util = "0.1.0" #lambda_http = "0.8.3" lambda_http = { git = "https://github.com/awslabs/aws-lambda-rust-runtime", branch = "hyper1_upgrade" } lambda_runtime = "0.8.3" -serde = { version = "1.0.193", features = [ "derive" ] } +serde = { version = "1.0.193", features = ["derive"] } serde_json = "1.0.108" tokio = { version = "1", features = ["macros"] } tower = "0.4.13" tracing = { version = "0.1", features = ["log"] } tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } +uuid = { version = "1.6.1", features = ["v4", "serde"] } [dev-dependencies] mockall = "0.12.0" diff --git a/src/controller/links.rs b/src/controller/links.rs index ca85727..533af83 100644 --- a/src/controller/links.rs +++ b/src/controller/links.rs @@ -1,12 +1,12 @@ use axum::{ - extract::{Path, State}, + extract::{self, Path, State}, http::StatusCode, - response::{IntoResponse, Json}, - routing, Router, + response::IntoResponse, + routing, Json, Router, }; use serde_json::json; -use crate::types::state::Router as RouterState; +use crate::types::{request::PostLink as PostLinkRequest, state::Router as RouterState}; const LINKS_ROUTE: &str = "/v1/links"; const LINKS_ID_ROUTE: &str = "/v1/links/:id"; @@ -35,10 +35,17 @@ async fn list(State(app_state): State) -> impl IntoResponse { } } -async fn post(State(app_state): State) -> impl IntoResponse { +async fn post( + State(app_state): State, + Json(payload): extract::Json, +) -> impl IntoResponse { let links_service = app_state.get_links_service(); - match links_service.post(&app_state).await { - Ok(link) => Json(link).into_response(), + + match links_service + .post(&app_state, &payload.to_string().into()) + .await + { + Ok(link) => (StatusCode::CREATED, Json(link)).into_response(), Err(e) => { tracing::error!("Error: {}", e); ( @@ -131,10 +138,12 @@ mod tests { async fn test_get_links_non_empty() { let mut mock_links_service = MockService::new(); let mock_links_repo = MockRepository::new(); + let item: LinkItem = "http://link".into(); + mock_links_service .expect_list() .times(1) - .returning(|_| Ok(vec![LinkItem::new("http://link")])); + .returning(move |_| Ok(vec![item.clone()])); let app_state = RouterState::new(Arc::new(mock_links_service), Arc::new(mock_links_repo)); diff --git a/src/repository/links.rs b/src/repository/links.rs index ade4d7a..ac10751 100644 --- a/src/repository/links.rs +++ b/src/repository/links.rs @@ -13,8 +13,8 @@ impl Links for Repository { Ok(vec![]) } - async fn post(&self) -> Result { - Err("Not implemented".into()) + async fn post(&self, item: &LinkItem) -> Result { + Ok(item.clone()) } async fn get(&self, _id: &str) -> Result { diff --git a/src/service/links.rs b/src/service/links.rs index acac4e6..7ffe70e 100644 --- a/src/service/links.rs +++ b/src/service/links.rs @@ -15,9 +15,9 @@ impl Links for Service { links_repo.list().await } - async fn post<'a>(&self, app_state: &'a state::Router) -> Result { + async fn post<'a>(&self, app_state: &'a state::Router, item: &LinkItem) -> Result { let links_repo = app_state.get_links_repo(); - links_repo.post().await + links_repo.post(item).await } async fn get<'a>(&self, id: &str, app_state: &'a state::Router) -> Result { @@ -69,12 +69,14 @@ mod tests { async fn test_get_links_non_empty() { let mock_links_service = MockService::new(); let mut mock_links_repo = MockRepository::new(); + + let item: LinkItem = "http://link".into(); + let expected_items = vec![item.clone()]; + mock_links_repo .expect_list() .times(1) - .returning(|| Ok(vec![LinkItem::new("http://link")])); - - let expected_items = vec![LinkItem::new("http://link")]; + .returning(move || Ok(vec![item.clone()])); let app_state = RouterState::new(Arc::new(mock_links_service), Arc::new(mock_links_repo)); diff --git a/src/types.rs b/src/types.rs index 180b07b..59b9ac0 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,4 +1,5 @@ pub mod links; pub mod repository; +pub mod request; pub mod service; pub mod state; diff --git a/src/types/links.rs b/src/types/links.rs index 923bf23..89401c4 100644 --- a/src/types/links.rs +++ b/src/types/links.rs @@ -1,6 +1,8 @@ +use chrono::Utc; use serde::{Deserialize, Serialize}; +use uuid::Uuid; -#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct LinkItem { id: String, owner: String, @@ -11,11 +13,12 @@ pub struct LinkItem { updated_at: String, } -impl LinkItem { - #[allow(dead_code)] - pub fn new(url: &str) -> Self { +impl From for LinkItem { + fn from(link_url: T) -> Self { Self { - url: url.to_string(), + url: link_url.to_string(), + id: Uuid::new_v4().to_string(), + created_at: Utc::now().to_rfc3339(), ..Default::default() } } diff --git a/src/types/repository.rs b/src/types/repository.rs index c0c9960..5996856 100644 --- a/src/types/repository.rs +++ b/src/types/repository.rs @@ -14,7 +14,7 @@ pub type Result = std::result::Result>; pub trait Links { async fn list(&self) -> Result>; - async fn post(&self) -> Result; + async fn post(&self, item: &LinkItem) -> Result; async fn get(&self, id: &str) -> Result; diff --git a/src/types/request.rs b/src/types/request.rs index e69de29..37b76c9 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct PostLink { + url: String, +} + +impl ToString for PostLink { + fn to_string(&self) -> String { + self.url.clone() + } +} diff --git a/src/types/service.rs b/src/types/service.rs index d900a29..ac1f205 100644 --- a/src/types/service.rs +++ b/src/types/service.rs @@ -14,7 +14,7 @@ pub type Result = std::result::Result>; pub trait Links { async fn list<'a>(&self, app_state: &'a state::Router) -> Result>; - async fn post<'a>(&self, app_state: &'a state::Router) -> Result; + async fn post<'a>(&self, app_state: &'a state::Router, item: &LinkItem) -> Result; async fn get<'a>(&self, id: &str, app_state: &'a state::Router) -> Result; diff --git a/tests/links.rs b/tests/links.rs index 34f171d..66e1615 100644 --- a/tests/links.rs +++ b/tests/links.rs @@ -1,23 +1,23 @@ -use std::error::Error; - use axum::{ body::Body, http::{Request, StatusCode}, }; use http_body_util::BodyExt; +use serde_json::Value; use tower::ServiceExt; // Verifies GET /v1/links request // -// GIVEN empty set of link items in the Server -// WHEN GET /v1/links request is sent to the Server -// THEN the response is 200 OK and empty set of link items are returned +// GIVEN an empty set of link items in the Server +// WHEN GET /v1/links request is sent to the Server +// THEN the response is 200 OK and empty set of link items are returned #[tokio::test] -async fn test_get_links_empty() -> Result<(), Box> { +async fn test_get_links() { let handler = link_for_later::router::new(); let response = handler .oneshot( Request::builder() + .method("GET") .uri("/v1/links") .body(Body::empty()) .unwrap(), @@ -29,5 +29,44 @@ async fn test_get_links_empty() -> Result<(), Box> { let body = response.into_body().collect().await.unwrap().to_bytes(); assert_eq!(&body[..], b"[]"); - Ok(()) +} + +// Verifies POST /v1/links request +// +// GIVEN an empty set of link items in the Server +// AND a request to post a link +// WHEN POST /v1/links request is sent to the Server +// THEN the response is 201 CREATED and created link item is returned +#[tokio::test] +async fn test_post_links() { + let request = r#"{ + "url": "http://link" + }"#; + + let handler = link_for_later::router::new(); + let response = handler + .oneshot( + Request::builder() + .method("POST") + .uri("/v1/links") + .header("Content-Type", "application/json") + .body(Body::from(request)) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::CREATED); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let body = std::str::from_utf8(&body).unwrap(); + let body: Value = serde_json::from_str(body).unwrap(); + + assert!(body["id"] != ""); + assert!(body["owner"] == ""); + assert!(body["url"] == "http://link"); + assert!(body["title"] == ""); + assert!(body["description"] == ""); + assert!(body["created_at"] != ""); + assert!(body["updated_at"] == ""); }