use std::hash::{Hash, Hasher};
use std::sync::Arc;
use crate::error::{Result, SchemaRegError};
use crate::types::EncodeTarget;
pub type CustomSubjectFn =
Arc<dyn Fn(&str, Option<&str>, EncodeTarget) -> Result<String> + Send + Sync>;
#[derive(Clone, Default)]
#[non_exhaustive]
pub enum SubjectNameStrategy {
#[default]
TopicName,
RecordName,
TopicRecordName,
ApicurioGroupRecordName {
group_id: String,
},
Custom(CustomSubjectFn),
}
impl std::fmt::Debug for SubjectNameStrategy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TopicName => write!(f, "TopicName"),
Self::RecordName => write!(f, "RecordName"),
Self::TopicRecordName => write!(f, "TopicRecordName"),
Self::ApicurioGroupRecordName { group_id } => f
.debug_struct("ApicurioGroupRecordName")
.field("group_id", group_id)
.finish(),
Self::Custom(_) => write!(f, "Custom(..)"),
}
}
}
impl PartialEq for SubjectNameStrategy {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::TopicName, Self::TopicName) => true,
(Self::RecordName, Self::RecordName) => true,
(Self::TopicRecordName, Self::TopicRecordName) => true,
(
Self::ApicurioGroupRecordName { group_id: a },
Self::ApicurioGroupRecordName { group_id: b },
) => a == b,
(Self::Custom(a), Self::Custom(b)) => Arc::ptr_eq(a, b),
_ => false,
}
}
}
impl Eq for SubjectNameStrategy {}
impl Hash for SubjectNameStrategy {
fn hash<H: Hasher>(&self, state: &mut H) {
std::mem::discriminant(self).hash(state);
match self {
Self::ApicurioGroupRecordName { group_id } => group_id.hash(state),
Self::Custom(f) => (Arc::as_ptr(f) as *const () as usize).hash(state),
_ => {}
}
}
}
impl SubjectNameStrategy {
pub fn subject_name(
&self,
topic: &str,
record_name: Option<&str>,
target: EncodeTarget,
) -> Result<String> {
match self {
Self::TopicName => Ok(format!("{topic}-{target}")),
Self::RecordName => {
let name = record_name.ok_or_else(|| {
SchemaRegError::config("RecordName strategy requires a record name")
})?;
Ok(name.to_string())
}
Self::TopicRecordName => {
let name = record_name.ok_or_else(|| {
SchemaRegError::config("TopicRecordName strategy requires a record name")
})?;
Ok(format!("{topic}-{name}"))
}
Self::ApicurioGroupRecordName { group_id } => {
let name = record_name.ok_or_else(|| {
SchemaRegError::config(
"ApicurioGroupRecordName strategy requires a record name",
)
})?;
Ok(format!("{group_id}/{name}"))
}
Self::Custom(f) => f(topic, record_name, target),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_subject_default_is_topic_name() {
assert_eq!(
SubjectNameStrategy::default(),
SubjectNameStrategy::TopicName
);
}
#[test]
fn test_subject_topic_name_key() {
let s = SubjectNameStrategy::TopicName
.subject_name("orders", None, EncodeTarget::Key)
.unwrap();
assert_eq!(s, "orders-key");
}
#[test]
fn test_subject_topic_name_value() {
let s = SubjectNameStrategy::TopicName
.subject_name("orders", None, EncodeTarget::Value)
.unwrap();
assert_eq!(s, "orders-value");
}
#[test]
fn test_subject_record_name() {
let s = SubjectNameStrategy::RecordName
.subject_name("orders", Some("com.example.Order"), EncodeTarget::Value)
.unwrap();
assert_eq!(s, "com.example.Order");
}
#[test]
fn test_subject_record_name_missing() {
let result =
SubjectNameStrategy::RecordName.subject_name("orders", None, EncodeTarget::Value);
assert!(result.is_err());
}
#[test]
fn test_subject_topic_record_name() {
let s = SubjectNameStrategy::TopicRecordName
.subject_name("orders", Some("Order"), EncodeTarget::Key)
.unwrap();
assert_eq!(s, "orders-Order");
}
#[test]
fn test_subject_topic_record_name_missing() {
let result =
SubjectNameStrategy::TopicRecordName.subject_name("orders", None, EncodeTarget::Key);
assert!(result.is_err());
}
#[test]
fn test_subject_apicurio_group_record_name() {
let s = SubjectNameStrategy::ApicurioGroupRecordName {
group_id: "my-group".to_string(),
}
.subject_name("orders", Some("com.example.Order"), EncodeTarget::Value)
.unwrap();
assert_eq!(s, "my-group/com.example.Order");
}
#[test]
fn test_subject_apicurio_group_record_name_missing() {
let result = SubjectNameStrategy::ApicurioGroupRecordName {
group_id: "g".to_string(),
}
.subject_name("orders", None, EncodeTarget::Value);
assert!(result.is_err());
}
#[test]
fn test_custom_strategy() {
let strategy = SubjectNameStrategy::Custom(Arc::new(|topic, _, target| {
Ok(format!("custom-{topic}-{target}"))
}));
let s = strategy
.subject_name("orders", None, EncodeTarget::Value)
.unwrap();
assert_eq!(s, "custom-orders-value");
}
#[test]
fn test_custom_strategy_ptr_eq() {
let f: Arc<dyn Fn(&str, Option<&str>, EncodeTarget) -> Result<String> + Send + Sync> =
Arc::new(|topic, _, target| Ok(format!("{topic}-{target}")));
let a = SubjectNameStrategy::Custom(Arc::clone(&f));
let b = SubjectNameStrategy::Custom(Arc::clone(&f));
let c = SubjectNameStrategy::Custom(Arc::new(|_, _, _| Ok("other".into())));
assert_eq!(a, b, "same Arc should be equal");
assert_ne!(a, c, "different Arcs should not be equal");
}
}