challenge: 23
This commit is contained in:
parent
91b18785c3
commit
0d1a1c152d
10
Cargo.toml
10
Cargo.toml
@ -29,14 +29,18 @@ sqlx = { version = "0.8.2", features = [
|
||||
"migrate",
|
||||
] }
|
||||
uuid = { version = "1.11.0", features = ["v4"] }
|
||||
tower-http = { version = "0.6.2", features = ["fs"], optional = true }
|
||||
html-escape = "0.2.13"
|
||||
regex = "1.11.1"
|
||||
hex = "0.4.3"
|
||||
|
||||
[dev-dependencies]
|
||||
axum-test = "16.4.0"
|
||||
|
||||
[features]
|
||||
default = ["shuttle-shared-db"]
|
||||
default = ["tower-http", "toml"]
|
||||
task1-9 = ["cargo-manifest", "serde_yml", "toml"]
|
||||
task12 = ["rand"]
|
||||
task16 = ["jsonwebtoken"]
|
||||
task19 = []
|
||||
task23 = []
|
||||
task19 = ["shuttle-shared-db"]
|
||||
task23 = ["tower-http", "toml"]
|
||||
|
2
Shuttle.toml
Normal file
2
Shuttle.toml
Normal file
@ -0,0 +1,2 @@
|
||||
[build]
|
||||
assets = ["assets/*"]
|
277
assets/23.html
Normal file
277
assets/23.html
Normal file
@ -0,0 +1,277 @@
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
<style>
|
||||
body {
|
||||
--darkgrey: #0d0d0d;
|
||||
--red: #a00;
|
||||
--green: #060;
|
||||
--white: #eee;
|
||||
background-color: var(--darkgrey);
|
||||
color: var(--white);
|
||||
}
|
||||
main {
|
||||
max-width: 600px;
|
||||
margin: auto;
|
||||
margin-top: 100px;
|
||||
}
|
||||
|
||||
.tree {
|
||||
position: relative;
|
||||
height: 500px;
|
||||
}
|
||||
.tree * {
|
||||
position: absolute;
|
||||
}
|
||||
#star {
|
||||
top: -20px;
|
||||
left: 150px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background-color: darkgoldenrod;
|
||||
clip-path: polygon(
|
||||
50% 0%,
|
||||
61% 35%,
|
||||
98% 35%,
|
||||
68% 57%,
|
||||
79% 91%,
|
||||
50% 70%,
|
||||
21% 91%,
|
||||
32% 57%,
|
||||
2% 35%,
|
||||
39% 35%
|
||||
);
|
||||
}
|
||||
#star.lit {
|
||||
background: linear-gradient(10deg, rgba(227,233,0,1) 0%, rgba(241,245,148,1) 100%);
|
||||
}
|
||||
.tree-part {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent green transparent;
|
||||
border-width: 0 100px 100px 100px;
|
||||
}
|
||||
.tree-part1 {
|
||||
left: 100px;
|
||||
top: 20px;
|
||||
}
|
||||
.tree-part2 {
|
||||
left: 100px;
|
||||
top: 60px;
|
||||
transform: scale(120%);
|
||||
filter: brightness(90%);
|
||||
}
|
||||
.tree-part3 {
|
||||
left: 100px;
|
||||
top: 100px;
|
||||
transform: scale(135%);
|
||||
}
|
||||
.tree-part4 {
|
||||
left: 100px;
|
||||
top: 140px;
|
||||
transform: scale(150%);
|
||||
filter: brightness(90%);
|
||||
}
|
||||
.tree-part5 {
|
||||
left: 100px;
|
||||
top: 180px;
|
||||
transform: scale(160%);
|
||||
}
|
||||
.tree-base {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
left: 180px;
|
||||
top: 310px;
|
||||
background-color: rgb(97, 47, 13);
|
||||
margin: 0 auto;
|
||||
}
|
||||
.ornament {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: darkred;
|
||||
border-radius: 50%;
|
||||
transition: background-color 1s ease;
|
||||
}
|
||||
.ornament.on {
|
||||
background-color: red;
|
||||
}
|
||||
#ornament1 {
|
||||
top: 90px;
|
||||
left: 150px;
|
||||
}
|
||||
#ornament2 {
|
||||
top: 140px;
|
||||
left: 210px;
|
||||
}
|
||||
#ornament3 {
|
||||
top: 172px;
|
||||
left: 160px;
|
||||
}
|
||||
#ornament4 {
|
||||
top: 253px;
|
||||
left: 110px;
|
||||
}
|
||||
#ornament5 {
|
||||
top: 200px;
|
||||
left: 260px;
|
||||
}
|
||||
#ornament6 {
|
||||
top: 230px;
|
||||
left: 200px;
|
||||
}
|
||||
#ornament7 {
|
||||
top: 270px;
|
||||
left: 250px;
|
||||
}
|
||||
.present {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
box-shadow: black 0px 0px 11px 1px;
|
||||
}
|
||||
.present.red {
|
||||
background-color: red;
|
||||
}
|
||||
.present.blue {
|
||||
background-color: blue;
|
||||
}
|
||||
.present.purple {
|
||||
background-color: purple;
|
||||
}
|
||||
.present:nth-child(1) {
|
||||
top: 280px;
|
||||
left: 350px;
|
||||
}
|
||||
.present:nth-child(2) {
|
||||
top: 280px;
|
||||
left: 460px;
|
||||
}
|
||||
.present:nth-child(3) {
|
||||
top: 320px;
|
||||
left: 420px;
|
||||
}
|
||||
.present .ribbon {
|
||||
background: rgb(227,233,0);
|
||||
background: linear-gradient(0deg, rgba(227,233,0,1) 0%, rgba(241,245,148,1) 100%);
|
||||
}
|
||||
.present .ribbon:nth-child(1) {
|
||||
width: 100px;
|
||||
height: 20px;
|
||||
top: 40px;
|
||||
}
|
||||
.present .ribbon:nth-child(2) {
|
||||
width: 20px;
|
||||
height: 100px;
|
||||
left: 40px;
|
||||
}
|
||||
.present .ribbon:nth-child(3) {
|
||||
width: 20px;
|
||||
height: 30px;
|
||||
left: 30px;
|
||||
top: -20px;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
.present .ribbon:nth-child(4) {
|
||||
width: 20px;
|
||||
height: 30px;
|
||||
left: 50px;
|
||||
top: -20px;
|
||||
transform: rotate(35deg);
|
||||
}
|
||||
#switch {
|
||||
top: 400px;
|
||||
left: 145px;
|
||||
border: none;
|
||||
background-color: #ccc;
|
||||
color: black;
|
||||
padding: 1em;
|
||||
border-radius: .5em;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
.text {
|
||||
font-size: 200%;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
.rocket {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
.spacer {
|
||||
height: 800px;
|
||||
}
|
||||
#lockfilecanvas {
|
||||
height: 276px;
|
||||
width: 276px;
|
||||
margin: auto;
|
||||
border: 1px solid grey;
|
||||
margin-bottom: 100px;
|
||||
position: relative;
|
||||
}
|
||||
#lockfilecanvas * {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="tree">
|
||||
<div class="present red" hx-get="/23/present/blue" hx-swap="outerHTML">
|
||||
<div class="ribbon"></div>
|
||||
<div class="ribbon"></div>
|
||||
<div class="ribbon"></div>
|
||||
<div class="ribbon"></div>
|
||||
</div>
|
||||
<div class="present red" hx-get="/23/present/blue" hx-swap="outerHTML">
|
||||
<div class="ribbon"></div>
|
||||
<div class="ribbon"></div>
|
||||
<div class="ribbon"></div>
|
||||
<div class="ribbon"></div>
|
||||
</div>
|
||||
<div class="present red" hx-get="/23/present/blue" hx-swap="outerHTML">
|
||||
<div class="ribbon"></div>
|
||||
<div class="ribbon"></div>
|
||||
<div class="ribbon"></div>
|
||||
<div class="ribbon"></div>
|
||||
</div>
|
||||
<div class="tree-base"></div>
|
||||
<div class="tree-part tree-part5"></div>
|
||||
<div class="tree-part tree-part4"></div>
|
||||
<div class="tree-part tree-part3"></div>
|
||||
<div class="tree-part tree-part2"></div>
|
||||
<div class="tree-part tree-part1"></div>
|
||||
<div class="ornament" id="ornament1" hx-trigger="load changed delay:2000ms once" hx-get="/23/ornament/on/1" hx-swap="outerHTML"></div>
|
||||
<div class="ornament" id="ornament2" hx-trigger="load changed delay:1600ms once" hx-get="/23/ornament/on/2" hx-swap="outerHTML"></div>
|
||||
<div class="ornament" id="ornament3" hx-trigger="load changed delay:200ms once" hx-get="/23/ornament/on/3" hx-swap="outerHTML"></div>
|
||||
<div class="ornament" id="ornament4" hx-trigger="load changed delay:400ms once" hx-get="/23/ornament/on/4" hx-swap="outerHTML"></div>
|
||||
<div class="ornament" id="ornament5" hx-trigger="load changed delay:800ms once" hx-get="/23/ornament/on/5" hx-swap="outerHTML"></div>
|
||||
<div class="ornament" id="ornament6" hx-trigger="load changed delay:1200ms once" hx-get="/23/ornament/on/6" hx-swap="outerHTML"></div>
|
||||
<div class="ornament" id="ornament7" hx-trigger="load changed delay:1400ms once" hx-get="/23/ornament/on/7" hx-swap="outerHTML"></div>
|
||||
<div class="present-wrap present-wrap1"></div>
|
||||
<div class="present-wrap present-wrap2"></div>
|
||||
<div class="present-wrap present-wrap3"></div>
|
||||
<div class="present-wrap present-wrap4"></div>
|
||||
<div id="star"></div>
|
||||
<button id="switch" hx-get="/23/star" hx-swap="outerHTML" hx-target="#star">
|
||||
Light the star
|
||||
</button>
|
||||
</div>
|
||||
<div class="text">Merry Christmas!</div>
|
||||
<div class="text">/ Shuttle</div>
|
||||
<div class="text"><img class="rocket" src="https://console.shuttle.dev/images/rocket.gif"></div>
|
||||
<div class="spacer"></div>
|
||||
<div class="text">Bonus task:</div>
|
||||
<form hx-post="/23/lockfile" enctype="multipart/form-data" hx-target="#lockfilecanvas">
|
||||
<input type="file" name="lockfile" required>
|
||||
<br>
|
||||
<br>
|
||||
<button type="submit">Submit lockfile</button>
|
||||
</form>
|
||||
<div id="lockfilecanvas"></div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
50
src/lib.rs
50
src/lib.rs
@ -1,6 +1,6 @@
|
||||
mod routes;
|
||||
|
||||
use axum::routing::{delete, put};
|
||||
#[cfg(feature = "task19")]
|
||||
use routes::task_nineteen::{cite, draft, list, remove, reset, undo};
|
||||
#[cfg(feature = "task12")]
|
||||
use routes::{board, place, random_board, reset, Board};
|
||||
@ -11,19 +11,37 @@ use routes::{
|
||||
hello_bird, hello_world, ipv4_dest, ipv4_key, ipv6_dest, ipv6_key, manifest, milk, minus_one,
|
||||
refill, MilkFactory,
|
||||
};
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use axum::{
|
||||
routing::{delete, get, post, put},
|
||||
Router,
|
||||
};
|
||||
#[cfg(feature = "task19")]
|
||||
#[allow(unused_imports)]
|
||||
pub fn router(pool: Option<sqlx::PgPool>) -> axum::Router {
|
||||
use axum::{
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
return pool.map_or_else(Router::new, |pool| {
|
||||
Router::new()
|
||||
.route("/19/reset", post(reset))
|
||||
.route("/19/draft", post(draft))
|
||||
.route("/19/undo/:id", put(undo))
|
||||
.route("/19/remove/:id", delete(remove))
|
||||
.route("/19/cite/:id", get(cite))
|
||||
.route("/19/list", get(list))
|
||||
.with_state(pool)
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "task19"))]
|
||||
pub fn router() -> axum::Router {
|
||||
use routes::task_twentythree::{lockfile, ornament, present, star};
|
||||
|
||||
#[cfg(feature = "task1-9")]
|
||||
let milk_factory = MilkFactory::new();
|
||||
|
||||
#[cfg(feature = "task1-9")]
|
||||
return Router::new()
|
||||
return return Router::new()
|
||||
.route("/hello_world", get(hello_world))
|
||||
.route("/-1/seek", get(minus_one))
|
||||
.route("/2/dest", get(ipv4_dest))
|
||||
@ -37,7 +55,7 @@ pub fn router(pool: Option<sqlx::PgPool>) -> axum::Router {
|
||||
.route("/", get(hello_bird));
|
||||
|
||||
#[cfg(feature = "task12")]
|
||||
Router::new()
|
||||
return Router::new()
|
||||
.route("/12/board", get(board))
|
||||
.route("/12/reset", post(reset))
|
||||
.route("/12/place/:team/:column", post(place))
|
||||
@ -45,19 +63,15 @@ pub fn router(pool: Option<sqlx::PgPool>) -> axum::Router {
|
||||
.with_state(Board::new());
|
||||
|
||||
#[cfg(feature = "task16")]
|
||||
Router::new()
|
||||
return Router::new()
|
||||
.route("/16/wrap", post(wrap))
|
||||
.route("/16/unwrap", get(unwrap))
|
||||
.route("/16/decode", post(decode));
|
||||
|
||||
pool.map_or_else(Router::new, |pool| {
|
||||
Router::new()
|
||||
.route("/19/reset", post(reset))
|
||||
.route("/19/draft", post(draft))
|
||||
.route("/19/undo/:id", put(undo))
|
||||
.route("/19/remove/:id", delete(remove))
|
||||
.route("/19/cite/:id", get(cite))
|
||||
.route("/19/list", get(list))
|
||||
.with_state(pool)
|
||||
})
|
||||
Router::new()
|
||||
.route("/23/star", get(star))
|
||||
.route("/23/lockfile", post(lockfile))
|
||||
.route("/23/present/:color", get(present))
|
||||
.route("/23/ornament/:state/:id", get(ornament))
|
||||
.nest_service("/assets/", ServeDir::new("assets"))
|
||||
}
|
||||
|
10
src/main.rs
10
src/main.rs
@ -9,6 +9,7 @@ async fn main() -> shuttle_axum::ShuttleAxum {
|
||||
Ok(router.into())
|
||||
}
|
||||
|
||||
#[cfg(feature = "task19")]
|
||||
#[shuttle_runtime::main]
|
||||
#[allow(clippy::unused_async)]
|
||||
async fn main(#[shuttle_shared_db::Postgres] pool: sqlx::PgPool) -> shuttle_axum::ShuttleAxum {
|
||||
@ -21,3 +22,12 @@ async fn main(#[shuttle_shared_db::Postgres] pool: sqlx::PgPool) -> shuttle_axum
|
||||
|
||||
Ok(router.into())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "task19"))]
|
||||
#[shuttle_runtime::main]
|
||||
#[allow(clippy::unused_async)]
|
||||
async fn main() -> shuttle_axum::ShuttleAxum {
|
||||
let router = router();
|
||||
|
||||
Ok(router.into())
|
||||
}
|
||||
|
@ -8,8 +8,11 @@ mod task_sixteen;
|
||||
#[cfg(feature = "task16")]
|
||||
pub use task_sixteen::{decode, unwrap, wrap};
|
||||
|
||||
#[cfg(feature = "task19")]
|
||||
pub mod task_nineteen;
|
||||
|
||||
pub mod task_twentythree;
|
||||
|
||||
#[cfg(feature = "task1-9")]
|
||||
mod hello_bird;
|
||||
|
||||
|
100
src/routes/task_twentythree/lockfile.rs
Normal file
100
src/routes/task_twentythree/lockfile.rs
Normal file
@ -0,0 +1,100 @@
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse},
|
||||
};
|
||||
use axum_extra::extract::Multipart;
|
||||
use regex::Regex;
|
||||
use toml::Value;
|
||||
use tracing::{error, info, instrument};
|
||||
|
||||
#[instrument()]
|
||||
pub async fn lockfile(
|
||||
mut multipart: Multipart,
|
||||
) -> Result<impl IntoResponse, (StatusCode, &'static str)> {
|
||||
let mut content = String::new();
|
||||
while let Some(field) = multipart.next_field().await.map_err(|_| {
|
||||
error!("Failed to read field from multipart");
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Failed to read field from multipart",
|
||||
)
|
||||
})? {
|
||||
let data = field.bytes().await.map_err(|_| {
|
||||
error!("Failed to read bytes from multipart");
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Failed to read bytes from multipart",
|
||||
)
|
||||
})?;
|
||||
|
||||
content.push_str(&String::from_utf8_lossy(&data));
|
||||
}
|
||||
|
||||
if toml::from_str::<Value>(&content).is_err() {
|
||||
error!("Failed to parse TOML from multipart");
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"Failed to parse TOML from multipart",
|
||||
));
|
||||
}
|
||||
|
||||
let re = Regex::new(r#"checksum = "([^"]+)"\n"#)
|
||||
.map_err(|_| (StatusCode::BAD_REQUEST, "Failed to compile regex"))?;
|
||||
|
||||
let mut output = Vec::new();
|
||||
for cap in re.captures_iter(&content) {
|
||||
if let Some(m) = cap.get(1) {
|
||||
let checksum = m.as_str();
|
||||
match checksum_to_sprinkle(checksum) {
|
||||
Ok(sprinkle) => output.push(sprinkle),
|
||||
Err(_) => return Err((StatusCode::UNPROCESSABLE_ENTITY, "Invalid checksum")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if output.is_empty() {
|
||||
error!("No checksums found");
|
||||
return Err((StatusCode::BAD_REQUEST, "No checksums found"));
|
||||
}
|
||||
|
||||
info!(output = ?output);
|
||||
Ok(Html(output.join("\n")).into_response())
|
||||
}
|
||||
|
||||
fn checksum_to_sprinkle(checksum: &str) -> Result<String, &'static str> {
|
||||
if hex::decode(checksum).is_err() {
|
||||
return Err("Invalid checksum");
|
||||
}
|
||||
|
||||
let color = checksum.chars().take(6).collect::<String>();
|
||||
let top = u32::from_str_radix(&checksum.chars().skip(6).take(2).collect::<String>(), 16)
|
||||
.map_err(|_| "Invalid top value")?;
|
||||
|
||||
let left = u32::from_str_radix(&checksum.chars().skip(8).take(2).collect::<String>(), 16)
|
||||
.map_err(|_| "Invalid top value")?;
|
||||
|
||||
Ok(format!(
|
||||
r#"<div style="background-color:#{color};top:{top}px;left:{left}px;"></div>"#
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_checksum_to_sprinkle() {
|
||||
assert_eq!(
|
||||
checksum_to_sprinkle(
|
||||
"337789faa0372648a8ac286b2f92a53121fe118f12e29009ac504872a5413cc6"
|
||||
),
|
||||
Ok(r#"<div style="background-color:#337789;top:250px;left:160px;"></div>"#.to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
checksum_to_sprinkle(
|
||||
"22ba454b13e4e29b5b892a62c334360a571de5a25c936283416c94328427dd57"
|
||||
),
|
||||
Ok(r#"<div style="background-color:#22ba45;top:75px;left:19px;"></div>"#.to_string())
|
||||
);
|
||||
}
|
||||
}
|
11
src/routes/task_twentythree/mod.rs
Normal file
11
src/routes/task_twentythree/mod.rs
Normal file
@ -0,0 +1,11 @@
|
||||
mod star;
|
||||
pub use star::star;
|
||||
|
||||
mod present;
|
||||
pub use present::present;
|
||||
|
||||
mod ornament;
|
||||
pub use ornament::ornament;
|
||||
|
||||
mod lockfile;
|
||||
pub use lockfile::lockfile;
|
64
src/routes/task_twentythree/ornament.rs
Normal file
64
src/routes/task_twentythree/ornament.rs
Normal file
@ -0,0 +1,64 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use axum::{
|
||||
extract::Path,
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse},
|
||||
};
|
||||
use html_escape::encode_safe;
|
||||
use tracing::{info, instrument};
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
enum State {
|
||||
On,
|
||||
Off,
|
||||
}
|
||||
|
||||
impl State {
|
||||
const fn next(&self) -> Self {
|
||||
match self {
|
||||
Self::On => Self::Off,
|
||||
Self::Off => Self::On,
|
||||
}
|
||||
}
|
||||
|
||||
const fn is_on(&self) -> &str {
|
||||
match self {
|
||||
Self::On => " on",
|
||||
Self::Off => "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for State {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"on" => Ok(Self::On),
|
||||
"off" => Ok(Self::Off),
|
||||
_ => Err("invalid state"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for State {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let state = match self {
|
||||
Self::On => "on",
|
||||
Self::Off => "off",
|
||||
};
|
||||
write!(f, "{state}")
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument()]
|
||||
pub async fn ornament(Path((state, id)): Path<(String, String)>) -> impl IntoResponse {
|
||||
State::try_from(state).map_or_else(|_| (StatusCode::IM_A_TEAPOT, "I'm a teapot").into_response(),
|
||||
|state| {
|
||||
let ornament = format!(r#"<div class="ornament{on}" id="ornament{id}" hx-trigger="load delay:2s once" hx-get="/23/ornament/{next_state}/{id}" hx-swap="outerHTML"></div>"#,on = state.is_on(), id = encode_safe(&id), next_state = state.next() );
|
||||
info!(ornament);
|
||||
Html(ornament).into_response()
|
||||
}
|
||||
)
|
||||
}
|
79
src/routes/task_twentythree/present.rs
Normal file
79
src/routes/task_twentythree/present.rs
Normal file
@ -0,0 +1,79 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use axum::{
|
||||
extract::Path,
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse},
|
||||
};
|
||||
use tracing::instrument;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
enum Color {
|
||||
Red,
|
||||
Blue,
|
||||
Purple,
|
||||
}
|
||||
|
||||
impl Color {
|
||||
const fn next(&self) -> Self {
|
||||
match self {
|
||||
Self::Red => Self::Blue,
|
||||
Self::Blue => Self::Purple,
|
||||
Self::Purple => Self::Red,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Color {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let color = match self {
|
||||
Self::Red => "red",
|
||||
Self::Blue => "blue",
|
||||
Self::Purple => "purple",
|
||||
};
|
||||
write!(f, "{color}")
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Color {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"blue" => Ok(Self::Blue),
|
||||
"purple" => Ok(Self::Purple),
|
||||
"red" => Ok(Self::Red),
|
||||
_ => Err("invalid color"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// impl From<String> for Color {
|
||||
// fn from(s: String) -> Self {
|
||||
// match s.to_lowercase().as_str() {
|
||||
// "blue" => Self::Blue,
|
||||
// "purple" => Self::Purple,
|
||||
// _ => Self::Red,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
#[instrument()]
|
||||
pub async fn present(Path(color): Path<String>) -> impl IntoResponse {
|
||||
Color::try_from(color).map_or_else(
|
||||
|_| (StatusCode::IM_A_TEAPOT, "I'm a teapot").into_response(),
|
||||
|color| {
|
||||
Html(format!(
|
||||
r#"<div class="present {}" hx-get="/23/present/{}" hx-swap="outerHTML">
|
||||
<div class="ribbon"></div>
|
||||
<div class="ribbon"></div>
|
||||
<div class="ribbon"></div>
|
||||
<div class="ribbon"></div>
|
||||
</div>"#,
|
||||
color,
|
||||
color.next()
|
||||
))
|
||||
.into_response()
|
||||
},
|
||||
)
|
||||
}
|
7
src/routes/task_twentythree/star.rs
Normal file
7
src/routes/task_twentythree/star.rs
Normal file
@ -0,0 +1,7 @@
|
||||
use axum::response::{Html, IntoResponse};
|
||||
use tracing::instrument;
|
||||
|
||||
#[instrument()]
|
||||
pub async fn star() -> impl IntoResponse {
|
||||
Html(r#"<div id="star" class="lit"></div>"#)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
// #[cfg(feature = "task19")]
|
||||
#[cfg(feature = "task19")]
|
||||
mod task_nineteen {
|
||||
use axum::http::StatusCode;
|
||||
use axum_test::TestServer;
|
||||
|
Loading…
x
Reference in New Issue
Block a user