challenge: 5
This commit is contained in:
parent
2eb4135c71
commit
ef5b3820e2
@ -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"
|
||||
|
221
src/lib.rs
221
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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
95
src/routes/task_five/manifest.rs
Normal file
95
src/routes/task_five/manifest.rs
Normal 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())
|
||||
}
|
98
src/routes/task_five/mod.rs
Normal file
98
src/routes/task_five/mod.rs
Normal 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");
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user