use std::{
collections::{HashMap, HashSet},
sync::{Arc, Mutex, OnceLock},
};
use regex::Regex;
use serde_json::Value;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Error, Debug)]
pub enum Error {
#[error("{0}: failed to convert to {1}")]
Cast(String, String),
#[error("{0}")]
Clap(#[from] clap::Error),
#[error("Configuration error: {0}")]
Config(String),
#[error("{0}: dest is not empty, use --overwrite to overwrite existing files")]
DestNotEmpty(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON serialization error: {0}")]
SerdeJson(#[from] serde_json::Error),
#[error("SQLite error: {0}")]
Rusqlite(#[from] rusqlite::Error),
#[error("{0}: Unknown serializer")]
UnknownSerializer(String),
}
pub mod config;
mod context;
pub mod filter;
pub mod orchestrator;
pub mod serializers;
mod static_files;
pub use config::{Config, Layout, SerializerConfig, StaticConfig, StaticSpec};
pub use orchestrator::run;
pub use serializers::{JSONSerializer, Serializer, SqliteSerializer, TypescriptSerializer};
use crate::config::FilterOp;
static TYPE_MISMATCH_WARNINGS: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
pub(crate) fn emit_type_mismatch_warning(op: &FilterOp, field: &str, lhs: &Value, rhs: &Value) {
let lhs_kind = value_kind(lhs);
let rhs_kind = value_kind(rhs);
let op_str = op.to_string();
let key = format!("{}|{}|{}|{}", field, op_str, lhs_kind, rhs_kind);
let warnings = TYPE_MISMATCH_WARNINGS.get_or_init(|| Mutex::new(HashSet::new()));
let mut guard = match warnings.lock() {
Ok(g) => g,
Err(_) => return,
};
if guard.insert(key) {
eprintln!(
"warning: $filter type mismatch for field '{}' with op '{}': lhs is {}, rhs is {}",
field, op_str, lhs_kind, rhs_kind
);
}
}
static REGEX_CACHE: OnceLock<Mutex<HashMap<String, Arc<Regex>>>> = OnceLock::new();
pub(crate) fn compile_regex(pattern: &str) -> std::result::Result<Arc<Regex>, regex::Error> {
let cache = REGEX_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
if let Ok(guard) = cache.lock()
&& let Some(re) = guard.get(pattern)
{
return Ok(Arc::clone(re));
}
let re = Arc::new(Regex::new(pattern)?);
if let Ok(mut guard) = cache.lock() {
guard.insert(pattern.to_string(), Arc::clone(&re));
}
Ok(re)
}
pub(crate) fn value_kind(v: &Value) -> &'static str {
match v {
Value::Null => "null",
Value::Bool(_) => "bool",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_identical_patterns_share_one_compiled_regex() {
let first = compile_regex("^ab+c$").expect("valid pattern");
let second = compile_regex("^ab+c$").expect("valid pattern");
assert!(Arc::ptr_eq(&first, &second));
}
#[test]
fn test_distinct_patterns_are_compiled_separately() {
let a = compile_regex("^shared-cache-a$").expect("valid pattern");
let b = compile_regex("^shared-cache-b$").expect("valid pattern");
assert!(!Arc::ptr_eq(&a, &b));
assert!(a.is_match("shared-cache-a"));
assert!(b.is_match("shared-cache-b"));
}
#[test]
fn test_invalid_pattern_returns_the_regex_error() {
assert!(compile_regex("([unclosed").is_err());
}
}