unleash-edge 19.2.1

Unleash edge is a proxy for Unleash. It can return both evaluated feature toggles as well as the raw data from Unleash's client API
use dashmap::mapref::one::Ref;
use unleash_types::client_features::{ClientFeature, ClientFeatures};

use crate::types::EdgeToken;

pub type FeatureFilter = Box<dyn Fn(&ClientFeature) -> bool>;

#[derive(Default)]
pub(crate) struct FeatureFilterSet {
    filters: Vec<FeatureFilter>,
}

impl FeatureFilterSet {
    pub fn from(filter: FeatureFilter) -> Self {
        Self {
            filters: vec![filter],
        }
    }

    pub fn with_filter(mut self, filter: FeatureFilter) -> Self {
        self.filters.push(filter);
        self
    }

    pub fn apply(&self, feature: &ClientFeature) -> bool {
        self.filters.iter().all(|filter| filter(feature))
    }
}

fn filter_features(
    feature_cache: &Ref<'_, String, ClientFeatures>,
    filters: &FeatureFilterSet,
) -> Vec<ClientFeature> {
    feature_cache
        .features
        .iter()
        .filter(|feature| filters.apply(feature))
        .cloned()
        .collect::<Vec<ClientFeature>>()
}

pub(crate) fn filter_client_features(
    feature_cache: &Ref<'_, String, ClientFeatures>,
    filters: &FeatureFilterSet,
) -> ClientFeatures {
    ClientFeatures {
        features: filter_features(feature_cache, filters),
        segments: feature_cache.segments.clone(),
        query: feature_cache.query.clone(),
        version: feature_cache.version,
    }
}

pub(crate) fn name_prefix_filter(name_prefix: String) -> FeatureFilter {
    Box::new(move |f| f.name.starts_with(&name_prefix))
}

pub(crate) fn name_match_filter(name_prefix: String) -> FeatureFilter {
    Box::new(move |f| f.name.starts_with(&name_prefix))
}

pub(crate) fn project_filter(token: &EdgeToken) -> FeatureFilter {
    let token = token.clone();
    Box::new(move |feature| {
        if let Some(feature_project) = &feature.project {
            token.projects.is_empty()
                || token.projects.contains(&"*".to_string())
                || token.projects.contains(feature_project)
        } else {
            false
        }
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use dashmap::DashMap;
    use unleash_types::client_features::{ClientFeature, ClientFeatures};

    #[test]
    pub fn filter_features_applies_filters() {
        let feature_name = "some-feature".to_string();

        let client_features = ClientFeatures {
            version: 0,
            features: vec![ClientFeature {
                enabled: true,
                ..ClientFeature::default()
            }],
            query: None,
            segments: None,
        };

        let map = DashMap::default();
        map.insert(feature_name.clone(), client_features.clone());

        let features = map.get(&feature_name).unwrap();
        let filter_for_enabled = FeatureFilterSet::from(Box::new(|f| f.enabled));
        let enabled_features = filter_features(&features, &filter_for_enabled);

        let features = map.get(&feature_name).unwrap();
        let filter_for_disabled = FeatureFilterSet::from(Box::new(|f| !f.enabled));
        let disabled_features = filter_features(&features, &filter_for_disabled);

        assert_eq!(enabled_features[0].name, client_features.features[0].name);

        assert!(disabled_features.is_empty());
    }

    #[test]
    pub fn chaining_filters_applies_all_filters() {
        let client_features = ClientFeatures {
            version: 0,
            features: vec![
                ClientFeature {
                    name: "feature-one".to_string(),
                    enabled: true,
                    impression_data: Some(false),
                    ..ClientFeature::default()
                },
                ClientFeature {
                    name: "feature-two".to_string(),
                    enabled: false,
                    impression_data: Some(true),
                    ..ClientFeature::default()
                },
                ClientFeature {
                    name: "feature-three".to_string(),
                    enabled: true,
                    impression_data: Some(true),
                    ..ClientFeature::default()
                },
            ],
            query: None,
            segments: None,
        };

        let map = DashMap::default();
        let map_key = "some-key".to_string();

        map.insert(map_key.clone(), client_features);
        let features = map.get(&map_key).unwrap();

        let chained_filter = FeatureFilterSet::from(Box::new(|f| f.enabled))
            .with_filter(Box::new(|f| f.impression_data.unwrap_or(false)));
        let enabled_features = filter_features(&features, &chained_filter);

        assert_eq!(enabled_features[0].name, "feature-three".to_string());
    }

    #[test]
    fn name_prefix_filter_filters_by_prefix() {
        let client_features = ClientFeatures {
            version: 0,
            features: vec![
                ClientFeature {
                    name: "feature-one".to_string(),
                    ..ClientFeature::default()
                },
                ClientFeature {
                    name: "feature-two".to_string(),
                    ..ClientFeature::default()
                },
                ClientFeature {
                    name: "feature-three".to_string(),
                    ..ClientFeature::default()
                },
            ],
            query: None,
            segments: None,
        };

        let map = DashMap::default();
        let map_key = "some-feature".to_string();

        map.insert(map_key.clone(), client_features);
        let features = map.get(&map_key).unwrap();

        let filter = FeatureFilterSet::from(name_prefix_filter("feature-".to_string()));
        let filtered_features = filter_features(&features, &filter);

        assert_eq!(filtered_features.len(), 3);

        let filter = FeatureFilterSet::from(name_prefix_filter("feature-t".to_string()));
        let filtered_features = filter_features(&features, &filter);

        assert_eq!(filtered_features.len(), 2);

        let filter = FeatureFilterSet::from(name_prefix_filter("feature-o".to_string()));
        let filtered_features = filter_features(&features, &filter);

        assert_eq!(filtered_features.len(), 1);

        let filter = FeatureFilterSet::from(name_prefix_filter("feature-four".to_string()));
        let filtered_features = filter_features(&features, &filter);

        assert_eq!(filtered_features.len(), 0);
    }

    #[test]
    fn project_filter_filters_on_project_tokens() {
        let client_features = ClientFeatures {
            version: 0,
            features: vec![
                ClientFeature {
                    name: "feature-one".to_string(),
                    project: Some("default".to_string()),
                    ..ClientFeature::default()
                },
                ClientFeature {
                    name: "feature-two".to_string(),
                    project: Some("default".to_string()),
                    ..ClientFeature::default()
                },
                ClientFeature {
                    name: "feature-three".to_string(),
                    project: Some("not-default".to_string()),
                    ..ClientFeature::default()
                },
            ],
            query: None,
            segments: None,
        };

        let map = DashMap::default();
        let map_key = "some-key".to_string();

        map.insert(map_key.clone(), client_features);
        let features = map.get(&map_key).unwrap();

        let token = EdgeToken {
            projects: vec!["default".to_string()],
            ..Default::default()
        };

        let filter = FeatureFilterSet::from(project_filter(&token));
        let filtered_features = filter_features(&features, &filter);

        assert_eq!(filtered_features.len(), 2);
        assert_eq!(filtered_features[0].name, "feature-one".to_string());
        assert_eq!(filtered_features[1].name, "feature-two".to_string());
    }
}