1use crate::{KevyError, KevyResult};
10
11use kevy_embedded::{HExpireCode, HExpireCond};
12use kevy_resp::Reply;
13use kevy_resp_client::RespClient;
14
15use crate::{Connection, string, unexpected};
16
17impl Connection {
18 pub fn hexpire(
23 &mut self,
24 key: &[u8],
25 fields: &[&[u8]],
26 ttl: std::time::Duration,
27 cond: HExpireCond,
28 ) -> KevyResult<Vec<HExpireCode>> {
29 check_fields(fields)?;
30 let secs = ttl.as_secs();
31 match self {
32 Self::Embedded(s) => {
33 s.hexpire(key, fields, std::time::Duration::from_secs(secs), cond)
34 }
35 Self::Remote(c) => {
36 hash_ttl_request(c, b"HEXPIRE", key, Some(secs.to_string()), cond, fields)
37 .map(to_codes)
38 }
39 }
40 }
41
42 pub fn hpexpire(
45 &mut self,
46 key: &[u8],
47 fields: &[&[u8]],
48 ttl: std::time::Duration,
49 cond: HExpireCond,
50 ) -> KevyResult<Vec<HExpireCode>> {
51 check_fields(fields)?;
52 let ms = ttl.as_millis().min(i64::MAX as u128) as u64;
53 match self {
54 Self::Embedded(s) => {
55 s.hexpire(key, fields, std::time::Duration::from_millis(ms), cond)
56 }
57 Self::Remote(c) => {
58 hash_ttl_request(c, b"HPEXPIRE", key, Some(ms.to_string()), cond, fields)
59 .map(to_codes)
60 }
61 }
62 }
63
64 pub fn hpersist(&mut self, key: &[u8], fields: &[&[u8]]) -> KevyResult<Vec<HExpireCode>> {
67 check_fields(fields)?;
68 match self {
69 Self::Embedded(s) => s.hpersist(key, fields),
70 Self::Remote(c) => {
71 hash_ttl_request(c, b"HPERSIST", key, None, HExpireCond::Always, fields)
72 .map(to_codes)
73 }
74 }
75 }
76
77 pub fn httl(&mut self, key: &[u8], fields: &[&[u8]]) -> KevyResult<Vec<i64>> {
83 check_fields(fields)?;
84 match self {
85 Self::Embedded(s) => s.httl(key, fields),
86 Self::Remote(c) => {
87 hash_ttl_request(c, b"HTTL", key, None, HExpireCond::Always, fields)
88 }
89 }
90 }
91
92 pub fn hpttl(&mut self, key: &[u8], fields: &[&[u8]]) -> KevyResult<Vec<i64>> {
95 check_fields(fields)?;
96 match self {
97 Self::Embedded(s) => s.hpttl(key, fields),
98 Self::Remote(c) => {
99 hash_ttl_request(c, b"HPTTL", key, None, HExpireCond::Always, fields)
100 }
101 }
102 }
103}
104
105fn check_fields(fields: &[&[u8]]) -> KevyResult<()> {
107 if fields.is_empty() {
108 return Err(KevyError::InvalidInput("hash field-TTL verbs need at least one field".into()));
109 }
110 Ok(())
111}
112
113fn cond_keyword(cond: HExpireCond) -> Option<&'static [u8]> {
114 match cond {
115 HExpireCond::Always => None,
116 HExpireCond::Nx => Some(b"NX"),
117 HExpireCond::Xx => Some(b"XX"),
118 HExpireCond::Gt => Some(b"GT"),
119 HExpireCond::Lt => Some(b"LT"),
120 }
121}
122
123fn hash_ttl_request(
126 c: &mut RespClient,
127 verb: &[u8],
128 key: &[u8],
129 arg: Option<String>,
130 cond: HExpireCond,
131 fields: &[&[u8]],
132) -> KevyResult<Vec<i64>> {
133 let mut args = Vec::with_capacity(fields.len() + 6);
134 args.push(verb.to_vec());
135 args.push(key.to_vec());
136 if let Some(a) = arg {
137 args.push(a.into_bytes());
138 }
139 if let Some(kw) = cond_keyword(cond) {
140 args.push(kw.to_vec());
141 }
142 args.push(b"FIELDS".to_vec());
143 args.push(fields.len().to_string().into_bytes());
144 args.extend(fields.iter().map(|f| f.to_vec()));
145 match c.request(&args)? {
146 Reply::Array(items) => items
147 .into_iter()
148 .map(|r| match r {
149 Reply::Int(n) => Ok(n),
150 other => Err(unexpected(other)),
151 })
152 .collect(),
153 Reply::Error(e) => Err(KevyError::Protocol(string(e))),
154 other => Err(unexpected(other)),
155 }
156}
157
158fn to_codes(v: Vec<i64>) -> Vec<HExpireCode> {
159 v.into_iter().map(|n| n as HExpireCode).collect()
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use std::time::Duration;
166
167 #[test]
168 fn embedded_hexpire_httl_hpersist_round_trip() {
169 let mut c = Connection::connect("mem://").unwrap();
170 c.hset(b"h", &[(b"a".as_ref(), b"1".as_ref()), (b"b".as_ref(), b"2".as_ref())])
171 .unwrap();
172
173 let codes = c
174 .hexpire(b"h", &[&b"a"[..], &b"nope"[..]], Duration::from_secs(60), HExpireCond::Always)
175 .unwrap();
176 assert_eq!(codes, vec![1, -2]);
177
178 let ttls = c.httl(b"h", &[&b"a"[..], &b"b"[..]]).unwrap();
179 assert!((0..=60_000).contains(&ttls[0]), "ttl = {}", ttls[0]);
180 assert_eq!(ttls[1], -1);
181
182 let codes = c.hpersist(b"h", &[&b"a"[..], &b"b"[..]]).unwrap();
183 assert_eq!(codes, vec![1, -1]);
184 assert_eq!(c.httl(b"h", &[&b"a"[..]]).unwrap(), vec![-1]);
185 }
186
187 #[test]
188 fn embedded_hpexpire_ms_precision() {
189 let mut c = Connection::connect("mem://").unwrap();
190 c.hset(b"h", &[(b"f".as_ref(), b"v".as_ref())]).unwrap();
191 let codes = c
192 .hpexpire(b"h", &[&b"f"[..]], Duration::from_millis(1500), HExpireCond::Always)
193 .unwrap();
194 assert_eq!(codes, vec![1]);
195 let ttl = c.httl(b"h", &[&b"f"[..]]).unwrap()[0];
196 assert!((0..=1500).contains(&ttl), "ttl = {ttl}");
197 }
198
199 #[test]
200 fn empty_fields_rejected() {
201 let mut c = Connection::connect("mem://").unwrap();
202 let err = c.httl(b"h", &[]).unwrap_err();
203 assert!(matches!(err, KevyError::InvalidInput(_)));
204 }
205}