Skip to content

Commit 8de0f2d

Browse files
Feat: added streaming feature
Added a streaming version of `Markup`, with support for Axum
1 parent 6d72b13 commit 8de0f2d

File tree

8 files changed

+190
-2
lines changed

8 files changed

+190
-2
lines changed

maud/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ include.workspace = true
1717
[features]
1818
default = []
1919

20+
# Streaming documents
21+
streaming = ["dep:futures", "dep:async-stream", "maud_macros/streaming"]
22+
2023
# Web framework integrations
2124
actix-web = ["actix-web-dep", "futures-util"]
2225
axum = ["axum-core", "http"]
@@ -35,6 +38,8 @@ http = { version = "1", optional = true }
3538
warp = { version = "0.4", optional = true }
3639
poem = { version = "3", optional = true }
3740
salvo_core = { version = "0.78.0", optional = true }
41+
futures = { version = "0.3.31", default-features = false, optional = true }
42+
async-stream = { version = "0.3.6", default-features = false, optional = true }
3843

3944
[dev-dependencies]
4045
trybuild = { version = "1.0.33", features = ["diff"] }

maud/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ pub use maud_macros::html;
1818

1919
mod escape;
2020

21+
#[cfg(feature = "streaming")]
22+
pub mod streaming;
23+
2124
/// An adapter that escapes HTML special characters.
2225
///
2326
/// The following characters are escaped:

maud/src/streaming.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//! # Streaming generation
2+
//!
3+
//! Generation of streaming responses
4+
5+
pub use async_stream;
6+
7+
/// A streaming document.
8+
///
9+
/// This is what the [`streaming_html`] macro extends to, with `S` implementing
10+
/// [`futures::Stream<Item=Markup>`]
11+
#[derive(Debug, Clone, Copy)]
12+
pub struct StreamingMarkup<S>(pub S);
13+
14+
#[cfg(feature = "axum")]
15+
mod axum_support {
16+
use core::convert::Infallible;
17+
18+
use crate::{PreEscaped, streaming::StreamingMarkup};
19+
use alloc::string::String;
20+
use axum_core::{
21+
body::Body,
22+
response::{IntoResponse, Response},
23+
};
24+
use futures::{Stream, StreamExt};
25+
use http::{HeaderValue, header};
26+
27+
impl<S> IntoResponse for StreamingMarkup<S>
28+
where
29+
S: Stream<Item = PreEscaped<String>> + Send + 'static,
30+
{
31+
fn into_response(self) -> Response {
32+
let headers = [(
33+
header::CONTENT_TYPE,
34+
HeaderValue::from_static("text/html; charset=utf-8"),
35+
)];
36+
37+
let body = Body::from_stream(self.0.map(|PreEscaped(s)| Ok::<_, Infallible>(s)));
38+
39+
(headers, body).into_response()
40+
}
41+
}
42+
}

maud_macros/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ repository.workspace = true
1212
edition.workspace = true
1313
include.workspace = true
1414

15+
[features]
16+
17+
# Streaming documents
18+
streaming = []
19+
1520
[dependencies]
1621
syn = { version = "2", features = ["extra-traits", "full"] }
1722
quote = "1.0.7"

maud_macros/src/ast.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use syn::{
1212
spanned::Spanned,
1313
token::{
1414
At, Brace, Bracket, Colon, Comma, Dot, Else, Eq, FatArrow, For, If, In, Let, Match, Minus,
15-
Paren, Pound, Question, Semi, Slash, While,
15+
Paren, Pound, Question, Semi, Slash, While, Yield,
1616
},
1717
};
1818

@@ -810,6 +810,15 @@ impl<E: MaybeElement> DiagnosticParse for ControlFlow<E> {
810810
};
811811

812812
ControlFlowKind::Let(local)
813+
} else if lookahead.peek(Yield) {
814+
#[cfg(feature = "streaming")]
815+
{
816+
ControlFlowKind::Yield(input.diagnostic_parse(diagnostics)?)
817+
}
818+
#[cfg(not(feature = "streaming"))]
819+
{
820+
return Err(lookahead.error());
821+
}
813822
} else {
814823
return Err(lookahead.error());
815824
}
@@ -827,6 +836,8 @@ impl<E: ToTokens> ToTokens for ControlFlow<E> {
827836
ControlFlowKind::For(for_) => for_.to_tokens(tokens),
828837
ControlFlowKind::While(while_) => while_.to_tokens(tokens),
829838
ControlFlowKind::Match(match_) => match_.to_tokens(tokens),
839+
#[cfg(feature = "streaming")]
840+
ControlFlowKind::Yield(yield_) => yield_.to_tokens(tokens),
830841
}
831842
}
832843
}
@@ -838,6 +849,8 @@ pub enum ControlFlowKind<E> {
838849
For(ForExpr<E>),
839850
While(WhileExpr<E>),
840851
Match(MatchExpr<E>),
852+
#[cfg(feature = "streaming")]
853+
Yield(YieldExpr),
841854
}
842855

843856
#[derive(Debug, Clone)]
@@ -1073,6 +1086,31 @@ impl<E: ToTokens> ToTokens for MatchArm<E> {
10731086
}
10741087
}
10751088

1089+
#[cfg(feature = "streaming")]
1090+
#[derive(Debug, Clone)]
1091+
pub struct YieldExpr {
1092+
pub yield_token: Yield,
1093+
}
1094+
1095+
#[cfg(feature = "streaming")]
1096+
impl DiagnosticParse for YieldExpr {
1097+
fn diagnostic_parse(
1098+
input: ParseStream,
1099+
_diagnostics: &mut Vec<Diagnostic>,
1100+
) -> syn::Result<Self> {
1101+
Ok(Self {
1102+
yield_token: input.parse()?,
1103+
})
1104+
}
1105+
}
1106+
1107+
#[cfg(feature = "streaming")]
1108+
impl ToTokens for YieldExpr {
1109+
fn to_tokens(&self, tokens: &mut TokenStream) {
1110+
self.yield_token.to_tokens(tokens);
1111+
}
1112+
}
1113+
10761114
pub trait DiagnosticParse: Sized {
10771115
fn diagnostic_parse(input: ParseStream, diagnostics: &mut Vec<Diagnostic>)
10781116
-> syn::Result<Self>;

maud_macros/src/generate.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,34 @@ pub fn generate(markups: Markups<Element>, output_ident: Ident) -> TokenStream {
1010
build.finish()
1111
}
1212

13+
#[cfg(feature = "streaming")]
14+
pub fn generate_streaming(markups: Markups<Element>, output_ident: Ident) -> TokenStream {
15+
let mut build = Builder::new(output_ident.clone());
16+
Generator::new_streaming(output_ident).markups(markups, &mut build);
17+
build.finish()
18+
}
19+
1320
struct Generator {
1421
output_ident: Ident,
22+
#[cfg(feature = "streaming")]
23+
streaming: bool,
1524
}
1625

1726
impl Generator {
1827
fn new(output_ident: Ident) -> Generator {
19-
Generator { output_ident }
28+
Generator {
29+
output_ident,
30+
#[cfg(feature = "streaming")]
31+
streaming: false,
32+
}
33+
}
34+
35+
#[cfg(feature = "streaming")]
36+
fn new_streaming(output_ident: Ident) -> Generator {
37+
Generator {
38+
output_ident,
39+
streaming: true,
40+
}
2041
}
2142

2243
fn builder(&self) -> Builder {
@@ -191,6 +212,8 @@ impl Generator {
191212
ControlFlowKind::For(for_) => self.control_flow_for(for_, build),
192213
ControlFlowKind::While(while_) => self.control_flow_while(while_, build),
193214
ControlFlowKind::Match(match_) => self.control_flow_match(match_, build),
215+
#[cfg(feature = "streaming")]
216+
ControlFlowKind::Yield(yield_) => self.control_flow_yield(yield_, build),
194217
}
195218
}
196219

@@ -303,6 +326,23 @@ impl Generator {
303326

304327
build.push_tokens(quote!(#match_token #expr #arm_block));
305328
}
329+
330+
#[cfg(feature = "streaming")]
331+
fn control_flow_yield(&self, YieldExpr { yield_token }: YieldExpr, build: &mut Builder) {
332+
if !self.streaming {
333+
build.push_tokens(
334+
syn::Error::new_spanned(
335+
yield_token,
336+
"`yield` instructions are permitted only in `streaming_html`",
337+
)
338+
.into_compile_error(),
339+
);
340+
}
341+
let output_ident = &self.output_ident;
342+
build.push_tokens(
343+
quote! { #yield_token maud::PreEscaped( ::core::mem::take( #output_ident ) ) ; },
344+
);
345+
}
306346
}
307347

308348
////////////////////////////////////////////////////////

maud_macros/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ use proc_macro2_diagnostics::Diagnostic;
1414
use quote::quote;
1515
use syn::parse::{ParseStream, Parser};
1616

17+
#[cfg(feature = "streaming")]
18+
mod streaming;
19+
1720
#[proc_macro]
1821
pub fn html(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
1922
expand(input.into()).into()
@@ -54,3 +57,9 @@ fn expand(input: TokenStream) -> TokenStream {
5457
maud::PreEscaped(#output_ident)
5558
}}
5659
}
60+
61+
#[cfg(feature = "streaming")]
62+
#[proc_macro]
63+
pub fn streaming_html(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
64+
streaming::expand(input.into()).into()
65+
}

maud_macros/src/streaming.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
use crate::ast::DiagnosticParse;
2+
use proc_macro2::{Ident, Span, TokenStream};
3+
use proc_macro2_diagnostics::Diagnostic;
4+
use quote::quote;
5+
use syn::parse::{ParseStream, Parser};
6+
7+
pub fn expand(input: TokenStream) -> TokenStream {
8+
// TODO: How to replace the size hint? Maybe measure the distance between
9+
// two yields?
10+
11+
let mut diagnostics = Vec::new();
12+
let markups = match Parser::parse2(
13+
|input: ParseStream| crate::ast::Markups::diagnostic_parse(input, &mut diagnostics),
14+
input,
15+
) {
16+
Ok(data) => data,
17+
Err(err) => {
18+
let err = err.to_compile_error();
19+
let diag_tokens = diagnostics.into_iter().map(Diagnostic::emit_as_expr_tokens);
20+
21+
return quote! {{
22+
#err
23+
#(#diag_tokens)*
24+
}};
25+
}
26+
};
27+
28+
let diag_tokens = diagnostics.into_iter().map(Diagnostic::emit_as_expr_tokens);
29+
30+
let output_ident = Ident::new("__maud_output", Span::mixed_site());
31+
let stmts = crate::generate::generate_streaming(markups, output_ident.clone());
32+
quote! {{
33+
extern crate alloc;
34+
extern crate maud;
35+
36+
use maud::streaming::async_stream::stream;
37+
38+
maud::streaming::StreamingMarkup(stream!{
39+
let mut #output_ident = alloc::string::String::new();
40+
41+
#stmts
42+
#(#diag_tokens)*
43+
yield maud::PreEscaped(#output_ident);
44+
})
45+
}}
46+
}

0 commit comments

Comments
 (0)