aro/src/main.rs

109 lines
3.3 KiB
Rust
Raw Normal View History

2023-01-27 14:18:33 +01:00
#![feature(option_result_contains, once_cell)]
use actix_web::{get, http::header, web, App, Either, HttpResponse, HttpServer};
use data::{Card, CardInfo};
2023-01-26 23:07:16 +01:00
use filter::SearchCard;
2023-01-27 15:48:07 +01:00
use itertools::Itertools;
2023-01-27 14:18:33 +01:00
use serde::Deserialize;
use std::{collections::HashMap, fmt::Write, fs::File, io::BufReader, net::Ipv4Addr, sync::LazyLock, time::Instant};
2023-01-26 23:07:16 +01:00
mod data;
mod filter;
mod parser;
2023-01-27 14:45:17 +01:00
const RESULT_LIMIT: usize = 100;
2023-01-27 14:18:33 +01:00
static CARDS: LazyLock<Vec<Card>> = LazyLock::new(|| {
serde_json::from_reader::<_, CardInfo>(BufReader::new(File::open("cards.json").expect("cards.json not found")))
.expect("Could not deserialize cards")
.data
});
static CARDS_BY_ID: LazyLock<HashMap<usize, Card>> =
LazyLock::new(|| CARDS.iter().map(|c| (c.id, Card { text: c.text.replace("\r", "").replace('\n', "<br/>"), ..c.clone() })).collect());
static SEARCH_CARDS: LazyLock<Vec<SearchCard>> = LazyLock::new(|| CARDS.iter().map(SearchCard::from).collect());
#[actix_web::main]
async fn main() -> std::io::Result<()> {
2023-01-27 00:03:00 +01:00
let now = Instant::now();
2023-01-27 14:18:33 +01:00
println!("Starting server");
// tap these so they’re initialized
let num_cards = (CARDS_BY_ID.len() + SEARCH_CARDS.len()) / 2;
println!("Read {num_cards} cards in {:?}", now.elapsed());
HttpServer::new(|| App::new().service(search)).bind((Ipv4Addr::from([127, 0, 0, 1]), 8080))?.run().await
}
#[derive(Debug, Deserialize)]
struct Query {
q: String,
}
#[get("/")]
async fn search(q: Option<Either<web::Query<Query>, web::Form<Query>>>) -> Result<HttpResponse, Box<dyn std::error::Error>> {
let q = match q {
Some(Either::Left(web::Query(Query { q }))) => Some(q),
Some(Either::Right(web::Form(Query { q }))) => Some(q),
None => None,
};
let mut res = String::with_capacity(10_000);
write!(
res,
r#"
2023-01-27 15:48:07 +01:00
<html>
<head>
<style>
html {{
padding-top: 2em;
background-color: #060404;
color: #ececef;
font-family: 'Lato', 'Segoe UI', sans-serif;
font-size: 14pt;
line-height: 130%;
}}
body {{
background-color: #241e1e;
border-radius:2em;
padding: 5%;
width: 80%;
margin: auto;
}}
em {{
font-size: 75%;
}}
</style>
</head>
<body>
2023-01-27 14:18:33 +01:00
<form action="/">
2023-01-27 15:48:07 +01:00
<input style="width: 80%" type="text" name="q" id="searchbox" placeholder="Enter search query" value="{}">
<input style="width: 15%; right: 0" type="submit" value="Submit">
2023-01-27 14:18:33 +01:00
</form>"#,
match &q {
Some(q) => q,
None => "",
}
)?;
if let Some(q) = q {
let query = parser::parse_filters(&q)?;
let now = Instant::now();
2023-01-27 14:45:17 +01:00
let matches: Vec<&Card> = SEARCH_CARDS
.iter()
2023-01-27 15:48:07 +01:00
.filter(|card| query.iter().all(|(_, q)| q(card)))
2023-01-27 14:45:17 +01:00
.map(|c| CARDS_BY_ID.get(&c.id).unwrap())
.take(RESULT_LIMIT)
.collect();
2023-01-27 15:48:07 +01:00
write!(
res,
"<em>Showing {} results where {} (took {:?})</em><br/><hr/><br/>",
matches.len(),
query.iter().map(|(f, _)| f.to_string()).join(" and "),
now.elapsed()
)?;
2023-01-27 14:18:33 +01:00
for card in matches {
res.push_str(&card.to_string());
2023-01-27 15:48:07 +01:00
res.push_str("<br/><hr/><br/>");
2023-01-27 14:18:33 +01:00
}
write!(res, "</body></html>")?;
} else {
res.write_str("Enter a query above to search")?;
}
2023-01-27 14:18:33 +01:00
Ok(HttpResponse::Ok().insert_header(header::ContentType::html()).body(res))
2022-10-05 17:57:01 +02:00
}