Skip to content

Commit fc3ebeb

Browse files
committed
Dynamic rendering mode
1 parent e418834 commit fc3ebeb

File tree

21 files changed

+1003
-30
lines changed

21 files changed

+1003
-30
lines changed

Diff for: Cargo.toml

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
11
[workspace]
2-
members = ["rinja", "rinja_derive", "rinja_parser", "testing", "testing-alloc", "testing-no-std"]
2+
members = [
3+
"rinja",
4+
"rinja_derive",
5+
"rinja_parser",
6+
"testing",
7+
"testing-alloc",
8+
"testing-no-std"
9+
]
310
resolver = "2"

Diff for: examples/axum-app/src/main.rs

+13-9
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
}

Diff for: rinja/Cargo.toml

+21-2
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,20 @@ harness = false
2626
[dependencies]
2727
rinja_derive = { version = "=0.3.5", path = "../rinja_derive" }
2828

29+
itoa = "1.0.11"
30+
31+
# needed by feature "urlencode"
2932
percent-encoding = { version = "2.1.0", optional = true, default-features = false }
33+
34+
# needed by feature "serde_json"
3035
serde = { version = "1.0", optional = true, default-features = false }
31-
serde_json = { version = "1.0", optional = true, default-features = false, features = [] }
36+
serde_json = { version = "1.0", optional = true, default-features = false }
3237

33-
itoa = "1.0.11"
38+
# needed by feature "dynamic"
39+
linkme = { version = "0.3.31", optional = true }
40+
notify = { version = "8.0.0", optional = true }
41+
parking_lot = { version = "0.12.3", optional = true, features = ["arc_lock", "send_guard"] }
42+
tokio = { version = "1.43.0", optional = true, features = ["macros", "io-util", "net", "process", "rt", "sync", "time"] }
3443

3544
[dev-dependencies]
3645
assert_matches = "1.5.0"
@@ -51,6 +60,16 @@ alloc = [
5160
]
5261
code-in-doc = ["rinja_derive/code-in-doc"]
5362
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+
]
5473
serde_json = ["rinja_derive/serde_json", "dep:serde", "dep:serde_json"]
5574
std = [
5675
"alloc",

Diff for: rinja/src/dynamic/child.rs

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

0 commit comments

Comments
 (0)