Skip to content

Commit ad08c26

Browse files
committed
[WIP] Dynamic rendering mode
1 parent e418834 commit ad08c26

File tree

21 files changed

+907
-22
lines changed

21 files changed

+907
-22
lines changed

Cargo.toml

Lines changed: 8 additions & 1 deletion
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/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: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ use axum::response::{Html, IntoResponse, Redirect, Response};
44
use axum::routing::get;
55
use axum::{Router, serve};
66
use rinja::Template;
7-
use serde::Deserialize;
7+
use serde::{Deserialize, Serialize};
88
use tower_http::trace::TraceLayer;
99
use tracing::{Level, info};
1010

11+
#[rinja::main]
1112
#[tokio::main]
1213
async fn main() -> Result<(), Error> {
1314
tracing_subscriber::fmt()
@@ -52,7 +53,7 @@ enum Error {
5253
/// * `PartialEq` so that we can use the type in comparisons with `==` or `!=`.
5354
/// * `serde::Deserialize` so that axum can parse the type in incoming URLs.
5455
/// * `strum::Display` so that rinja can write the value in templates.
55-
#[derive(Default, Debug, Clone, Copy, PartialEq, Deserialize, strum::Display)]
56+
#[derive(Default, Debug, Clone, Copy, PartialEq, Deserialize, Serialize, strum::Display)]
5657
#[allow(non_camel_case_types)]
5758
enum Lang {
5859
#[default]
@@ -130,8 +131,8 @@ async fn index_handler(
130131
// In `IndexHandlerQuery` we annotated the field with `#[serde(default)]`, so if the value is
131132
// absent, an empty string is selected by default, which is visible to the user an empty
132133
// `<input type="text" />` element.
133-
#[derive(Debug, Template)]
134-
#[template(path = "index.html")]
134+
#[derive(Debug, Template, Serialize, Deserialize)]
135+
#[template(path = "index.html", dynamic = true)]
135136
struct Tmpl {
136137
lang: Lang,
137138
name: String,
@@ -158,8 +159,8 @@ async fn greeting_handler(
158159
Path((lang,)): Path<(Lang,)>,
159160
Query(query): Query<GreetingHandlerQuery>,
160161
) -> Result<impl IntoResponse, AppError> {
161-
#[derive(Debug, Template)]
162-
#[template(path = "greet.html")]
162+
#[derive(Debug, Template, Serialize, Deserialize)]
163+
#[template(path = "greet.html", dynamic = true)]
163164
struct Tmpl {
164165
lang: Lang,
165166
name: String,

rinja/Cargo.toml

Lines changed: 21 additions & 2 deletions
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

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

0 commit comments

Comments
 (0)