aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main.rs179
1 files changed, 179 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..766bc4b
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,179 @@
+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<ExitCode, Box<dyn std::error::Error + Send + Sync>> {
+ let args: Vec<String> = 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<Vec<&'static str>>,
+ pub dict_set: Arc<HashSet<&'static str>>,
+}
+
+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<Request<hyper::body::Incoming>> for RandomPageService {
+ type Response = Response<Full<Bytes>>;
+ type Error = hyper::Error;
+
+ type Future =
+ Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
+
+ fn call(&self, req: Request<hyper::body::Incoming>) -> Self::Future {
+ fn mk_response(
+ s: String,
+ status_code: StatusCode,
+ ) -> Result<Response<Full<Bytes>>, 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<usize> = 10..=200;
+const N_LINKS: RangeInclusive<usize> = 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::<Vec<&str>>()
+ .join(" ");
+
+ let random_links = (0..n_links)
+ .map(|_| random_route_link(&mut rng, dictionary))
+ .map(|link| format!("<p>{}</p>", link))
+ .collect::<Vec<String>>()
+ .join("\n");
+
+ format!(
+ r#"
+ <html>
+ <head>
+ </head>
+ <body>
+ <p>{}</p>
+ {}
+ </body>
+ </html>
+ "#,
+ 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::<Vec<&str>>()
+ .join("/");
+ let label = random_word(rng, dictionary);
+ format!("<a href=\"/{}\">{}</a>", 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()
+}