statsig-rust 0.19.1-beta.2604130314

Statsig Rust SDK for usage in multi-user server environments.
Documentation
use super::target_app_id_utils::should_filter_spec_for_app;
use crate::{
    evaluation::{
        evaluator::{Evaluator, SpecType},
        evaluator_context::EvaluatorContext,
        evaluator_result::EvaluatorResult,
    },
    gcir::gcir_formatter::GCIRHashable,
    hashing::{self, HashUtil},
    interned_string::InternedString,
    specs_response::{spec_types::Spec, specs_hash_map::SpecsHashMap},
    ClientInitResponseOptions, HashAlgorithm, SecondaryExposure, StatsigErr,
};
use std::collections::{HashMap, HashSet};

pub(crate) fn gcir_process_iter<T: GCIRHashable>(
    context: &mut EvaluatorContext,
    options: &ClientInitResponseOptions,
    sec_expo_hash_memo: &mut HashMap<InternedString, InternedString>,
    specs_map: &SpecsHashMap,
    get_spec_type: impl Fn(&Spec) -> SpecType,
    mut evaluation_factory: impl FnMut(&str, &str, &mut EvaluatorContext) -> T,
) -> Result<HashMap<String, T>, StatsigErr> {
    let mut results = HashMap::new();
    let mut hashes = Vec::new();
    let mut keys = specs_map.keys().cloned().collect::<Vec<_>>();
    if options.previous_response_hash.is_some() {
        keys.sort_by(|a, b| a.as_str().cmp(b.as_str()));
    }
    for name in keys {
        let spec_ptr = match specs_map.get(&name) {
            Some(s) => s,
            None => continue,
        };
        let spec = spec_ptr.as_spec_ref();
        if spec.entity == "segment" || spec.entity == "holdout" {
            continue;
        }

        if should_filter_entity(spec, name.as_str(), options) {
            continue;
        }

        if should_filter_spec_for_app(spec, &context.app_id, &options.client_sdk_key) {
            continue;
        }

        context.reset_result();

        let spec_type = get_spec_type(spec);
        Evaluator::evaluate(context, name.as_str(), &spec_type)?;

        if options.remove_default_value_gates.unwrap_or(false)
            && spec.entity == "feature_gate"
            && context.result.rule_id.as_deref() == Some("default")
            && !context.result.bool_value
            && context.result.secondary_exposures.is_empty()
        {
            continue;
        }

        if spec.entity == "experiment"
            && options.remove_experiments_in_layers.unwrap_or(false)
            && context.result.is_in_layer
        {
            continue;
        }

        let hashed_name = context
            .hashing
            .hash(name.as_str(), options.get_hash_algorithm());
        hash_secondary_exposures(
            &mut context.result,
            context.hashing,
            options.get_hash_algorithm(),
            sec_expo_hash_memo,
        );

        let eval = evaluation_factory(&spec.entity, &hashed_name, context);

        if options.previous_response_hash.is_some() {
            hashes.push(eval.create_hash(&name));
        }
        results.insert(hashed_name, eval);
    }

    if options.previous_response_hash.is_some() {
        context.gcir_hashes.push(hashing::hash_one(hashes));
    }

    Ok(results)
}

fn should_filter_entity(spec: &Spec, name: &str, options: &ClientInitResponseOptions) -> bool {
    match spec.entity.as_str() {
        "feature_gate" => options
            .feature_gate_filter
            .as_ref()
            .is_some_and(|f| !f.contains(name)),
        "experiment" => options
            .experiment_filter
            .as_ref()
            .is_some_and(|f| !f.contains(name)),
        "dynamic_config" => options
            .dynamic_config_filter
            .as_ref()
            .is_some_and(|f| !f.contains(name)),
        "layer" => options
            .layer_filter
            .as_ref()
            .is_some_and(|f| !f.contains(name)),
        _ => false,
    }
}

pub fn hash_secondary_exposures(
    result: &mut EvaluatorResult,
    hashing: &HashUtil,
    hash_algorithm: &HashAlgorithm,
    memo: &mut HashMap<InternedString, InternedString>,
) {
    fn loop_filter_n_hash(
        exposures: &mut Vec<SecondaryExposure>,
        hashing: &HashUtil,
        hash_algorithm: &HashAlgorithm,
        memo: &mut HashMap<InternedString, InternedString>,
    ) {
        let mut seen = HashSet::<String>::with_capacity(exposures.len());
        exposures.retain_mut(|expo| {
            let expo_key = expo.get_dedupe_key();
            if seen.contains(&expo_key) {
                return false;
            }
            seen.insert(expo_key);

            match memo.get(&expo.gate) {
                Some(hash) => {
                    expo.gate = hash.clone();
                }
                None => {
                    let hash = hashing.hash(&expo.gate, hash_algorithm);
                    let interned_hash = InternedString::from_string(hash);
                    let old = std::mem::replace(&mut expo.gate, interned_hash.clone());
                    memo.insert(old, interned_hash);
                }
            }
            true
        });
    }

    if !result.secondary_exposures.is_empty() {
        loop_filter_n_hash(
            &mut result.secondary_exposures,
            hashing,
            hash_algorithm,
            memo,
        );
    }

    if let Some(undelegated_secondary_exposures) = result.undelegated_secondary_exposures.as_mut() {
        loop_filter_n_hash(
            undelegated_secondary_exposures,
            hashing,
            hash_algorithm,
            memo,
        );
    }
}