growthbook-rust 0.1.1

Official Growthbook Rust SDK
Documentation
use std::hash::Hasher;

use hashers::fnv::FNV1aHasher32;

pub enum HashCodeVersion {
    V1,
    V2,
    Invalid,
}

impl From<i64> for HashCodeVersion {
    fn from(value: i64) -> Self {
        match value {
            1 => Self::V1,
            2 => Self::V2,
            _ => Self::Invalid,
        }
    }
}

impl From<Option<i64>> for HashCodeVersion {
    fn from(option: Option<i64>) -> Self {
        match option {
            None => Self::V1,
            Some(value) => Self::from(value),
        }
    }
}

pub struct HashCode;

impl HashCode {
    pub fn hash_code(
        input: &str,
        seed: &str,
        version: HashCodeVersion,
    ) -> Option<f32> {
        match version {
            HashCodeVersion::V1 => Some(Self::hash_v1(input, seed)),
            HashCodeVersion::V2 => Some(Self::hash_v2(input, seed)),
            _ => None,
        }
    }

    fn hash_v1(
        input: &str,
        seed: &str,
    ) -> f32 {
        let concatenated = format!("{}{}", input, seed);
        let hash_value = Self::fnv1a_32(concatenated.as_bytes());
        let remainder = hash_value % 1000;
        remainder as f32 / 1000.0
    }

    fn hash_v2(
        input: &str,
        seed: &str,
    ) -> f32 {
        let concatenated = format!("{}{}", seed, input);
        let first = Self::fnv1a_32(concatenated.as_bytes());

        let first_as_string = format!("{}", first);
        let second = Self::fnv1a_32(first_as_string.as_bytes());

        let remainder = (second as u64) % 10000;

        remainder as f32 / 10000.0
    }

    fn fnv1a_32(data: &[u8]) -> u32 {
        let mut hasher = FNV1aHasher32::default();
        hasher.write(data);
        hasher.finish() as u32
    }
}

#[cfg(test)]
mod test {
    use std::fs;

    use serde::Deserialize;
    use serde_json::Value;

    use crate::hash::{HashCode, HashCodeVersion};

    #[tokio::test]
    async fn evaluate_hashes() -> Result<(), Box<dyn std::error::Error>> {
        let cases = Cases::new();

        for value in cases.hash {
            let eval_hash = EvalHash::new(value);
            let result = HashCode::hash_code(&eval_hash.value, &eval_hash.seed, HashCodeVersion::from(eval_hash.version));
            if result != eval_hash.result {
                panic!(
                    "EvalHash failed: value={} seed={} version={} expected={:?} result={:?}",
                    eval_hash.value, eval_hash.seed, eval_hash.version, eval_hash.result, result
                )
            }
        }

        Ok(())
    }

    #[derive(Deserialize, Clone)]
    #[serde(rename_all = "camelCase")]
    struct Cases {
        hash: Vec<Value>,
    }

    #[derive(Deserialize, Clone)]
    #[serde(rename_all = "camelCase")]
    pub struct EvalHash {
        seed: String,
        value: String,
        version: i64,
        result: Option<f32>,
    }

    impl EvalHash {
        fn new(value: Value) -> Self {
            let array = value.as_array().expect("Failed to convert to array");
            Self {
                seed: array[0].as_str().expect("Failed to convert do str").to_string(),
                value: array[1].as_str().expect("Failed to convert do str").to_string(),
                version: array[2].as_i64().expect("Failed to convert to i64"),
                result: array[3].as_f64().map(|it| it as f32),
            }
        }
    }

    impl Cases {
        pub fn new() -> Self {
            let contents = fs::read_to_string("./tests/all_cases.json").expect("Should have been able to read the file");

            serde_json::from_str(&contents).expect("Failed to create cases")
        }
    }
}