Skip to content

Commit 0c53740

Browse files
committed
implement top-level csv component
1 parent 7046285 commit 0c53740

File tree

2 files changed

+149
-54
lines changed

2 files changed

+149
-54
lines changed

src/render.rs

Lines changed: 94 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::templates::SplitTemplate;
2-
use crate::webserver::http::RequestContext;
2+
use crate::webserver::http::{RequestContext, ResponseWriter};
33
use crate::webserver::ErrorWithStatus;
44
use crate::AppState;
55
use actix_web::cookie::time::format_description::well_known::Rfc3339;
@@ -15,31 +15,35 @@ use serde_json::{json, Value};
1515
use std::borrow::Cow;
1616
use std::sync::Arc;
1717

18-
pub enum PageContext<W: std::io::Write> {
18+
pub enum PageContext {
1919
/// Indicates that we should stay in the header context
20-
Header(HeaderContext<W>),
20+
Header(HeaderContext),
2121

2222
/// Indicates that we should start rendering the body
2323
Body {
2424
http_response: HttpResponseBuilder,
25-
renderer: AnyRenderBodyContext<W>,
25+
renderer: AnyRenderBodyContext,
2626
},
2727

2828
/// The response is ready, and should be sent as is. No further statements should be executed
2929
Close(HttpResponse),
3030
}
3131

3232
/// Handles the first SQL statements, before the headers have been sent to
33-
pub struct HeaderContext<W: std::io::Write> {
33+
pub struct HeaderContext {
3434
app_state: Arc<AppState>,
3535
request_context: RequestContext,
36-
pub writer: W,
36+
pub writer: ResponseWriter,
3737
response: HttpResponseBuilder,
3838
has_status: bool,
3939
}
4040

41-
impl<'a, W: std::io::Write> HeaderContext<W> {
42-
pub fn new(app_state: Arc<AppState>, request_context: RequestContext, writer: W) -> Self {
41+
impl HeaderContext {
42+
pub fn new(
43+
app_state: Arc<AppState>,
44+
request_context: RequestContext,
45+
writer: ResponseWriter,
46+
) -> Self {
4347
let mut response = HttpResponseBuilder::new(StatusCode::OK);
4448
response.content_type("text/html; charset=utf-8");
4549
if app_state.config.content_security_policy.is_none() {
@@ -53,20 +57,21 @@ impl<'a, W: std::io::Write> HeaderContext<W> {
5357
has_status: false,
5458
}
5559
}
56-
pub async fn handle_row(self, data: JsonValue) -> anyhow::Result<PageContext<W>> {
60+
pub async fn handle_row(self, data: JsonValue) -> anyhow::Result<PageContext> {
5761
log::debug!("Handling header row: {data}");
5862
match get_object_str(&data, "component") {
5963
Some("status_code") => self.status_code(&data).map(PageContext::Header),
6064
Some("http_header") => self.add_http_header(&data).map(PageContext::Header),
6165
Some("redirect") => self.redirect(&data).map(PageContext::Close),
6266
Some("json") => self.json(&data),
67+
Some("csv") => self.csv(&data),
6368
Some("cookie") => self.add_cookie(&data).map(PageContext::Header),
6469
Some("authentication") => self.authentication(data).await,
6570
_ => self.start_body(data).await,
6671
}
6772
}
6873

69-
pub async fn handle_error(self, err: anyhow::Error) -> anyhow::Result<PageContext<W>> {
74+
pub async fn handle_error(self, err: anyhow::Error) -> anyhow::Result<PageContext> {
7075
if self.app_state.config.environment.is_prod() {
7176
return Err(err);
7277
}
@@ -187,7 +192,7 @@ impl<'a, W: std::io::Write> HeaderContext<W> {
187192
}
188193

189194
/// Answers to the HTTP request with a single json object
190-
fn json(mut self, data: &JsonValue) -> anyhow::Result<PageContext<W>> {
195+
fn json(mut self, data: &JsonValue) -> anyhow::Result<PageContext> {
191196
self.response
192197
.insert_header((header::CONTENT_TYPE, "application/json"));
193198
if let Some(contents) = data.get("contents") {
@@ -220,7 +225,19 @@ impl<'a, W: std::io::Write> HeaderContext<W> {
220225
}
221226
}
222227

223-
async fn authentication(mut self, mut data: JsonValue) -> anyhow::Result<PageContext<W>> {
228+
fn csv(mut self, data: &JsonValue) -> anyhow::Result<PageContext> {
229+
self.response
230+
.insert_header((header::CONTENT_TYPE, "text/csv"));
231+
let csv_renderer = CsvBodyRenderer::new(self.writer);
232+
let renderer = AnyRenderBodyContext::Csv(csv_renderer);
233+
let http_response = self.response.take();
234+
Ok(PageContext::Body {
235+
renderer,
236+
http_response,
237+
})
238+
}
239+
240+
async fn authentication(mut self, mut data: JsonValue) -> anyhow::Result<PageContext> {
224241
let password_hash = take_object_str(&mut data, "password_hash");
225242
let password = take_object_str(&mut data, "password");
226243
if let (Some(password), Some(password_hash)) = (password, password_hash) {
@@ -250,7 +267,7 @@ impl<'a, W: std::io::Write> HeaderContext<W> {
250267
Ok(PageContext::Close(http_response))
251268
}
252269

253-
async fn start_body(self, data: JsonValue) -> anyhow::Result<PageContext<W>> {
270+
async fn start_body(self, data: JsonValue) -> anyhow::Result<PageContext> {
254271
let html_renderer =
255272
HtmlRenderContext::new(self.app_state, self.request_context, self.writer, data)
256273
.await
@@ -307,15 +324,16 @@ fn take_object_str(json: &mut JsonValue, key: &str) -> Option<String> {
307324
/**
308325
* Can receive rows, and write them in a given format to an `io::Write`
309326
*/
310-
pub enum AnyRenderBodyContext<W: std::io::Write> {
311-
Html(HtmlRenderContext<W>),
312-
Json(JsonBodyRenderer<W>),
327+
pub enum AnyRenderBodyContext {
328+
Html(HtmlRenderContext<ResponseWriter>),
329+
Json(JsonBodyRenderer<ResponseWriter>),
330+
Csv(CsvBodyRenderer),
313331
}
314332

315333
/**
316334
* Dummy impl to dispatch method calls to the underlying renderer
317335
*/
318-
impl<W: std::io::Write> AnyRenderBodyContext<W> {
336+
impl AnyRenderBodyContext {
319337
pub async fn handle_row(&mut self, data: &JsonValue) -> anyhow::Result<()> {
320338
log::debug!(
321339
"<- Rendering properties: {}",
@@ -324,6 +342,7 @@ impl<W: std::io::Write> AnyRenderBodyContext<W> {
324342
match self {
325343
AnyRenderBodyContext::Html(render_context) => render_context.handle_row(data).await,
326344
AnyRenderBodyContext::Json(json_body_renderer) => json_body_renderer.handle_row(data),
345+
AnyRenderBodyContext::Csv(csv_renderer) => csv_renderer.handle_row(data).await,
327346
}
328347
}
329348
pub async fn handle_error(&mut self, error: &anyhow::Error) -> anyhow::Result<()> {
@@ -333,26 +352,33 @@ impl<W: std::io::Write> AnyRenderBodyContext<W> {
333352
AnyRenderBodyContext::Json(json_body_renderer) => {
334353
json_body_renderer.handle_error(error)
335354
}
355+
AnyRenderBodyContext::Csv(csv_renderer) => csv_renderer.handle_error(error).await,
336356
}
337357
}
338358
pub async fn finish_query(&mut self) -> anyhow::Result<()> {
339359
match self {
340360
AnyRenderBodyContext::Html(render_context) => render_context.finish_query().await,
341361
AnyRenderBodyContext::Json(_json_body_renderer) => Ok(()),
362+
AnyRenderBodyContext::Csv(_csv_renderer) => Ok(()),
342363
}
343364
}
344365

345-
pub fn writer_mut(&mut self) -> &mut W {
366+
pub async fn flush(&mut self) -> anyhow::Result<()> {
346367
match self {
347368
AnyRenderBodyContext::Html(HtmlRenderContext { writer, .. })
348-
| AnyRenderBodyContext::Json(JsonBodyRenderer { writer, .. }) => writer,
369+
| AnyRenderBodyContext::Json(JsonBodyRenderer { writer, .. }) => {
370+
writer.async_flush().await?
371+
}
372+
AnyRenderBodyContext::Csv(csv_renderer) => csv_renderer.flush().await?,
349373
}
374+
Ok(())
350375
}
351376

352-
pub async fn close(self) -> W {
377+
pub async fn close(self) -> ResponseWriter {
353378
match self {
354379
AnyRenderBodyContext::Html(render_context) => render_context.close().await,
355380
AnyRenderBodyContext::Json(json_body_renderer) => json_body_renderer.close(),
381+
AnyRenderBodyContext::Csv(csv_renderer) => csv_renderer.close().await,
356382
}
357383
}
358384
}
@@ -424,6 +450,54 @@ impl<W: std::io::Write> JsonBodyRenderer<W> {
424450
}
425451
}
426452

453+
pub struct CsvBodyRenderer {
454+
writer: csv_async::AsyncWriter<ResponseWriter>,
455+
is_first: bool,
456+
}
457+
458+
impl CsvBodyRenderer {
459+
pub fn new(writer: ResponseWriter) -> CsvBodyRenderer {
460+
CsvBodyRenderer {
461+
writer: csv_async::AsyncWriter::from_writer(writer),
462+
is_first: true,
463+
}
464+
}
465+
466+
pub async fn handle_row(&mut self, data: &JsonValue) -> anyhow::Result<()> {
467+
if self.is_first {
468+
self.is_first = false;
469+
if let Some(obj) = data.as_object() {
470+
let headers: Vec<_> = obj.keys().collect();
471+
self.writer.write_record(&headers).await?;
472+
}
473+
}
474+
475+
if let Some(obj) = data.as_object() {
476+
let values: Vec<_> = obj.values().map(|v| v.to_string()).collect();
477+
self.writer.write_record(&values).await?;
478+
}
479+
480+
Ok(())
481+
}
482+
483+
pub async fn handle_error(&mut self, error: &anyhow::Error) -> anyhow::Result<()> {
484+
self.writer.write_record(&[error.to_string()]).await?;
485+
Ok(())
486+
}
487+
488+
pub async fn flush(&mut self) -> anyhow::Result<()> {
489+
self.writer.flush().await?;
490+
Ok(())
491+
}
492+
493+
pub async fn close(self) -> ResponseWriter {
494+
self.writer
495+
.into_inner()
496+
.await
497+
.expect("Failed to get inner writer")
498+
}
499+
}
500+
427501
#[allow(clippy::module_name_repetitions)]
428502
pub struct HtmlRenderContext<W: std::io::Write> {
429503
app_state: Arc<AppState>,

0 commit comments

Comments
 (0)