1use std::collections::HashMap;
7
8use anyhow::Result;
9use async_trait::async_trait;
10use links_id::Id;
11use links_normalized::{Link, Normalized};
12use parking_lot::RwLock;
13use tracing::instrument;
14
15use crate::{
16 stats::{Statistic, StatisticDescription, StatisticValue},
17 store::{BackendType, StoreBackend},
18};
19
20#[derive(Debug)]
31pub struct Store {
32 redirects: RwLock<HashMap<Id, Link>>,
33 vanity: RwLock<HashMap<Normalized, Id>>,
34 stats: RwLock<HashMap<Statistic, StatisticValue>>,
35}
36
37#[async_trait]
38impl StoreBackend for Store {
39 fn store_type() -> BackendType
40 where
41 Self: Sized,
42 {
43 BackendType::Memory
44 }
45
46 fn get_store_type(&self) -> BackendType {
47 BackendType::Memory
48 }
49
50 #[instrument(level = "trace", ret, err)]
51 async fn new(_config: &HashMap<String, String>) -> Result<Self> {
52 Ok(Self {
53 redirects: RwLock::new(HashMap::new()),
54 vanity: RwLock::new(HashMap::new()),
55 stats: RwLock::new(HashMap::new()),
56 })
57 }
58
59 #[instrument(level = "trace", ret, err)]
60 async fn get_redirect(&self, from: Id) -> Result<Option<Link>> {
61 let redirects = self.redirects.read();
62 Ok(redirects.get(&from).map(ToOwned::to_owned))
63 }
64
65 #[instrument(level = "trace", ret, err)]
66 async fn set_redirect(&self, from: Id, to: Link) -> Result<Option<Link>> {
67 let mut redirects = self.redirects.write();
68 Ok(redirects.insert(from, to))
69 }
70
71 #[instrument(level = "trace", ret, err)]
72 async fn rem_redirect(&self, from: Id) -> Result<Option<Link>> {
73 let mut redirects = self.redirects.write();
74 Ok(redirects.remove(&from))
75 }
76
77 #[instrument(level = "trace", ret, err)]
78 async fn get_vanity(&self, from: Normalized) -> Result<Option<Id>> {
79 let vanity = self.vanity.read();
80 Ok(vanity.get(&from).map(ToOwned::to_owned))
81 }
82
83 #[instrument(level = "trace", ret, err)]
84 async fn set_vanity(&self, from: Normalized, to: Id) -> Result<Option<Id>> {
85 let mut vanity = self.vanity.write();
86 Ok(vanity.insert(from, to))
87 }
88
89 #[instrument(level = "trace", ret, err)]
90 async fn rem_vanity(&self, from: Normalized) -> Result<Option<Id>> {
91 let mut vanity = self.vanity.write();
92 Ok(vanity.remove(&from))
93 }
94
95 #[instrument(level = "trace", ret, err)]
96 async fn get_statistics(
97 &self,
98 description: StatisticDescription,
99 ) -> Result<Vec<(Statistic, StatisticValue)>> {
100 let stats = self.stats.read();
101 Ok(stats
102 .iter()
103 .filter(|&(k, _)| description.matches(k))
104 .map(|(k, v)| (k.clone(), *v))
105 .collect())
106 }
107
108 #[instrument(level = "trace", ret, err)]
109 #[expect(clippy::significant_drop_tightening, reason = "false positive")]
110 async fn incr_statistic(&self, statistic: Statistic) -> Result<Option<StatisticValue>> {
111 let mut stats = self.stats.write();
112
113 #[expect(
114 clippy::option_if_let_else,
115 reason = "this is more readable than clippy's suggestion"
116 )]
117 if let Some(value) = stats.get_mut(&statistic) {
118 let new_value = value.increment();
119 *value = new_value;
120 Ok(Some(new_value))
121 } else {
122 let new_value = StatisticValue::default();
123 stats.insert(statistic, new_value);
124 Ok(Some(new_value))
125 }
126 }
127
128 #[expect(clippy::significant_drop_tightening, reason = "false positive")]
129 async fn rem_statistics(
130 &self,
131 description: StatisticDescription,
132 ) -> Result<Vec<(Statistic, StatisticValue)>> {
133 let mut stats = self.stats.write();
134 let matches = stats
135 .keys()
136 .filter(|&k| description.matches(k))
137 .map(Clone::clone)
138 .collect::<Vec<_>>();
139
140 Ok(matches
141 .iter()
142 .filter_map(|k| stats.remove_entry(k))
143 .collect())
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use std::collections::HashMap;
150
151 use super::Store;
152 use crate::store::{tests, StoreBackend as _};
153
154 async fn get_store() -> Store {
155 Store::new(&HashMap::from([])).await.unwrap()
156 }
157
158 #[test]
159 fn store_type() {
160 tests::store_type::<Store>();
161 }
162
163 #[tokio::test]
164 async fn get_store_type() {
165 tests::get_store_type::<Store>(&get_store().await);
166 }
167
168 #[tokio::test]
169 async fn get_redirect() {
170 tests::get_redirect(&get_store().await).await;
171 }
172
173 #[tokio::test]
174 async fn set_redirect() {
175 tests::set_redirect(&get_store().await).await;
176 }
177
178 #[tokio::test]
179 async fn rem_redirect() {
180 tests::rem_redirect(&get_store().await).await;
181 }
182
183 #[tokio::test]
184 async fn get_vanity() {
185 tests::get_vanity(&get_store().await).await;
186 }
187
188 #[tokio::test]
189 async fn set_vanity() {
190 tests::set_vanity(&get_store().await).await;
191 }
192
193 #[tokio::test]
194 async fn rem_vanity() {
195 tests::rem_vanity(&get_store().await).await;
196 }
197
198 #[tokio::test]
199 async fn get_statistics() {
200 tests::get_statistics(&get_store().await).await;
201 }
202
203 #[tokio::test]
204 async fn incr_statistic() {
205 tests::incr_statistic(&get_store().await).await;
206 }
207
208 #[tokio::test]
209 async fn rem_statistics() {
210 tests::rem_statistics(&get_store().await).await;
211 }
212}