use crate::spiders::request::SpiderRequest;
use crate::spiders::response::SpiderResponse;
use crate::spiders::spider::Spider;
use async_trait::async_trait;
use std::collections::HashSet;
use std::sync::Arc;
pub type ParseRowFn = Arc<
dyn Fn(&SpiderResponse, &serde_json::Map<String, serde_json::Value>) -> Vec<serde_json::Value>
+ Send
+ Sync,
>;
pub struct CsvFeedSpider {
name: String,
feed_urls: Vec<String>,
delimiter: char,
headers: Option<Vec<String>>,
parse_row: Option<ParseRowFn>,
allowed_domains: HashSet<String>,
concurrent_requests: u32,
development_mode: bool,
robots_txt_obey: bool,
}
impl CsvFeedSpider {
pub fn builder(name: &str) -> CsvFeedSpiderBuilder {
CsvFeedSpiderBuilder::new(name)
}
}
fn parse_csv(text: &str, delimiter: char) -> Vec<Vec<String>> {
let mut rows: Vec<Vec<String>> = Vec::new();
let mut row: Vec<String> = Vec::new();
let mut field = String::new();
let mut in_quotes = false;
let mut chars = text.chars().peekable();
let mut row_started = false;
while let Some(c) = chars.next() {
if in_quotes {
match c {
'"' => {
if chars.peek() == Some(&'"') {
chars.next();
field.push('"');
} else {
in_quotes = false;
}
}
c => field.push(c),
}
continue;
}
match c {
'"' if field.is_empty() => {
in_quotes = true;
row_started = true;
}
c if c == delimiter => {
row.push(std::mem::take(&mut field));
row_started = true;
}
'\r' => {
if chars.peek() == Some(&'\n') {
continue;
}
field.push('\r');
}
'\n' => {
if row_started || !field.is_empty() {
row.push(std::mem::take(&mut field));
rows.push(std::mem::take(&mut row));
}
row_started = false;
}
c => {
field.push(c);
row_started = true;
}
}
}
if row_started || !field.is_empty() {
row.push(field);
rows.push(row);
}
rows
}
#[async_trait]
impl Spider for CsvFeedSpider {
fn name(&self) -> &str {
&self.name
}
fn start_urls(&self) -> Vec<String> {
self.feed_urls.clone()
}
fn allowed_domains(&self) -> HashSet<String> {
self.allowed_domains.clone()
}
fn concurrent_requests(&self) -> u32 {
self.concurrent_requests
}
fn development_mode(&self) -> bool {
self.development_mode
}
fn robots_txt_obey(&self) -> bool {
self.robots_txt_obey
}
async fn parse(
&self,
response: SpiderResponse,
) -> (Vec<serde_json::Value>, Vec<SpiderRequest>) {
let rows = parse_csv(response.text(), self.delimiter);
let mut rows_iter = rows.into_iter();
let headers: Vec<String> = match &self.headers {
Some(h) => h.clone(),
None => match rows_iter.next() {
Some(first) => first,
None => return (vec![], vec![]),
},
};
let mut items = Vec::new();
for row in rows_iter {
let mut obj = serde_json::Map::new();
for (i, header) in headers.iter().enumerate() {
let cell = row.get(i).cloned().unwrap_or_default();
obj.insert(header.clone(), serde_json::Value::String(cell));
}
match &self.parse_row {
Some(f) => items.extend(f(&response, &obj)),
None => items.push(serde_json::Value::Object(obj)),
}
}
(items, vec![])
}
}
#[must_use = "a builder does nothing until `.build()` is called"]
pub struct CsvFeedSpiderBuilder {
spider: CsvFeedSpider,
}
impl CsvFeedSpiderBuilder {
fn new(name: &str) -> Self {
Self {
spider: CsvFeedSpider {
name: name.to_string(),
feed_urls: Vec::new(),
delimiter: ',',
headers: None,
parse_row: None,
allowed_domains: HashSet::new(),
concurrent_requests: 4,
development_mode: false,
robots_txt_obey: false,
},
}
}
pub fn feed_url(mut self, url: &str) -> Self {
self.spider.feed_urls.push(url.to_string());
self
}
pub fn feed_urls(mut self, urls: impl IntoIterator<Item = String>) -> Self {
self.spider.feed_urls.extend(urls);
self
}
pub fn delimiter(mut self, delimiter: char) -> Self {
self.spider.delimiter = delimiter;
self
}
pub fn headers(mut self, headers: impl IntoIterator<Item = String>) -> Self {
self.spider.headers = Some(headers.into_iter().collect());
self
}
pub fn parse_row(mut self, f: ParseRowFn) -> Self {
self.spider.parse_row = Some(f);
self
}
pub fn allowed_domains(mut self, domains: impl IntoIterator<Item = String>) -> Self {
self.spider.allowed_domains.extend(domains);
self
}
pub fn concurrent_requests(mut self, n: u32) -> Self {
self.spider.concurrent_requests = n;
self
}
pub fn development_mode(mut self, on: bool) -> Self {
self.spider.development_mode = on;
self
}
pub fn robots_txt_obey(mut self, on: bool) -> Self {
self.spider.robots_txt_obey = on;
self
}
pub fn build(self) -> CsvFeedSpider {
self.spider
}
}
#[cfg(test)]
mod tests {
use super::parse_csv;
#[test]
fn parses_quoted_fields_delimiters_and_crlf() {
let text = "a,b,c\r\n\"x,1\",\"say \"\"hi\"\"\",plain\r\nlast,\"multi\nline\",end\r\n";
let rows = parse_csv(text, ',');
assert_eq!(
rows,
vec![
vec!["a", "b", "c"],
vec!["x,1", "say \"hi\"", "plain"],
vec!["last", "multi\nline", "end"],
]
);
}
#[test]
fn trailing_newline_produces_no_phantom_row() {
assert_eq!(
parse_csv("a,b\n1,2\n", ','),
vec![vec!["a", "b"], vec!["1", "2"]]
);
assert_eq!(
parse_csv("a,b\n1,2", ','),
vec![vec!["a", "b"], vec!["1", "2"]]
);
}
#[test]
fn blank_lines_are_skipped() {
assert_eq!(
parse_csv("a,b\n\n1,2\n\r\n3,4\n", ','),
vec![vec!["a", "b"], vec!["1", "2"], vec!["3", "4"]]
);
}
#[test]
fn custom_delimiter_and_empty_fields() {
assert_eq!(
parse_csv("a;;c\n;;\n", ';'),
vec![vec!["a", "", "c"], vec!["", "", ""]]
);
}
}