indieweb 0.10.0

A collection of utilities for working with the IndieWeb.
Documentation
//! Registry for Micropub extensions.
//!
//! The [`ExtensionRegistry`] provides a unified interface for discovering and
//! managing Micropub extensions, combining compile-time feature detection with
//! runtime extension additions.

/// A registry for Micropub extensions.
///
/// Combines compile-time detected extensions (via feature flags) with
/// runtime additions for custom extensions.
///
/// # Example
///
/// ```
/// # use indieweb::standards::micropub::extension::ExtensionRegistry;
/// let registry = ExtensionRegistry::new();
/// let extensions = registry.extensions();
///
/// // Add a custom extension at runtime
/// let mut registry = ExtensionRegistry::new();
/// registry.add("custom-extension".to_string());
/// ```
#[derive(Debug, Clone)]
pub struct ExtensionRegistry {
    /// Extensions enabled at compile time via feature flags.
    built_in: Vec<String>,
    /// Extensions added at runtime.
    custom: Vec<String>,
}

impl ExtensionRegistry {
    /// Creates a new registry with extensions detected from enabled features.
    ///
    /// Extensions are automatically detected based on the `experimental_*`
    /// feature flags enabled at compile time.
    pub fn new() -> Self {
        let mut built_in = Vec::new();

        #[cfg(feature = "experimental_batch")]
        built_in.push("batch".to_string());

        #[cfg(feature = "experimental_publish_delay")]
        built_in.push("publish-delay".to_string());

        #[cfg(feature = "experimental_media_query")]
        built_in.push("media-query".to_string());

        #[cfg(feature = "experimental_channels")]
        built_in.push("channels".to_string());

        #[cfg(feature = "experimental_syndication")]
        built_in.push("syndication".to_string());

        #[cfg(feature = "experimental_relation")]
        built_in.push("relation".to_string());

        Self {
            built_in,
            custom: Vec::new(),
        }
    }

    /// Returns all registered extensions.
    ///
    /// This includes both compile-time detected extensions and runtime additions.
    /// The returned vector contains built-in extensions first, followed by custom ones.
    pub fn extensions(&self) -> Vec<String> {
        let mut exts = self.built_in.clone();
        exts.extend(self.custom.clone());
        exts
    }

    /// Adds a custom extension to the registry.
    ///
    /// The extension is only added if it's not already present in either
    /// the built-in or custom extensions list.
    ///
    /// # Arguments
    ///
    /// * `extension` - The name of the extension to add.
    pub fn add(&mut self, extension: String) {
        if !self.built_in.contains(&extension) && !self.custom.contains(&extension) {
            self.custom.push(extension);
        }
    }
}

impl Default for ExtensionRegistry {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_new_creates_empty_registry_without_features() {
        let registry = ExtensionRegistry::new();
        // Without any experimental features, built_in should be empty
        #[cfg(not(any(
            feature = "experimental_batch",
            feature = "experimental_publish_delay",
            feature = "experimental_media_query",
            feature = "experimental_channels",
            feature = "experimental_syndication",
            feature = "experimental_relation"
        )))]
        assert!(registry.extensions().is_empty());
    }

    #[test]
    fn test_default_is_same_as_new() {
        let registry_new = ExtensionRegistry::new();
        let registry_default = ExtensionRegistry::default();
        assert_eq!(registry_new.extensions(), registry_default.extensions());
    }

    #[test]
    fn test_add_extension() {
        let mut registry = ExtensionRegistry::new();
        registry.add("custom-extension".to_string());

        let extensions = registry.extensions();
        assert!(extensions.contains(&"custom-extension".to_string()));
    }

    #[test]
    fn test_add_prevents_duplicates() {
        let mut registry = ExtensionRegistry::new();
        registry.add("custom-extension".to_string());
        registry.add("custom-extension".to_string());
        registry.add("custom-extension".to_string());

        let extensions = registry.extensions();
        let count = extensions
            .iter()
            .filter(|e| *e == "custom-extension")
            .count();
        assert_eq!(count, 1);
    }

    #[test]
    fn test_add_does_not_duplicate_builtin() {
        let mut registry = ExtensionRegistry::new();

        #[cfg(feature = "experimental_batch")]
        {
            registry.add("batch".to_string());
            let extensions = registry.extensions();
            let count = extensions.iter().filter(|e| *e == "batch").count();
            assert_eq!(count, 1, "batch should only appear once");
        }

        #[cfg(not(feature = "experimental_batch"))]
        {
            // Without the feature, we can add "batch" as custom
            registry.add("batch".to_string());
            let extensions = registry.extensions();
            assert!(extensions.contains(&"batch".to_string()));
        }
    }

    #[test]
    fn test_extensions_returns_built_in_first() {
        let mut registry = ExtensionRegistry::new();
        registry.add("zzz-custom".to_string());
        registry.add("aaa-custom".to_string());

        let extensions = registry.extensions();

        // Custom extensions come after built-in
        let custom_positions: Vec<_> = extensions
            .iter()
            .enumerate()
            .filter(|(_, e)| e.ends_with("-custom"))
            .map(|(i, _)| i)
            .collect();

        // All custom extensions should be at the end (when there are built-in extensions)
        #[cfg(any(
            feature = "experimental_batch",
            feature = "experimental_publish_delay",
            feature = "experimental_media_query",
            feature = "experimental_channels",
            feature = "experimental_syndication",
            feature = "experimental_relation"
        ))]
        {
            // Count built-in extensions from the extensions list
            let builtin_count = extensions
                .iter()
                .filter(|e| !e.ends_with("-custom"))
                .count();
            for pos in custom_positions {
                assert!(
                    pos >= builtin_count,
                    "Custom extension should come after built-in"
                );
            }
        }
    }

    #[test]
    fn test_multiple_custom_extensions() {
        let mut registry = ExtensionRegistry::new();
        registry.add("ext-one".to_string());
        registry.add("ext-two".to_string());
        registry.add("ext-three".to_string());

        let extensions = registry.extensions();
        assert!(extensions.contains(&"ext-one".to_string()));
        assert!(extensions.contains(&"ext-two".to_string()));
        assert!(extensions.contains(&"ext-three".to_string()));
    }

    #[test]
    fn test_clone() {
        let mut registry = ExtensionRegistry::new();
        registry.add("cloned-ext".to_string());

        let cloned = registry.clone();
        assert_eq!(registry.extensions(), cloned.extensions());
    }

    #[test]
    #[cfg(feature = "experimental_batch")]
    fn test_batch_feature_detected() {
        let registry = ExtensionRegistry::new();
        let extensions = registry.extensions();
        assert!(
            extensions.contains(&"batch".to_string()),
            "batch extension should be detected with experimental_batch feature"
        );
    }

    #[test]
    #[cfg(feature = "experimental_publish_delay")]
    fn test_publish_delay_feature_detected() {
        let registry = ExtensionRegistry::new();
        let extensions = registry.extensions();
        assert!(
            extensions.contains(&"publish-delay".to_string()),
            "publish-delay extension should be detected with experimental_publish_delay feature"
        );
    }

    #[test]
    #[cfg(feature = "experimental_media_query")]
    fn test_media_query_feature_detected() {
        let registry = ExtensionRegistry::new();
        let extensions = registry.extensions();
        assert!(
            extensions.contains(&"media-query".to_string()),
            "media-query extension should be detected with experimental_media_query feature"
        );
    }

    #[test]
    #[cfg(feature = "experimental_channels")]
    fn test_channels_feature_detected() {
        let registry = ExtensionRegistry::new();
        let extensions = registry.extensions();
        assert!(
            extensions.contains(&"channels".to_string()),
            "channels extension should be detected with experimental_channels feature"
        );
    }

    #[test]
    #[cfg(feature = "experimental_syndication")]
    fn test_syndication_feature_detected() {
        let registry = ExtensionRegistry::new();
        let extensions = registry.extensions();
        assert!(
            extensions.contains(&"syndication".to_string()),
            "syndication extension should be detected with experimental_syndication feature"
        );
    }

    #[test]
    #[cfg(feature = "experimental_relation")]
    fn test_relation_feature_detected() {
        let registry = ExtensionRegistry::new();
        let extensions = registry.extensions();
        assert!(
            extensions.contains(&"relation".to_string()),
            "relation extension should be detected with experimental_relation feature"
        );
    }
}