use std::collections::HashSet; use std::future::Future; use std::net::SocketAddr; use std::ops::RangeInclusive; use std::pin::Pin; use std::process::ExitCode; use std::sync::Arc; use http_body_util::Full; use hyper::body::Bytes; use hyper::server::conn::http1; use hyper::service::Service; use hyper::{Request, Response, StatusCode}; use hyper_util::rt::TokioIo; use rand::prelude::*; use rand_pcg::Pcg64; use rand_seeder::Seeder; use tokio::net::TcpListener; #[tokio::main] async fn main() -> Result> { let args: Vec = std::env::args().collect(); if args.len() != 2 { eprintln!("must supply exactly one arg for the seed"); return Ok(ExitCode::FAILURE); } let seed = args.get(1).unwrap(); eprintln!("seed: {}", seed); tracing_subscriber::fmt::init(); let svc = RandomPageService::new(seed.as_str()); let addr = SocketAddr::from(([127, 0, 0, 1], 3200)); let listener = TcpListener::bind(addr).await?; loop { let (stream, _) = listener.accept().await?; let io = TokioIo::new(stream); let svc = svc.clone(); tokio::task::spawn(async move { if let Err(err) = http1::Builder::new().serve_connection(io, svc).await { eprintln!("Error serving connection: {:?}", err); } }); } } #[derive(Debug, Clone)] struct RandomPageService { pub seed: String, pub dict: Arc>, pub dict_set: Arc>, } impl RandomPageService { pub fn new(seed: &str) -> Self { let dictionary_data = include_bytes!(env!("DICTIONARY_FILE_PATH")); let dictionary_string: &'static str = std::str::from_utf8(dictionary_data).unwrap(); Self { seed: seed.to_string(), dict: Arc::new(dictionary_string.split_whitespace().collect()), dict_set: Arc::new(dictionary_string.split_whitespace().collect()), } } } impl Service> for RandomPageService { type Response = Response>; type Error = hyper::Error; type Future = Pin> + Send>>; fn call(&self, req: Request) -> Self::Future { fn mk_response( s: String, status_code: StatusCode, ) -> Result>, hyper::Error> { Ok( Response::builder() .status(status_code) .body(Full::new(Bytes::from(s))) .unwrap(), ) } let path = req.uri().path(); tracing::info!(path, "path"); let path_segments = path.split('/').filter(|v| v.len() > 0); for segment in path_segments { if !self.dict_set.contains(segment) { return Box::pin(async { mk_response("not found".to_string(), StatusCode::NOT_FOUND) }); } } let res_body = build_page(&self.seed, path, &self.dict); let res = mk_response(res_body, StatusCode::OK); Box::pin(async { res }) } } const MAX_ROUTE_SEGMENTS: usize = 6; const PARAGRAPH_WORDS: RangeInclusive = 10..=200; const N_LINKS: RangeInclusive = 3..=10; pub fn build_page( base_seed: &str, route: &str, dictionary: &[&'static str], ) -> String { let mut rng: Pcg64 = Seeder::from(format!("{}---{}", base_seed, route)).make_rng(); let n_words = rng.gen_range(PARAGRAPH_WORDS); let n_links = rng.gen_range(N_LINKS); let random_paragraph = (0..n_words) .map(|_| random_word(&mut rng, dictionary)) .collect::>() .join(" "); let random_links = (0..n_links) .map(|_| random_route_link(&mut rng, dictionary)) .map(|link| format!("

{}

", link)) .collect::>() .join("\n"); format!( r#"

{}

{} "#, random_paragraph, random_links, ) } fn random_route_link(rng: &mut Pcg64, dictionary: &[&'static str]) -> String { let n_segments = rng.gen_range(1..=MAX_ROUTE_SEGMENTS); let random_route = (0..n_segments) .map(|_| random_word(rng, dictionary)) .collect::>() .join("/"); let label = random_word(rng, dictionary); format!("{}", random_route, label) } pub fn random_word( rng: &mut Pcg64, dictionary: &[&'static str], ) -> &'static str { let i = rng.gen_range(0..dictionary.len()); dictionary[i] } pub fn build_dict() -> Vec<&'static str> { let dictionary_data = include_bytes!(env!("DICTIONARY_FILE_PATH")); let dictionary_string: &'static str = std::str::from_utf8(dictionary_data).unwrap(); dictionary_string.split_whitespace().collect() }