1pub mod backend;
6mod memory;
7mod redis;
8
9#[cfg(test)]
10mod tests;
11
12use std::{collections::HashMap, sync::Arc};
13
14use anyhow::Result;
15use backend::StoreBackend;
16use links_id::Id;
17use links_normalized::{Link, Normalized};
18use parking_lot::RwLock;
19use serde::{Deserialize, Serialize};
20use strum::{Display as EnumDisplay, EnumString, IntoStaticStr};
21use tokio::spawn;
22use tracing::{debug, instrument, trace};
23
24pub use self::{memory::Store as Memory, redis::Store as Redis};
25use crate::stats::{Statistic, StatisticDescription, StatisticValue};
26
27#[derive(
31 Copy,
32 Clone,
33 Debug,
34 Default,
35 PartialEq,
36 Eq,
37 Serialize,
38 Deserialize,
39 EnumString,
40 EnumDisplay,
41 IntoStaticStr,
42)]
43#[non_exhaustive]
44#[serde(rename_all = "snake_case")]
45#[strum(serialize_all = "snake_case")]
46pub enum BackendType {
47 #[default]
51 Memory,
52 Redis,
54}
55
56impl BackendType {
57 #[must_use]
59 pub fn as_str(self) -> &'static str {
60 self.into()
61 }
62}
63
64#[derive(Debug)]
66pub struct Current {
67 store: RwLock<Store>,
68}
69
70impl Current {
71 #[must_use]
74 pub const fn new(store: Store) -> Self {
75 Self {
76 store: RwLock::new(store),
77 }
78 }
79
80 #[must_use]
87 pub fn new_static(store: Store) -> &'static Self {
88 Box::leak(Box::new(Self::new(store)))
89 }
90
91 pub fn get(&self) -> Store {
94 self.store.read().clone()
95 }
96
97 pub fn update(&self, store: Store) {
101 *self.store.write() = store;
102 }
103
104 pub fn backend_name(&self) -> &'static str {
108 self.store.read().backend_name()
109 }
110}
111
112#[derive(Debug, Clone)]
115pub struct Store {
116 store: Arc<dyn StoreBackend>,
117}
118
119impl Store {
120 #[instrument(level = "debug", ret, err)]
130 pub async fn new(store_type: BackendType, config: &HashMap<String, String>) -> Result<Self> {
131 match store_type {
132 BackendType::Memory => Ok(Self {
133 store: Arc::new(Memory::new(config).await?),
134 }),
135 BackendType::Redis => Ok(Self {
136 store: Arc::new(Redis::new(config).await?),
137 }),
138 }
139 }
140
141 #[must_use]
145 pub fn backend_name(&self) -> &'static str {
146 self.store.get_store_type().as_str()
147 }
148
149 #[instrument(level = "debug", skip(self), fields(name = self.backend_name()), ret, err)]
158 pub async fn get_redirect(&self, from: Id) -> Result<Option<Link>> {
159 self.store.get_redirect(from).await
160 }
161
162 #[instrument(level = "debug", skip(self), fields(name = self.backend_name()), ret, err)]
172 pub async fn set_redirect(&self, from: Id, to: Link) -> Result<Option<Link>> {
173 self.store.set_redirect(from, to).await
174 }
175
176 #[instrument(level = "debug", skip(self), fields(name = self.backend_name()), ret, err)]
186 pub async fn rem_redirect(&self, from: Id) -> Result<Option<Link>> {
187 self.store.rem_redirect(from).await
188 }
189
190 #[instrument(level = "debug", skip(self), fields(name = self.backend_name()), ret, err)]
199 pub async fn get_vanity(&self, from: Normalized) -> Result<Option<Id>> {
200 self.store.get_vanity(from).await
201 }
202
203 #[instrument(level = "debug", skip(self), fields(name = self.backend_name()), ret, err)]
213 pub async fn set_vanity(&self, from: Normalized, to: Id) -> Result<Option<Id>> {
214 self.store.set_vanity(from, to).await
215 }
216
217 #[instrument(level = "debug", skip(self), fields(name = self.backend_name()), ret, err)]
226 pub async fn rem_vanity(&self, from: Normalized) -> Result<Option<Id>> {
227 self.store.rem_vanity(from).await
228 }
229
230 #[instrument(level = "debug", skip(self), fields(name = self.backend_name()), ret, err)]
241 pub async fn get_statistics(
242 &self,
243 description: StatisticDescription,
244 ) -> Result<impl Iterator<Item = (Statistic, StatisticValue)>> {
245 Ok(self.store.get_statistics(description).await?.into_iter())
246 }
247
248 pub fn incr_statistics<I>(&self, statistics: I)
258 where
259 I: IntoIterator<Item = Statistic> + Send + 'static,
260 <I as IntoIterator>::IntoIter: Send,
261 {
262 let store = self.store.clone();
263 spawn(async move {
264 for stat in statistics {
265 match store.incr_statistic(stat.clone()).await {
266 Ok(val) => trace!(?val, ?stat, "statistic incremented"),
267 Err(err) => debug!(?err, ?stat, "statistic incrementing failed"),
268 }
269 }
270 });
271 }
272
273 #[instrument(level = "debug", skip(self), fields(name = self.backend_name()), ret, err)]
285 pub async fn rem_statistics(
286 &self,
287 description: StatisticDescription,
288 ) -> Result<impl Iterator<Item = (Statistic, StatisticValue)>> {
289 Ok(self.store.rem_statistics(description).await?.into_iter())
290 }
291}
292
293#[cfg(test)]
294mod store_tests {
295 use std::str::FromStr;
296
297 use super::*;
298
299 #[tokio::test]
300 async fn current() {
301 let id = Id::from([1, 2, 3, 4, 5]);
302 let link = Link::from_str("https://example.com").unwrap();
303 let store = Store::new("memory".parse().unwrap(), &HashMap::new())
304 .await
305 .unwrap();
306 store.set_redirect(id, link.clone()).await.unwrap();
307
308 let current = Current::new(store.clone());
309 let static_current = Current::new_static(store);
310
311 assert_eq!(
312 current.get().get_redirect(id).await.unwrap(),
313 Some(link.clone())
314 );
315 assert_eq!(
316 static_current.get().get_redirect(id).await.unwrap(),
317 Some(link)
318 );
319
320 let new_store = Store::new("memory".parse().unwrap(), &HashMap::new())
321 .await
322 .unwrap();
323 current.update(new_store.clone());
324 static_current.update(new_store.clone());
325
326 assert_eq!(current.get().get_redirect(id).await.unwrap(), None);
327 assert_eq!(static_current.get().get_redirect(id).await.unwrap(), None);
328 }
329
330 #[test]
331 fn type_to_from() {
332 assert_eq!(
333 BackendType::Memory,
334 BackendType::Memory.as_str().parse().unwrap()
335 );
336
337 assert_eq!(
338 BackendType::Redis,
339 BackendType::Redis.as_str().parse().unwrap()
340 );
341 }
342
343 #[tokio::test]
344 async fn cheap_clone() {
345 let store_a = Store::new("memory".parse().unwrap(), &HashMap::new())
346 .await
347 .unwrap();
348 let store_b = Store::new("memory".parse().unwrap(), &HashMap::new())
349 .await
350 .unwrap();
351 let store_c = store_a.clone();
352 let id = Id::from([0, 1, 2, 3, 4]);
353
354 store_a
355 .set_redirect(id, Link::new("https://example.com/test").unwrap())
356 .await
357 .unwrap();
358
359 assert_eq!(
360 store_a.get_redirect(id).await.unwrap(),
361 store_c.get_redirect(id).await.unwrap(),
362 );
363 assert_ne!(
364 store_a.get_redirect(id).await.unwrap(),
365 store_b.get_redirect(id).await.unwrap(),
366 );
367 }
368
369 #[tokio::test]
370 async fn new() {
371 Store::new("memory".parse().unwrap(), &HashMap::new())
372 .await
373 .unwrap();
374 }
375
376 #[tokio::test]
377 async fn backend_name() {
378 let store = Store::new("memory".parse().unwrap(), &HashMap::new())
379 .await
380 .unwrap();
381
382 let name = store.backend_name();
383
384 assert_eq!(name, "memory");
385 }
386
387 #[tokio::test]
388 async fn get_redirect() {
389 let store = Store::new("memory".parse().unwrap(), &HashMap::new())
390 .await
391 .unwrap();
392
393 let id = Id::from([0x10, 0x20, 0x30, 0x40, 0x50]);
394 let link = Link::new("https://example.com/test").unwrap();
395
396 store.set_redirect(id, link.clone()).await.unwrap();
397
398 assert_eq!(store.get_redirect(Id::new()).await.unwrap(), None);
399 assert_eq!(store.get_redirect(id).await.unwrap(), Some(link));
400 }
401
402 #[tokio::test]
403 async fn set_redirect() {
404 let store = Store::new("memory".parse().unwrap(), &HashMap::new())
405 .await
406 .unwrap();
407
408 let id = Id::from([0x11, 0x21, 0x31, 0x41, 0x51]);
409 let link = Link::new("https://example.com/test").unwrap();
410
411 store.set_redirect(id, link.clone()).await.unwrap();
412
413 assert_eq!(store.get_redirect(id).await.unwrap(), Some(link));
414 }
415
416 #[tokio::test]
417 async fn rem_redirect() {
418 let store = Store::new("memory".parse().unwrap(), &HashMap::new())
419 .await
420 .unwrap();
421
422 let id = Id::from([0x12, 0x22, 0x32, 0x42, 0x52]);
423 let link = Link::new("https://example.com/test").unwrap();
424
425 store.set_redirect(id, link.clone()).await.unwrap();
426
427 assert_eq!(store.get_redirect(id).await.unwrap(), Some(link.clone()));
428 store.rem_redirect(id).await.unwrap();
429 assert_eq!(store.get_redirect(id).await.unwrap(), None);
430 }
431
432 #[tokio::test]
433 async fn get_vanity() {
434 let store = Store::new("memory".parse().unwrap(), &HashMap::new())
435 .await
436 .unwrap();
437
438 let vanity = Normalized::new("Example Test");
439 let id = Id::from([0x13, 0x23, 0x33, 0x43, 0x53]);
440
441 store.set_vanity(vanity.clone(), id).await.unwrap();
442
443 assert_eq!(
444 store
445 .get_vanity(Normalized::new("Doesn't exist."))
446 .await
447 .unwrap(),
448 None
449 );
450 assert_eq!(store.get_vanity(vanity.clone()).await.unwrap(), Some(id));
451 }
452
453 #[tokio::test]
454 async fn set_vanity() {
455 let store = Store::new("memory".parse().unwrap(), &HashMap::new())
456 .await
457 .unwrap();
458
459 let vanity = Normalized::new("Example Test");
460 let id = Id::from([0x13, 0x23, 0x33, 0x43, 0x53]);
461
462 store.set_vanity(vanity.clone(), id).await.unwrap();
463
464 assert_eq!(store.get_vanity(vanity.clone()).await.unwrap(), Some(id));
465 }
466
467 #[tokio::test]
468 async fn rem_vanity() {
469 let store = Store::new("memory".parse().unwrap(), &HashMap::new())
470 .await
471 .unwrap();
472
473 let vanity = Normalized::new("Example Test");
474 let id = Id::from([0x13, 0x23, 0x33, 0x43, 0x53]);
475
476 store.set_vanity(vanity.clone(), id).await.unwrap();
477
478 assert_eq!(store.get_vanity(vanity.clone()).await.unwrap(), Some(id));
479 store.rem_vanity(vanity.clone()).await.unwrap();
480 assert_eq!(store.get_vanity(vanity.clone()).await.unwrap(), None);
481 }
482}