unleash-edge 19.8.2

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 crate::delta_cache::DeltaCache;
use crate::filters::FeatureFilterSet;
use unleash_types::client_features::{ClientFeature, ClientFeaturesDelta, DeltaEvent};

pub type DeltaFilter = Box<dyn Fn(&DeltaEvent) -> bool>;

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

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

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

pub(crate) fn revision_id_filter(required_revision_id: u32) -> DeltaFilter {
    Box::new(move |event| event.get_event_id() > required_revision_id)
}

pub(crate) fn projects_filter(projects: Vec<String>) -> DeltaFilter {
    let all_projects = projects.contains(&"*".to_string());
    Box::new(move |event| match event {
        DeltaEvent::FeatureUpdated { feature, .. } => {
            if let Some(feature_project) = &feature.project {
                all_projects || projects.contains(feature_project)
            } else {
                false
            }
        }
        DeltaEvent::FeatureRemoved { project, .. } => all_projects || projects.contains(project),
        _ => true,
    })
}

pub(crate) fn name_prefix_filter(name_prefix: Option<String>) -> DeltaFilter {
    Box::new(move |event| match (event, &name_prefix) {
        (DeltaEvent::FeatureUpdated { feature, .. }, Some(prefix)) => {
            feature.name.starts_with(prefix)
        }
        (DeltaEvent::FeatureRemoved { feature_name, .. }, Some(prefix)) => {
            feature_name.starts_with(prefix)
        }
        (_, None) => true,
        _ => true,
    })
}

pub(crate) fn is_segment_event_filter() -> DeltaFilter {
    Box::new(|event| {
        matches!(
            event,
            DeltaEvent::SegmentUpdated { .. } | DeltaEvent::SegmentRemoved { .. }
        )
    })
}

pub(crate) fn combined_filter(
    required_revision_id: u32,
    projects: Vec<String>,
    name_prefix: Option<String>,
) -> DeltaFilter {
    let revision_filter = revision_id_filter(required_revision_id);
    let projects_filter = projects_filter(projects);
    let name_filter = name_prefix_filter(name_prefix);
    let segment_filter = is_segment_event_filter();

    Box::new(move |event| {
        (segment_filter(event) && revision_filter(event))
            || (name_filter(event) && projects_filter(event) && revision_filter(event))
    })
}

fn filter_deltas(
    delta_cache: &DeltaCache,
    feature_filters: &FeatureFilterSet,
    delta_filters: &DeltaFilterSet,
    revision: u32,
) -> Vec<DeltaEvent> {
    let hydration_event = delta_cache.get_hydration_event();
    if revision > hydration_event.event_id {
        return vec![];
    }
    if revision > 0 && delta_cache.has_revision(revision) {
        let events = delta_cache.get_events().clone();
        events
            .iter()
            .filter(|delta| delta_filters.apply(delta))
            .cloned()
            .collect::<Vec<DeltaEvent>>()
    } else {
        let hydration_event = delta_cache.get_hydration_event().clone();
        vec![DeltaEvent::Hydration {
            event_id: hydration_event.event_id,
            segments: hydration_event.segments,
            features: hydration_event
                .features
                .iter()
                .filter(|feature| feature_filters.apply(feature))
                .cloned()
                .collect::<Vec<ClientFeature>>(),
        }]
    }
}

pub(crate) fn filter_delta_events(
    delta_cache: &DeltaCache,
    feature_filters: &FeatureFilterSet,
    delta_filters: &DeltaFilterSet,
    revision: u32,
) -> ClientFeaturesDelta {
    ClientFeaturesDelta {
        events: filter_deltas(delta_cache, feature_filters, delta_filters, revision),
    }
}

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

    fn mock_events() -> Vec<DeltaEvent> {
        vec![
            DeltaEvent::FeatureUpdated {
                event_id: 1,
                feature: ClientFeature {
                    name: "test-feature".to_string(),
                    project: Some("project1".to_string()),
                    enabled: true,
                    ..Default::default()
                },
            },
            DeltaEvent::FeatureUpdated {
                event_id: 2,
                feature: ClientFeature {
                    name: "alpha-feature".to_string(),
                    project: Some("project2".to_string()),
                    enabled: true,
                    ..Default::default()
                },
            },
            DeltaEvent::FeatureRemoved {
                event_id: 3,
                feature_name: "beta-feature".to_string(),
                project: "project3".to_string(),
            },
            DeltaEvent::SegmentUpdated {
                event_id: 4,
                segment: Segment {
                    id: 0,
                    constraints: vec![],
                },
            },
            DeltaEvent::SegmentRemoved {
                event_id: 5,
                segment_id: 2,
            },
        ]
    }

    #[test]
    fn filters_events_based_on_event_id() {
        let events = mock_events();
        let delta_filters = DeltaFilterSet::default().with_filter(revision_id_filter(2));
        let filtered: Vec<_> = events
            .iter()
            .filter(|e| delta_filters.apply(e))
            .cloned()
            .collect();

        assert_eq!(filtered.len(), 3);
        assert!(matches!(
            filtered[0],
            DeltaEvent::FeatureRemoved { event_id: 3, .. }
        ));
        assert!(matches!(
            filtered[1],
            DeltaEvent::SegmentUpdated { event_id: 4, .. }
        ));
        assert!(matches!(
            filtered[2],
            DeltaEvent::SegmentRemoved { event_id: 5, .. }
        ));
    }

    #[test]
    fn allows_all_projects_when_wildcard_is_provided() {
        let events = mock_events();
        let delta_filters =
            DeltaFilterSet::default().with_filter(projects_filter(vec!["*".to_string()]));
        let filtered: Vec<_> = events
            .iter()
            .filter(|e| delta_filters.apply(e))
            .cloned()
            .collect();

        assert_eq!(filtered, events);
    }

    #[test]
    fn filters_by_name_prefix() {
        let events = mock_events();
        let delta_filters =
            DeltaFilterSet::default().with_filter(name_prefix_filter(Some("alpha".to_string())));
        let filtered: Vec<_> = events
            .iter()
            .filter(|e| delta_filters.apply(e))
            .cloned()
            .collect();

        assert_eq!(filtered.len(), 3);
        assert!(matches!(
            filtered[0],
            DeltaEvent::FeatureUpdated { event_id: 2, .. }
        ));
        assert!(matches!(
            filtered[1],
            DeltaEvent::SegmentUpdated { event_id: 4, .. }
        ));
        assert!(matches!(
            filtered[2],
            DeltaEvent::SegmentRemoved { event_id: 5, .. }
        ));
    }

    #[test]
    fn filters_by_project_list() {
        let events = mock_events();
        let delta_filters = DeltaFilterSet::default()
            .with_filter(projects_filter(vec!["project3".to_string()]))
            .with_filter(name_prefix_filter(Some("beta".to_string())));
        let filtered: Vec<_> = events
            .iter()
            .filter(|e| delta_filters.apply(e))
            .cloned()
            .collect();

        assert_eq!(filtered.len(), 3);
        assert!(matches!(
            filtered[0],
            DeltaEvent::FeatureRemoved { event_id: 3, .. }
        ));
        assert!(matches!(
            filtered[1],
            DeltaEvent::SegmentUpdated { event_id: 4, .. }
        ));
        assert!(matches!(
            filtered[2],
            DeltaEvent::SegmentRemoved { event_id: 5, .. }
        ));
    }
}