// // Axum/Tera & Real Shortcodes in Rust: A WordPress-Like Implementation // use tera_shortcodes; use axum::{ extract::{Extension, Request, Json, Query}, response::Html, routing::{get, post}, Router, ServiceExt, }; use serde::{Serialize, Deserialize}; use tera::{Tera, Context}; use std::collections::HashMap; const ADDRESS: &str = "127.0.0.1:8080"; #[derive(Clone, Serialize)] struct Product { id: u32, name: String, image_url: String, price: f64, } #[derive(Serialize, Deserialize)] struct ProductsShortcode { limit: Option, orderby: Option, } // {{ shortcode(display="products", limit=4) | safe }} fn products_shortcode_fn( args: &HashMap, ) -> String { let js_caller = match args.get("jscaller") { Some(value) => value .as_str() .unwrap() .trim_matches(|c| c == '"' || c == '\'') .parse() .unwrap_or(false), None => false, }; let mut parameters = vec![]; if let Some(limit) = args.get("limit") { parameters.push(format!("limit={}", limit.as_str() .unwrap() .trim_matches(|c| c == '"' || c == '\''))); } if let Some(orderby) = args.get("orderby") { parameters.push(format!("orderby={}", orderby.as_str() .unwrap() .trim_matches(|c| c == '"' || c == '\''))); } let url = format!("http://{}/products?{}", ADDRESS, parameters.join("&")); if js_caller { tera_shortcodes::fetch_shortcode_js( &url, Some("get"), None, Some("Products"), ) } else { tera_shortcodes::fetch_shortcode( &url, Some("get"), None, ) } } async fn products( Query(parameters): Query, Extension(tera): Extension, ) -> Html { let limit = match parameters.limit { Some(limit) => limit, None => 4, }; let orderby = match parameters.orderby { Some(ref orderby) => orderby.as_str(), None => "id", }; let mut products = vec![ Product { id: 1, name: "Lorem ipsum dolor".to_string(), image_url: "https://picsum.photos/210/300".to_string(), price: 39.99, }, Product { id: 2, name: "Donec rutrum dui".to_string(), image_url: "https://picsum.photos/220/300".to_string(), price: 59.99, }, Product { id: 3, name: "Mauris imperdiet massa".to_string(), image_url: "https://picsum.photos/230/300".to_string(), price: 29.99, }, Product { id: 4, name: "Sed tristique tellus".to_string(), image_url: "https://picsum.photos/240/300".to_string(), price: 9.99, }, Product { id: 5, name: "Vivamus tempus".to_string(), image_url: "https://picsum.photos/250/300".to_string(), price: 49.99, }, Product { id: 6, name: "Aliquam rutrum viverra".to_string(), image_url: "https://picsum.photos/260/300".to_string(), price: 19.99, }, ]; match orderby { "name" => products.sort_by(|a, b| a.name.cmp(&b.name)), "price" => products.sort_by(|a, b| a.price.partial_cmp(&b.price).unwrap()), _ => products.sort_by(|a, b| a.id.cmp(&b.id)), }; // Convert the limit from i32 to usize let limit = std::cmp::min(limit as usize, products.len()); let products_by_limit = products[0..limit].to_vec(); let mut data = Context::new(); data.insert("products", &products_by_limit); let rendered = tera.render("shortcodes/products.html", &data).unwrap(); Html(rendered) } fn another_shortcode_fn( args: &HashMap, ) -> String { let width = match args.get("width") { Some(value) => value .as_str() .unwrap() .trim_matches(|c| c == '"' || c == '\''), None => "200", }; let height = match args.get("height") { Some(value) => value .as_str() .unwrap() .trim_matches(|c| c == '"' || c == '\''), None => "200", }; let image_src = match args.get("image_src") { Some(value) => value .as_str() .unwrap() .trim_matches(|c| c == '"' || c == '\''), None => "No image attribute specified", }; format!(r#""#, image_src, width, height) } #[derive(Serialize, Deserialize)] struct DataTest { foo: String, bar: String, } // Handler function that returns JSON content async fn data( Json(payload): Json, ) -> Json { let data = DataTest { foo: format!("ok {}", payload.foo), bar: format!("ok {}", payload.bar), }; // Return the JSON response Json(data) } async fn test( Extension(tera): Extension, ) -> Html { let context = Context::new(); // Render the template with the context let rendered = tera .render("test_shortcode.html", &context) .unwrap(); Html(rendered) } #[tokio::main] async fn main() { let shortcodes = tera_shortcodes::Shortcodes::new() .register("my_shortcode", |args| -> String { let js_caller = match args.get("jscaller") { Some(value) => value .as_str() .unwrap() .trim_matches(|c| c == '"' || c == '\'') .parse() .unwrap_or(false), None => false, }; let foo = match args.get("foo") { Some(value) => value .as_str() .unwrap() .trim_matches(|c| c == '"' || c == '\''), None => "no foo", }; let bar = match args.get("bar") { Some(value) => value .as_str() .unwrap() .trim_matches(|c| c == '"' || c == '\''), None => "no bar", }; let json_body = serde_json::to_string(&DataTest { foo: foo.to_string(), bar: bar.to_string(), }).unwrap(); let url = format!("http://{}/data", ADDRESS); if js_caller { tera_shortcodes::fetch_shortcode_js( &url, Some("post"), Some(&json_body), None, ) } else { tera_shortcodes::fetch_shortcode( &url, Some("post"), Some(&json_body), ) } }) .register("another_shortcode", another_shortcode_fn) .register("products", products_shortcode_fn); let mut tera = Tera::new("examples/templates/**/*").unwrap(); // Register the custom function tera.register_function("shortcode", shortcodes); // Build our application with a route let app = Router::new() .route("/", get(|| async { "Hello world!" })) .route("/test", get(test)) .route("/data", post(data)) // content to shorcode .route("/products", get(products)) // content to shorcode .layer(Extension(tera)); // Run the server let listener = tokio::net::TcpListener::bind(ADDRESS) .await .unwrap(); let url = format!("http://{}/test", ADDRESS); if let Err(e) = open::that(&url) { eprintln!("Failed to open URL: {}", e); } println!("Point your browser to this url: {} if not opened automatically", url); axum::serve(listener, ServiceExt::::into_make_service(app)) .await .unwrap(); }