diff --git a/examples/axum-app/Cargo.toml b/examples/axum-app/Cargo.toml
index fde58513..165b3874 100644
--- a/examples/axum-app/Cargo.toml
+++ b/examples/axum-app/Cargo.toml
@@ -9,7 +9,7 @@ publish = false
# and axum as your web-framework.
[dependencies]
axum = "0.8.1"
-rinja = { version = "0.3.5", path = "../../rinja" }
+rinja = { version = "0.3.5", path = "../../rinja", features = ["dynamic"] }
tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread"] }
# serde and strum are used to parse (deserialize) and generate (serialize) information
diff --git a/examples/axum-app/src/main.rs b/examples/axum-app/src/main.rs
index a9112e35..defc2b16 100644
--- a/examples/axum-app/src/main.rs
+++ b/examples/axum-app/src/main.rs
@@ -1,13 +1,16 @@
+use std::borrow::Cow;
+
use axum::extract::{Path, Query};
use axum::http::StatusCode;
use axum::response::{Html, IntoResponse, Redirect, Response};
use axum::routing::get;
use axum::{Router, serve};
use rinja::Template;
-use serde::Deserialize;
+use serde::{Deserialize, Serialize};
use tower_http::trace::TraceLayer;
use tracing::{Level, info};
+#[rinja::main]
#[tokio::main]
async fn main() -> Result<(), Error> {
tracing_subscriber::fmt()
@@ -52,7 +55,7 @@ enum Error {
/// * `PartialEq` so that we can use the type in comparisons with `==` or `!=`.
/// * `serde::Deserialize` so that axum can parse the type in incoming URLs.
/// * `strum::Display` so that rinja can write the value in templates.
-#[derive(Default, Debug, Clone, Copy, PartialEq, Deserialize, strum::Display)]
+#[derive(Default, Debug, Clone, Copy, PartialEq, Deserialize, Serialize, strum::Display)]
#[allow(non_camel_case_types)]
enum Lang {
#[default]
@@ -130,8 +133,8 @@ async fn index_handler(
// In `IndexHandlerQuery` we annotated the field with `#[serde(default)]`, so if the value is
// absent, an empty string is selected by default, which is visible to the user an empty
// `` element.
- #[derive(Debug, Template)]
- #[template(path = "index.html")]
+ #[derive(Debug, Template, Serialize, Deserialize)]
+ #[template(path = "index.html", dynamic = true)]
struct Tmpl {
lang: Lang,
name: String,
@@ -158,16 +161,17 @@ async fn greeting_handler(
Path((lang,)): Path<(Lang,)>,
Query(query): Query,
) -> Result {
- #[derive(Debug, Template)]
- #[template(path = "greet.html")]
- struct Tmpl {
+ #[derive(Debug, Template, Serialize, Deserialize)]
+ #[template(path = "greet.html", dynamic = true, print = "code")]
+ struct Tmpl<'a> {
lang: Lang,
- name: String,
+ #[serde(borrow)]
+ name: Cow<'a, str>,
}
let template = Tmpl {
lang,
- name: query.name,
+ name: query.name.into(),
};
Ok(Html(template.render()?))
}
diff --git a/rinja/Cargo.toml b/rinja/Cargo.toml
index efc71169..6001e454 100644
--- a/rinja/Cargo.toml
+++ b/rinja/Cargo.toml
@@ -28,6 +28,12 @@ rinja_derive = { version = "=0.3.5", path = "../rinja_derive" }
itoa = "1.0.11"
+# needed by feature "dynamic"
+linkme = { version = "0.3.31", optional = true }
+notify = { version = "8.0.0", optional = true }
+parking_lot = { version = "0.12.3", optional = true, features = ["arc_lock", "send_guard"] }
+tokio = { version = "1.43.0", optional = true, features = ["macros", "io-util", "net", "process", "rt", "sync", "time"] }
+
# needed by feature "serde_json"
serde = { version = "1.0", optional = true, default-features = false }
serde_json = { version = "1.0", optional = true, default-features = false, features = [] }
@@ -43,7 +49,7 @@ criterion = "0.5"
maintenance = { status = "actively-developed" }
[features]
-default = ["config", "std", "urlencode"]
+default = ["config", "std", "urlencode", "dynamic"]
full = ["default", "code-in-doc", "serde_json"]
alloc = [
@@ -54,6 +60,16 @@ alloc = [
]
code-in-doc = ["rinja_derive/code-in-doc"]
config = ["rinja_derive/config"]
+dynamic = [
+ "std",
+ "rinja_derive/dynamic",
+ "serde/derive",
+ "dep:linkme",
+ "dep:notify",
+ "dep:parking_lot",
+ "dep:serde_json",
+ "dep:tokio",
+]
serde_json = ["rinja_derive/serde_json", "dep:serde", "dep:serde_json"]
std = [
"alloc",
diff --git a/rinja/src/dynamic/child.rs b/rinja/src/dynamic/child.rs
new file mode 100644
index 00000000..94b1d205
--- /dev/null
+++ b/rinja/src/dynamic/child.rs
@@ -0,0 +1,236 @@
+use std::borrow::Cow;
+use std::env::args;
+use std::fmt::Write;
+use std::io::ErrorKind;
+use std::net::SocketAddr;
+use std::process::exit;
+use std::string::String;
+use std::sync::Arc;
+use std::time::Duration;
+use std::vec::Vec;
+use std::{eprintln, format};
+
+use linkme::distributed_slice;
+use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
+use tokio::net::TcpStream;
+use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
+use tokio::spawn;
+use tokio::sync::{Mutex, oneshot};
+
+use super::{DYNAMIC_ENVIRON_KEY, MainRequest, MainResponse, Outcome};
+
+const PROCESSORS: usize = 4;
+
+#[inline(never)]
+pub(crate) fn run_dynamic_main() {
+ std::env::set_var(DYNAMIC_ENVIRON_KEY, "-");
+
+ let mut entries: Vec<_> = DYNAMIC_TEMPLATES.iter().map(|entry| entry.name()).collect();
+ entries.sort_unstable();
+ eprintln!("templates implemented by subprocess: {entries:?}");
+ for window in entries.windows(2) {
+ if let &[a, b] = window {
+ if a == b {
+ eprintln!("duplicated dynamic template {a:?}");
+ }
+ }
+ }
+
+ let sock_addr: SocketAddr = {
+ let mut args = args().fuse();
+ let (_, Some("--__rinja_dynamic"), Some(sock_addr), None) = (
+ args.next(),
+ args.next().as_deref(),
+ args.next(),
+ args.next(),
+ ) else {
+ eprintln!("child process got unexpected arguments");
+ exit(1);
+ };
+ match serde_json::from_str(&sock_addr) {
+ Ok(sock_addr) => sock_addr,
+ Err(err) => {
+ eprintln!("subprocess could not interpret socket addr: {err}");
+ exit(1);
+ }
+ }
+ };
+
+ let rt = match tokio::runtime::Builder::new_current_thread()
+ .enable_all()
+ .build()
+ {
+ Ok(rt) => rt,
+ Err(err) => {
+ eprintln!("could not start tokio runtime: {err}");
+ exit(1);
+ }
+ };
+ let _ = rt.block_on(async move {
+ let sock = match TcpStream::connect(sock_addr).await {
+ Ok(sock) => sock,
+ Err(err) => {
+ eprintln!("subprocess could not connect to parent process: {err}");
+ exit(1);
+ }
+ };
+ let _: Result<(), std::io::Error> = sock.set_linger(None);
+ let _: Result<(), std::io::Error> = sock.set_nodelay(true);
+ let (read, write) = sock.into_split();
+
+ let stdout = Arc::new(Mutex::new(write));
+ let stdin = Arc::new(Mutex::new(BufReader::new(read)));
+ let (done_tx, done_rx) = oneshot::channel();
+ let done = Arc::new(Mutex::new(Some(done_tx)));
+
+ let mut threads = Vec::with_capacity(PROCESSORS);
+ for _ in 0..PROCESSORS {
+ threads.push(spawn(dynamic_processor(
+ Arc::clone(&stdout),
+ Arc::clone(&stdin),
+ Arc::clone(&done),
+ )));
+ }
+
+ done_rx.await.map_err(|err| {
+ std::io::Error::new(ErrorKind::BrokenPipe, format!("lost result channel: {err}"));
+ })
+ });
+ rt.shutdown_timeout(Duration::from_secs(5));
+ exit(0)
+}
+
+async fn dynamic_processor(
+ stdout: Arc>,
+ stdin: Arc>>,
+ done: Arc>>>>,
+) {
+ let done = move |result: Result<(), std::io::Error>| {
+ let done = Arc::clone(&done);
+ async move {
+ let mut lock = done.lock().await;
+ if let Some(done) = lock.take() {
+ let _: Result<_, _> = done.send(result);
+ }
+ }
+ };
+
+ let mut line_buf = String::new();
+ let mut response_buf = String::new();
+ loop {
+ line_buf.clear();
+ match stdin.lock().await.read_line(&mut line_buf).await {
+ Ok(n) if n > 0 => {}
+ result => return done(result.map(|_| ())).await,
+ }
+ let line = line_buf.trim_ascii();
+ if line.is_empty() {
+ continue;
+ }
+
+ let MainRequest { callid, name, data } = match serde_json::from_str(line) {
+ Ok(req) => req,
+ Err(err) => {
+ let err = format!("could not deserialize request: {err}");
+ return done(Err(std::io::Error::new(ErrorKind::InvalidData, err))).await;
+ }
+ };
+ response_buf.clear();
+
+ let mut outcome = Outcome::NotFound;
+ for entry in DYNAMIC_TEMPLATES {
+ if entry.name() == name {
+ outcome = entry.dynamic_render(&mut response_buf, &data);
+ break;
+ }
+ }
+
+ // SAFETY: `serde_json` writes valid UTF-8 data
+ let mut line = unsafe { line_buf.as_mut_vec() };
+
+ line.clear();
+ if let Err(err) = serde_json::to_writer(&mut line, &MainResponse { callid, outcome }) {
+ let err = format!("could not serialize response: {err}");
+ return done(Err(std::io::Error::new(ErrorKind::InvalidData, err))).await;
+ }
+ line.push(b'\n');
+
+ let is_done = {
+ let mut stdout = stdout.lock().await;
+ stdout.write_all(line).await.is_err() || stdout.flush().await.is_err()
+ };
+ if is_done {
+ return done(Ok(())).await;
+ }
+ }
+}
+
+/// Used by [`Template`][rinja_derive::Template] to register a template for dynamic processing.
+#[macro_export]
+macro_rules! register_dynamic_template {
+ (
+ name: $Name:ty,
+ type: $Type:ty,
+ ) => {
+ const _: () = {
+ #[$crate::helpers::linkme::distributed_slice($crate::helpers::DYNAMIC_TEMPLATES)]
+ #[linkme(crate = $crate::helpers::linkme)]
+ static DYNAMIC_TEMPLATES: &'static dyn $crate::helpers::DynamicTemplate = &Dynamic;
+
+ struct Dynamic;
+
+ impl $crate::helpers::DynamicTemplate for Dynamic {
+ #[inline]
+ fn name(&self) -> &$crate::helpers::core::primitive::str {
+ $crate::helpers::core::any::type_name::<$Name>()
+ }
+
+ fn dynamic_render<'a>(
+ &self,
+ buf: &'a mut rinja::helpers::alloc::string::String,
+ value: &rinja::helpers::core::primitive::str,
+ ) -> rinja::helpers::Outcome<'a> {
+ let result = rinja::helpers::from_json::<$Type>(value).map(|tmpl| {
+ buf.clear();
+ let _ = buf.try_reserve(::SIZE_HINT);
+ tmpl.render_into(buf)
+ });
+ $crate::helpers::use_dynamic_render_result(buf, result)
+ }
+ }
+ };
+ };
+}
+
+/// Convert the result of [`serde::from_json()`] → [`Template::render()`] to an [`Outcome`].
+pub fn use_dynamic_render_result(
+ buf: &mut String,
+ result: Result, serde_json::Error>,
+) -> Outcome<'_> {
+ let result = match &result {
+ Ok(Ok(())) => return Outcome::Success(Cow::Borrowed(buf)),
+ Ok(Err(err)) => Ok(err),
+ Err(err) => Err(err),
+ };
+
+ buf.clear();
+ let result = match result {
+ Ok(e) => write!(buf, "{e}").map(|_| Outcome::Render(Cow::Borrowed(buf))),
+ Err(e) => write!(buf, "{e}").map(|_| Outcome::Deserialize(Cow::Borrowed(buf))),
+ };
+ result.unwrap_or(Outcome::Fmt)
+}
+
+/// List of implemented dynamic templates. Filled through
+/// [`register_dynamic_template!`][crate::register_dynamic_template].
+#[distributed_slice]
+pub static DYNAMIC_TEMPLATES: [&'static dyn DynamicTemplate];
+
+/// A dynamic template implementation
+pub trait DynamicTemplate: Send + Sync {
+ /// The type name of the template.
+ fn name(&self) -> &str;
+
+ /// Take a JSON `value` to to render the template into `buf`.
+ fn dynamic_render<'a>(&self, buf: &'a mut String, value: &str) -> Outcome<'a>;
+}
diff --git a/rinja/src/dynamic/mod.rs b/rinja/src/dynamic/mod.rs
new file mode 100644
index 00000000..1f14bf38
--- /dev/null
+++ b/rinja/src/dynamic/mod.rs
@@ -0,0 +1,102 @@
+pub(crate) mod child;
+pub(crate) mod parent;
+
+use std::borrow::Cow;
+use std::env::var_os;
+use std::process::exit;
+use std::sync::OnceLock;
+use std::{eprintln, fmt};
+
+use serde::{Deserialize, Serialize};
+
+const DYNAMIC_ENVIRON_KEY: &str = "__rinja_dynamic";
+const DYNAMIC_ENVIRON_VALUE: &str = env!("CARGO_PKG_VERSION");
+
+#[derive(Debug, Serialize, Deserialize)]
+struct MainRequest<'a> {
+ callid: u64,
+ #[serde(borrow)]
+ name: Cow<'a, str>,
+ data: Cow<'a, str>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+struct MainResponse<'a> {
+ callid: u64,
+ #[serde(borrow, flatten)]
+ outcome: Outcome<'a>,
+}
+
+/// The outcome of a dynamic template call.
+#[derive(Debug, Serialize, Deserialize)]
+pub enum Outcome<'a> {
+ /// The template was rendered correctly.
+ #[serde(borrow)]
+ Success(Cow<'a, str>),
+ /// The JSON serialized template could not be deserialized.
+ #[serde(borrow)]
+ Deserialize(Cow<'a, str>),
+ /// The template was not rendered correctly.
+ #[serde(borrow)]
+ Render(Cow<'a, str>),
+ /// The template's type name was not known to the subprocess.
+ NotFound,
+ /// An error occurred but the error could not be printed.
+ Fmt,
+}
+
+impl std::error::Error for Outcome<'_> {}
+
+impl fmt::Display for Outcome<'_> {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ match self {
+ Outcome::Success(_) => write!(f, "not an error"),
+ Outcome::Deserialize(err) => write!(f, "could not deserialize: {err}"),
+ Outcome::Render(err) => write!(f, "could not render: {err}"),
+ Outcome::NotFound => write!(f, "template not found"),
+ Outcome::Fmt => write!(f, "could not format error message"),
+ }
+ }
+}
+
+/// True if the current process is a dynamic subprocess.
+#[inline]
+#[track_caller]
+fn am_dynamic_child() -> bool {
+ #[inline(never)]
+ #[cold]
+ #[track_caller]
+ fn uninitialized() -> bool {
+ unreachable!("init_am_dynamic_child() was never called");
+ }
+
+ *AM_DYNAMIC_CHILD.get_or_init(uninitialized)
+}
+
+pub(crate) fn init_am_dynamic_child() -> bool {
+ let value = if let Some(var) = var_os(DYNAMIC_ENVIRON_KEY) {
+ let Some(var) = var.to_str() else {
+ eprintln!("Environment variable {DYNAMIC_ENVIRON_KEY} does not contain UTF-8 data");
+ exit(1);
+ };
+ match var {
+ DYNAMIC_ENVIRON_VALUE => true,
+ "" => false,
+ var => {
+ eprintln!(
+ "\
+ Environment variable {DYNAMIC_ENVIRON_KEY} contains wrong value. \
+ Expected: {DYNAMIC_ENVIRON_VALUE:?}, actual: {var:?}"
+ );
+ exit(1);
+ }
+ }
+ } else {
+ false
+ };
+
+ AM_DYNAMIC_CHILD.set(value).unwrap();
+ value
+}
+
+static AM_DYNAMIC_CHILD: OnceLock = OnceLock::new();
diff --git a/rinja/src/dynamic/parent.rs b/rinja/src/dynamic/parent.rs
new file mode 100644
index 00000000..76462abc
--- /dev/null
+++ b/rinja/src/dynamic/parent.rs
@@ -0,0 +1,423 @@
+use std::boxed::Box;
+use std::collections::BTreeMap;
+use std::convert::Infallible;
+use std::env::current_exe;
+use std::io::ErrorKind;
+use std::net::Ipv4Addr;
+use std::ops::ControlFlow;
+use std::process::{Stdio, exit};
+use std::string::{String, ToString};
+use std::sync::Arc;
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::time::Duration;
+use std::{eprintln, format};
+
+use notify::{Watcher, recommended_watcher};
+use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
+use tokio::net::TcpListener;
+use tokio::net::tcp::OwnedReadHalf;
+use tokio::process::Command;
+use tokio::runtime::Handle;
+use tokio::sync::{Mutex, mpsc, oneshot};
+use tokio::time::{Instant, sleep, sleep_until, timeout, timeout_at};
+use tokio::{select, try_join};
+
+use super::{Outcome, am_dynamic_child};
+use crate::dynamic::{DYNAMIC_ENVIRON_KEY, DYNAMIC_ENVIRON_VALUE, MainRequest, MainResponse};
+use crate::{Error, Values};
+
+static QUEUE: Queue = Queue::new();
+static RUNTIME: std::sync::Mutex