// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. #[cfg(feature = "stateful")] use std::sync::{Arc, Mutex}; #[cfg(feature = "stateful")] use anyhow::anyhow; #[cfg(feature = "stateful")] use firefox_versioning::compare::version_compare; use jexl_eval::Evaluator; use serde::Serialize; use serde_json::Value; #[cfg(feature = "stateful")] use crate::TargetingAttributes; #[cfg(feature = "stateful")] use crate::stateful::behavior::{EventQueryType, EventStore, query_event_store}; #[cfg(feature = "stateful")] use crate::stateful::gecko_prefs::{GeckoPrefStore, query_gecko_pref_store}; use crate::{NimbusError, Result}; #[derive(Clone)] pub struct NimbusTargetingHelper { pub(crate) context: Value, #[cfg(feature = "stateful")] pub(crate) event_store: Arc>, #[cfg(feature = "stateful")] pub(crate) gecko_pref_store: Option>, #[cfg(feature = "stateful")] pub(crate) targeting_attributes: Option, } impl NimbusTargetingHelper { pub fn new( context: C, #[cfg(feature = "stateful")] event_store: Arc>, #[cfg(feature = "stateful")] gecko_pref_store: Option>, ) -> Self { Self { context: serde_json::to_value(context).unwrap(), #[cfg(feature = "stateful")] event_store, #[cfg(feature = "stateful")] gecko_pref_store, #[cfg(feature = "stateful")] targeting_attributes: None, } } pub fn eval_jexl(&self, expr: String) -> Result { cfg_if::cfg_if! { if #[cfg(feature = "stateful")] { jexl_eval(&expr, &self.context, self.event_store.clone(), self.gecko_pref_store.clone()) } else { jexl_eval(&expr, &self.context) } } } #[cfg(feature = "stateful")] pub fn eval_jexl_debug(&self, expression: String) -> Result { let eval_result = jexl_eval_raw( &expression, &self.context, self.event_store.clone(), self.gecko_pref_store.clone(), ); let response = match eval_result { Ok(value) => { serde_json::json!({ "success": true, "result": value }) } Err(e) => { serde_json::json!({ "success": false, "error": e.to_string() }) } }; serde_json::to_string_pretty(&response).map_err(|e| { NimbusError::JSONError("Failed to serialize JEXL result".to_string(), e.to_string()) }) } pub fn evaluate_jexl_raw_value(&self, expr: &str) -> Result { cfg_if::cfg_if! { if #[cfg(feature = "stateful")] { jexl_eval_raw(expr, &self.context, self.event_store.clone(), self.gecko_pref_store.clone()) } else { jexl_eval_raw(expr, &self.context) } } } pub(crate) fn put(&self, key: &str, value: bool) -> Self { let context = if let Value::Object(map) = &self.context { let mut map = map.clone(); map.insert(key.to_string(), Value::Bool(value)); Value::Object(map) } else { self.context.clone() }; Self { context, #[cfg(feature = "stateful")] event_store: self.event_store.clone(), #[cfg(feature = "stateful")] gecko_pref_store: self.gecko_pref_store.clone(), #[cfg(feature = "stateful")] targeting_attributes: self.targeting_attributes.clone(), } } } pub fn jexl_eval_raw( expression_statement: &str, context: &Context, #[cfg(feature = "stateful")] event_store: Arc>, #[cfg(feature = "stateful")] gecko_pref_store: Option>, ) -> Result { let evaluator = Evaluator::new(); #[cfg(feature = "stateful")] let evaluator = evaluator .with_transform("versionCompare", |args| Ok(version_compare(args)?)) .with_transform("eventSum", |args| { Ok(query_event_store( event_store.clone(), EventQueryType::Sum, args, )?) }) .with_transform("eventCountNonZero", |args| { Ok(query_event_store( event_store.clone(), EventQueryType::CountNonZero, args, )?) }) .with_transform("eventAveragePerInterval", |args| { Ok(query_event_store( event_store.clone(), EventQueryType::AveragePerInterval, args, )?) }) .with_transform("eventAveragePerNonZeroInterval", |args| { Ok(query_event_store( event_store.clone(), EventQueryType::AveragePerNonZeroInterval, args, )?) }) .with_transform("eventLastSeen", |args| { Ok(query_event_store( event_store.clone(), EventQueryType::LastSeen, args, )?) }) .with_transform("preferenceIsUserSet", |args| { Ok(query_gecko_pref_store(gecko_pref_store.clone(), args)?) }) .with_transform("bucketSample", bucket_sample); evaluator .eval_in_context(expression_statement, context) .map_err(|err| NimbusError::EvaluationError(err.to_string())) } // This is the common entry point to JEXL evaluation. // The targeting attributes and additional context should have been merged and calculated before // getting here. // Any additional transforms should be added here. pub fn jexl_eval( expression_statement: &str, context: &Context, #[cfg(feature = "stateful")] event_store: Arc>, #[cfg(feature = "stateful")] gecko_pref_store: Option>, ) -> Result { let res = jexl_eval_raw( expression_statement, context, #[cfg(feature = "stateful")] event_store, #[cfg(feature = "stateful")] gecko_pref_store, )?; match res.as_bool() { Some(v) => Ok(v), None => Err(NimbusError::InvalidExpression), } } #[cfg(feature = "stateful")] fn bucket_sample(args: &[Value]) -> anyhow::Result { fn get_arg_as_u32(args: &[Value], idx: usize, name: &str) -> anyhow::Result { match args.get(idx) { None => Err(anyhow!("{} doesn't exist in jexl transform", name)), Some(Value::Number(n)) => { let n: f64 = if let Some(n) = n.as_u64() { n as f64 } else if let Some(n) = n.as_i64() { n as f64 } else if let Some(n) = n.as_f64() { n } else { unreachable!(); }; debug_assert!(n >= 0.0, "JEXL parser does not support negative values"); if n > u32::MAX as f64 { Err(anyhow!("{} is out of range", name)) } else { Ok(n as u32) } } Some(_) => Err(anyhow!("{} is not a number", name)), } } let input = args .first() .ok_or_else(|| anyhow!("input doesn't exist in jexl transform"))?; let start = get_arg_as_u32(args, 1, "start")?; let count = get_arg_as_u32(args, 2, "count")?; let total = get_arg_as_u32(args, 3, "total")?; let result = crate::sampling::bucket_sample(input, start, count, total)?; Ok(Value::Bool(result)) }