Skip to content

Commit

Permalink
Add support for CSV exporter
Browse files Browse the repository at this point in the history
  • Loading branch information
photino committed Jul 3, 2023
1 parent ed63548 commit 1528182
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 46 deletions.
4 changes: 3 additions & 1 deletion examples/actix-app/src/router/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ fn user_router(cfg: &mut ServiceConfig) {
.route("/user/{id}/delete", post().to(User::delete))
.route("/user/{id}/update", post().to(User::update))
.route("/user/{id}/view", get().to(user::view))
.route("/user/list", get().to(User::list));
.route("/user/list", get().to(User::list))
.route("/user/import", post().to(User::import))
.route("/user/export", get().to(User::export));
}

fn tag_router(cfg: &mut ServiceConfig) {
Expand Down
7 changes: 4 additions & 3 deletions zino-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ cfg-if = "1.0"
convert_case = "0.6.0"
cookie = "0.16.2"
cron = "0.12.0"
csv = "1.2.2"
fluent = "0.16.0"
futures = "0.3.28"
hkdf = "0.12.3"
Expand All @@ -103,7 +104,7 @@ rmp-serde = "1.1.1"
serde_qs = "0.12.0"
serde_urlencoded = "0.7.1"
sha2 = "0.10.7"
sysinfo = "0.29.2"
sysinfo = "0.29.3"
task-local-extensions = "0.1.4"
toml = "0.7.5"
tracing = "0.1.37"
Expand All @@ -123,11 +124,11 @@ version = "0.4.26"
features = ["serde"]

[dependencies.datafusion]
version = "26.0.0"
version = "27.0.0"
optional = true

[dependencies.lru]
version = "0.10.0"
version = "0.10.1"
optional = true

[dependencies.minijinja]
Expand Down
68 changes: 52 additions & 16 deletions zino-core/src/extension/json_value.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::JsonValue;
use crate::{extension::JsonObjectExt, JsonValue};
use csv::{ByteRecord, Writer};
use std::{
borrow::Cow,
io::{self, ErrorKind, Write},
num::{ParseFloatError, ParseIntError},
str::{FromStr, ParseBoolError},
};
Expand Down Expand Up @@ -31,47 +33,50 @@ pub trait JsonValueExt {
/// Returns `None` otherwise.
fn as_f32(&self) -> Option<f32>;

/// Parses the json value as `bool`.
/// Parses the JSON value as `bool`.
fn parse_bool(&self) -> Option<Result<bool, ParseBoolError>>;

/// Parses the json value as `u8`.
/// Parses the JSON value as `u8`.
fn parse_u8(&self) -> Option<Result<u8, ParseIntError>>;

/// Parses the json value as `u16`.
/// Parses the JSON value as `u16`.
fn parse_u16(&self) -> Option<Result<u16, ParseIntError>>;

/// Parses the json value as `u32`.
/// Parses the JSON value as `u32`.
fn parse_u32(&self) -> Option<Result<u32, ParseIntError>>;

/// Parses the json value as `u64`.
/// Parses the JSON value as `u64`.
fn parse_u64(&self) -> Option<Result<u64, ParseIntError>>;

/// Parses the json value as `usize`.
/// Parses the JSON value as `usize`.
fn parse_usize(&self) -> Option<Result<usize, ParseIntError>>;

/// Parses the json value as `i32`.
/// Parses the JSON value as `i32`.
fn parse_i32(&self) -> Option<Result<i32, ParseIntError>>;

/// Parses the json value as `i64`.
/// Parses the JSON value as `i64`.
fn parse_i64(&self) -> Option<Result<i64, ParseIntError>>;

/// Parses the json value as `f32`.
/// Parses the JSON value as `f32`.
fn parse_f32(&self) -> Option<Result<f32, ParseFloatError>>;

/// Parses the json value as `f64`.
/// Parses the JSON value as `f64`.
fn parse_f64(&self) -> Option<Result<f64, ParseFloatError>>;

/// Parses the json value as `Cow<'_, str>`.
/// Parses the JSON value as `Cow<'_, str>`.
/// If the str is empty, it also returns `None`.
fn parse_string(&self) -> Option<Cow<'_, str>>;

/// Parses the json value as `Vec<T>`.
/// Parses the JSON value as `Vec<T>`.
/// If the vec is empty, it also returns `None`.
fn parse_array<T: FromStr>(&self) -> Option<Vec<T>>;

/// Parses the json value as `Vec<&str>`.
/// Parses the JSON value as `Vec<&str>`.
/// If the vec is empty, it also returns `None`.
fn parse_str_array(&self) -> Option<Vec<&str>>;

/// Attempts to convert the JSON value to a CSV writer.
fn to_csv_writer<W: Write>(&self, writer: W) -> Result<W, csv::Error>;
}

impl JsonValueExt for JsonValue {
Expand Down Expand Up @@ -185,7 +190,7 @@ impl JsonValueExt for JsonValue {
fn parse_array<T: FromStr>(&self) -> Option<Vec<T>> {
let values = match &self {
JsonValue::String(s) => Some(crate::format::parse_str_array(s)),
JsonValue::Array(v) => Some(v.iter().filter_map(|v| v.as_str()).collect()),
JsonValue::Array(vec) => Some(vec.iter().filter_map(|v| v.as_str()).collect()),
_ => None,
};
let vec = values?
Expand All @@ -198,7 +203,7 @@ impl JsonValueExt for JsonValue {
fn parse_str_array(&self) -> Option<Vec<&str>> {
let values = match &self {
JsonValue::String(s) => Some(crate::format::parse_str_array(s)),
JsonValue::Array(v) => Some(v.iter().filter_map(|v| v.as_str()).collect()),
JsonValue::Array(vec) => Some(vec.iter().filter_map(|v| v.as_str()).collect()),
_ => None,
};
let vec = values?
Expand All @@ -208,4 +213,35 @@ impl JsonValueExt for JsonValue {
.collect::<Vec<_>>();
(!vec.is_empty()).then_some(vec)
}

fn to_csv_writer<W: Write>(&self, writer: W) -> Result<W, csv::Error> {
match &self {
JsonValue::Array(vec) => {
let mut wtr = Writer::from_writer(writer);
let mut headers = Vec::new();
if let Some(JsonValue::Object(map)) = vec.first() {
for key in map.keys() {
headers.push(key.to_owned());
}
}
wtr.write_record(&headers)?;

let num_fields = headers.len();
let buffer_size = num_fields * 8;
for value in vec {
if let JsonValue::Object(map) = value {
let mut record = ByteRecord::with_capacity(buffer_size, num_fields);
for field in headers.iter() {
let value = map.parse_string(field).unwrap_or("".into());
record.push_field(value.as_ref().as_bytes());
}
wtr.write_byte_record(&record)?;
}
}
wtr.flush()?;
wtr.into_inner().map_err(|err| err.into_error().into())
}
_ => Err(io::Error::new(ErrorKind::InvalidData, "invalid JSON value for CSV").into()),
}
}
}
42 changes: 22 additions & 20 deletions zino-core/src/response/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ pub type StatusCode = http::StatusCode;
/// An Http response with the body that consists of a single chunk.
pub type FullResponse = http::Response<Full<Bytes>>;

/// A function pointer of transforming the response data.
pub type ResponseDataTransformer = fn(data: JsonValue) -> Result<Vec<u8>, Error>;

/// An HTTP response.
#[derive(Debug, Serialize)]
#[serde(rename_all = "snake_case")]
Expand Down Expand Up @@ -67,9 +70,9 @@ pub struct Response<S = StatusCode> {
/// Response data.
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<Box<RawValue>>,
/// JSON Pointer for identifying a specific value within the response data.
/// Transformer of the response data.
#[serde(skip)]
json_pointer: Option<SharedString>,
data_transformer: Option<ResponseDataTransformer>,
/// Content type.
#[serde(skip)]
content_type: Option<SharedString>,
Expand Down Expand Up @@ -104,7 +107,7 @@ impl<S: ResponseCode> Response<S> {
start_time: Instant::now(),
request_id: Uuid::nil(),
data: None,
json_pointer: None,
data_transformer: None,
content_type: None,
trace_context: None,
server_timing: ServerTiming::new(),
Expand Down Expand Up @@ -135,7 +138,7 @@ impl<S: ResponseCode> Response<S> {
start_time: ctx.start_time(),
request_id: ctx.request_id(),
data: None,
json_pointer: ctx.get_query("json_pointer").map(|s| s.to_owned().into()),
data_transformer: None,
content_type: None,
trace_context: None,
server_timing: ServerTiming::new(),
Expand All @@ -157,7 +160,6 @@ impl<S: ResponseCode> Response<S> {
self.start_time = ctx.start_time();
self.request_id = ctx.request_id();
self.trace_context = Some(ctx.new_trace_context());
self.json_pointer = ctx.get_query("json_pointer").map(|s| s.to_owned().into());
self
}

Expand Down Expand Up @@ -264,10 +266,10 @@ impl<S: ResponseCode> Response<S> {
}
}

/// Sets a JSON Pointer for identifying a specific value within the response data.
/// Sets a transformer for the response data.
#[inline]
pub fn set_json_pointer(&mut self, pointer: impl Into<SharedString>) {
self.json_pointer = Some(pointer.into());
pub fn set_data_transformer(&mut self, transformer: ResponseDataTransformer) {
self.data_transformer = Some(transformer);
}

/// Sets the content type.
Expand Down Expand Up @@ -414,21 +416,21 @@ impl<S: ResponseCode> Response<S> {

/// Reads the response into a byte buffer.
pub fn read_bytes(&self) -> Result<Vec<u8>, Error> {
if let Some(transformer) = self.data_transformer.as_ref() {
let data = serde_json::to_value(&self.data)?;
return transformer(data);
}

let content_type = self.content_type();
let bytes = if extension::header::check_json_content_type(content_type) {
if let Some(pointer) = self.json_pointer.as_deref() {
let data = serde_json::to_value(&self.data)?;
serde_json::to_vec(&data.pointer(pointer))?
let capacity = if let Some(data) = &self.data {
data.get().len() + 128
} else {
let capacity = if let Some(data) = &self.data {
data.get().len() + 128
} else {
128
};
let mut bytes = Vec::with_capacity(capacity);
serde_json::to_writer(&mut bytes, &self)?;
bytes
}
128
};
let mut bytes = Vec::with_capacity(capacity);
serde_json::to_writer(&mut bytes, &self)?;
bytes
} else if let Some(data) = &self.data {
let capacity = data.get().len();
match serde_json::to_value(data)? {
Expand Down
2 changes: 1 addition & 1 deletion zino/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ version = "3.1.3"
optional = true

[dependencies.tokio]
version = "1.29.0"
version = "1.29.1"
optional = true
features = ["parking_lot", "rt-multi-thread"]

Expand Down
30 changes: 25 additions & 5 deletions zino/src/controller/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ pub trait DefaultController<T, U = T> {
#[cfg(feature = "orm")]
use zino_core::{
database::ModelAccessor,
error::Error,
extension::JsonObjectExt,
request::RequestContext,
response::{ExtractRejection, Rejection},
response::{ExtractRejection, Rejection, StatusCode},
Map,
};

Expand All @@ -57,7 +58,7 @@ where

let data = Map::data_entry(model.snapshot());
model.insert().await.extract(&req)?;
res.set_code(zino_core::response::StatusCode::CREATED);
res.set_code(StatusCode::CREATED);
res.set_data(&data);
Ok(res.into())
}
Expand Down Expand Up @@ -111,8 +112,6 @@ where
}

async fn import(mut req: Self::Request) -> Self::Result {
use zino_core::response::StatusCode;

let data = req.parse_body::<Vec<Map>>().await?;
let mut models = Vec::with_capacity(data.len());
let mut validations = Vec::new();
Expand Down Expand Up @@ -150,7 +149,28 @@ where
let models = Self::find(&query).await.extract(&req)?;
let data = Map::data_entries(models);
res.set_data(&data);
res.set_json_pointer("/entries");

let format = req.get_query("format").unwrap_or("json");
match format {
"csv" => {
use zino_core::extension::JsonValueExt;

res.set_content_type("text/csv; charset=utf-8");
res.set_data_transformer(|data| {
let bytes = if let Some(value) = data.pointer("/entries") {
value.to_csv_writer(Vec::new())?
} else {
Vec::new()
};
Ok(bytes)
});
}
_ => {
res.set_data_transformer(|data| {
serde_json::to_vec(&data.pointer("/entries")).map_err(Error::from)
});
}
}
Ok(res.into())
}
}

0 comments on commit 1528182

Please sign in to comment.