use crate::ShardingError;
use std::any::Any;
pub trait ShardKeyExtractor: Send + Sync {
fn extract(&self, data: &dyn Any) -> Result<String, ShardingError>;
}
pub struct FieldExtractor {
extractor: Box<dyn Fn() -> String + Send + Sync>,
}
impl FieldExtractor {
pub fn new<F>(f: F) -> Self
where
F: Fn() -> String + Send + Sync + 'static,
{
Self {
extractor: Box::new(f),
}
}
}
impl ShardKeyExtractor for FieldExtractor {
fn extract(&self, _data: &dyn Any) -> Result<String, ShardingError> {
Ok((self.extractor)())
}
}
impl std::fmt::Debug for FieldExtractor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FieldExtractor")
.field("extractor", &"<closure>")
.finish()
}
}
pub struct CompositeKeyExtractor {
extractors: Vec<Box<dyn ShardKeyExtractor>>,
}
impl CompositeKeyExtractor {
pub fn new() -> Self {
Self {
extractors: Vec::new(),
}
}
pub fn with<E>(mut self, extractor: E) -> Self
where
E: ShardKeyExtractor + 'static,
{
self.extractors.push(Box::new(extractor));
self
}
pub fn len(&self) -> usize {
self.extractors.len()
}
pub fn is_empty(&self) -> bool {
self.extractors.is_empty()
}
}
impl Default for CompositeKeyExtractor {
fn default() -> Self {
Self::new()
}
}
impl ShardKeyExtractor for CompositeKeyExtractor {
fn extract(&self, data: &dyn Any) -> Result<String, ShardingError> {
let mut parts: Vec<String> = Vec::with_capacity(self.extractors.len());
for ext in &self.extractors {
parts.push(ext.extract(data)?);
}
Ok(parts.join(":"))
}
}
impl std::fmt::Debug for CompositeKeyExtractor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CompositeKeyExtractor")
.field("count", &self.extractors.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ShardingRouter, ShardingStrategy};
use std::collections::HashMap;
#[test]
fn test_field_extractor_basic() {
let ext = FieldExtractor::new(|| "key:123".to_string());
let key = ext.extract(&"ignored").unwrap();
assert_eq!(key, "key:123");
}
#[test]
fn test_field_extractor_deterministic() {
let ext = FieldExtractor::new(|| "stable_key".to_string());
let k1 = ext.extract(&42i32).unwrap();
let k2 = ext.extract(&"other").unwrap();
assert_eq!(k1, k2);
assert_eq!(k1, "stable_key");
}
#[test]
fn test_composite_key_extractor_combines() {
let ext = CompositeKeyExtractor::new()
.with(FieldExtractor::new(|| "cn".to_string()))
.with(FieldExtractor::new(|| "user:123".to_string()));
let key = ext.extract(&"ignored").unwrap();
assert_eq!(key, "cn:user:123");
}
#[test]
fn test_composite_key_extractor_three_parts() {
let ext = CompositeKeyExtractor::new()
.with(FieldExtractor::new(|| "2026-07-18".to_string()))
.with(FieldExtractor::new(|| "cn".to_string()))
.with(FieldExtractor::new(|| "user:42".to_string()));
let key = ext.extract(&"x").unwrap();
assert_eq!(key, "2026-07-18:cn:user:42");
}
#[test]
fn test_composite_key_extractor_empty() {
let ext = CompositeKeyExtractor::new();
assert!(ext.is_empty());
assert_eq!(ext.len(), 0);
let key = ext.extract(&"ignored").unwrap();
assert_eq!(key, "");
}
#[test]
fn test_route_by_data_with_field_extractor_enum() {
let mut mapping = HashMap::new();
mapping.insert("key:123".to_string(), "shard_a".to_string());
let router = ShardingRouter::new_enum(mapping, None);
let ext = FieldExtractor::new(|| "key:123".to_string());
let result = router.route_by_data(&"data", &ext).unwrap();
assert_eq!(result, "shard_a");
}
#[test]
fn test_route_by_data_no_match_errors() {
let router = ShardingRouter::new_enum(HashMap::new(), None);
let ext = FieldExtractor::new(|| "missing".to_string());
let result = router.route_by_data(&"data", &ext);
assert!(matches!(result, Err(ShardingError::NoMappingForKey(_))));
}
#[test]
fn test_route_by_data_with_hash_strategy() {
let router = ShardingRouter::new(ShardingStrategy::Hash, vec!["s0", "s1"]);
let ext = FieldExtractor::new(|| "user:42".to_string());
let result = router.route_by_data(&"data", &ext).unwrap();
assert!(result == "s0" || result == "s1");
}
#[test]
fn test_route_by_data_with_composite_extractor() {
let mut mapping = HashMap::new();
mapping.insert("cn:user:123".to_string(), "shard_x".to_string());
let router = ShardingRouter::new_enum(mapping, None);
let ext = CompositeKeyExtractor::new()
.with(FieldExtractor::new(|| "cn".to_string()))
.with(FieldExtractor::new(|| "user:123".to_string()));
let result = router.route_by_data(&"data", &ext).unwrap();
assert_eq!(result, "shard_x");
}
#[test]
fn test_field_extractor_debug() {
let ext = FieldExtractor::new(|| "k".to_string());
let s = format!("{:?}", ext);
assert!(s.contains("FieldExtractor"));
}
#[test]
fn test_composite_key_extractor_debug() {
let ext = CompositeKeyExtractor::new().with(FieldExtractor::new(|| "a".to_string()));
let s = format!("{:?}", ext);
assert!(s.contains("CompositeKeyExtractor"));
assert!(s.contains("count"));
}
}