Skip to content

Commit edb222b

Browse files
committed
Dynamic rendering mode
1 parent e418834 commit edb222b

21 files changed

+947
-30
lines changed

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"

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
}

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-std", "io-util", "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",

rinja/src/dynamic/child.rs

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

0 commit comments

Comments
 (0)