From a84e3772a58ab19f8807beb32616547f1cdc9742 Mon Sep 17 00:00:00 2001 From: Vincent Thomas <77443389+vincent-thomas@users.noreply.github.com> Date: Thu, 16 Jan 2025 17:59:13 +0100 Subject: [PATCH] ssg --- Cargo.lock | 11 +++++ Cargo.toml | 2 +- titan-core/src/respond.rs | 16 ++++---- titan-derive/Cargo.toml | 19 +++++++++ titan-derive/src/lib.rs | 77 +++++++++++++++++++++++++++++++++++ titan-html-derive/src/lib.rs | 34 ++++++++++++++-- titan-html/src/lib.rs | 4 +- titan/Cargo.toml | 2 + titan/examples/hello_world.rs | 15 ++++++- titan/examples/nice_fns.rs | 10 +++++ titan/src/lib.rs | 4 ++ titan/src/utils.rs | 5 +++ 12 files changed, 181 insertions(+), 18 deletions(-) create mode 100644 titan-derive/Cargo.toml create mode 100644 titan-derive/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 6285cc1..382cfbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1570,11 +1570,13 @@ version = "0.4.0" dependencies = [ "futures-util", "lambda_http", + "lazy_static", "pin-project-lite", "serde", "serde_json", "serde_urlencoded", "titan-core", + "titan-derive", "titan-html", "titan-http", "titan-router", @@ -1594,6 +1596,15 @@ dependencies = [ "tower 0.5.2", ] +[[package]] +name = "titan-derive" +version = "0.4.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.95", +] + [[package]] name = "titan-html" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index a0a22ac..9d16096 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ members = [ "titan-html-core", "titan-html-derive", "titan-lambda" -] +, "titan-derive"] [workspace.package] authors = ["Vincent Thomas"] diff --git a/titan-core/src/respond.rs b/titan-core/src/respond.rs index 1fa2de7..de5499d 100644 --- a/titan-core/src/respond.rs +++ b/titan-core/src/respond.rs @@ -42,7 +42,7 @@ use titan_http::{body::Body, header, Response, ResponseBuilder, StatusCode}; /// // converted into an HTTP response. /// ``` pub trait Respondable { - fn respond(self) -> Response; + fn respond(self) -> Response; } impl Respondable for Result @@ -50,7 +50,7 @@ where T: Respondable, E: Respondable, { - fn respond(self) -> Response { + fn respond(self) -> Response { match self { Ok(t) => t.respond(), Err(e) => e.respond(), @@ -58,20 +58,20 @@ where } } -impl Respondable for Response { - fn respond(self) -> Response { +impl Respondable for Response { + fn respond(self) -> Response { self } } impl Respondable for Infallible { - fn respond(self) -> Response { + fn respond(self) -> Response { panic!("Not fallible :(") } } impl Respondable for () { - fn respond(self) -> Response { + fn respond(self) -> Response { ResponseBuilder::new().status(204).body(Body::from(())).unwrap() } } @@ -80,7 +80,7 @@ impl Respondable for (StatusCode, T) where T: Respondable, { - fn respond(self) -> Response { + fn respond(self) -> Response { let (status, body) = self; let mut res = body.respond(); @@ -111,7 +111,7 @@ macro_rules! impl_respondable_for_int { ($($t:ty)*) => { $( impl Respondable for $t { - fn respond(self) -> Response { + fn respond(self) -> Response { let body = Body::from(self); let mut res = Response::new(body); diff --git a/titan-derive/Cargo.toml b/titan-derive/Cargo.toml new file mode 100644 index 0000000..793ed1a --- /dev/null +++ b/titan-derive/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "titan-derive" +authors.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true +edition.workspace = true +version.workspace = true +repository.workspace = true +documentation.workspace = true +rust-version.workspace = true + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2", features = ["full"] } +quote = "1" +proc-macro2 = "1.0.92" diff --git a/titan-derive/src/lib.rs b/titan-derive/src/lib.rs new file mode 100644 index 0000000..6ed8002 --- /dev/null +++ b/titan-derive/src/lib.rs @@ -0,0 +1,77 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use syn::parse_macro_input; +use syn::ItemFn; + +#[proc_macro_attribute] +pub fn ssg(_: TokenStream, input: TokenStream) -> TokenStream { + let item = parse_macro_input!(input as ItemFn); + + if !item.sig.inputs.is_empty() { + return syn::Error::new( + item.sig.ident.span(), + "Error: SSG routes cannot have arguments", + ) + .into_compile_error() + .into(); + } + impl_ssg(item).into() +} + +fn impl_ssg(item: ItemFn) -> TokenStream2 { + let struct_ident = item.sig.ident.clone(); + + let ident_cache_str = + format!("{}_CACHE", item.sig.ident.to_string().to_uppercase()); + let ident_cache = syn::Ident::new(&ident_cache_str, item.sig.ident.span()); + + let nice_fn = item.block; + + quote::quote! { + titan::lazy_static! { + static ref #ident_cache: std::sync::RwLock>> = std::sync::RwLock::new(None); + } + + #[allow(non_camel_case_types)] + #[derive(Clone)] + pub struct #struct_ident; + + impl titan::Handler<()> for #struct_ident { + type Output = titan::http::Response; + type Future = + std::pin::Pin + Send>>; + fn call(&self, _: ()) -> Self::Future { + let _lock = match #ident_cache.read() { + Ok(v) => v, + Err(_) => panic!("oh no"), + }; + + if let Some(cache) = _lock.as_ref() { + let body = + titan::http::body::Body::Full(cache.clone().into_boxed_slice()); + return Box::pin(async move { + titan::http::ResponseBuilder::new().status(200).body(body).unwrap() + }); + }; + + Box::pin(async move { + let response = titan::FutureExt::map(async #nice_fn, |x| x.respond()).await; + + + match response.body() { + titan::http::body::Body::Full(ref body) => { + let mut refs = #ident_cache.write().unwrap(); + *refs = Some(body.clone().to_vec()); + } + titan::http::body::Body::Stream(_) => { + panic!( + "Body::Stream is not available in a cached request response :(" + ) + } + }; + response + }) + } + } + } +} diff --git a/titan-html-derive/src/lib.rs b/titan-html-derive/src/lib.rs index 4d66465..f3bad31 100644 --- a/titan-html-derive/src/lib.rs +++ b/titan-html-derive/src/lib.rs @@ -2,8 +2,10 @@ use lightningcss::{printer::PrinterOptions, properties::Property}; use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::quote; -use syn::{parse_macro_input, Field, Fields, ItemStruct, LitStr}; -use titan_utils::validatecss::{self, CSSValidationError}; +use syn::{parse_macro_input, Field, Fields, Ident, ItemStruct, LitStr}; +use titan_utils::validatecss::{ + validate_css, validate_globalcss, CSSValidationError, +}; fn from_stylerules_to_tokenstream( prop: Vec<(String, Property<'_>)>, @@ -29,6 +31,30 @@ fn from_stylerules_to_tokenstream( #(#styles_tokens),* } } +// +//// Helper function to find and extract variables from CSS values +//fn extract_variables(css: &str) -> Vec<(usize, usize, String)> { +// let mut variables = Vec::new(); +// let mut start = 0; +// while let Some(var_start) = css[start..].find("{") { +// if let Some(var_end) = css[start + var_start..].find('}') { +// let abs_start = start + var_start; +// let abs_end = start + var_start + var_end + 1; +// let var_name = css[abs_start + 1..abs_end - 1].to_string(); +// variables.push((abs_start, abs_end, var_name)); +// start = abs_end; +// } else { +// break; +// } +// } +// variables +//} +// +//#[derive(Debug)] +//enum StringBit { +// Var(Ident), +// Text(String), +//} #[proc_macro] pub fn global_css(input: TokenStream) -> TokenStream { @@ -37,7 +63,7 @@ pub fn global_css(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as LitStr); let result = input.value(); - let err = validatecss::validate_globalcss(&result); + let err = validate_globalcss(&result); quote! { titan::html::tags::Style::Text(#err.to_string()) }.into() } @@ -49,7 +75,7 @@ pub fn css(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as LitStr); let result = input.value(); - if let Err(err) = validatecss::validate_css(&result) { + if let Err(err) = validate_css(&result) { match err { CSSValidationError::FieldError(field) => { let span = input.span(); diff --git a/titan-html/src/lib.rs b/titan-html/src/lib.rs index aedd16a..1b37b4c 100644 --- a/titan-html/src/lib.rs +++ b/titan-html/src/lib.rs @@ -31,7 +31,5 @@ pub fn render(root: Html) -> String { if let Some(nonce) = root.with_csp_nonce { html.apply_nonce(&nonce); } - dbg!(&html); - - dbg!(format!("{}{}", DOCTYPE, html.to_string())) + format!("{}{}", DOCTYPE, html.to_string()) } diff --git a/titan/Cargo.toml b/titan/Cargo.toml index 34cbaa6..3d6118c 100644 --- a/titan/Cargo.toml +++ b/titan/Cargo.toml @@ -39,10 +39,12 @@ titan-html = { path = "../titan-html", version = "0.4.0" } titan-router = { path = "../titan-router", version = "0.4.0" } titan-server = { path = "../titan-server", version = "0.4.0" } titan-utils = { path = "../titan-utils", version = "0.4.0" } +titan-derive = { path = "../titan-derive", version = "0.4.0" } pin-project-lite = "0.2.14" futures-util = "0.3.31" lambda_http = { version = "0.14.0", optional = true } +lazy_static = "1.5.0" [dev-dependencies] tokio = { version = "1.38.1", features = ["macros", "rt-multi-thread", "time"] } diff --git a/titan/examples/hello_world.rs b/titan/examples/hello_world.rs index fe58ccd..934d1bf 100644 --- a/titan/examples/hello_world.rs +++ b/titan/examples/hello_world.rs @@ -27,7 +27,9 @@ const TESTING: &[StyleRule] = css!( " ); -async fn index() -> impl Respondable { +#[ssg] +fn index() -> impl Respondable { + println!("ran"); Html::from(( Head::default().global_style(global_css!("")).reset_css(), Body::default().children([ @@ -52,11 +54,20 @@ async fn index() -> impl Respondable { .with_csp("examplenonce") } +use titan_derive::ssg; + +#[ssg] +pub fn testing() -> titan_html::tags::html::Html { + println!("ran"); + Html::from((Head::default(), Body::default())) +} + #[tokio::main] async fn main() -> io::Result<()> { let listener = TcpListener::bind("0.0.0.0:4000").await.unwrap(); - let app = App::default().at("/", web::get(index)); + let app = + App::default().at("/", web::get(index)).at("/test", web::get(testing)); titan::serve(listener, app).await } diff --git a/titan/examples/nice_fns.rs b/titan/examples/nice_fns.rs index 8d9fe37..e69865d 100644 --- a/titan/examples/nice_fns.rs +++ b/titan/examples/nice_fns.rs @@ -19,6 +19,16 @@ struct Queries { test: Option, } +use titan_derive::ssg; + +#[titan::ssg] +pub fn testing() -> titan_html::tags::html::Html { + titan::html::tags::html::Html::from(( + titan::html::tags::head::Head::default(), + titan::html::tags::Body::default(), + )) +} + async fn index( Cookies(cookies): Cookies, authorization::Bearer(token): authorization::Bearer, diff --git a/titan/src/lib.rs b/titan/src/lib.rs index e3583ec..77541a1 100644 --- a/titan/src/lib.rs +++ b/titan/src/lib.rs @@ -4,6 +4,10 @@ pub mod prelude; pub mod route; mod utils; +// For titan-derive +pub use utils::lazy_static; +pub use utils::FutureExt; + #[doc(hidden)] #[cfg(feature = "internal-titan-lambda")] pub mod lambda; diff --git a/titan/src/utils.rs b/titan/src/utils.rs index e11fc8b..93d6d4c 100644 --- a/titan/src/utils.rs +++ b/titan/src/utils.rs @@ -3,6 +3,11 @@ use std::task::{Context, Poll}; use futures_util::future::BoxFuture; use titan_core::{Service, ServiceExt}; +// Required for titan-derive +pub use lazy_static::lazy_static; + +pub use futures_util::FutureExt; + pub trait CloneService: Service { fn clone_box( &self,