refactor serialization, add json
This commit is contained in:
parent
921b62ed97
commit
aa508a43cb
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -995,6 +995,12 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "json"
|
||||||
|
version = "0.12.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "language-tags"
|
name = "language-tags"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
|
@ -1381,6 +1387,7 @@ dependencies = [
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures",
|
"futures",
|
||||||
|
"json",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"pdf",
|
"pdf",
|
||||||
"regex",
|
"regex",
|
||||||
|
|
|
@ -11,6 +11,7 @@ actix-files = "0.6.0"
|
||||||
actix-web = "4.0.1"
|
actix-web = "4.0.1"
|
||||||
bytes = "1.1"
|
bytes = "1.1"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
|
json = "0.12.4"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
pdf = "0.7.2"
|
pdf = "0.7.2"
|
||||||
regex = "1.5.5"
|
regex = "1.5.5"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{collections::HashMap, fmt::Write, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use pdf::{backend::Backend, content::Operation, primitive::Primitive};
|
use pdf::{backend::Backend, content::Operation, primitive::Primitive};
|
||||||
|
@ -15,8 +15,6 @@ lazy_static! {
|
||||||
|
|
||||||
const DATE_PARSE_FORMAT: &[time::format_description::FormatItem] =
|
const DATE_PARSE_FORMAT: &[time::format_description::FormatItem] =
|
||||||
time::macros::format_description!("[month padding:none]/[day padding:none]/[year]");
|
time::macros::format_description!("[month padding:none]/[day padding:none]/[year]");
|
||||||
const DATE_DISPLAY_FORMAT: &[time::format_description::FormatItem] =
|
|
||||||
time::macros::format_description!("[year]-[month]-[day]");
|
|
||||||
|
|
||||||
pub struct DataSet {
|
pub struct DataSet {
|
||||||
pub columns: Vec<Arc<String>>,
|
pub columns: Vec<Arc<String>>,
|
||||||
|
@ -88,34 +86,6 @@ impl DataSet {
|
||||||
rows,
|
rows,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn csv_header(&self) -> Result<String, std::fmt::Error> {
|
|
||||||
let mut header = String::from("Date");
|
|
||||||
for column in self.columns.iter() {
|
|
||||||
write!(&mut header, ",{}", column)?;
|
|
||||||
}
|
|
||||||
Ok(header)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn csv_row(&self, datapoint: &DataPoint) -> Result<String, std::fmt::Error> {
|
|
||||||
let mut csv_row = datapoint
|
|
||||||
.date
|
|
||||||
.format(DATE_DISPLAY_FORMAT)
|
|
||||||
.expect("Failed to format date!");
|
|
||||||
for column in self.columns.iter() {
|
|
||||||
if let Some(val) = datapoint.values.get(column) {
|
|
||||||
write!(&mut csv_row, ",{}", val)?;
|
|
||||||
} else {
|
|
||||||
write!(&mut csv_row, ",")?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(csv_row)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn csv_rows(&self) -> impl Iterator<Item = Result<String, std::fmt::Error>> + '_ {
|
|
||||||
std::iter::once_with(|| self.csv_header())
|
|
||||||
.chain(self.rows.iter().map(|datapoint| self.csv_row(datapoint)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DataPoint {
|
pub struct DataPoint {
|
||||||
|
|
57
src/main.rs
57
src/main.rs
|
@ -1,14 +1,16 @@
|
||||||
use std::{sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
use actix_web::{get, web, App, HttpResponse, HttpServer, Responder};
|
use actix_web::{get, http::header::ContentType, web, App, HttpResponse, HttpServer, Responder};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
mod extract;
|
mod extract;
|
||||||
mod fetch;
|
mod fetch;
|
||||||
|
mod serialize;
|
||||||
|
|
||||||
use extract::DataSet;
|
use extract::DataSet;
|
||||||
use fetch::PdfFetcher;
|
use fetch::PdfFetcher;
|
||||||
|
use serialize::{Csv, DataSerializer, Json};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref UPDATE_INTERVAL: Duration = Duration::from_secs(3600);
|
static ref UPDATE_INTERVAL: Duration = Duration::from_secs(3600);
|
||||||
|
@ -64,6 +66,7 @@ async fn main() -> std::io::Result<()> {
|
||||||
App::new()
|
App::new()
|
||||||
.app_data(state.clone())
|
.app_data(state.clone())
|
||||||
.service(csv)
|
.service(csv)
|
||||||
|
.service(json)
|
||||||
.service(actix_files::Files::new("/", "./static/").index_file("index.html"))
|
.service(actix_files::Files::new("/", "./static/").index_file("index.html"))
|
||||||
})
|
})
|
||||||
.bind("127.0.0.1:8080")?
|
.bind("127.0.0.1:8080")?
|
||||||
|
@ -71,48 +74,26 @@ async fn main() -> std::io::Result<()> {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DataIterator {
|
|
||||||
dataset: Arc<DataSet>,
|
|
||||||
index: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DataIterator {
|
|
||||||
fn new(dataset: Arc<DataSet>) -> Self {
|
|
||||||
Self {
|
|
||||||
dataset,
|
|
||||||
index: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Iterator for DataIterator {
|
|
||||||
type Item = Result<String, std::fmt::Error>;
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
match self.index {
|
|
||||||
None => {
|
|
||||||
self.index = Some(0);
|
|
||||||
Some(self.dataset.csv_header().map(|s| s + "\n"))
|
|
||||||
}
|
|
||||||
Some(i) => {
|
|
||||||
if let Some(row) = self.dataset.rows.get(i) {
|
|
||||||
self.index = Some(i + 1);
|
|
||||||
Some(self.dataset.csv_row(row).map(|s| s + "\n"))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/data.csv")]
|
#[get("/data.csv")]
|
||||||
async fn csv(data: web::Data<AppState>) -> impl Responder {
|
async fn csv(data: web::Data<AppState>) -> impl Responder {
|
||||||
let dataset = { data.dataset.read().await.clone() };
|
let dataset = { data.dataset.read().await.clone() };
|
||||||
|
|
||||||
let rows =
|
let rows = tokio_stream::iter(
|
||||||
tokio_stream::iter(DataIterator::new(dataset).map(|item| item.map(bytes::Bytes::from)));
|
DataSerializer::new(dataset, Csv).map(|item| item.map(bytes::Bytes::from)),
|
||||||
|
);
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
.content_type("text/csv; charset=utf-8")
|
.content_type("text/csv; charset=utf-8")
|
||||||
.body(actix_web::body::BodyStream::new(rows))
|
.body(actix_web::body::BodyStream::new(rows))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/data.json")]
|
||||||
|
async fn json(data: web::Data<AppState>) -> impl Responder {
|
||||||
|
let dataset = { data.dataset.read().await.clone() };
|
||||||
|
|
||||||
|
let rows = tokio_stream::iter(
|
||||||
|
DataSerializer::new(dataset, Json).map(|item| item.map(bytes::Bytes::from)),
|
||||||
|
);
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.insert_header(ContentType::json())
|
||||||
|
.body(actix_web::body::BodyStream::new(rows))
|
||||||
|
}
|
||||||
|
|
127
src/serialize.rs
Normal file
127
src/serialize.rs
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
use std::{fmt::Write, marker::PhantomData, sync::Arc};
|
||||||
|
|
||||||
|
use crate::extract::{DataPoint, DataSet};
|
||||||
|
|
||||||
|
const DATE_DISPLAY_FORMAT: &[time::format_description::FormatItem] =
|
||||||
|
time::macros::format_description!("[year]-[month]-[day]");
|
||||||
|
|
||||||
|
type SerializationChunk = Result<String, std::fmt::Error>;
|
||||||
|
|
||||||
|
pub trait DataFormat {
|
||||||
|
fn header(dataset: &DataSet) -> SerializationChunk;
|
||||||
|
fn row(dataset: &DataSet, row: &DataPoint) -> SerializationChunk;
|
||||||
|
const ROW_SEPARATOR: &'static str;
|
||||||
|
const END: &'static str;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DataSerializer<F: DataFormat> {
|
||||||
|
dataset: Arc<DataSet>,
|
||||||
|
index: Option<usize>,
|
||||||
|
serializer: PhantomData<F>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: DataFormat> DataSerializer<F> {
|
||||||
|
pub fn new(dataset: Arc<DataSet>, _: F) -> Self {
|
||||||
|
Self {
|
||||||
|
dataset,
|
||||||
|
index: None,
|
||||||
|
serializer: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: DataFormat> Iterator for DataSerializer<F> {
|
||||||
|
type Item = SerializationChunk;
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
match self.index {
|
||||||
|
None => {
|
||||||
|
self.index = Some(0);
|
||||||
|
let header = F::header(&self.dataset);
|
||||||
|
if self.dataset.rows.is_empty() {
|
||||||
|
Some(header.map(|s| s + F::END))
|
||||||
|
} else {
|
||||||
|
Some(header)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(i) => {
|
||||||
|
if let Some(row) = self.dataset.rows.get(i) {
|
||||||
|
self.index = Some(i + 1);
|
||||||
|
let serialized_row = F::row(&self.dataset, row);
|
||||||
|
let suffix = if i == self.dataset.rows.len() - 1 {
|
||||||
|
F::END
|
||||||
|
} else {
|
||||||
|
F::ROW_SEPARATOR
|
||||||
|
};
|
||||||
|
Some(serialized_row.map(|s| s + suffix))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Csv;
|
||||||
|
impl DataFormat for Csv {
|
||||||
|
fn header(dataset: &DataSet) -> SerializationChunk {
|
||||||
|
let mut header = String::from("Date");
|
||||||
|
for column in dataset.columns.iter() {
|
||||||
|
write!(&mut header, ",{}", column)?;
|
||||||
|
}
|
||||||
|
writeln!(&mut header)?;
|
||||||
|
Ok(header)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn row(dataset: &DataSet, datapoint: &DataPoint) -> SerializationChunk {
|
||||||
|
let mut csv_row = datapoint
|
||||||
|
.date
|
||||||
|
.format(DATE_DISPLAY_FORMAT)
|
||||||
|
.expect("Failed to format date!");
|
||||||
|
for column in dataset.columns.iter() {
|
||||||
|
if let Some(val) = datapoint.values.get(column) {
|
||||||
|
write!(&mut csv_row, ",{}", val)?;
|
||||||
|
} else {
|
||||||
|
write!(&mut csv_row, ",")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeln!(&mut csv_row)?;
|
||||||
|
Ok(csv_row)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROW_SEPARATOR: &'static str = "";
|
||||||
|
const END: &'static str = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Json;
|
||||||
|
impl DataFormat for Json {
|
||||||
|
fn header(dataset: &DataSet) -> SerializationChunk {
|
||||||
|
let mut header = String::from(r#"{"columns":["Date""#);
|
||||||
|
for column in dataset.columns.iter() {
|
||||||
|
write!(&mut header, ",{}", json::stringify(column.as_str()))?;
|
||||||
|
}
|
||||||
|
write!(&mut header, r#"],"rows":["#)?;
|
||||||
|
Ok(header)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn row(dataset: &DataSet, datapoint: &DataPoint) -> SerializationChunk {
|
||||||
|
let mut row = String::from(r#"{"Date":"#);
|
||||||
|
write!(
|
||||||
|
&mut row,
|
||||||
|
r#""{}""#,
|
||||||
|
datapoint
|
||||||
|
.date
|
||||||
|
.format(DATE_DISPLAY_FORMAT)
|
||||||
|
.expect("Failed to format date!")
|
||||||
|
)?;
|
||||||
|
for column in dataset.columns.iter() {
|
||||||
|
if let Some(val) = datapoint.values.get(column) {
|
||||||
|
write!(&mut row, ",{}:{}", json::stringify(column.as_str()), val)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
row += "}";
|
||||||
|
Ok(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROW_SEPARATOR: &'static str = ",";
|
||||||
|
const END: &'static str = "]}";
|
||||||
|
}
|
Loading…
Reference in a new issue