use serde_json::{json, Value as Json};
#[derive(Debug, Clone, PartialEq)]
pub enum FilterError {
PropertyNotString { op: String },
}
impl std::fmt::Display for FilterError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FilterError::PropertyNotString { op } => write!(
f,
"legacy filter operator {op:?} expects a string property name"
),
}
}
}
impl std::error::Error for FilterError {}
pub fn is_expression_filter(filter: &Json) -> bool {
if filter.is_boolean() {
return true;
}
let Some(arr) = filter.as_array() else {
return false;
};
if arr.is_empty() {
return false;
}
let op = arr[0].as_str();
let is_string = |v: Option<&Json>| v.is_some_and(Json::is_string);
let is_array = |v: Option<&Json>| v.is_some_and(Json::is_array);
match op {
Some("has") => arr.len() >= 2 && !matches!(arr[1].as_str(), Some("$id") | Some("$type")),
Some("in") => arr.len() >= 3 && (!is_string(arr.get(1)) || is_array(arr.get(2))),
Some("!in") | Some("!has") => false,
Some("==") | Some("!=") | Some(">") | Some(">=") | Some("<") | Some("<=") => {
arr.len() != 3 || is_array(arr.get(1)) || is_array(arr.get(2))
}
Some("none") => {
for f in &arr[1..] {
if f.is_boolean() {
continue;
}
if is_expression_filter(f) {
return true;
}
}
false
}
Some("any") | Some("all") => {
let mut has_legacy = false;
for f in &arr[1..] {
if f.is_boolean() {
continue;
}
if is_expression_filter(f) {
return true;
}
has_legacy = true;
}
!has_legacy
}
_ => true,
}
}
pub fn convert_legacy_filter(filter: &Json) -> Result<Json, FilterError> {
let mut expected = ExpectedTypes::new();
convert(filter, &mut expected)
}
struct ExpectedTypes {
entries: Vec<(String, &'static str)>,
}
impl ExpectedTypes {
fn new() -> ExpectedTypes {
ExpectedTypes {
entries: Vec::new(),
}
}
fn set(&mut self, property: &str, ty: &'static str) {
if let Some(slot) = self.entries.iter_mut().find(|(k, _)| k == property) {
slot.1 = ty;
} else {
self.entries.push((property.to_string(), ty));
}
}
}
fn convert(filter: &Json, expected: &mut ExpectedTypes) -> Result<Json, FilterError> {
if is_expression_filter(filter) {
return Ok(filter.clone());
}
if filter.is_null() {
return Ok(json!(true));
}
let Some(arr) = filter.as_array() else {
return Ok(json!(true));
};
let op = arr[0].as_str();
if arr.len() <= 1 {
return Ok(json!(op != Some("any")));
}
match op {
Some(cmp @ ("==" | "!=" | "<" | ">" | "<=" | ">=")) => {
convert_comparison_op(&arr[1], &arr[2], cmp, expected)
}
Some("any") => {
let mut children = vec![json!("any")];
for f in &arr[1..] {
let mut types = ExpectedTypes::new();
let child = convert(f, &mut types)?;
let checks = runtime_type_checks(&types);
if checks == json!(true) {
children.push(child);
} else {
children.push(json!(["case", checks, child, false]));
}
}
Ok(Json::Array(children))
}
Some("all") => {
let mut children = Vec::with_capacity(arr.len() - 1);
for f in &arr[1..] {
children.push(convert(f, expected)?);
}
if children.len() > 1 {
let mut out = vec![json!("all")];
out.extend(children);
Ok(Json::Array(out))
} else {
Ok(children.into_iter().next().unwrap())
}
}
Some("none") => {
let mut any = vec![json!("any")];
any.extend(arr[1..].iter().cloned());
let mut types = ExpectedTypes::new();
let inner = convert(&Json::Array(any), &mut types)?;
Ok(json!(["!", inner]))
}
Some("in") => convert_in_op(&arr[1], &arr[2..], false),
Some("!in") => convert_in_op(&arr[1], &arr[2..], true),
Some("has") => convert_has_op(&arr[1]),
Some("!has") => Ok(json!(["!", convert_has_op(&arr[1])?])),
_ => Ok(json!(true)),
}
}
fn runtime_type_checks(expected: &ExpectedTypes) -> Json {
let mut conditions: Vec<Json> = Vec::new();
for (property, ty) in &expected.entries {
let get = if property == "$id" {
json!(["id"])
} else {
json!(["get", property])
};
conditions.push(json!(["==", ["typeof", get], ty]));
}
match conditions.len() {
0 => json!(true),
1 => conditions.into_iter().next().unwrap(),
_ => {
let mut out = vec![json!("all")];
out.extend(conditions);
Json::Array(out)
}
}
}
fn convert_comparison_op(
property: &Json,
value: &Json,
op: &str,
expected: &mut ExpectedTypes,
) -> Result<Json, FilterError> {
if property.as_str() == Some("$type") {
return Ok(json!([op, ["geometry-type"], value]));
}
let is_id = property.as_str() == Some("$id");
let get = if is_id {
json!(["id"])
} else {
json!(["get", property_str(property, op)?])
};
if !value.is_null() {
let key = if is_id {
"$id"
} else {
property_str(property, op)?
};
expected.set(key, js_typeof(value));
}
if op == "==" && !is_id && value.is_null() {
let p = property_str(property, op)?;
return Ok(json!(["all", ["has", p], ["==", get, Json::Null]]));
}
if op == "!=" && !is_id && value.is_null() {
let p = property_str(property, op)?;
return Ok(json!(["any", ["!", ["has", p]], ["!=", get, Json::Null]]));
}
Ok(json!([op, get, value]))
}
fn convert_in_op(property: &Json, values: &[Json], negate: bool) -> Result<Json, FilterError> {
if values.is_empty() {
return Ok(json!(negate));
}
let op = if negate { "!in" } else { "in" };
let get = match property.as_str() {
Some("$type") => json!(["geometry-type"]),
Some("$id") => json!(["id"]),
_ => json!(["get", property_str(property, op)?]),
};
let type0 = js_typeof(&values[0]);
let uniform = values.iter().all(|v| js_typeof(v) == type0);
if uniform && (type0 == "string" || type0 == "number") {
let unique = sort_and_dedupe(values);
return Ok(json!(["match", get, unique, !negate, negate]));
}
let (combiner, cmp) = if negate { ("all", "!=") } else { ("any", "==") };
let mut out = vec![json!(combiner)];
for v in values {
out.push(json!([cmp, get, v]));
}
Ok(Json::Array(out))
}
fn convert_has_op(property: &Json) -> Result<Json, FilterError> {
match property.as_str() {
Some("$type") => Ok(json!(true)),
Some("$id") => Ok(json!(["!=", ["id"], Json::Null])),
_ => Ok(json!(["has", property_str(property, "has")?])),
}
}
fn property_str<'a>(property: &'a Json, op: &str) -> Result<&'a str, FilterError> {
property
.as_str()
.ok_or_else(|| FilterError::PropertyNotString { op: op.to_string() })
}
fn js_typeof(v: &Json) -> &'static str {
match v {
Json::Bool(_) => "boolean",
Json::Number(_) => "number",
Json::String(_) => "string",
_ => "object",
}
}
fn sort_and_dedupe(values: &[Json]) -> Vec<Json> {
let mut sorted = values.to_vec();
sorted.sort_by_cached_key(js_string);
let mut unique: Vec<Json> = Vec::with_capacity(sorted.len());
for v in sorted {
if unique.last() != Some(&v) {
unique.push(v);
}
}
unique
}
fn js_string(v: &Json) -> String {
match v {
Json::String(s) => s.clone(),
Json::Number(n) => n.to_string(),
Json::Bool(b) => b.to_string(),
Json::Null => "null".to_string(),
other => other.to_string(),
}
}