Skip to main content

gfeh_http/
exposure.rs

1//! Where a published link comes from.
2//!
3//! The view resolves a token through this trait rather than by reading the metadata
4//! index, because a protocol crate depends on `gfeh-core` and nothing else. That is
5//! what lets the HTTP view be developed and tested against an in-memory store with no
6//! database, no daemon, and no Town OS -- and it is what keeps the exposure table's
7//! shape out of a crate whose job is HTTP.
8//!
9//! `gfehd` implements this over `gfeh-index`. Tests implement it over a map.
10
11use gfeh_core::NodeRef;
12use std::collections::HashMap;
13use std::sync::{Arc, RwLock};
14
15/// A file published under a token.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct Exposed {
18    /// The node the token resolves to.
19    pub node: NodeRef,
20    /// The filename to advertise, when it should differ from the node's own name.
21    pub filename: Option<String>,
22    /// Whether the link currently resolves.
23    ///
24    /// Kept rather than deleted so a link can be switched off and on again without
25    /// minting a new token -- and so the view can answer a disabled link the same way
26    /// it answers an unknown one.
27    pub enabled: bool,
28}
29
30/// Resolves published tokens.
31pub trait Exposures: Send + Sync + 'static {
32    /// The file a token names, if any.
33    fn resolve(&self, token: &str) -> Option<Exposed>;
34}
35
36/// An in-memory exposure table.
37///
38/// For tests, and for a daemon serving a fixed set of published files from
39/// configuration rather than from the index.
40#[derive(Debug, Default, Clone)]
41pub struct StaticExposures {
42    entries: Arc<RwLock<HashMap<String, Exposed>>>,
43}
44
45impl StaticExposures {
46    /// An empty table.
47    #[must_use]
48    pub fn new() -> Self {
49        Self::default()
50    }
51
52    /// Publish a node under a token.
53    #[must_use]
54    pub fn with(self, token: &str, exposed: Exposed) -> Self {
55        self.publish(token, exposed);
56        self
57    }
58
59    /// Publish a node under a token, in place.
60    pub fn publish(&self, token: &str, exposed: Exposed) {
61        if let Ok(mut entries) = self.entries.write() {
62            entries.insert(token.to_string(), exposed);
63        }
64    }
65
66    /// Switch a link off or on without forgetting its token.
67    pub fn set_enabled(&self, token: &str, enabled: bool) {
68        if let Ok(mut entries) = self.entries.write()
69            && let Some(entry) = entries.get_mut(token)
70        {
71            entry.enabled = enabled;
72        }
73    }
74
75    /// Withdraw a link entirely.
76    pub fn withdraw(&self, token: &str) {
77        if let Ok(mut entries) = self.entries.write() {
78            entries.remove(token);
79        }
80    }
81}
82
83impl Exposures for StaticExposures {
84    fn resolve(&self, token: &str) -> Option<Exposed> {
85        self.entries.read().ok()?.get(token).cloned()
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use gfeh_core::{NodeId, PartitionId};
93
94    fn exposed() -> Exposed {
95        Exposed {
96            // One partition per call would make two `exposed()` values unequal, which
97            // says nothing about the table under test.
98            node: NodeRef::Id(PartitionId(uuid_nil()), NodeId(7)),
99            filename: None,
100            enabled: true,
101        }
102    }
103
104    /// A fixed partition, so equality compares what the test means to compare.
105    fn uuid_nil() -> uuid::Uuid {
106        uuid::Uuid::nil()
107    }
108
109    #[test]
110    fn a_published_token_resolves_and_an_unknown_one_does_not() {
111        let table = StaticExposures::new().with("tok", exposed());
112        assert_eq!(table.resolve("tok"), Some(exposed()));
113        assert_eq!(table.resolve("other"), None);
114    }
115
116    #[test]
117    fn disabling_keeps_the_token_and_clears_the_link() {
118        let table = StaticExposures::new().with("tok", exposed());
119        table.set_enabled("tok", false);
120        assert_eq!(table.resolve("tok").map(|e| e.enabled), Some(false));
121
122        table.set_enabled("tok", true);
123        assert_eq!(table.resolve("tok").map(|e| e.enabled), Some(true));
124    }
125
126    #[test]
127    fn withdrawing_removes_it() {
128        let table = StaticExposures::new().with("tok", exposed());
129        table.withdraw("tok");
130        assert_eq!(table.resolve("tok"), None);
131    }
132}