add basic web server

This commit is contained in:
kageru 2023-01-27 14:18:33 +01:00
parent fee7dcc62a
commit 8fcb2824de
5 changed files with 993 additions and 22 deletions

922
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -7,4 +7,7 @@ edition = "2021"
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
nom = "7.1.3" nom = "7.1.3"
actix-web = { version = "4.3.0", default_features = false, features = ["macros"] }
[dev-dependencies]
test-case = "2.2.2" test-case = "2.2.2"

@ -40,10 +40,10 @@ impl Display for Card {
write!(f, "{attr}/")?; write!(f, "{attr}/")?;
} }
write!(f, "{} {})", self.r#type, self.card_type)?; write!(f, "{} {})", self.r#type, self.card_type)?;
f.write_str("\n")?; f.write_str("<br/>")?;
f.write_str(&self.text)?; f.write_str(&self.text)?;
if self.card_type.contains(&String::from("Monster")) { if self.card_type.contains(&String::from("Monster")) {
f.write_str("\n")?; f.write_str("<br/>")?;
match (self.atk, self.def) { match (self.atk, self.def) {
(Some(atk), Some(def)) => write!(f, "{atk} ATK / {def} DEF")?, (Some(atk), Some(def)) => write!(f, "{atk} ATK / {def} DEF")?,
(Some(atk), None) if self.link_rating.is_some() => write!(f, "{atk} ATK")?, (Some(atk), None) if self.link_rating.is_some() => write!(f, "{atk} ATK")?,

@ -61,6 +61,7 @@ pub fn build_filter(query: RawCardFilter) -> Result<CardFilter, String> {
} }
(Field::Level, op, Value::Numerical(n)) => Box::new(move |card| op.filter_number(card.level, n)), (Field::Level, op, Value::Numerical(n)) => Box::new(move |card| op.filter_number(card.level, n)),
(Field::Type, Operator::Equals, Value::String(s)) => Box::new(move |card| card.r#type == s), (Field::Type, Operator::Equals, Value::String(s)) => Box::new(move |card| card.r#type == s),
(Field::Attribute, Operator::Equals, Value::String(s)) => Box::new(move |card| card.attribute.contains(&s)),
(Field::Class, Operator::Equals, Value::String(s)) => Box::new(move |card| card.card_type.contains(&s)), (Field::Class, Operator::Equals, Value::String(s)) => Box::new(move |card| card.card_type.contains(&s)),
(Field::Text, Operator::Equals, Value::String(s)) => Box::new(move |card| card.text.contains(&s)), (Field::Text, Operator::Equals, Value::String(s)) => Box::new(move |card| card.text.contains(&s)),
(Field::Name, Operator::Equals, Value::String(s)) => Box::new(move |card| card.name.contains(&s)), (Field::Name, Operator::Equals, Value::String(s)) => Box::new(move |card| card.name.contains(&s)),

@ -1,28 +1,73 @@
#![feature(option_result_contains)] #![feature(option_result_contains, once_cell)]
use std::{collections::HashMap, time::Instant}; use actix_web::{get, http::header, web, App, Either, HttpResponse, HttpServer};
use data::{Card, CardInfo};
use data::CardInfo;
use filter::SearchCard; use filter::SearchCard;
use serde::Deserialize;
use std::{collections::HashMap, fmt::Write, fs::File, io::BufReader, net::Ipv4Addr, sync::LazyLock, time::Instant};
mod data; mod data;
mod filter; mod filter;
mod parser; mod parser;
fn main() -> Result<(), Box<dyn std::error::Error>> { static CARDS: LazyLock<Vec<Card>> = LazyLock::new(|| {
let cards = serde_json::from_reader::<_, CardInfo>(std::io::BufReader::new(std::fs::File::open("cards.json")?))?.data; serde_json::from_reader::<_, CardInfo>(BufReader::new(File::open("cards.json").expect("cards.json not found")))
let search_cards: Vec<_> = cards.iter().map(SearchCard::from).collect(); .expect("Could not deserialize cards")
let cards_by_id: HashMap<_, _> = cards.into_iter().map(|c| (c.id, c)).collect(); .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<()> {
let now = Instant::now(); let now = Instant::now();
let raw_query = std::env::args().nth(1).unwrap(); println!("Starting server");
let query = parser::parse_filters(&raw_query)?; // tap these so they’re initialized
let query_time = now.elapsed(); 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#"
<html><body>
<form action="/">
<label for="fname">Search query:</label><br>
<input type="text" name="q" value="{}"><br>
<input type="submit" value="Submit">
</form>"#,
match &q {
Some(q) => q,
None => "",
}
)?;
if let Some(q) = q {
let query = parser::parse_filters(&q)?;
let now = Instant::now(); let now = Instant::now();
let matches: Vec<_> = search_cards.iter().filter(|card| query.iter().all(|q| q(card))).collect(); let matches: Vec<&Card> =
let filter_time = now.elapsed(); SEARCH_CARDS.iter().filter(|card| query.iter().all(|q| q(card))).map(|c| CARDS_BY_ID.get(&c.id).unwrap()).collect();
for c in &matches { write!(res, "Showing {} results (took {:?})<br/><br/>", matches.len(), now.elapsed())?;
println!("{}\n", cards_by_id.get(&c.id).unwrap()); for card in matches {
res.push_str(&card.to_string());
res.push_str("<br/><br/>");
} }
println!("Parsed query in {:?}", query_time); write!(res, "</body></html>")?;
println!("Searched {} cards in {:?} ({} hits)", search_cards.len(), filter_time, matches.len()); } else {
Ok(()) res.write_str("Enter a query above to search")?;
}
Ok(HttpResponse::Ok().insert_header(header::ContentType::html()).body(res))
} }