#![feature(option_result_contains, once_cell)] use actix_web::{get, http::header, web, App, Either, HttpResponse, HttpServer}; use data::{Card, CardInfo}; use filter::SearchCard; use itertools::Itertools; use serde::Deserialize; use std::{collections::HashMap, fmt::Write, fs::File, io::BufReader, net::Ipv4Addr, sync::LazyLock, time::Instant}; mod data; mod filter; mod parser; const RESULT_LIMIT: usize = 100; static CARDS: LazyLock> = 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> = LazyLock::new(|| CARDS.iter().map(|c| (c.id, Card { text: c.text.replace("\r", "").replace('\n', "
"), ..c.clone() })).collect()); static SEARCH_CARDS: LazyLock> = LazyLock::new(|| CARDS.iter().map(SearchCard::from).collect()); #[actix_web::main] async fn main() -> std::io::Result<()> { let now = Instant::now(); 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, web::Form>>) -> Result> { 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#"
"#, match &q { Some(q) => q, None => "", } )?; if let Some(q) = q { let query = parser::parse_filters(&q)?; let now = Instant::now(); let matches: Vec<&Card> = SEARCH_CARDS .iter() .filter(|card| query.iter().all(|(_, q)| q(card))) .map(|c| CARDS_BY_ID.get(&c.id).unwrap()) .take(RESULT_LIMIT) .collect(); write!( res, "Showing {} results where {} (took {:?})


", matches.len(), query.iter().map(|(f, _)| f.to_string()).join(" and "), now.elapsed() )?; for card in matches { res.push_str(&card.to_string()); res.push_str("


"); } write!(res, "")?; } else { res.write_str("Enter a query above to search")?; } Ok(HttpResponse::Ok().insert_header(header::ContentType::html()).body(res)) }