Skip to main content

rate_net/
key.rs

1//! The per-key identity a limit is tracked against.
2
3use core::fmt;
4use core::hash::{Hash, Hasher};
5use std::net::IpAddr;
6
7/// Keys up to this many bytes are stored inline, with no heap allocation. It
8/// covers the common identities — an IPv6 address (16 bytes), a `u64` id (8),
9/// and short string keys — so the steady-state check path allocates nothing.
10const INLINE_CAP: usize = 23;
11
12#[derive(Clone)]
13enum Repr {
14    /// Bytes stored inline; `len` of them are live.
15    Inline { buf: [u8; INLINE_CAP], len: u8 },
16    /// Bytes too long to inline, on the heap.
17    Heap(Box<[u8]>),
18}
19
20/// An opaque identity a rate limit is tracked against.
21///
22/// A `Key` is whatever distinguishes one caller (or tenant, route, or resource)
23/// from another: an IP address, a user id, an API token, an endpoint name. It is
24/// stored as an owned byte string — inline for keys up to a couple dozen bytes,
25/// on the heap beyond that — and two keys are equal exactly when their bytes are
26/// equal, so the identity is the byte content, not the source type.
27///
28/// You rarely name `Key` directly. The check methods accept `impl Into<Key>`,
29/// and this module provides `From` conversions for the common identity types, so
30/// `limiter.check("user:42")` and `limiter.check(ip)` both work. Small keys are
31/// stored inline, which is what keeps an existing-key check allocation-free.
32///
33/// # Examples
34///
35/// ```
36/// use rate_net::Key;
37///
38/// let a: Key = "tenant:acme".into();
39/// let b: Key = String::from("tenant:acme").into();
40/// assert_eq!(a, b);
41/// assert_eq!(a.as_bytes(), b"tenant:acme");
42/// ```
43#[derive(Clone)]
44pub struct Key(Repr);
45
46impl Key {
47    /// Builds a key from raw bytes, storing them inline when they fit.
48    fn from_bytes(bytes: &[u8]) -> Self {
49        if bytes.len() <= INLINE_CAP {
50            let mut buf = [0u8; INLINE_CAP];
51            buf[..bytes.len()].copy_from_slice(bytes);
52            // `bytes.len() <= INLINE_CAP <= u8::MAX`, so the cast cannot truncate.
53            Self(Repr::Inline {
54                buf,
55                len: bytes.len() as u8,
56            })
57        } else {
58            Self(Repr::Heap(bytes.into()))
59        }
60    }
61
62    /// Borrows the raw bytes that make up the key.
63    ///
64    /// # Examples
65    ///
66    /// ```
67    /// use rate_net::Key;
68    ///
69    /// let key: Key = "abc".into();
70    /// assert_eq!(key.as_bytes(), b"abc");
71    /// ```
72    #[must_use]
73    pub fn as_bytes(&self) -> &[u8] {
74        match &self.0 {
75            Repr::Inline { buf, len } => &buf[..*len as usize],
76            Repr::Heap(bytes) => bytes,
77        }
78    }
79}
80
81impl PartialEq for Key {
82    fn eq(&self, other: &Self) -> bool {
83        self.as_bytes() == other.as_bytes()
84    }
85}
86
87impl Eq for Key {}
88
89impl Hash for Key {
90    fn hash<H: Hasher>(&self, state: &mut H) {
91        self.as_bytes().hash(state);
92    }
93}
94
95impl fmt::Debug for Key {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        f.debug_tuple("Key").field(&self.as_bytes()).finish()
98    }
99}
100
101impl From<&str> for Key {
102    fn from(value: &str) -> Self {
103        Self::from_bytes(value.as_bytes())
104    }
105}
106
107impl From<String> for Key {
108    fn from(value: String) -> Self {
109        if value.len() <= INLINE_CAP {
110            Self::from_bytes(value.as_bytes())
111        } else {
112            Self(Repr::Heap(value.into_bytes().into_boxed_slice()))
113        }
114    }
115}
116
117impl From<&[u8]> for Key {
118    fn from(value: &[u8]) -> Self {
119        Self::from_bytes(value)
120    }
121}
122
123impl From<Vec<u8>> for Key {
124    fn from(value: Vec<u8>) -> Self {
125        if value.len() <= INLINE_CAP {
126            Self::from_bytes(&value)
127        } else {
128            Self(Repr::Heap(value.into_boxed_slice()))
129        }
130    }
131}
132
133impl From<u64> for Key {
134    fn from(value: u64) -> Self {
135        Self::from_bytes(&value.to_be_bytes())
136    }
137}
138
139impl From<IpAddr> for Key {
140    fn from(value: IpAddr) -> Self {
141        match value {
142            IpAddr::V4(addr) => Self::from_bytes(&addr.octets()),
143            IpAddr::V6(addr) => Self::from_bytes(&addr.octets()),
144        }
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::{INLINE_CAP, Key, Repr};
151    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
152
153    fn is_inline(key: &Key) -> bool {
154        matches!(key.0, Repr::Inline { .. })
155    }
156
157    #[test]
158    fn test_str_and_string_are_equal_for_same_bytes() {
159        let a: Key = "user:42".into();
160        let b: Key = String::from("user:42").into();
161        assert_eq!(a, b);
162        assert_eq!(a.as_bytes(), b"user:42");
163    }
164
165    #[test]
166    fn test_distinct_content_is_distinct() {
167        let a: Key = "a".into();
168        let b: Key = "b".into();
169        assert_ne!(a, b);
170    }
171
172    #[test]
173    fn test_byte_slice_and_vec_round_trip() {
174        let bytes: &[u8] = &[1, 2, 3, 4];
175        let a: Key = bytes.into();
176        let b: Key = vec![1u8, 2, 3, 4].into();
177        assert_eq!(a, b);
178        assert_eq!(a.as_bytes(), bytes);
179    }
180
181    #[test]
182    fn test_u64_uses_big_endian_bytes() {
183        let key: Key = 1u64.into();
184        assert_eq!(key.as_bytes(), &[0, 0, 0, 0, 0, 0, 0, 1]);
185    }
186
187    #[test]
188    fn test_ip_addresses_encode_their_octets() {
189        let v4: Key = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)).into();
190        assert_eq!(v4.as_bytes(), &[10, 0, 0, 1]);
191
192        let v6: Key = IpAddr::V6(Ipv6Addr::LOCALHOST).into();
193        assert_eq!(v6.as_bytes().len(), 16);
194    }
195
196    #[test]
197    fn test_small_keys_are_stored_inline() {
198        // The common identities all fit inline (no heap allocation).
199        assert!(is_inline(&Key::from("user:42")));
200        assert!(is_inline(&Key::from(1u64)));
201        assert!(is_inline(&Key::from(IpAddr::V6(Ipv6Addr::LOCALHOST))));
202        // Exactly at the inline boundary.
203        let at_cap = vec![b'x'; INLINE_CAP];
204        assert!(is_inline(&Key::from(at_cap.as_slice())));
205    }
206
207    #[test]
208    fn test_large_keys_spill_to_heap_and_compare_equal() {
209        let long = vec![b'y'; INLINE_CAP + 1];
210        let from_slice: Key = long.as_slice().into();
211        let from_vec: Key = long.clone().into();
212        assert!(!is_inline(&from_slice));
213        assert_eq!(from_slice, from_vec);
214        assert_eq!(from_slice.as_bytes(), long.as_slice());
215    }
216
217    #[test]
218    fn test_inline_and_heap_hash_consistently_by_bytes() {
219        use std::collections::hash_map::DefaultHasher;
220        use std::hash::{Hash, Hasher};
221
222        fn hash(key: &Key) -> u64 {
223            let mut h = DefaultHasher::new();
224            key.hash(&mut h);
225            h.finish()
226        }
227
228        // Same bytes always take the same representation, but confirm equal keys
229        // hash equal regardless.
230        let a: Key = "short".into();
231        let b: Key = String::from("short").into();
232        assert_eq!(hash(&a), hash(&b));
233    }
234}