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 } ;
2023-02-02 11:34:58 +01:00
use data ::{ Card , CardInfo , Set } ;
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-02-02 11:34:58 +01:00
use time ::Date ;
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 ( | | {
2023-02-02 11:34:58 +01:00
let mut cards = serde_json ::from_reader ::< _ , CardInfo > ( BufReader ::new ( File ::open ( " cards.json " ) . expect ( " cards.json not found " ) ) )
2023-01-27 14:18:33 +01:00
. expect ( " Could not deserialize cards " )
2023-02-02 11:34:58 +01:00
. data ;
cards . iter_mut ( ) . for_each ( | c | {
c . card_sets . sort_unstable_by_key ( | s | SETS_BY_NAME . get ( & s . set_name . to_lowercase ( ) ) . and_then ( | s | s . tcg_date ) . unwrap_or ( Date ::MAX ) )
} ) ;
cards
2023-01-27 14:18:33 +01:00
} ) ;
static CARDS_BY_ID : LazyLock < HashMap < usize , Card > > =
2023-01-31 11:47:59 +01:00
LazyLock ::new ( | | CARDS . iter ( ) . map ( | c | ( c . id , Card { text : c . text . replace ( '\r' , " " ) . replace ( '\n' , " <br/> " ) , .. c . clone ( ) } ) ) . collect ( ) ) ;
2023-01-27 14:18:33 +01:00
static SEARCH_CARDS : LazyLock < Vec < SearchCard > > = LazyLock ::new ( | | CARDS . iter ( ) . map ( SearchCard ::from ) . collect ( ) ) ;
2023-02-02 11:34:58 +01:00
static SETS_BY_NAME : LazyLock < HashMap < String , Set > > = LazyLock ::new ( | | {
serde_json ::from_reader ::< _ , Vec < Set > > ( BufReader ::new ( File ::open ( " sets.json " ) . expect ( " sets.json not found " ) ) )
. expect ( " Could not deserialize sets " )
. into_iter ( )
. map ( | s | ( s . set_name . to_lowercase ( ) , s ) )
. collect ( )
} ) ;
2023-01-27 14:18:33 +01:00
2023-01-31 17:08:37 +01:00
static IMG_HOST : LazyLock < String > = LazyLock ::new ( | | std ::env ::var ( " IMG_HOST " ) . unwrap_or_else ( | _ | String ::new ( ) ) ) ;
2023-01-27 14:18:33 +01:00
#[ 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 17:27:44 +01:00
HttpServer ::new ( | | App ::new ( ) . service ( search ) . service ( card_info ) . service ( help ) )
2023-01-30 18:00:46 +01:00
. bind ( ( Ipv4Addr ::from ( [ 127 , 0 , 0 , 1 ] ) , 1961 ) ) ?
2023-01-30 17:27:44 +01:00
. 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-30 17:27:44 +01:00
const HELP_CONTENT : & str = include_str! ( " ../static/help.html " ) ;
const FOOTER : & str = r #" <div id= " bottom " >
< a href = " / " > Home < / a >
& nbsp ; & nbsp ; & nbsp ; & nbsp ; & nbsp ;
< a href = " /help " > Query Syntax < / a >
< / div >
< / body > < / html > " #;
2023-01-30 11:39:42 +01:00
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 ) )
}
2023-01-30 17:27:44 +01:00
#[ get( " /card/{id} " ) ]
2023-01-30 15:57:08 +01:00
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 #"
2023-01-31 17:08:37 +01:00
< div >
< img class = " fullimage " src = " {}/static/full/{}.jpg " / >
{ card }
2023-02-01 18:47:27 +01:00
{ }
2023-01-30 15:57:08 +01:00
< / div > " #,
2023-01-31 17:08:37 +01:00
IMG_HOST . as_str ( ) ,
2023-01-30 15:57:08 +01:00
card . id ,
2023-02-01 18:47:27 +01:00
card . extended_info ( ) . unwrap_or_else ( | _ | String ::new ( ) ) ,
2023-01-30 15:57:08 +01:00
) ? ;
}
None = > res . push_str ( " Card not found " ) ,
}
2023-01-30 18:19:52 +01:00
finish_document ( & mut res ) ;
2023-01-30 15:57:08 +01:00
Ok ( HttpResponse ::Ok ( ) . insert_header ( header ::ContentType ::html ( ) ) . body ( res ) )
}
2023-01-30 17:27:44 +01:00
#[ get( " /help " ) ]
async fn help ( ) -> Result < HttpResponse , Box < dyn std ::error ::Error > > {
let mut res = String ::with_capacity ( HEADER . len ( ) + HELP_CONTENT . len ( ) + FOOTER . len ( ) + 250 ) ;
res . push_str ( HEADER ) ;
render_searchbox ( & mut res , & None ) ? ;
res . push_str ( HELP_CONTENT ) ;
res . push_str ( FOOTER ) ;
Ok ( HttpResponse ::Ok ( ) . insert_header ( header ::ContentType ::html ( ) ) . body ( res ) )
}
2023-01-30 15:57:08 +01:00
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-31 11:47:59 +01:00
Some ( q ) = > q . replace ( '"' , " " " ) ,
None = > String ::new ( ) ,
2023-01-27 14:18:33 +01:00
}
2023-01-30 15:57:08 +01:00
)
}
fn render_results ( res : & mut String , query : & str ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
2023-01-31 11:47:59 +01:00
let ( raw_filters , query ) = match parser ::parse_filters ( query ) {
2023-01-30 15:57:08 +01:00
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 ( )
2023-01-31 11:47:59 +01:00
. filter ( | card | query . iter ( ) . all ( | q | q ( card ) ) )
2023-01-30 15:57:08 +01:00
. 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 ( ) ,
2023-01-31 11:47:59 +01:00
raw_filters . iter ( ) . map ( | f | f . to_string ( ) ) . join ( " and " ) ,
2023-01-30 15:57:08 +01:00
now . elapsed ( )
2023-01-27 14:18:33 +01:00
) ? ;
2023-01-30 15:57:08 +01:00
if matches . is_empty ( ) {
return Ok ( ( ) ) ;
}
2023-01-31 17:08:37 +01:00
res . push_str ( " <div style= \" display: flex; flex-wrap: wrap; \" > " ) ;
2023-01-30 15:57:08 +01:00
for card in matches {
2023-01-27 15:48:07 +01:00
write! (
res ,
2023-02-02 15:46:15 +01:00
r # "<a class="cardresult" href="/card/{}"><img src="{}/static/thumb/{}.jpg" class="thumb"/>{card}</a>"# ,
2023-01-31 17:08:37 +01:00
card . id ,
IMG_HOST . as_str ( ) ,
card . id
2023-01-27 15:48:07 +01:00
) ? ;
2023-01-26 15:04:39 +01:00
}
2023-01-31 17:08:37 +01:00
res . push_str ( " </div> " ) ;
2023-01-30 15:57:08 +01:00
Ok ( ( ) )
}
fn finish_document ( res : & mut String ) {
2023-01-30 17:27:44 +01:00
res . push_str ( FOOTER )
2022-10-05 17:57:01 +02:00
}