Skip to content

Commit df17fc6

Browse files
committed
Dynamic rendering mode
1 parent 535df5e commit df17fc6

File tree

13 files changed

+962
-12
lines changed

13 files changed

+962
-12
lines changed

examples/axum-app/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ publish = false
99
# and axum as your web-framework.
1010
[dependencies]
1111
axum = "0.8.1"
12-
rinja = { version = "0.3.5", path = "../../rinja" }
12+
rinja = { version = "0.3.5", path = "../../rinja", features = ["dynamic"] }
1313
tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread"] }
1414

1515
# serde and strum are used to parse (deserialize) and generate (serialize) information

examples/axum-app/src/main.rs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1+
use std::borrow::Cow;
2+
13
use axum::extract::{Path, Query};
24
use axum::http::StatusCode;
35
use axum::response::{Html, IntoResponse, Redirect, Response};
46
use axum::routing::get;
57
use axum::{Router, serve};
68
use rinja::Template;
7-
use serde::Deserialize;
9+
use serde::{Deserialize, Serialize};
810
use tower_http::trace::TraceLayer;
911
use tracing::{Level, info};
1012

13+
#[rinja::main]
1114
#[tokio::main]
1215
async fn main() -> Result<(), Error> {
1316
tracing_subscriber::fmt()
@@ -52,7 +55,7 @@ enum Error {
5255
/// * `PartialEq` so that we can use the type in comparisons with `==` or `!=`.
5356
/// * `serde::Deserialize` so that axum can parse the type in incoming URLs.
5457
/// * `strum::Display` so that rinja can write the value in templates.
55-
#[derive(Default, Debug, Clone, Copy, PartialEq, Deserialize, strum::Display)]
58+
#[derive(Default, Debug, Clone, Copy, PartialEq, Deserialize, Serialize, strum::Display)]
5659
#[allow(non_camel_case_types)]
5760
enum Lang {
5861
#[default]
@@ -130,8 +133,8 @@ async fn index_handler(
130133
// In `IndexHandlerQuery` we annotated the field with `#[serde(default)]`, so if the value is
131134
// absent, an empty string is selected by default, which is visible to the user an empty
132135
// `<input type="text" />` element.
133-
#[derive(Debug, Template)]
134-
#[template(path = "index.html")]
136+
#[derive(Debug, Template, Serialize, Deserialize)]
137+
#[template(path = "index.html", dynamic = true)]
135138
struct Tmpl {
136139
lang: Lang,
137140
name: String,
@@ -158,16 +161,17 @@ async fn greeting_handler(
158161
Path((lang,)): Path<(Lang,)>,
159162
Query(query): Query<GreetingHandlerQuery>,
160163
) -> Result<impl IntoResponse, AppError> {
161-
#[derive(Debug, Template)]
162-
#[template(path = "greet.html")]
163-
struct Tmpl {
164+
#[derive(Debug, Template, Serialize, Deserialize)]
165+
#[template(path = "greet.html", dynamic = true, print = "code")]
166+
struct Tmpl<'a> {
164167
lang: Lang,
165-
name: String,
168+
#[serde(borrow)]
169+
name: Cow<'a, str>,
166170
}
167171

168172
let template = Tmpl {
169173
lang,
170-
name: query.name,
174+
name: query.name.into(),
171175
};
172176
Ok(Html(template.render()?))
173177
}

rinja/Cargo.toml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ rinja_derive = { version = "=0.3.5", path = "../rinja_derive" }
2828

2929
itoa = "1.0.11"
3030

31+
# needed by feature "dynamic"
32+
linkme = { version = "0.3.31", optional = true }
33+
notify = { version = "8.0.0", optional = true }
34+
parking_lot = { version = "0.12.3", optional = true, features = ["arc_lock", "send_guard"] }
35+
tokio = { version = "1.43.0", optional = true, features = ["macros", "io-util", "net", "process", "rt", "sync", "time"] }
36+
3137
# needed by feature "serde_json"
3238
serde = { version = "1.0", optional = true, default-features = false }
3339
serde_json = { version = "1.0", optional = true, default-features = false, features = [] }
@@ -43,7 +49,7 @@ criterion = "0.5"
4349
maintenance = { status = "actively-developed" }
4450

4551
[features]
46-
default = ["config", "std", "urlencode"]
52+
default = ["config", "std", "urlencode", "dynamic"]
4753
full = ["default", "code-in-doc", "serde_json"]
4854

4955
alloc = [
@@ -54,6 +60,16 @@ alloc = [
5460
]
5561
code-in-doc = ["rinja_derive/code-in-doc"]
5662
config = ["rinja_derive/config"]
63+
dynamic = [
64+
"std",
65+
"rinja_derive/dynamic",
66+
"serde/derive",
67+
"dep:linkme",
68+
"dep:notify",
69+
"dep:parking_lot",
70+
"dep:serde_json",
71+
"dep:tokio",
72+
]
5773
serde_json = ["rinja_derive/serde_json", "dep:serde", "dep:serde_json"]
5874
std = [
5975
"alloc",

rinja/src/dynamic/child.rs

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
use std::borrow::Cow;
2+
use std::env::args;
3+
use std::fmt::Write;
4+
use std::io::ErrorKind;
5+
use std::net::SocketAddr;
6+
use std::process::exit;
7+
use std::string::String;
8+
use std::sync::Arc;
9+
use std::time::Duration;
10+
use std::vec::Vec;
11+
use std::{eprintln, format};
12+
13+
use linkme::distributed_slice;
14+
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
15+
use tokio::net::TcpStream;
16+
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
17+
use tokio::spawn;
18+
use tokio::sync::{Mutex, oneshot};
19+
20+
use super::{DYNAMIC_ENVIRON_KEY, MainRequest, MainResponse, Outcome};
21+
22+
const PROCESSORS: usize = 4;
23+
24+
#[inline(never)]
25+
pub(crate) fn run_dynamic_main() {
26+
std::env::set_var(DYNAMIC_ENVIRON_KEY, "-");
27+
28+
let mut entries: Vec<_> = DYNAMIC_TEMPLATES.iter().map(|entry| entry.name()).collect();
29+
entries.sort_unstable();
30+
eprintln!("templates implemented by subprocess: {entries:?}");
31+
for window in entries.windows(2) {
32+
if let &[a, b] = window {
33+
if a == b {
34+
eprintln!("duplicated dynamic template {a:?}");
35+
}
36+
}
37+
}
38+
39+
let sock_addr: SocketAddr = {
40+
let mut args = args().fuse();
41+
let (_, Some("--__rinja_dynamic"), Some(sock_addr), None) = (
42+
args.next(),
43+
args.next().as_deref(),
44+
args.next(),
45+
args.next(),
46+
) else {
47+
eprintln!("child process got unexpected arguments");
48+
exit(1);
49+
};
50+
match serde_json::from_str(&sock_addr) {
51+
Ok(sock_addr) => sock_addr,
52+
Err(err) => {
53+
eprintln!("subprocess could not interpret socket addr: {err}");
54+
exit(1);
55+
}
56+
}
57+
};
58+
59+
let rt = match tokio::runtime::Builder::new_current_thread()
60+
.enable_all()
61+
.build()
62+
{
63+
Ok(rt) => rt,
64+
Err(err) => {
65+
eprintln!("could not start tokio runtime: {err}");
66+
exit(1);
67+
}
68+
};
69+
let _ = rt.block_on(async move {
70+
let sock = match TcpStream::connect(sock_addr).await {
71+
Ok(sock) => sock,
72+
Err(err) => {
73+
eprintln!("subprocess could not connect to parent process: {err}");
74+
exit(1);
75+
}
76+
};
77+
let _: Result<(), std::io::Error> = sock.set_linger(None);
78+
let _: Result<(), std::io::Error> = sock.set_nodelay(true);
79+
let (read, write) = sock.into_split();
80+
81+
let stdout = Arc::new(Mutex::new(write));
82+
let stdin = Arc::new(Mutex::new(BufReader::new(read)));
83+
let (done_tx, done_rx) = oneshot::channel();
84+
let done = Arc::new(Mutex::new(Some(done_tx)));
85+
86+
let mut threads = Vec::with_capacity(PROCESSORS);
87+
for _ in 0..PROCESSORS {
88+
threads.push(spawn(dynamic_processor(
89+
Arc::clone(&stdout),
90+
Arc::clone(&stdin),
91+
Arc::clone(&done),
92+
)));
93+
}
94+
95+
done_rx.await.map_err(|err| {
96+
std::io::Error::new(ErrorKind::BrokenPipe, format!("lost result channel: {err}"));
97+
})
98+
});
99+
rt.shutdown_timeout(Duration::from_secs(5));
100+
exit(0)
101+
}
102+
103+
async fn dynamic_processor(
104+
stdout: Arc<Mutex<OwnedWriteHalf>>,
105+
stdin: Arc<Mutex<BufReader<OwnedReadHalf>>>,
106+
done: Arc<Mutex<Option<oneshot::Sender<std::io::Result<()>>>>>,
107+
) {
108+
let done = move |result: Result<(), std::io::Error>| {
109+
let done = Arc::clone(&done);
110+
async move {
111+
let mut lock = done.lock().await;
112+
if let Some(done) = lock.take() {
113+
let _: Result<_, _> = done.send(result);
114+
}
115+
}
116+
};
117+
118+
let mut line_buf = String::new();
119+
let mut response_buf = String::new();
120+
loop {
121+
line_buf.clear();
122+
match stdin.lock().await.read_line(&mut line_buf).await {
123+
Ok(n) if n > 0 => {}
124+
result => return done(result.map(|_| ())).await,
125+
}
126+
let line = line_buf.trim_ascii();
127+
if line.is_empty() {
128+
continue;
129+
}
130+
131+
let MainRequest { callid, name, data } = match serde_json::from_str(line) {
132+
Ok(req) => req,
133+
Err(err) => {
134+
let err = format!("could not deserialize request: {err}");
135+
return done(Err(std::io::Error::new(ErrorKind::InvalidData, err))).await;
136+
}
137+
};
138+
response_buf.clear();
139+
140+
let mut outcome = Outcome::NotFound;
141+
for entry in DYNAMIC_TEMPLATES {
142+
if entry.name() == name {
143+
outcome = entry.dynamic_render(&mut response_buf, &data);
144+
break;
145+
}
146+
}
147+
148+
// SAFETY: `serde_json` writes valid UTF-8 data
149+
let mut line = unsafe { line_buf.as_mut_vec() };
150+
151+
line.clear();
152+
if let Err(err) = serde_json::to_writer(&mut line, &MainResponse { callid, outcome }) {
153+
let err = format!("could not serialize response: {err}");
154+
return done(Err(std::io::Error::new(ErrorKind::InvalidData, err))).await;
155+
}
156+
line.push(b'\n');
157+
158+
let is_done = {
159+
let mut stdout = stdout.lock().await;
160+
stdout.write_all(line).await.is_err() || stdout.flush().await.is_err()
161+
};
162+
if is_done {
163+
return done(Ok(())).await;
164+
}
165+
}
166+
}
167+
168+
/// Used by [`Template`][rinja_derive::Template] to register a template for dynamic processing.
169+
#[macro_export]
170+
macro_rules! register_dynamic_template {
171+
(
172+
name: $Name:ty,
173+
type: $Type:ty,
174+
) => {
175+
const _: () = {
176+
#[$crate::helpers::linkme::distributed_slice($crate::helpers::DYNAMIC_TEMPLATES)]
177+
#[linkme(crate = $crate::helpers::linkme)]
178+
static DYNAMIC_TEMPLATES: &'static dyn $crate::helpers::DynamicTemplate = &Dynamic;
179+
180+
struct Dynamic;
181+
182+
impl $crate::helpers::DynamicTemplate for Dynamic {
183+
#[inline]
184+
fn name(&self) -> &$crate::helpers::core::primitive::str {
185+
$crate::helpers::core::any::type_name::<$Name>()
186+
}
187+
188+
fn dynamic_render<'a>(
189+
&self,
190+
buf: &'a mut rinja::helpers::alloc::string::String,
191+
value: &rinja::helpers::core::primitive::str,
192+
) -> rinja::helpers::Outcome<'a> {
193+
let result = rinja::helpers::from_json::<$Type>(value).map(|tmpl| {
194+
buf.clear();
195+
let _ = buf.try_reserve(<Tmpl as $crate::Template>::SIZE_HINT);
196+
tmpl.render_into(buf)
197+
});
198+
$crate::helpers::use_dynamic_render_result(buf, result)
199+
}
200+
}
201+
};
202+
};
203+
}
204+
205+
/// Convert the result of [`serde::from_json()`] → [`Template::render()`] to an [`Outcome`].
206+
pub fn use_dynamic_render_result(
207+
buf: &mut String,
208+
result: Result<Result<(), crate::Error>, serde_json::Error>,
209+
) -> Outcome<'_> {
210+
let result = match &result {
211+
Ok(Ok(())) => return Outcome::Success(Cow::Borrowed(buf)),
212+
Ok(Err(err)) => Ok(err),
213+
Err(err) => Err(err),
214+
};
215+
216+
buf.clear();
217+
let result = match result {
218+
Ok(e) => write!(buf, "{e}").map(|_| Outcome::Render(Cow::Borrowed(buf))),
219+
Err(e) => write!(buf, "{e}").map(|_| Outcome::Deserialize(Cow::Borrowed(buf))),
220+
};
221+
result.unwrap_or(Outcome::Fmt)
222+
}
223+
224+
/// List of implemented dynamic templates. Filled through
225+
/// [`register_dynamic_template!`][crate::register_dynamic_template].
226+
#[distributed_slice]
227+
pub static DYNAMIC_TEMPLATES: [&'static dyn DynamicTemplate];
228+
229+
/// A dynamic template implementation
230+
pub trait DynamicTemplate: Send + Sync {
231+
/// The type name of the template.
232+
fn name(&self) -> &str;
233+
234+
/// Take a JSON `value` to to render the template into `buf`.
235+
fn dynamic_render<'a>(&self, buf: &'a mut String, value: &str) -> Outcome<'a>;
236+
}

0 commit comments

Comments
 (0)