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-26 15:04:39 +01:00
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 ( ) ) ;
2023-01-30 15:57:08 +01:00
HttpServer ::new ( | | App ::new ( ) . service ( search ) . service ( card_info ) ) . bind ( ( Ipv4Addr ::from ( [ 127 , 0 , 0 , 1 ] ) , 8080 ) ) ? . run ( ) . await
2023-01-27 14:18:33 +01:00
}
#[ derive(Debug, Deserialize) ]
struct Query {
q : String ,
}
2023-01-30 11:39:42 +01:00
const HEADER : & str = include_str! ( " ../static/header.html " ) ;
2023-01-27 14:18:33 +01:00
#[ 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 ,
2023-01-30 15:57:08 +01:00
}
. filter ( | s | ! s . is_empty ( ) ) ;
2023-01-27 14:18:33 +01:00
let mut res = String ::with_capacity ( 10_000 ) ;
2023-01-30 15:57:08 +01:00
res . push_str ( HEADER ) ;
render_searchbox ( & mut res , & q ) ? ;
match q {
Some ( q ) = > render_results ( & mut res , & q ) ? ,
None = > res . push_str ( " Enter a query above to search " ) ,
}
finish_document ( & mut res ) ;
Ok ( HttpResponse ::Ok ( ) . insert_header ( header ::ContentType ::html ( ) ) . body ( res ) )
}
#[ get( " /{id} " ) ]
async fn card_info ( card_id : web ::Path < usize > ) -> Result < HttpResponse , Box < dyn std ::error ::Error > > {
let mut res = String ::with_capacity ( 2_000 ) ;
res . push_str ( HEADER ) ;
render_searchbox ( & mut res , & None ) ? ;
match CARDS_BY_ID . get ( & card_id ) {
Some ( card ) = > {
res . push_str ( r # ""# ) ;
write! (
res ,
r #"
< div class = " row " >
< div class = " column left " > { card } < / div >
< div class = " column right " > < img style = " width: 100%; " src = " http://localhost:80/img/{}.jpg " / > < / div >
< / div > " #,
card . id ,
) ? ;
}
None = > res . push_str ( " Card not found " ) ,
}
Ok ( HttpResponse ::Ok ( ) . insert_header ( header ::ContentType ::html ( ) ) . body ( res ) )
}
fn render_searchbox ( res : & mut String , query : & Option < String > ) -> std ::fmt ::Result {
2023-01-27 14:18:33 +01:00
write! (
res ,
r #"
< form action = " / " >
2023-01-30 11:39:42 +01:00
< input type = " text " name = " q " id = " searchbox " placeholder = " Enter query (e.g. l:5 c:synchro atk>2000) " value = " {} " > < input type = " submit " id = " submit " value = " 🔍 " >
2023-01-27 14:18:33 +01:00
< / form > " #,
2023-01-30 15:57:08 +01:00
match & query {
2023-01-27 14:18:33 +01:00
Some ( q ) = > q ,
None = > " " ,
}
2023-01-30 15:57:08 +01:00
)
}
fn render_results ( res : & mut String , query : & str ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
let query = match parser ::parse_filters ( query ) {
Ok ( q ) = > q ,
Err ( e ) = > {
write! ( res , " Could not parse query: {e:?} " ) ? ;
return Ok ( ( ) ) ;
}
} ;
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 ,
" <span class= \" meta \" >Showing {} results where {} (took {:?})</span> " ,
matches . len ( ) ,
query . iter ( ) . map ( | ( f , _ ) | f . to_string ( ) ) . join ( " and " ) ,
now . elapsed ( )
2023-01-27 14:18:33 +01:00
) ? ;
2023-01-30 15:57:08 +01:00
if matches . is_empty ( ) {
return Ok ( ( ) ) ;
}
res . push_str ( " <table> " ) ;
for card in matches {
2023-01-27 15:48:07 +01:00
write! (
res ,
2023-01-30 15:57:08 +01:00
r # "<tr><td>{card}</td><td><a href="/{}"><img src="http://localhost:80/img/{}.jpg" class="thumb"/></a></td></tr>"# ,
card . id , card . id
2023-01-27 15:48:07 +01:00
) ? ;
2023-01-26 15:04:39 +01:00
}
2023-01-30 15:57:08 +01:00
Ok ( ( ) )
}
fn finish_document ( res : & mut String ) {
res . push_str ( " </body></html> " )
2022-10-05 17:57:01 +02:00
}