challenge: 5
This commit is contained in:
parent
2eb4135c71
commit
ef5b3820e2
@ -5,10 +5,14 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { version = "0.7.4", features = ["query"] }
|
axum = { version = "0.7.4", features = ["query"] }
|
||||||
|
cargo-manifest = "0.17.0"
|
||||||
serde = { version = "1.0.215", features = ["derive"] }
|
serde = { version = "1.0.215", features = ["derive"] }
|
||||||
|
serde_json = "1.0.133"
|
||||||
|
serde_yml = "0.0.12"
|
||||||
shuttle-axum = "0.49.0"
|
shuttle-axum = "0.49.0"
|
||||||
shuttle-runtime = "0.49.0"
|
shuttle-runtime = "0.49.0"
|
||||||
tokio = "1.28.2"
|
tokio = "1.28.2"
|
||||||
|
toml = "0.8.19"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
axum-test = "16.4.0"
|
axum-test = "16.4.0"
|
||||||
|
221
src/lib.rs
221
src/lib.rs
@ -1,7 +1,8 @@
|
|||||||
mod routes;
|
mod routes;
|
||||||
|
|
||||||
|
use axum::routing::post;
|
||||||
pub use routes::hello_world;
|
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 {
|
pub fn router() -> axum::Router {
|
||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
@ -14,6 +15,7 @@ pub fn router() -> axum::Router {
|
|||||||
.route("/2/key", get(ipv4_key))
|
.route("/2/key", get(ipv4_key))
|
||||||
.route("/2/v6/dest", get(ipv6_dest))
|
.route("/2/v6/dest", get(ipv6_dest))
|
||||||
.route("/2/v6/key", get(ipv6_key))
|
.route("/2/v6/key", get(ipv6_key))
|
||||||
|
.route("/5/manifest", post(manifest))
|
||||||
.route("/", get(hello_bird))
|
.route("/", get(hello_bird))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,4 +133,221 @@ mod test {
|
|||||||
let response = server.get("/2/v6/dest?from=fe80::1&to=invalid-to").await;
|
let response = server.get("/2/v6/dest?from=fe80::1&to=invalid-to").await;
|
||||||
response.assert_status_bad_request();
|
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_bird;
|
||||||
mod hello_world;
|
mod hello_world;
|
||||||
mod minus_one;
|
mod minus_one;
|
||||||
|
mod task_five;
|
||||||
mod task_two;
|
mod task_two;
|
||||||
|
|
||||||
pub use hello_bird::hello_bird;
|
pub use hello_bird::hello_bird;
|
||||||
pub use hello_world::hello_world;
|
pub use hello_world::hello_world;
|
||||||
pub use minus_one::minus_one;
|
pub use minus_one::minus_one;
|
||||||
|
pub use task_five::manifest;
|
||||||
pub use task_two::ipv4_dest;
|
pub use task_two::ipv4_dest;
|
||||||
pub use task_two::ipv4_key;
|
pub use task_two::ipv4_key;
|
||||||
pub use task_two::ipv6_dest;
|
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