challenge: 5

This commit is contained in:
itsscb 2024-12-06 22:26:07 +01:00
parent 2eb4135c71
commit ef5b3820e2
5 changed files with 419 additions and 1 deletions

View File

@ -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"

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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<String, Box<dyn std::error::Error>> {
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<String, Box<dyn std::error::Error>> {
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<String, Response> {
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())
}

View File

@ -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<Order>);
impl Orders {
pub const fn new(orders: Vec<Order>) -> Self {
Self(orders)
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
impl From<&Vec<TomlValue>> for Orders {
fn from(value: &Vec<TomlValue>) -> 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::<Self>()
}
}
impl std::iter::FromIterator<Order> for Orders {
fn from_iter<I: IntoIterator<Item = Order>>(iter: I) -> Self {
let orders: Vec<Order> = 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");
}
}