Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Changed

- Normalized WASM interface to match Python API ([#120](https://github.com/developmentseed/cql2-rs/pull/120))
- Renamed `CQL2` to `Expr` for consistency with Python
- Changed `Expr.matches()` and `Expr.reduce()` to accept JS objects instead of strings
- Changed `Expr.to_json()` to return JS objects instead of strings

## [0.4.2] - 2025-11-12

### Changed
Expand Down
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,10 @@ impl Cli {

pub fn run_inner(self) -> Result<()> {
if let Some(filter_path) = self.filter.as_ref() {
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::{
fs::File,
io::{BufRead, BufReader},
};
// Use self.input as the CQL2 expression
let expr_str = self.input.as_ref().ok_or_else(|| {
anyhow!("CQL2 expression required as positional argument when using --filter")
Expand Down
9 changes: 9 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@
{'op': '=', 'args': [{'property': 'landsat:scene_id'}, 'LC82030282019133LGN00']}
```

## Javascript

```js
> import { Expr } from 'cql2-wasm';
> const expr = new Expr("landsat:scene_id = 'LC82030282019133LGN00'");
> expr.to_json()
{'op': '=', 'args': [{'property': 'landsat:scene_id'}, 'LC82030282019133LGN00']}
```

## CLI

```shell
Expand Down
2 changes: 1 addition & 1 deletion docs/playground.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
</style>

<script type="module">
import init, { CQL2 } from '../pkg/cql2_wasm.js';
import init, { Expr as CQL2 } from '../pkg/cql2_wasm.js';

await init();
window.CQL2 = CQL2;
Expand Down
8 changes: 2 additions & 6 deletions src/duckdb.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
use crate::sql::func;
use crate::Error;
use crate::Expr;
use crate::ToSqlAst;
use sqlparser::ast::visit_expressions_mut;
use sqlparser::ast::Expr as SqlExpr;
use crate::{sql::func, Error, Expr, ToSqlAst};
use sqlparser::ast::{visit_expressions_mut, Expr as SqlExpr};
use std::ops::ControlFlow;

/// Traits for generating SQL for DuckDB with Spatial Extension
Expand Down
13 changes: 7 additions & 6 deletions src/expr.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
use crate::{geometry::spatial_op, temporal::temporal_op, Error, Geometry, Validator};
use geo_types::Geometry as GGeom;
use geo_types::{coord, Rect};
use geo_types::{coord, Geometry as GGeom, Rect};
use json_dotpath::DotPaths;
use like::Like;
use pg_escape::{quote_identifier, quote_literal};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashSet;
use std::fmt::Debug;
use std::ops::{Add, Deref};
use std::str::FromStr;
use std::{
collections::HashSet,
fmt::Debug,
ops::{Add, Deref},
str::FromStr,
};
use unaccent::unaccent;
use wkt::TryFromWkt;

Expand Down
14 changes: 6 additions & 8 deletions src/sql.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
use crate::Error;
use crate::Expr;
use crate::Geometry;
use crate::{Error, Expr, Geometry};
use pg_escape::quote_identifier;
use sqlparser::ast::DataType::{Date, Timestamp};
use sqlparser::ast::Expr::Value as ValExpr;
use sqlparser::ast::Expr::{Cast, Nested};
use sqlparser::ast::{
Array as SqlArray, BinaryOperator, CastKind, Expr as SqlExpr, FunctionArgumentList,
FunctionArguments, Ident, TimezoneInfo, Value,
Array as SqlArray, BinaryOperator, CastKind,
DataType::{Date, Timestamp},
Expr as SqlExpr,
Expr::{Cast, Nested, Value as ValExpr},
FunctionArgumentList, FunctionArguments, Ident, TimezoneInfo, Value,
};
use std::vec;

Expand Down
2 changes: 2 additions & 0 deletions wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ cql2 = { path = ".." }
wasm-bindgen = "0.2"
getrandom = { version = "0.3.3", features = ["wasm_js"] }
serde_json = "1.0"
serde-wasm-bindgen = "0.6"

[dependencies.web-sys]
version = "0.3.82"
features = ['Document', 'Element', 'HtmlElement', 'Node', 'Window']

[dev-dependencies]
wasm-bindgen-test = "0.3.34"
js-sys = "0.3"
29 changes: 15 additions & 14 deletions wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ pub fn parse_json(s: &str) -> Result<CQL2Expression, JsError> {
Ok(CQL2Expression(expr))
}

#[wasm_bindgen(js_name = CQL2)]
#[wasm_bindgen(js_name = Expr)]
pub struct CQL2Expression(cql2::Expr);

#[wasm_bindgen(js_class = CQL2)]
#[wasm_bindgen(js_class = Expr)]
impl CQL2Expression {
#[wasm_bindgen(constructor)]
pub fn new(v: String) -> Result<CQL2Expression, JsError> {
Expand All @@ -42,33 +42,34 @@ impl CQL2Expression {
/// Check if the expression matches the given item
///
/// # Arguments
/// * `item` - JSON string representing the item to match against
pub fn matches(&self, item: Option<String>) -> Result<bool, JsError> {
let value = if let Some(item_str) = item {
Some(serde_json::from_str(&item_str)?)
} else {
/// * `item` - JavaScript object representing the item to match against
pub fn matches(&self, item: JsValue) -> Result<bool, JsError> {
let value = if item.is_null() || item.is_undefined() {
None
} else {
Some(serde_wasm_bindgen::from_value(item)?)
};
Ok(self.0.clone().matches(value.as_ref())?)
}

/// Reduce the expression, optionally with an item context
///
/// # Arguments
/// * `item` - Optional JSON string representing the item context for reduction
pub fn reduce(&self, item: Option<String>) -> Result<CQL2Expression, JsError> {
let value = if let Some(item_str) = item {
Some(serde_json::from_str(&item_str)?)
} else {
/// * `item` - JavaScript object representing the item context for reduction
pub fn reduce(&self, item: JsValue) -> Result<CQL2Expression, JsError> {
let value = if item.is_null() || item.is_undefined() {
None
} else {
Some(serde_wasm_bindgen::from_value(item)?)
};
let r = self.0.clone().reduce(value.as_ref())?;
Ok(CQL2Expression(r))
}

pub fn to_json(&self) -> Result<String, JsError> {
pub fn to_json(&self) -> Result<JsValue, JsError> {
let r = self.0.to_json()?;
Ok(r)
let js_value = serde_wasm_bindgen::to_value(&r)?;
Ok(js_value)
}

pub fn to_json_pretty(&self) -> Result<String, JsError> {
Expand Down
25 changes: 18 additions & 7 deletions wasm/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#![cfg(target_arch = "wasm32")]

use cql2_wasm::{parse_json, parse_text, CQL2Expression};
use js_sys;
use wasm_bindgen::JsValue;
use wasm_bindgen_test::*;

#[wasm_bindgen_test]
Expand Down Expand Up @@ -40,8 +42,11 @@ fn test_to_json() {
let expr =
CQL2Expression::new("landsat:scene_id = 'LC82030282019133LGN00'".to_string()).unwrap();
let json = expr.to_json().unwrap();
assert!(json.contains("landsat:scene_id"));
assert!(json.contains("LC82030282019133LGN00"));
// Convert JsValue to JSON string for validation
let json_str = js_sys::JSON::stringify(&json).unwrap();
let json_str = json_str.as_string().unwrap();
assert!(json_str.contains("landsat:scene_id"));
assert!(json_str.contains("LC82030282019133LGN00"));
}

#[wasm_bindgen_test]
Expand Down Expand Up @@ -75,29 +80,33 @@ fn test_to_sql() {
fn test_matches_with_matching_item() {
let expr = CQL2Expression::new("id = 1".to_string()).unwrap();
let item = r#"{"id": 1, "name": "test"}"#;
let result = expr.matches(Some(item.to_string())).unwrap();
let value: serde_json::Value = serde_json::from_str(item).unwrap();
let js_value: JsValue = serde_wasm_bindgen::to_value(&value).unwrap();
let result = expr.matches(js_value).unwrap();
assert!(result);
}

#[wasm_bindgen_test]
fn test_matches_with_non_matching_item() {
let expr = CQL2Expression::new("id = 1".to_string()).unwrap();
let item = r#"{"id": 2, "name": "test"}"#;
let result = expr.matches(Some(item.to_string())).unwrap();
let value: serde_json::Value = serde_json::from_str(item).unwrap();
let js_value: JsValue = serde_wasm_bindgen::to_value(&value).unwrap();
let result = expr.matches(js_value).unwrap();
assert!(!result);
}

#[wasm_bindgen_test]
fn test_matches_without_item() {
let expr = CQL2Expression::new("true".to_string()).unwrap();
let result = expr.matches(None).unwrap();
let result = expr.matches(JsValue::NULL).unwrap();
assert!(result);
}

#[wasm_bindgen_test]
fn test_reduce_without_item() {
let expr = CQL2Expression::new("1 + 2".to_string()).unwrap();
let reduced = expr.reduce(None).unwrap();
let reduced = expr.reduce(JsValue::NULL).unwrap();
let text = reduced.to_text().unwrap();
assert_eq!(text, "3");
}
Expand All @@ -106,7 +115,9 @@ fn test_reduce_without_item() {
fn test_reduce_with_item() {
let expr = CQL2Expression::new("id + 10".to_string()).unwrap();
let item = r#"{"id": 5}"#;
let reduced = expr.reduce(Some(item.to_string())).unwrap();
let value: serde_json::Value = serde_json::from_str(item).unwrap();
let js_value: JsValue = serde_wasm_bindgen::to_value(&value).unwrap();
let reduced = expr.reduce(js_value).unwrap();
let text = reduced.to_text().unwrap();
assert_eq!(text, "15");
}
Expand Down
Loading