gfeh-http 0.1.2

The plain-HTTP view of gfeh: per-file public exposure with ranges and conditional requests
Documentation
//! Where a published link comes from.
//!
//! The view resolves a token through this trait rather than by reading the metadata
//! index, because a protocol crate depends on `gfeh-core` and nothing else. That is
//! what lets the HTTP view be developed and tested against an in-memory store with no
//! database, no daemon, and no Town OS -- and it is what keeps the exposure table's
//! shape out of a crate whose job is HTTP.
//!
//! `gfehd` implements this over `gfeh-index`. Tests implement it over a map.

use gfeh_core::NodeRef;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};

/// A file published under a token.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Exposed {
    /// The node the token resolves to.
    pub node: NodeRef,
    /// The filename to advertise, when it should differ from the node's own name.
    pub filename: Option<String>,
    /// Whether the link currently resolves.
    ///
    /// Kept rather than deleted so a link can be switched off and on again without
    /// minting a new token -- and so the view can answer a disabled link the same way
    /// it answers an unknown one.
    pub enabled: bool,
}

/// Resolves published tokens.
pub trait Exposures: Send + Sync + 'static {
    /// The file a token names, if any.
    fn resolve(&self, token: &str) -> Option<Exposed>;
}

/// An in-memory exposure table.
///
/// For tests, and for a daemon serving a fixed set of published files from
/// configuration rather than from the index.
#[derive(Debug, Default, Clone)]
pub struct StaticExposures {
    entries: Arc<RwLock<HashMap<String, Exposed>>>,
}

impl StaticExposures {
    /// An empty table.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Publish a node under a token.
    #[must_use]
    pub fn with(self, token: &str, exposed: Exposed) -> Self {
        self.publish(token, exposed);
        self
    }

    /// Publish a node under a token, in place.
    pub fn publish(&self, token: &str, exposed: Exposed) {
        if let Ok(mut entries) = self.entries.write() {
            entries.insert(token.to_string(), exposed);
        }
    }

    /// Switch a link off or on without forgetting its token.
    pub fn set_enabled(&self, token: &str, enabled: bool) {
        if let Ok(mut entries) = self.entries.write()
            && let Some(entry) = entries.get_mut(token)
        {
            entry.enabled = enabled;
        }
    }

    /// Withdraw a link entirely.
    pub fn withdraw(&self, token: &str) {
        if let Ok(mut entries) = self.entries.write() {
            entries.remove(token);
        }
    }
}

impl Exposures for StaticExposures {
    fn resolve(&self, token: &str) -> Option<Exposed> {
        self.entries.read().ok()?.get(token).cloned()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use gfeh_core::{NodeId, PartitionId};

    fn exposed() -> Exposed {
        Exposed {
            // One partition per call would make two `exposed()` values unequal, which
            // says nothing about the table under test.
            node: NodeRef::Id(PartitionId(uuid_nil()), NodeId(7)),
            filename: None,
            enabled: true,
        }
    }

    /// A fixed partition, so equality compares what the test means to compare.
    fn uuid_nil() -> uuid::Uuid {
        uuid::Uuid::nil()
    }

    #[test]
    fn a_published_token_resolves_and_an_unknown_one_does_not() {
        let table = StaticExposures::new().with("tok", exposed());
        assert_eq!(table.resolve("tok"), Some(exposed()));
        assert_eq!(table.resolve("other"), None);
    }

    #[test]
    fn disabling_keeps_the_token_and_clears_the_link() {
        let table = StaticExposures::new().with("tok", exposed());
        table.set_enabled("tok", false);
        assert_eq!(table.resolve("tok").map(|e| e.enabled), Some(false));

        table.set_enabled("tok", true);
        assert_eq!(table.resolve("tok").map(|e| e.enabled), Some(true));
    }

    #[test]
    fn withdrawing_removes_it() {
        let table = StaticExposures::new().with("tok", exposed());
        table.withdraw("tok");
        assert_eq!(table.resolve("tok"), None);
    }
}