1use core::cmp::Ordering;
66use core::fmt;
67use core::hash::{Hash, Hasher};
68use core::str::FromStr;
69
70#[cfg(feature = "serde")]
71use serde::{Deserialize, Serialize};
72
73use crate::domain::KeyDomain;
74use crate::error::CompositeKeyParseError;
75use crate::key::Key;
76
77pub struct CompositeKey<A: KeyDomain, B: KeyDomain, const SEP: char = ':'> {
85 first: Key<A>,
86 second: Key<B>,
87}
88
89impl<A: KeyDomain, B: KeyDomain, const SEP: char> fmt::Debug for CompositeKey<A, B, SEP> {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 f.debug_struct("CompositeKey")
92 .field("first", &self.first.as_str())
93 .field("second", &self.second.as_str())
94 .field("sep", &SEP)
95 .finish()
96 }
97}
98
99impl<A: KeyDomain, B: KeyDomain, const SEP: char> Clone for CompositeKey<A, B, SEP> {
100 fn clone(&self) -> Self {
101 Self {
102 first: self.first.clone(),
103 second: self.second.clone(),
104 }
105 }
106}
107
108impl<A: KeyDomain, B: KeyDomain, const SEP: char> CompositeKey<A, B, SEP> {
113 #[must_use]
121 pub fn new(first: Key<A>, second: Key<B>) -> Self {
122 debug_assert!(
123 !first.as_str().contains(SEP),
124 "CompositeKey::new: first component {:?} contains separator {:?}",
125 first.as_str(),
126 SEP,
127 );
128 Self { first, second }
129 }
130
131 #[must_use]
133 #[inline]
134 pub fn first(&self) -> &Key<A> {
135 &self.first
136 }
137
138 #[must_use]
140 #[inline]
141 pub fn second(&self) -> &Key<B> {
142 &self.second
143 }
144}
145
146impl<A: KeyDomain, B: KeyDomain, const SEP: char> fmt::Display for CompositeKey<A, B, SEP> {
151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152 write!(f, "{}{}{}", self.first.as_str(), SEP, self.second.as_str())
153 }
154}
155
156impl<A: KeyDomain, B: KeyDomain, const SEP: char> FromStr for CompositeKey<A, B, SEP> {
161 type Err = CompositeKeyParseError;
162
163 fn from_str(s: &str) -> Result<Self, Self::Err> {
172 let sep_pos = s
173 .find(SEP)
174 .ok_or(CompositeKeyParseError::MissingSeparator { separator: SEP })?;
175
176 let first_str = &s[..sep_pos];
177 let second_str = &s[sep_pos + SEP.len_utf8()..];
179
180 let first = Key::<A>::new(first_str).map_err(CompositeKeyParseError::InvalidFirst)?;
181 let second =
182 Key::<B>::new(second_str).map_err(CompositeKeyParseError::InvalidSecond)?;
183
184 Ok(Self { first, second })
185 }
186}
187
188impl<A: KeyDomain, B: KeyDomain, const SEP: char> PartialEq for CompositeKey<A, B, SEP> {
193 #[inline]
194 fn eq(&self, other: &Self) -> bool {
195 self.first == other.first && self.second == other.second
196 }
197}
198
199impl<A: KeyDomain, B: KeyDomain, const SEP: char> Eq for CompositeKey<A, B, SEP> {}
200
201impl<A: KeyDomain, B: KeyDomain, const SEP: char> PartialOrd for CompositeKey<A, B, SEP> {
206 #[inline]
207 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
208 Some(self.cmp(other))
209 }
210}
211
212impl<A: KeyDomain, B: KeyDomain, const SEP: char> Ord for CompositeKey<A, B, SEP> {
213 #[inline]
226 fn cmp(&self, other: &Self) -> Ordering {
227 self.first
228 .cmp(&other.first)
229 .then_with(|| self.second.cmp(&other.second))
230 }
231}
232
233impl<A: KeyDomain, B: KeyDomain, const SEP: char> Hash for CompositeKey<A, B, SEP> {
238 #[inline]
241 fn hash<H: Hasher>(&self, state: &mut H) {
242 self.first.as_str().hash(state);
243 SEP.hash(state);
244 self.second.as_str().hash(state);
245 }
246}
247
248#[cfg(feature = "serde")]
253impl<A: KeyDomain, B: KeyDomain, const SEP: char> Serialize for CompositeKey<A, B, SEP> {
254 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
255 where
256 S: serde::Serializer,
257 {
258 self.to_string().serialize(serializer)
259 }
260}
261
262#[cfg(feature = "serde")]
263impl<'de, A: KeyDomain, B: KeyDomain, const SEP: char> Deserialize<'de>
264 for CompositeKey<A, B, SEP>
265{
266 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
267 where
268 D: serde::Deserializer<'de>,
269 {
270 let s = String::deserialize(deserializer)?;
274 CompositeKey::from_str(&s).map_err(serde::de::Error::custom)
275 }
276}
277
278#[cfg(test)]
283mod tests {
284 use std::collections::HashMap;
285 use std::hash::{Hash, Hasher};
286
287 use super::*;
288 use crate::error::KeyParseError;
289 use crate::{Domain, KeyDomain};
290
291 #[derive(Debug)]
294 struct UserDomain;
295 impl Domain for UserDomain {
296 const DOMAIN_NAME: &'static str = "user";
297 }
298 impl KeyDomain for UserDomain {}
299
300 #[derive(Debug)]
301 struct PostDomain;
302 impl Domain for PostDomain {
303 const DOMAIN_NAME: &'static str = "post";
304 }
305 impl KeyDomain for PostDomain {}
306
307 type TestKey = CompositeKey<UserDomain, PostDomain>;
309
310 fn user(s: &str) -> Key<UserDomain> {
311 Key::new(s).unwrap()
312 }
313 fn post(s: &str) -> Key<PostDomain> {
314 Key::new(s).unwrap()
315 }
316
317 #[derive(Debug)]
320 struct PermissiveDomain;
321 impl Domain for PermissiveDomain {
322 const DOMAIN_NAME: &'static str = "permissive";
323 }
324 impl KeyDomain for PermissiveDomain {
325 fn allowed_characters(_c: char) -> bool {
326 true
327 }
328 }
329
330 #[test]
333 fn ae1_roundtrip_default_separator() {
334 let ck: TestKey = CompositeKey::new(user("user123"), post("post456"));
335 assert_eq!(ck.to_string(), "user123:post456");
336
337 let parsed: TestKey = "user123:post456".parse().unwrap();
338 assert_eq!(parsed.first(), &user("user123"));
339 assert_eq!(parsed.second(), &post("post456"));
340 }
341
342 #[test]
345 fn ae2_missing_separator() {
346 let err = "user123".parse::<TestKey>().unwrap_err();
347 assert_eq!(
348 err,
349 CompositeKeyParseError::MissingSeparator { separator: ':' }
350 );
351 }
352
353 #[test]
356 fn ae3_empty_first_segment() {
357 let err = ":post456".parse::<TestKey>().unwrap_err();
358 assert!(
359 matches!(err, CompositeKeyParseError::InvalidFirst(KeyParseError::Empty)),
360 "expected InvalidFirst(Empty), got {:?}",
361 err
362 );
363 }
364
365 #[test]
368 fn ae4_custom_separator() {
369 let ck: CompositeKey<UserDomain, PostDomain, '/'> =
370 CompositeKey::new(user("user123"), post("post456"));
371 assert_eq!(ck.to_string(), "user123/post456");
372
373 let parsed: CompositeKey<UserDomain, PostDomain, '/'> =
374 "user123/post456".parse().unwrap();
375 assert_eq!(parsed.first(), &user("user123"));
376 assert_eq!(parsed.second(), &post("post456"));
377
378 let err = "user123:post456"
380 .parse::<CompositeKey<UserDomain, PostDomain, '/'>>()
381 .unwrap_err();
382 assert!(matches!(
383 err,
384 CompositeKeyParseError::MissingSeparator { separator: '/' }
385 ));
386 }
387
388 #[test]
391 fn ae5_equality_and_hash_consistency() {
392 let ck1: TestKey = CompositeKey::new(user("user123"), post("post456"));
393 let ck2: TestKey = CompositeKey::new(user("user123"), post("post456"));
394 assert_eq!(ck1, ck2);
395
396 fn compute_hash<T: Hash>(value: &T) -> u64 {
397 let mut h = std::collections::hash_map::DefaultHasher::new();
398 value.hash(&mut h);
399 h.finish()
400 }
401 assert_eq!(compute_hash(&ck1), compute_hash(&ck2));
402 }
403
404 #[test]
407 fn split_on_first_colon_second_may_contain_colon() {
408 let result = "user123:extra:part".parse::<TestKey>();
409 let _ = result;
410 }
411
412 #[test]
413 fn empty_input_missing_separator() {
414 let err = "".parse::<TestKey>().unwrap_err();
415 assert!(matches!(
416 err,
417 CompositeKeyParseError::MissingSeparator { separator: ':' }
418 ));
419 }
420
421 #[test]
422 fn only_separator_empty_first_and_second() {
423 let err = ":".parse::<TestKey>().unwrap_err();
424 assert!(
425 matches!(err, CompositeKeyParseError::InvalidFirst(KeyParseError::Empty)),
426 "expected InvalidFirst(Empty), got {:?}",
427 err
428 );
429 }
430
431 #[test]
432 #[should_panic]
433 #[cfg(debug_assertions)]
434 fn debug_assert_fires_when_first_contains_sep() {
435 let first_with_sep = Key::<PermissiveDomain>::new("user:id").unwrap();
439 let _ = CompositeKey::<PermissiveDomain, PermissiveDomain>::new(
440 first_with_sep,
441 Key::<PermissiveDomain>::new("other").unwrap(),
442 );
443 }
444
445 #[test]
446 fn ord_lexicographic() {
447 let a_b: TestKey = "aaa:bbb".parse().unwrap();
448 let a_c: TestKey = "aaa:ccc".parse().unwrap();
449 let b_a: TestKey = "bbb:aaa".parse().unwrap();
450
451 assert!(a_b < a_c);
452 assert!(a_b < b_a);
453 assert!(a_c < b_a);
454 }
455
456 #[test]
457 fn clone_is_independent() {
458 let ck: TestKey = CompositeKey::new(user("user123"), post("post456"));
459 let cloned = ck.clone();
460 assert_eq!(ck, cloned);
461 }
462
463 #[test]
464 fn can_use_as_hash_map_key() {
465 let mut map: HashMap<TestKey, &str> = HashMap::new();
466 let ck: TestKey = CompositeKey::new(user("u1"), post("p1"));
467 map.insert(ck.clone(), "value");
468 assert_eq!(map[&ck], "value");
469 }
470
471 #[cfg(feature = "serde")]
472 mod serde_tests {
473 use super::*;
474
475 #[test]
476 fn json_roundtrip() {
477 let ck: TestKey = CompositeKey::new(user("user123"), post("post456"));
478 let json = serde_json::to_string(&ck).unwrap();
479 assert_eq!(json, r#""user123:post456""#);
480
481 let parsed: TestKey = serde_json::from_str(&json).unwrap();
482 assert_eq!(parsed, ck);
483 }
484
485 #[test]
486 fn json_error_no_separator() {
487 let result = serde_json::from_str::<TestKey>(r#""user123""#);
488 assert!(result.is_err());
489 }
490
491 #[test]
492 fn custom_separator_json_roundtrip() {
493 let ck: CompositeKey<UserDomain, PostDomain, '/'> =
494 CompositeKey::new(user("user123"), post("post456"));
495 let json = serde_json::to_string(&ck).unwrap();
496 assert_eq!(json, r#""user123/post456""#);
497
498 let parsed: CompositeKey<UserDomain, PostDomain, '/'> =
499 serde_json::from_str(&json).unwrap();
500 assert_eq!(parsed, ck);
501 }
502 }
503}