use super::*;
pub(super) const MAX_BUCKET_CANDIDATES: usize = 100_000;
impl Expression {
pub(crate) fn synthesize_context(
&self,
want: bool,
variable: &mut dyn FnMut(&str, bool) -> Option<JsonValue>,
lists: &mut dyn FnMut(&str) -> Option<Vec<JsonValue>>,
) -> Option<JsonValue> {
synthesize_bool(&self.cel_ast, want, variable, lists)
}
}
pub(super) fn synthesize_bool(
expr: &IdedExpr,
want: bool,
variable: &mut dyn FnMut(&str, bool) -> Option<JsonValue>,
lists: &mut dyn FnMut(&str) -> Option<Vec<JsonValue>>,
) -> Option<JsonValue> {
if let Some(Reference::Variable(id)) = cel_reference(expr) {
return variable(&id, want);
}
if let Some(path) = cel_context_path(expr) {
return context_with_path(&path, JsonValue::Bool(want));
}
match &expr.expr {
Expr::Literal(LiteralValue::Boolean(value)) => (**value == want).then(empty_context),
Expr::Call(call) => synthesize_call(call, want, variable, lists),
_ => None,
}
}
pub(super) fn synthesize_call(
call: &cel::common::ast::CallExpr,
want: bool,
variable: &mut dyn FnMut(&str, bool) -> Option<JsonValue>,
lists: &mut dyn FnMut(&str) -> Option<Vec<JsonValue>>,
) -> Option<JsonValue> {
match call.func_name.as_str() {
operators::LOGICAL_NOT => synthesize_bool(call.args.first()?, !want, variable, lists),
operators::LOGICAL_AND => synthesize_junction(&call.args, want, true, variable, lists),
operators::LOGICAL_OR => synthesize_junction(&call.args, want, false, variable, lists),
operators::EQUALS => synthesize_equality(&call.args, want, true),
operators::NOT_EQUALS => synthesize_equality(&call.args, want, false),
operators::LESS => synthesize_ordering(&call.args, false, false, want),
operators::LESS_EQUALS => synthesize_ordering(&call.args, false, true, want),
operators::GREATER => synthesize_ordering(&call.args, true, false, want),
operators::GREATER_EQUALS => synthesize_ordering(&call.args, true, true, want),
operators::IN => synthesize_membership(&call.args, want, lists),
"bucket" => synthesize_bucket(call, want),
_ => None,
}
}
pub(super) fn synthesize_junction(
args: &[IdedExpr],
want: bool,
is_and: bool,
variable: &mut dyn FnMut(&str, bool) -> Option<JsonValue>,
lists: &mut dyn FnMut(&str) -> Option<Vec<JsonValue>>,
) -> Option<JsonValue> {
if want == is_and {
let mut context = empty_context();
for arg in args {
merge_context(&mut context, synthesize_bool(arg, want, variable, lists)?)?;
}
Some(context)
} else {
args.iter()
.find_map(|arg| synthesize_bool(arg, want, variable, lists))
}
}
pub(super) fn synthesize_equality(args: &[IdedExpr], want: bool, equal: bool) -> Option<JsonValue> {
let (path, literal) = context_path_and_literal(args)?;
let value = if want == equal {
literal
} else {
alternative_value(&literal)
};
context_with_path(&path, value)
}
pub(super) fn synthesize_ordering(
args: &[IdedExpr],
is_greater: bool,
or_equal: bool,
want: bool,
) -> Option<JsonValue> {
if args.len() != 2 {
return None;
}
let (path, number, greater) = if let Some(path) = cel_context_path(&args[0]) {
(path, cel_number(&args[1])?, is_greater)
} else {
let path = cel_context_path(&args[1])?;
(path, cel_number(&args[0])?, !is_greater)
};
let value = ordering_value(greater, or_equal, want, &number)?;
context_with_path(&path, value)
}
pub(super) fn synthesize_membership(
args: &[IdedExpr],
want: bool,
lists: &mut dyn FnMut(&str) -> Option<Vec<JsonValue>>,
) -> Option<JsonValue> {
if args.len() != 2 {
return None;
}
let path = cel_context_path(&args[0])?;
let elements: Vec<JsonValue> = match &args[1].expr {
Expr::List(list) => list.elements.iter().filter_map(cel_literal_json).collect(),
_ => match cel_reference(&args[1]) {
Some(Reference::List(id)) => lists(&id)?,
_ => return None,
},
};
let value = if want {
elements.first()?.clone()
} else {
list_non_member(&elements)?
};
context_with_path(&path, value)
}
pub(super) fn synthesize_bucket(
call: &cel::common::ast::CallExpr,
want: bool,
) -> Option<JsonValue> {
if call.args.len() != 4 {
return None;
}
let path = cel_context_path(&call.args[0])?;
let salt = cel_string_literal(&call.args[1])?;
let start = cel_int_literal(&call.args[2])?;
let end = cel_int_literal(&call.args[3])?;
for index in 0..MAX_BUCKET_CANDIDATES {
let candidate = format!("bucket-fixture-{index:05}");
let bucket = i64::from(bucket_value(&salt, &JsonValue::String(candidate.clone())));
if (bucket >= start && bucket < end) == want {
return context_with_path(&path, JsonValue::String(candidate));
}
}
None
}
pub(super) fn context_path_and_literal(args: &[IdedExpr]) -> Option<(String, JsonValue)> {
if args.len() != 2 {
return None;
}
for (path_side, literal_side) in [(0, 1), (1, 0)] {
if let Some(path) = cel_context_path(&args[path_side])
&& let Some(literal) = cel_literal_json(&args[literal_side])
{
return Some((path, literal));
}
}
None
}
const FIXTURE_OTHER: &str = "fixture-other";
pub(super) fn alternative_value(value: &JsonValue) -> JsonValue {
match value {
JsonValue::String(text) if text == FIXTURE_OTHER => {
JsonValue::String(format!("{FIXTURE_OTHER}-2"))
}
JsonValue::String(_) => JsonValue::String(FIXTURE_OTHER.to_owned()),
JsonValue::Bool(flag) => JsonValue::Bool(!flag),
JsonValue::Number(_) => {
step_number(value, 1).unwrap_or_else(|| JsonValue::String("fixture-other".to_owned()))
}
_ => JsonValue::String("fixture-other".to_owned()),
}
}
pub(super) fn list_non_member(elements: &[JsonValue]) -> Option<JsonValue> {
if !elements.is_empty() && elements.iter().all(JsonValue::is_number) {
let max = elements
.iter()
.filter_map(JsonValue::as_i64)
.max()
.unwrap_or(0);
return Some(JsonValue::Number(Number::from(max + 1)));
}
[
JsonValue::String("fixture-not-in-list".to_owned()),
JsonValue::String("fixture-other".to_owned()),
JsonValue::Bool(false),
]
.into_iter()
.find(|candidate| {
elements
.iter()
.all(|element| !json_values_equal(element, candidate))
})
}
pub(super) fn ordering_value(
greater: bool,
or_equal: bool,
want: bool,
number: &JsonValue,
) -> Option<JsonValue> {
let delta: i64 = match (want, or_equal, greater) {
(true, true, _) => 0,
(true, false, true) => 1,
(true, false, false) => -1,
(false, true, true) => -1,
(false, true, false) => 1,
(false, false, _) => 0,
};
step_number(number, delta)
}
pub(super) fn step_number(number: &JsonValue, delta: i64) -> Option<JsonValue> {
if let Some(value) = number.as_i64() {
Some(JsonValue::Number(Number::from(value + delta)))
} else if let Some(value) = number.as_u64() {
let stepped = i128::from(value) + i128::from(delta);
u64::try_from(stepped)
.ok()
.map(|n| JsonValue::Number(Number::from(n)))
} else if let Some(value) = number.as_f64() {
Number::from_f64(value + delta as f64).map(JsonValue::Number)
} else {
None
}
}
pub(super) fn cel_number(expr: &IdedExpr) -> Option<JsonValue> {
let value = cel_literal_json(expr)?;
value.is_number().then_some(value)
}
pub(super) fn cel_literal_json(expr: &IdedExpr) -> Option<JsonValue> {
match &expr.expr {
Expr::Literal(literal) => literal_to_json(literal),
_ => None,
}
}
pub(super) fn literal_to_json(literal: &LiteralValue) -> Option<JsonValue> {
match literal {
LiteralValue::Boolean(value) => Some(JsonValue::Bool(**value)),
LiteralValue::Int(value) => Some(JsonValue::Number(Number::from(**value))),
LiteralValue::UInt(value) => Some(JsonValue::Number(Number::from(**value))),
LiteralValue::Double(value) => Number::from_f64(**value).map(JsonValue::Number),
LiteralValue::String(value) => Some(JsonValue::String(value.to_string())),
LiteralValue::Null | LiteralValue::Bytes(_) => None,
}
}
pub(super) fn cel_string_literal(expr: &IdedExpr) -> Option<String> {
match &expr.expr {
Expr::Literal(LiteralValue::String(value)) => Some(value.to_string()),
_ => None,
}
}
pub(super) fn cel_int_literal(expr: &IdedExpr) -> Option<i64> {
match &expr.expr {
Expr::Literal(LiteralValue::Int(value)) => Some(**value),
Expr::Literal(LiteralValue::UInt(value)) => i64::try_from(**value).ok(),
_ => None,
}
}
pub(crate) fn empty_context() -> JsonValue {
JsonValue::Object(serde_json::Map::new())
}
pub(crate) fn context_with_path(path: &str, value: JsonValue) -> Option<JsonValue> {
let mut root = serde_json::Map::new();
insert_context_path(&mut root, path, value)?;
Some(JsonValue::Object(root))
}
pub(super) fn insert_context_path(
object: &mut serde_json::Map<String, JsonValue>,
path: &str,
value: JsonValue,
) -> Option<()> {
let mut segments = path.split('.').peekable();
let mut current = object;
while let Some(segment) = segments.next() {
if segment.is_empty() {
return None;
}
if segments.peek().is_none() {
current.insert(segment.to_owned(), value);
return Some(());
}
let entry = current
.entry(segment.to_owned())
.or_insert_with(|| JsonValue::Object(serde_json::Map::new()));
current = entry.as_object_mut()?;
}
None
}
pub(crate) fn merge_context(target: &mut JsonValue, source: JsonValue) -> Option<()> {
let (Some(target), Some(source)) = (target.as_object_mut(), source.as_object()) else {
return None;
};
merge_context_objects(target, source)
}
pub(super) fn merge_context_objects(
target: &mut serde_json::Map<String, JsonValue>,
source: &serde_json::Map<String, JsonValue>,
) -> Option<()> {
for (key, value) in source {
match (target.get_mut(key), value) {
(Some(existing), JsonValue::Object(source_object)) if existing.is_object() => {
merge_context_objects(existing.as_object_mut()?, source_object)?;
}
(Some(existing), value) if existing != value => return None,
(Some(_), _) => {}
(None, value) => {
target.insert(key.clone(), value.clone());
}
}
}
Some(())
}