1use std::io;
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 ) -> io::Result<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 ) -> io::Result<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]]) -> io::Result<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]]) -> io::Result<Vec<i64>> {
80 check_fields(fields)?;
81 match self {
82 Self::Embedded(s) => s.httl(key, fields),
83 Self::Remote(c) => {
84 hash_ttl_request(c, b"HTTL", key, None, HExpireCond::Always, fields)
85 }
86 }
87 }
88}
89
90fn check_fields(fields: &[&[u8]]) -> io::Result<()> {
92 if fields.is_empty() {
93 return Err(io::Error::new(
94 io::ErrorKind::InvalidInput,
95 "hash field-TTL verbs need at least one field",
96 ));
97 }
98 Ok(())
99}
100
101fn cond_keyword(cond: HExpireCond) -> Option<&'static [u8]> {
102 match cond {
103 HExpireCond::Always => None,
104 HExpireCond::Nx => Some(b"NX"),
105 HExpireCond::Xx => Some(b"XX"),
106 HExpireCond::Gt => Some(b"GT"),
107 HExpireCond::Lt => Some(b"LT"),
108 }
109}
110
111fn hash_ttl_request(
114 c: &mut RespClient,
115 verb: &[u8],
116 key: &[u8],
117 arg: Option<String>,
118 cond: HExpireCond,
119 fields: &[&[u8]],
120) -> io::Result<Vec<i64>> {
121 let mut args = Vec::with_capacity(fields.len() + 6);
122 args.push(verb.to_vec());
123 args.push(key.to_vec());
124 if let Some(a) = arg {
125 args.push(a.into_bytes());
126 }
127 if let Some(kw) = cond_keyword(cond) {
128 args.push(kw.to_vec());
129 }
130 args.push(b"FIELDS".to_vec());
131 args.push(fields.len().to_string().into_bytes());
132 args.extend(fields.iter().map(|f| f.to_vec()));
133 match c.request(&args)? {
134 Reply::Array(items) => items
135 .into_iter()
136 .map(|r| match r {
137 Reply::Int(n) => Ok(n),
138 other => Err(unexpected(other)),
139 })
140 .collect(),
141 Reply::Error(e) => Err(io::Error::other(string(e))),
142 other => Err(unexpected(other)),
143 }
144}
145
146fn to_codes(v: Vec<i64>) -> Vec<HExpireCode> {
147 v.into_iter().map(|n| n as HExpireCode).collect()
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use std::time::Duration;
154
155 #[test]
156 fn embedded_hexpire_httl_hpersist_round_trip() {
157 let mut c = Connection::open("mem://").unwrap();
158 c.hset(b"h", &[(b"a".as_ref(), b"1".as_ref()), (b"b".as_ref(), b"2".as_ref())])
159 .unwrap();
160
161 let codes = c
162 .hexpire(b"h", &[&b"a"[..], &b"nope"[..]], Duration::from_secs(60), HExpireCond::Always)
163 .unwrap();
164 assert_eq!(codes, vec![1, -2]);
165
166 let ttls = c.httl(b"h", &[&b"a"[..], &b"b"[..]]).unwrap();
167 assert!((0..=60_000).contains(&ttls[0]), "ttl = {}", ttls[0]);
168 assert_eq!(ttls[1], -1);
169
170 let codes = c.hpersist(b"h", &[&b"a"[..], &b"b"[..]]).unwrap();
171 assert_eq!(codes, vec![1, -1]);
172 assert_eq!(c.httl(b"h", &[&b"a"[..]]).unwrap(), vec![-1]);
173 }
174
175 #[test]
176 fn embedded_hpexpire_ms_precision() {
177 let mut c = Connection::open("mem://").unwrap();
178 c.hset(b"h", &[(b"f".as_ref(), b"v".as_ref())]).unwrap();
179 let codes = c
180 .hpexpire(b"h", &[&b"f"[..]], Duration::from_millis(1500), HExpireCond::Always)
181 .unwrap();
182 assert_eq!(codes, vec![1]);
183 let ttl = c.httl(b"h", &[&b"f"[..]]).unwrap()[0];
184 assert!((0..=1500).contains(&ttl), "ttl = {ttl}");
185 }
186
187 #[test]
188 fn empty_fields_rejected() {
189 let mut c = Connection::open("mem://").unwrap();
190 let err = c.httl(b"h", &[]).unwrap_err();
191 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
192 }
193}