use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(bound(deserialize = "T: Deserialize<'de> + Default"))]
pub struct TemplateSpec<T = ()>
where
T: Default,
{
#[serde(default)]
pub template: String,
#[serde(flatten, default)]
pub extension: T,
}
impl TemplateSpec {
pub fn new(template: impl Into<String>) -> Self {
Self {
template: template.into(),
extension: (),
}
}
}
impl<T: Default> TemplateSpec<T> {
pub fn with_extension(template: impl Into<String>, extension: T) -> Self {
Self {
template: template.into(),
extension,
}
}
}
impl<T: Default> Default for TemplateSpec<T> {
fn default() -> Self {
Self {
template: String::new(),
extension: T::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(bound(deserialize = "T: Deserialize<'de> + Default"))]
pub struct QueryConfig<T = ()>
where
T: Default,
{
#[serde(skip_serializing_if = "Option::is_none")]
pub added: Option<TemplateSpec<T>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated: Option<TemplateSpec<T>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deleted: Option<TemplateSpec<T>>,
}
impl<T: Default> Default for QueryConfig<T> {
fn default() -> Self {
Self {
added: None,
updated: None,
deleted: None,
}
}
}
pub trait TemplateRouting<T = ()>
where
T: Default,
{
fn routes(&self) -> &HashMap<String, QueryConfig<T>>;
fn default_template(&self) -> Option<&QueryConfig<T>>;
fn get_template_spec(
&self,
query_id: &str,
operation: OperationType,
) -> Option<&TemplateSpec<T>> {
if let Some(query_config) = self.routes().get(query_id) {
if let Some(spec) = Self::get_spec_from_config(query_config, operation) {
return Some(spec);
}
}
if let Some(default_config) = self.default_template() {
return Self::get_spec_from_config(default_config, operation);
}
None
}
fn get_spec_from_config(
config: &QueryConfig<T>,
operation: OperationType,
) -> Option<&TemplateSpec<T>> {
match operation {
OperationType::Add => config.added.as_ref(),
OperationType::Update => config.updated.as_ref(),
OperationType::Delete => config.deleted.as_ref(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OperationType {
Add,
Update,
Delete,
}
impl std::str::FromStr for OperationType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"add" => Ok(Self::Add),
"update" => Ok(Self::Update),
"delete" => Ok(Self::Delete),
_ => Err(format!("Invalid operation type: {s}")),
}
}
}
impl OperationType {
pub fn as_str(&self) -> &'static str {
match self {
Self::Add => "add",
Self::Update => "update",
Self::Delete => "delete",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_operation_type_from_str() {
use std::str::FromStr;
assert_eq!(OperationType::from_str("add"), Ok(OperationType::Add));
assert_eq!(OperationType::from_str("ADD"), Ok(OperationType::Add));
assert_eq!(OperationType::from_str("update"), Ok(OperationType::Update));
assert_eq!(OperationType::from_str("UPDATE"), Ok(OperationType::Update));
assert_eq!(OperationType::from_str("delete"), Ok(OperationType::Delete));
assert_eq!(OperationType::from_str("DELETE"), Ok(OperationType::Delete));
assert!(OperationType::from_str("invalid").is_err());
}
#[test]
fn test_operation_type_as_str() {
assert_eq!(OperationType::Add.as_str(), "add");
assert_eq!(OperationType::Update.as_str(), "update");
assert_eq!(OperationType::Delete.as_str(), "delete");
}
#[test]
fn test_template_spec_serialization() {
let spec: TemplateSpec = TemplateSpec {
template: "{{after.id}}".to_string(),
extension: (),
};
let json = serde_json::to_string(&spec).unwrap();
let deserialized: TemplateSpec = serde_json::from_str(&json).unwrap();
assert_eq!(spec, deserialized);
}
#[test]
fn test_query_config_serialization() {
let config: QueryConfig = QueryConfig {
added: Some(TemplateSpec {
template: "[ADD] {{after.id}}".to_string(),
extension: (),
}),
updated: None,
deleted: Some(TemplateSpec {
template: "[DEL] {{before.id}}".to_string(),
extension: (),
}),
};
let json = serde_json::to_string(&config).unwrap();
let deserialized: QueryConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config, deserialized);
}
#[derive(Debug, Clone)]
struct TestReactionConfig {
routes: HashMap<String, QueryConfig>,
default_template: Option<QueryConfig>,
}
impl TemplateRouting for TestReactionConfig {
fn routes(&self) -> &HashMap<String, QueryConfig> {
&self.routes
}
fn default_template(&self) -> Option<&QueryConfig> {
self.default_template.as_ref()
}
}
#[test]
fn test_template_routing_query_specific() {
let mut routes = HashMap::new();
routes.insert(
"query1".to_string(),
QueryConfig {
added: Some(TemplateSpec {
template: "query1 add".to_string(),
extension: (),
}),
updated: None,
deleted: None,
},
);
let config = TestReactionConfig {
routes,
default_template: None,
};
let spec = config.get_template_spec("query1", OperationType::Add);
assert!(spec.is_some());
assert_eq!(spec.unwrap().template, "query1 add");
let spec = config.get_template_spec("query1", OperationType::Update);
assert!(spec.is_none());
}
#[test]
fn test_template_routing_default_fallback() {
let config = TestReactionConfig {
routes: HashMap::new(),
default_template: Some(QueryConfig {
added: Some(TemplateSpec {
template: "default add".to_string(),
extension: (),
}),
updated: Some(TemplateSpec {
template: "default update".to_string(),
extension: (),
}),
deleted: Some(TemplateSpec {
template: "default delete".to_string(),
extension: (),
}),
}),
};
let spec = config.get_template_spec("any_query", OperationType::Add);
assert!(spec.is_some());
assert_eq!(spec.unwrap().template, "default add");
let spec = config.get_template_spec("any_query", OperationType::Update);
assert!(spec.is_some());
assert_eq!(spec.unwrap().template, "default update");
}
#[test]
fn test_template_routing_query_overrides_default() {
let mut routes = HashMap::new();
routes.insert(
"query1".to_string(),
QueryConfig {
added: Some(TemplateSpec {
template: "query1 add override".to_string(),
extension: (),
}),
updated: None,
deleted: None,
},
);
let config = TestReactionConfig {
routes,
default_template: Some(QueryConfig {
added: Some(TemplateSpec {
template: "default add".to_string(),
extension: (),
}),
updated: Some(TemplateSpec {
template: "default update".to_string(),
extension: (),
}),
deleted: None,
}),
};
let spec = config.get_template_spec("query1", OperationType::Add);
assert_eq!(spec.unwrap().template, "query1 add override");
let spec = config.get_template_spec("query1", OperationType::Update);
assert_eq!(spec.unwrap().template, "default update");
let spec = config.get_template_spec("query2", OperationType::Add);
assert_eq!(spec.unwrap().template, "default add");
}
}