use core::cmp::Ordering;
use core::fmt;
use core::hash::{Hash, Hasher};
use core::str::FromStr;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::domain::KeyDomain;
use crate::error::CompositeKeyParseError;
use crate::key::Key;
pub struct CompositeKey<A: KeyDomain, B: KeyDomain, const SEP: char = ':'> {
first: Key<A>,
second: Key<B>,
}
impl<A: KeyDomain, B: KeyDomain, const SEP: char> fmt::Debug for CompositeKey<A, B, SEP> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CompositeKey")
.field("first", &self.first.as_str())
.field("second", &self.second.as_str())
.field("sep", &SEP)
.finish()
}
}
impl<A: KeyDomain, B: KeyDomain, const SEP: char> Clone for CompositeKey<A, B, SEP> {
fn clone(&self) -> Self {
Self {
first: self.first.clone(),
second: self.second.clone(),
}
}
}
impl<A: KeyDomain, B: KeyDomain, const SEP: char> CompositeKey<A, B, SEP> {
#[must_use]
pub fn new(first: Key<A>, second: Key<B>) -> Self {
debug_assert!(
!first.as_str().contains(SEP),
"CompositeKey::new: first component {:?} contains separator {:?}",
first.as_str(),
SEP,
);
Self { first, second }
}
#[must_use]
#[inline]
pub fn first(&self) -> &Key<A> {
&self.first
}
#[must_use]
#[inline]
pub fn second(&self) -> &Key<B> {
&self.second
}
}
impl<A: KeyDomain, B: KeyDomain, const SEP: char> fmt::Display for CompositeKey<A, B, SEP> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{}{}", self.first.as_str(), SEP, self.second.as_str())
}
}
impl<A: KeyDomain, B: KeyDomain, const SEP: char> FromStr for CompositeKey<A, B, SEP> {
type Err = CompositeKeyParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let sep_pos = s
.find(SEP)
.ok_or(CompositeKeyParseError::MissingSeparator { separator: SEP })?;
let first_str = &s[..sep_pos];
let second_str = &s[sep_pos + SEP.len_utf8()..];
let first = Key::<A>::new(first_str).map_err(CompositeKeyParseError::InvalidFirst)?;
let second =
Key::<B>::new(second_str).map_err(CompositeKeyParseError::InvalidSecond)?;
Ok(Self { first, second })
}
}
impl<A: KeyDomain, B: KeyDomain, const SEP: char> PartialEq for CompositeKey<A, B, SEP> {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.first == other.first && self.second == other.second
}
}
impl<A: KeyDomain, B: KeyDomain, const SEP: char> Eq for CompositeKey<A, B, SEP> {}
impl<A: KeyDomain, B: KeyDomain, const SEP: char> PartialOrd for CompositeKey<A, B, SEP> {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl<A: KeyDomain, B: KeyDomain, const SEP: char> Ord for CompositeKey<A, B, SEP> {
#[inline]
fn cmp(&self, other: &Self) -> Ordering {
self.first
.cmp(&other.first)
.then_with(|| self.second.cmp(&other.second))
}
}
impl<A: KeyDomain, B: KeyDomain, const SEP: char> Hash for CompositeKey<A, B, SEP> {
#[inline]
fn hash<H: Hasher>(&self, state: &mut H) {
self.first.as_str().hash(state);
SEP.hash(state);
self.second.as_str().hash(state);
}
}
#[cfg(feature = "serde")]
impl<A: KeyDomain, B: KeyDomain, const SEP: char> Serialize for CompositeKey<A, B, SEP> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.to_string().serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl<'de, A: KeyDomain, B: KeyDomain, const SEP: char> Deserialize<'de>
for CompositeKey<A, B, SEP>
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
CompositeKey::from_str(&s).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use super::*;
use crate::error::KeyParseError;
use crate::{Domain, KeyDomain};
#[derive(Debug)]
struct UserDomain;
impl Domain for UserDomain {
const DOMAIN_NAME: &'static str = "user";
}
impl KeyDomain for UserDomain {}
#[derive(Debug)]
struct PostDomain;
impl Domain for PostDomain {
const DOMAIN_NAME: &'static str = "post";
}
impl KeyDomain for PostDomain {}
type TestKey = CompositeKey<UserDomain, PostDomain>;
fn user(s: &str) -> Key<UserDomain> {
Key::new(s).unwrap()
}
fn post(s: &str) -> Key<PostDomain> {
Key::new(s).unwrap()
}
#[derive(Debug)]
struct PermissiveDomain;
impl Domain for PermissiveDomain {
const DOMAIN_NAME: &'static str = "permissive";
}
impl KeyDomain for PermissiveDomain {
fn allowed_characters(_c: char) -> bool {
true
}
}
#[test]
fn ae1_roundtrip_default_separator() {
let ck: TestKey = CompositeKey::new(user("user123"), post("post456"));
assert_eq!(ck.to_string(), "user123:post456");
let parsed: TestKey = "user123:post456".parse().unwrap();
assert_eq!(parsed.first(), &user("user123"));
assert_eq!(parsed.second(), &post("post456"));
}
#[test]
fn ae2_missing_separator() {
let err = "user123".parse::<TestKey>().unwrap_err();
assert_eq!(
err,
CompositeKeyParseError::MissingSeparator { separator: ':' }
);
}
#[test]
fn ae3_empty_first_segment() {
let err = ":post456".parse::<TestKey>().unwrap_err();
assert!(
matches!(err, CompositeKeyParseError::InvalidFirst(KeyParseError::Empty)),
"expected InvalidFirst(Empty), got {:?}",
err
);
}
#[test]
fn ae4_custom_separator() {
let ck: CompositeKey<UserDomain, PostDomain, '/'> =
CompositeKey::new(user("user123"), post("post456"));
assert_eq!(ck.to_string(), "user123/post456");
let parsed: CompositeKey<UserDomain, PostDomain, '/'> =
"user123/post456".parse().unwrap();
assert_eq!(parsed.first(), &user("user123"));
assert_eq!(parsed.second(), &post("post456"));
let err = "user123:post456"
.parse::<CompositeKey<UserDomain, PostDomain, '/'>>()
.unwrap_err();
assert!(matches!(
err,
CompositeKeyParseError::MissingSeparator { separator: '/' }
));
}
#[test]
fn ae5_equality_and_hash_consistency() {
let ck1: TestKey = CompositeKey::new(user("user123"), post("post456"));
let ck2: TestKey = CompositeKey::new(user("user123"), post("post456"));
assert_eq!(ck1, ck2);
fn compute_hash<T: Hash>(value: &T) -> u64 {
let mut h = std::collections::hash_map::DefaultHasher::new();
value.hash(&mut h);
h.finish()
}
assert_eq!(compute_hash(&ck1), compute_hash(&ck2));
}
#[test]
fn split_on_first_colon_second_may_contain_colon() {
let result = "user123:extra:part".parse::<TestKey>();
let _ = result;
}
#[test]
fn empty_input_missing_separator() {
let err = "".parse::<TestKey>().unwrap_err();
assert!(matches!(
err,
CompositeKeyParseError::MissingSeparator { separator: ':' }
));
}
#[test]
fn only_separator_empty_first_and_second() {
let err = ":".parse::<TestKey>().unwrap_err();
assert!(
matches!(err, CompositeKeyParseError::InvalidFirst(KeyParseError::Empty)),
"expected InvalidFirst(Empty), got {:?}",
err
);
}
#[test]
#[should_panic]
#[cfg(debug_assertions)]
fn debug_assert_fires_when_first_contains_sep() {
let first_with_sep = Key::<PermissiveDomain>::new("user:id").unwrap();
let _ = CompositeKey::<PermissiveDomain, PermissiveDomain>::new(
first_with_sep,
Key::<PermissiveDomain>::new("other").unwrap(),
);
}
#[test]
fn ord_lexicographic() {
let a_b: TestKey = "aaa:bbb".parse().unwrap();
let a_c: TestKey = "aaa:ccc".parse().unwrap();
let b_a: TestKey = "bbb:aaa".parse().unwrap();
assert!(a_b < a_c);
assert!(a_b < b_a);
assert!(a_c < b_a);
}
#[test]
fn clone_is_independent() {
let ck: TestKey = CompositeKey::new(user("user123"), post("post456"));
let cloned = ck.clone();
assert_eq!(ck, cloned);
}
#[test]
fn can_use_as_hash_map_key() {
let mut map: HashMap<TestKey, &str> = HashMap::new();
let ck: TestKey = CompositeKey::new(user("u1"), post("p1"));
map.insert(ck.clone(), "value");
assert_eq!(map[&ck], "value");
}
#[cfg(feature = "serde")]
mod serde_tests {
use super::*;
#[test]
fn json_roundtrip() {
let ck: TestKey = CompositeKey::new(user("user123"), post("post456"));
let json = serde_json::to_string(&ck).unwrap();
assert_eq!(json, r#""user123:post456""#);
let parsed: TestKey = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, ck);
}
#[test]
fn json_error_no_separator() {
let result = serde_json::from_str::<TestKey>(r#""user123""#);
assert!(result.is_err());
}
#[test]
fn custom_separator_json_roundtrip() {
let ck: CompositeKey<UserDomain, PostDomain, '/'> =
CompositeKey::new(user("user123"), post("post456"));
let json = serde_json::to_string(&ck).unwrap();
assert_eq!(json, r#""user123/post456""#);
let parsed: CompositeKey<UserDomain, PostDomain, '/'> =
serde_json::from_str(&json).unwrap();
assert_eq!(parsed, ck);
}
}
}