diff --git a/Cargo.lock b/Cargo.lock index be463a4..7d12215 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,6 +120,60 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "axum" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -442,6 +496,12 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -673,6 +733,58 @@ dependencies = [ "syn", ] +[[package]] +name = "http" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "humansize" version = "2.1.3" @@ -688,6 +800,41 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hyper" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "iana-time-zone" version = "0.1.61" @@ -1039,12 +1186,34 @@ dependencies = [ "xml5ever", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.8.2" @@ -1426,6 +1595,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + [[package]] name = "ryu" version = "1.0.18" @@ -1479,6 +1654,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_spanned" version = "0.6.8" @@ -1488,6 +1673,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha2" version = "0.10.8" @@ -1604,6 +1801,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + [[package]] name = "synstructure" version = "0.13.1" @@ -1733,6 +1936,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.8.19" @@ -1767,6 +1983,79 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" +dependencies = [ + "bitflags 2.6.0", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + [[package]] name = "typenum" version = "1.17.0" @@ -1896,6 +2185,7 @@ name = "v7" version = "0.1.0" dependencies = [ "anyhow", + "axum", "base64", "chrono", "clap", @@ -1918,6 +2208,7 @@ dependencies = [ "tokio", "tokio-stream", "toml", + "tower-http", "unicode-normalization", "url", ] diff --git a/Cargo.toml b/Cargo.toml index 0874939..a11ab77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ serde_json = "1.0" [dependencies] anyhow = "1.0.95" +axum = "0.8.1" base64 = "0.22.1" chrono = { version = "0.4.39", features = ["serde"] } clap = { version = "4.5.23", features = ["cargo"] } @@ -40,5 +41,6 @@ tera = "1.20.0" tokio = { version = "1.42.0", features = ["full"] } tokio-stream = "0.1.17" toml = "0.8.19" +tower-http = { version = "0.6.2", features = ["fs"] } unicode-normalization = "0.1.24" url = "2.5.4" diff --git a/src/generator/mod.rs b/src/generator/mod.rs index 3a9b7db..0371894 100644 --- a/src/generator/mod.rs +++ b/src/generator/mod.rs @@ -11,7 +11,7 @@ use std::cell::RefCell; use std::rc::Rc; use compute_graph::{AsyncGraph, builder::GraphBuilder}; -use util::{Combine, MapToVoid}; +use util::Combine; pub use util::content_base_path; pub use util::file_watcher::FileWatcher; @@ -23,8 +23,6 @@ pub async fn generate(watcher: Rc>) -> anyhow::Result>) -> anyhow::Result>) -> anyhow::Result, DynamicInput, Input>>, - Input>, ) { let (post_files, invalidate_posts) = builder.add_invalidatable_rule(ListPostFiles); watcher.borrow_mut().watch(content_path("posts/"), move || { @@ -41,7 +39,7 @@ pub fn make_graph( let posts = builder.add_dynamic_rule(MakeReadNodes::new(post_files, Rc::clone(&watcher))); - let extract_metadatas = builder.add_dynamic_rule(MakeExtractMetadatas::new(posts.clone())); + // let extract_metadatas = builder.add_dynamic_rule(MakeExtractMetadatas::new(posts.clone())); let article_path = content_path("layout/article.html"); let (article_template, invalidate_template) = builder.add_invalidatable_rule(AddTemplate::new( @@ -60,7 +58,6 @@ pub fn make_graph( builder.add_rule(MapDynamicToVoid(write_posts)), posts.clone(), builder.add_rule(AllPosts(posts)), - builder.add_rule(AllMetadatas(extract_metadatas)), ) } @@ -162,52 +159,52 @@ impl Rule for ReadPost { } } -#[derive(InputVisitable)] -struct MakeExtractMetadatas { - posts: DynamicInput, - node_factory: DynamicNodeFactory, -} -impl MakeExtractMetadatas { - fn new(posts: DynamicInput) -> Self { - Self { - posts, - node_factory: DynamicNodeFactory::new(), - } - } -} -impl DynamicRule for MakeExtractMetadatas { - type ChildOutput = ExtractMetadataOutput; - fn evaluate(&mut self, ctx: &mut impl DynamicRuleContext) -> Vec> { - for post_input in self.posts.value().inputs.iter() { - let post_ = post_input.value(); - let post = post_.as_ref().unwrap(); - self.node_factory.add_node(ctx, post.path.clone(), |ctx| { - ctx.add_rule(ExtractMetadata(post_input.clone())) - }); - } - self.node_factory.all_nodes(ctx) - } -} +// #[derive(InputVisitable)] +// struct MakeExtractMetadatas { +// posts: DynamicInput, +// node_factory: DynamicNodeFactory, +// } +// impl MakeExtractMetadatas { +// fn new(posts: DynamicInput) -> Self { +// Self { +// posts, +// node_factory: DynamicNodeFactory::new(), +// } +// } +// } +// impl DynamicRule for MakeExtractMetadatas { +// type ChildOutput = ExtractMetadataOutput; +// fn evaluate(&mut self, ctx: &mut impl DynamicRuleContext) -> Vec> { +// for post_input in self.posts.value().inputs.iter() { +// let post_ = post_input.value(); +// let post = post_.as_ref().unwrap(); +// self.node_factory.add_node(ctx, post.path.clone(), |ctx| { +// ctx.add_rule(ExtractMetadata(post_input.clone())) +// }); +// } +// self.node_factory.all_nodes(ctx) +// } +// } -type ExtractMetadataOutput = Option; +// type ExtractMetadataOutput = Option; -#[derive(InputVisitable)] -struct ExtractMetadata(Input); -impl Rule for ExtractMetadata { - type Output = ExtractMetadataOutput; +// #[derive(InputVisitable)] +// struct ExtractMetadata(Input); +// impl Rule for ExtractMetadata { +// type Output = ExtractMetadataOutput; - fn evaluate(&mut self) -> Self::Output { - self.input_0().as_ref().map(|post| post.metadata.clone()) - } +// fn evaluate(&mut self) -> Self::Output { +// self.input_0().as_ref().map(|post| post.metadata.clone()) +// } - fn node_label(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - if let Some(post) = self.input_0().as_ref() { - write!(f, "{}", post.slug) - } else { - Ok(()) - } - } -} +// fn node_label(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { +// if let Some(post) = self.input_0().as_ref() { +// write!(f, "{}", post.slug) +// } else { +// Ok(()) +// } +// } +// } #[derive(InputVisitable)] struct MakeWritePosts { @@ -278,17 +275,3 @@ impl Rule for AllPosts { .collect() } } - -/// Flattens Vec> into Vec -#[derive(InputVisitable)] -struct AllMetadatas(DynamicInput); -impl Rule for AllMetadatas { - type Output = Vec; - fn evaluate(&mut self) -> Self::Output { - self.input_0() - .inputs - .iter() - .flat_map(|inp| inp.value().clone()) - .collect() - } -} diff --git a/src/main.rs b/src/main.rs index f9d7c7c..c353242 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,17 +2,20 @@ mod generator; -use clap::{Command, arg, command}; +use axum::Router; +use clap::{Arg, ArgAction, ArgMatches, Command, command}; +use compute_graph::AsyncGraph; use debounced::debounced; -use futures::FutureExt; +use futures::join; use generator::{FileWatcher, content_base_path}; use log::info; use std::cell::RefCell; use std::rc::Rc; use std::time::Duration; -use tokio::pin; +use tokio::net::TcpListener; use tokio_stream::StreamExt; use tokio_stream::wrappers::UnboundedReceiverStream; +use tower_http::services::{ServeDir, ServeFile}; #[tokio::main] async fn main() { @@ -21,14 +24,20 @@ async fn main() { let matches = command!() .subcommand_required(true) .arg_required_else_help(true) - .subcommand( - Command::new("gen") - .arg(arg!(--watch "Watch the site directory and regenerate on changes")), + .arg( + Arg::new("watch") + .long("watch") + .help("Watch the site directory and regenerate on changes") + .action(ArgAction::SetTrue), ) - .subcommand( - Command::new("serve") - .arg(arg!(--watch "Watch the site directory and regenerate on changes")), + .arg( + Arg::new("dump-graph") + .long("dump-graph") + .help("Print the graphviz representation of the graph after initial generation") + .action(ArgAction::SetTrue), ) + .subcommand(Command::new("gen")) + .subcommand(Command::new("serve")) .get_matches(); if cfg!(debug_assertions) { @@ -37,51 +46,58 @@ async fn main() { info!("Running in release mode"); } - match matches.subcommand() { - Some(("gen", matches)) => { - let watcher = Rc::new(RefCell::new(FileWatcher::new())); + let watcher = Rc::new(RefCell::new(FileWatcher::new())); - let mut graph = generator::generate(Rc::clone(&watcher)) - .await - .expect("generating"); + let graph = generator::generate(Rc::clone(&watcher)) + .await + .expect("generating"); - if matches.contains_id("watch") { - let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<()>(); + if let Some(true) = matches.get_one("dump-graph") { + println!("{}", graph.as_dot_string()); + } - watcher.borrow_mut().watch(content_base_path(), move || { - tx.send(()).expect("sending regenerate signal"); - }); + let serve_future = serve(&matches); + let watch_future = watch(&matches, graph, watcher); - let mut debounced = - debounced(UnboundedReceiverStream::new(rx), Duration::from_millis(100)); + join!(serve_future, watch_future); +} - let watch = FileWatcher::start(watcher).fuse(); - - let regenerate = async move { - while let Some(_) = debounced.next().await { - info!("Regenerating"); - graph.evaluate_async().await; - } - } - .fuse(); - - pin!(regenerate, watch); - - loop { - futures::select! { - watcher_res = watch => { - watcher_res.expect("watching files"); - } - _ = regenerate => { - info!("regenerate channel closed"); - } - } - } - } - } - Some(("serve", _matches)) => { - todo!() - } - _ => unreachable!(), +async fn serve(matches: &ArgMatches) { + if let Some(("serve", _)) = matches.subcommand() { + // TODO: consider not pulling in all of axum just for this + let app = Router::new().fallback_service( + ServeDir::new("out/").not_found_service(ServeFile::new("out/404.html")), + ); + let addr = if cfg!(debug_assertions) { + "0.0.0.0:8084" + } else { + "127.0.0.1:8084" + }; + info!("Listening on {}", addr); + let listener = TcpListener::bind(addr).await.unwrap(); + axum::serve(listener, app).await.unwrap(); + } +} + +async fn watch(matches: &ArgMatches, mut graph: AsyncGraph<()>, watcher: Rc>) { + if let Some(true) = matches.get_one("watch") { + let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<()>(); + + watcher.borrow_mut().watch(content_base_path(), move || { + tx.send(()).expect("sending regenerate signal"); + }); + + let mut debounced = debounced(UnboundedReceiverStream::new(rx), Duration::from_millis(100)); + + let watch = async move { FileWatcher::start(watcher).await.expect("watching files") }; + + let regenerate = async move { + while let Some(_) = debounced.next().await { + info!("Regenerating"); + graph.evaluate_async().await; + } + }; + + join!(watch, regenerate); } }