use super::copy::CopySource;
use super::extraction::ExtractionMethod;
use super::request::RequestContext;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::sync::Arc;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LookupBehavior {
pub key: LookupKey,
#[serde(rename = "fromDataSource")]
pub from_data_source: DataSource,
pub into: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LookupKey {
pub from: CopySource,
#[serde(rename = "using")]
pub extraction: ExtractionMethod,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DataSource {
pub csv: CsvDataSource,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CsvDataSource {
pub path: String,
#[serde(rename = "keyColumn")]
pub key_column: String,
#[serde(default = "default_delimiter")]
pub delimiter: char,
}
fn default_delimiter() -> char {
','
}
pub struct CsvCache {
data: RwLock<HashMap<String, Arc<CsvData>>>,
}
impl Default for CsvCache {
fn default() -> Self {
Self::new()
}
}
impl CsvCache {
pub fn new() -> Self {
Self {
data: RwLock::new(HashMap::new()),
}
}
pub fn get_or_load(&self, path: &str, delimiter: char) -> Option<Arc<CsvData>> {
{
let cache = self.data.read();
if let Some(data) = cache.get(path) {
return Some(Arc::clone(data));
}
}
let data = CsvData::load(path, delimiter).ok()?;
let data = Arc::new(data);
{
let mut cache = self.data.write();
cache.insert(path.to_string(), Arc::clone(&data));
}
Some(data)
}
pub fn clear(&self) {
self.data.write().clear();
}
}
pub struct CsvData {
headers: Vec<String>,
rows: HashMap<String, Vec<String>>,
}
impl CsvData {
pub fn load<P: AsRef<Path>>(path: P, delimiter: char) -> Result<Self, std::io::Error> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut lines = reader.lines();
let header_line = lines
.next()
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "Empty CSV"))??;
let headers: Vec<String> = header_line
.split(delimiter)
.map(|s| s.trim().to_string())
.collect();
let mut rows = HashMap::new();
for line in lines {
let line = line?;
let values: Vec<String> = line
.split(delimiter)
.map(|s| s.trim().to_string())
.collect();
if !values.is_empty() {
rows.insert(values[0].clone(), values);
}
}
Ok(Self { headers, rows })
}
pub fn lookup(&self, key: &str, key_column: &str) -> HashMap<String, String> {
let mut result = HashMap::new();
let key_col_idx = self.headers.iter().position(|h| h == key_column);
if let Some(key_idx) = key_col_idx {
for (row_key, values) in &self.rows {
let matches = if key_idx == 0 {
row_key == key
} else {
values.get(key_idx).map(|v| v == key).unwrap_or(false)
};
if matches {
for (i, header) in self.headers.iter().enumerate() {
if let Some(value) = values.get(i) {
result.insert(format!("[{header}]"), value.clone());
}
}
break;
}
}
}
result
}
}
pub fn apply_lookup_behaviors(
body: &str,
headers: &mut HashMap<String, String>,
behaviors: &[LookupBehavior],
request: &RequestContext,
csv_cache: &CsvCache,
) -> String {
let mut result = body.to_string();
for behavior in behaviors {
let key_value = behavior
.key
.from
.extract(request)
.and_then(|v| behavior.key.extraction.extract(&v));
if let Some(key) = key_value {
if let Some(csv_data) = csv_cache.get_or_load(
&behavior.from_data_source.csv.path,
behavior.from_data_source.csv.delimiter,
) {
let replacements = csv_data.lookup(&key, &behavior.from_data_source.csv.key_column);
for (token, value) in replacements {
let full_token = format!("{}{}", behavior.into, token);
result = result.replace(&full_token, &value);
for header_value in headers.values_mut() {
*header_value = header_value.replace(&full_token, &value);
}
}
}
}
}
result
}