From ef5b3820e2b9cb8c8ce634a877b7df4cbab284f2 Mon Sep 17 00:00:00 2001 From: itsscb Date: Fri, 6 Dec 2024 22:26:07 +0100 Subject: [PATCH] challenge: 5 --- Cargo.toml | 4 + src/lib.rs | 221 ++++++++++++++++++++++++++++++- src/routes/mod.rs | 2 + src/routes/task_five/manifest.rs | 95 +++++++++++++ src/routes/task_five/mod.rs | 98 ++++++++++++++ 5 files changed, 419 insertions(+), 1 deletion(-) create mode 100644 src/routes/task_five/manifest.rs create mode 100644 src/routes/task_five/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 13beb15..5a92b54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,10 +5,14 @@ edition = "2021" [dependencies] axum = { version = "0.7.4", features = ["query"] } +cargo-manifest = "0.17.0" serde = { version = "1.0.215", features = ["derive"] } +serde_json = "1.0.133" +serde_yml = "0.0.12" shuttle-axum = "0.49.0" shuttle-runtime = "0.49.0" tokio = "1.28.2" +toml = "0.8.19" [dev-dependencies] axum-test = "16.4.0" diff --git a/src/lib.rs b/src/lib.rs index d21b250..a1ce688 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,8 @@ mod routes; +use axum::routing::post; pub use routes::hello_world; -use routes::{hello_bird, ipv4_dest, ipv4_key, ipv6_dest, ipv6_key, minus_one}; +use routes::{hello_bird, ipv4_dest, ipv4_key, ipv6_dest, ipv6_key, manifest, minus_one}; pub fn router() -> axum::Router { use axum::routing::get; @@ -14,6 +15,7 @@ pub fn router() -> axum::Router { .route("/2/key", get(ipv4_key)) .route("/2/v6/dest", get(ipv6_dest)) .route("/2/v6/key", get(ipv6_key)) + .route("/5/manifest", post(manifest)) .route("/", get(hello_bird)) } @@ -131,4 +133,221 @@ mod test { let response = server.get("/2/v6/dest?from=fe80::1&to=invalid-to").await; response.assert_status_bad_request(); } + + #[tokio::test] + async fn test_manifest_1() { + let server = test_server(); + + let want = "Toy car: 2\nLego brick: 230"; + + let payload = r#"[package] +name = "not-a-gift-order" +authors = ["Not Santa"] +keywords = ["Christmas 2024"] + +[[package.metadata.orders]] +item = "Toy car" +quantity = 2 + +[[package.metadata.orders]] +item = "Lego brick" +quantity = 230"#; + let response = server + .post("/5/manifest") + .text(payload) + .content_type("application/toml") + .await; + + response.assert_status_ok(); + response.assert_text(want); + + let payload = r#"[package] +name = "coal-in-a-bowl" +authors = ["H4CK3R_13E7"] +keywords = ["Christmas 2024"] + +[[package.metadata.orders]] +item = "Coal" +quantity = "Hahaha get rekt""#; + + let response = server + .post("/5/manifest") + .text(payload) + .content_type("application/toml") + .await; + response.assert_status(StatusCode::NO_CONTENT); + } + + #[tokio::test] + async fn test_manifest_2() { + let server = test_server(); + + let payload = r#"[package] +name = false +authors = ["Not Santa"] +keywords = ["Christmas 2024"]"#; + + let response = server + .post("/5/manifest") + .text(payload) + .content_type("application/toml") + .await; + response.assert_status(StatusCode::BAD_REQUEST); + + let payload = r#"[package] +name = "not-a-gift-order" +authors = ["Not Santa"] +keywords = ["Christmas 2024"] + +[profile.release] +incremental = "stonks""#; + let response = server + .post("/5/manifest") + .text(payload) + .content_type("application/toml") + .await; + response.assert_status(StatusCode::BAD_REQUEST); + } + + #[tokio::test] + async fn test_manifest_3() { + let server = test_server(); + + let want = "Toy car: 2\nLego brick: 230"; + + let payload = r#"[package] +name = "not-a-gift-order" +authors = ["Not Santa"] +keywords = ["Christmas 2024"] + +[[package.metadata.orders]] +item = "Toy car" +quantity = 2 + +[[package.metadata.orders]] +item = "Lego brick" +quantity = 230"#; + let response = server + .post("/5/manifest") + .text(payload) + .content_type("application/toml") + .await; + + response.assert_status_ok(); + response.assert_text(want); + + let payload = r#"[package] +name = "not-a-gift-order" +authors = ["Not Santa"] + +[[package.metadata.orders]] +item = "Toy car" +quantity = 2 + +[[package.metadata.orders]] +item = "Lego brick" +quantity = 230"#; + let response = server + .post("/5/manifest") + .text(payload) + .content_type("application/toml") + .await; + + response.assert_status(StatusCode::BAD_REQUEST); + response.assert_text("Magic keyword not provided"); + + let payload = r#"[package] +name = "grass" +authors = ["A vegan cow"] +keywords = ["Moooooo"]"#; + + let response = server + .post("/5/manifest") + .text(payload) + .content_type("application/toml") + .await; + response.assert_status(StatusCode::BAD_REQUEST); + response.assert_text("Magic keyword not provided"); + + let payload = r#"[package] +name = "grass" +authors = ["A vegan cow"] +keywords = ["Christmas 2024"]"#; + + let response = server + .post("/5/manifest") + .text(payload) + .content_type("application/toml") + .await; + response.assert_status(StatusCode::NO_CONTENT); + } + + #[tokio::test] + async fn test_manifest_4() { + let server = test_server(); + + let payload = r#"[package] +name = "grass" +authors = ["A vegan cow"] +keywords = ["Christmas 2024"]"#; + + let response = server + .post("/5/manifest") + .text(payload) + .content_type("text/html") + .await; + response.assert_status(StatusCode::UNSUPPORTED_MEDIA_TYPE); + + let want = "Toy train: 5"; + + let payload = r#"package: + name: big-chungus-sleigh + version: "2.0.24" + metadata: + orders: + - item: "Toy train" + quantity: 5 + rust-version: "1.69" + keywords: + - "Christmas 2024""#; + + let response = server + .post("/5/manifest") + .text(payload) + .content_type("application/yaml") + .await; + + response.assert_status_ok(); + response.assert_text(want); + + let want = "Toy train: 5"; + + let payload = r#"{ + "package": { + "name": "big-chungus-sleigh", + "version": "2.0.24", + "metadata": { + "orders": [ + { + "item": "Toy train", + "quantity": 5 + } + ] + }, + "rust-version": "1.69", + "keywords": [ + "Christmas 2024" + ] + } +}"#; + + let response = server + .post("/5/manifest") + .text(payload) + .content_type("application/json") + .await; + + response.assert_status_ok(); + response.assert_text(want); + } } diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 0d095b7..02f96e8 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,11 +1,13 @@ mod hello_bird; mod hello_world; mod minus_one; +mod task_five; mod task_two; pub use hello_bird::hello_bird; pub use hello_world::hello_world; pub use minus_one::minus_one; +pub use task_five::manifest; pub use task_two::ipv4_dest; pub use task_two::ipv4_key; pub use task_two::ipv6_dest; diff --git a/src/routes/task_five/manifest.rs b/src/routes/task_five/manifest.rs new file mode 100644 index 0000000..3f0cd36 --- /dev/null +++ b/src/routes/task_five/manifest.rs @@ -0,0 +1,95 @@ +use axum::{ + http::{header::CONTENT_TYPE, HeaderMap, StatusCode}, + response::{IntoResponse, Response}, +}; +use cargo_manifest::{Manifest, Package}; +use toml::Value; + +use super::Orders; + +use serde_json::Value as JsonValue; +use serde_yml::Value as YamlValue; + +pub fn convert_json_to_toml(json: &str) -> Result> { + let value: JsonValue = serde_json::from_str(json)?; + let toml = toml::to_string(&value)?; + Ok(toml) +} + +pub fn convert_yaml_to_toml(yaml: &str) -> Result> { + let value: YamlValue = serde_yml::from_str(yaml)?; + let toml = toml::to_string(&value)?; + Ok(toml) +} + +pub async fn manifest(headers: HeaderMap, payload: String) -> Result { + let payload: String = match headers.get(CONTENT_TYPE) { + Some(content_type) => match content_type.to_str().unwrap() { + "application/toml" => payload, + "application/json" => match convert_json_to_toml(&payload) { + Err(_) => return Err((StatusCode::UNSUPPORTED_MEDIA_TYPE, "").into_response()), + Ok(toml) => toml, + }, + "application/yaml" => match convert_yaml_to_toml(&payload) { + Err(_) => return Err((StatusCode::UNSUPPORTED_MEDIA_TYPE, "").into_response()), + Ok(toml) => toml, + }, + + _ => { + return Err((StatusCode::UNSUPPORTED_MEDIA_TYPE, "").into_response()); + } + }, + None => { + return Err(( + StatusCode::UNSUPPORTED_MEDIA_TYPE, + "Content type not provided", + ) + .into_response()); + } + }; + + let orders: Orders = match Manifest::from_slice_with_metadata(payload.as_bytes()) { + Ok(manifest) => { + let package: Package = match manifest.package { + Some(package) => package, + None => { + return Err( + (StatusCode::BAD_REQUEST, "Invalid manifest: package").into_response() + ); + } + }; + + if let Some(cargo_manifest::MaybeInherited::Local(keywords)) = package.keywords { + if !keywords.iter().any(|k| k == "Christmas 2024") { + return Err( + (StatusCode::BAD_REQUEST, "Magic keyword not provided").into_response() + ); + } + } else { + return Err((StatusCode::BAD_REQUEST, "Magic keyword not provided").into_response()); + } + + let metadata: Value = match package.metadata { + Some(metadata) => metadata, + None => { + return Err((StatusCode::NO_CONTENT, "").into_response()); + } + }; + + Orders::from( + metadata + .get("orders") + .and_then(Value::as_array) + .ok_or_else(|| (StatusCode::NO_CONTENT, "orders").into_response())?, + ) + } + Err(_) => { + return Err((StatusCode::BAD_REQUEST, "Invalid manifest").into_response()); + } + }; + + if orders.0.is_empty() { + return Err((StatusCode::NO_CONTENT, "").into_response()); + } + Ok(orders.to_string()) +} diff --git a/src/routes/task_five/mod.rs b/src/routes/task_five/mod.rs new file mode 100644 index 0000000..98d4bfc --- /dev/null +++ b/src/routes/task_five/mod.rs @@ -0,0 +1,98 @@ +use std::fmt::Display; + +use serde::Deserialize; + +mod manifest; +pub use manifest::manifest; +use toml::Value as TomlValue; + +#[derive(Debug, Clone, Deserialize)] +pub struct Order { + item: String, + quantity: u32, +} + +impl Order { + pub const fn new(item: String, quantity: u32) -> Self { + Self { item, quantity } + } +} + +impl Display for Order { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: {}", self.item, self.quantity) + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Orders(Vec); + +impl Orders { + pub const fn new(orders: Vec) -> Self { + Self(orders) + } +} + +#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] +impl From<&Vec> for Orders { + fn from(value: &Vec) -> Self { + value + .iter() + .filter_map(|order| { + let table = order.as_table()?; + let item = table.get("item")?.as_str()?.trim_matches('"').to_string(); + let quantity = table.get("quantity")?.as_integer()?; + let quantity = if u32::try_from(quantity).is_ok() { + quantity as u32 + } else { + return None; + }; + Some(Order::new(item, quantity)) + }) + .collect::() + } +} + +impl std::iter::FromIterator for Orders { + fn from_iter>(iter: I) -> Self { + let orders: Vec = iter.into_iter().collect(); + Self::new(orders) + } +} + +impl Display for Orders { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let len = &self.0.len(); + for (i, order) in self.0.iter().enumerate() { + write!(f, "{order}")?; + if i + 1 < *len { + writeln!(f)?; + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_order_display() { + let order1 = Order { + item: "Toy car".to_string(), + quantity: 2, + }; + assert_eq!(order1.to_string(), "Toy car: 2"); + + let order2 = Order { + item: "Lego brick".to_string(), + quantity: 230, + }; + assert_eq!(order2.to_string(), "Lego brick: 230"); + + let order_list = Orders::new(vec![order1, order2]); + assert_eq!(order_list.to_string(), "Toy car: 2\nLego brick: 230"); + } +}