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
37 changes: 33 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ jobs:
run: uv run pytest
- name: CLI smoke test
run: uv run cql2 < examples/text/example01.txt
build-wasm:
name: Build wasm
wasm-nodejs:
name: WASM (Node.js)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
Expand All @@ -62,5 +62,34 @@ jobs:
uses: jetli/wasm-pack-action@v0.4.0
with:
version: "latest"
- name: Build WASM
run: scripts/buildwasm
- name: Build for Node.js
run: wasm-pack build --target nodejs wasm
- name: Test (Rust unit tests in Node.js)
run: wasm-pack test --node wasm
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Test (JavaScript integration tests)
run: npm --prefix wasm test

wasm-web:
name: WASM (web)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: Swatinem/rust-cache@v2
- name: Install wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
with:
version: "latest"
- name: Build for web
run: wasm-pack build --target web wasm
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Test (JavaScript integration tests with web target)
run: npm --prefix wasm test
- name: Test (Rust unit tests in web)
run: wasm-pack test --headless --firefox wasm
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ debug/
**/*.rs.bk
.cache
site/
wasm/pkg/
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ crate-type = ["cdylib", "rlib"]
cql2 = { path = ".." }
wasm-bindgen = "0.2"
getrandom = { version = "0.3.3", features = ["wasm_js"] }
serde_json = "1.0"

[dependencies.web-sys]
version = "0.3.82"
Expand Down
36 changes: 36 additions & 0 deletions wasm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,44 @@ There is a live version available at <http://developmentseed.org/cql2-rs/latest/

## Testing

This package includes two types of tests:

### Rust Unit Tests

Unit tests are written in `tests/common/mod.rs` and support execution in various environments.

> [!NOTE]
> All demonstrated commands are to be run from the root of the repository

#### Firefox

Run `tests/web.rs` browser tests in a WASM environment using `wasm-bindgen-test`:

```shell
wasm-pack test --firefox wasm
```

Then, open <http://127.0.0.1:8000/> to see the test(s) run.

#### Node

Run `tests/node.rs` browser tests in a WASM environment using `wasm-bindgen-test`:

```shell
wasm-pack test --node wasm
```

### JavaScript Integration Tests

These tests are written in JavaScript and verify the actual JavaScript API surface that developers will use.
The tests work with both nodejs and web targets:

```shell
# Test with nodejs target
wasm-pack build --target nodejs wasm
npm --prefix wasm test

# Test with web target
wasm-pack build --target web wasm
npm --prefix wasm test
```
10 changes: 10 additions & 0 deletions wasm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "cql2-wasm-tests",
"version": "0.4.1",
"private": true,
"type": "module",
"scripts": {
"test": "node tests/integration.mjs"
},
"devDependencies": {}
}
67 changes: 64 additions & 3 deletions wasm/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
use cql2::ToSqlAst;
use wasm_bindgen::prelude::*;

/// Parse CQL2 text format
#[wasm_bindgen(js_name = parseText)]
pub fn parse_text(s: &str) -> Result<CQL2Expression, JsError> {
let expr = cql2::parse_text(s)?;
Ok(CQL2Expression(expr))
}

/// Parse CQL2 JSON format
#[wasm_bindgen(js_name = parseJson)]
pub fn parse_json(s: &str) -> Result<CQL2Expression, JsError> {
let expr = cql2::parse_json(s)?;
Ok(CQL2Expression(expr))
}

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

Expand All @@ -11,10 +26,46 @@ impl CQL2Expression {
Ok(CQL2Expression(e))
}

/// Validate the CQL2 expression against the JSON schema
pub fn validate(&self) -> Result<(), JsError> {
let validator = cql2::Validator::new()?;
let value = self.0.to_value()?;
validator.validate(&value)?;
Ok(())
}

/// Check if the expression is valid (deprecated, use validate() instead)
pub fn is_valid(&self) -> bool {
self.0.is_valid()
}

/// 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 {
None
};
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 {
None
};
let r = self.0.clone().reduce(value.as_ref())?;
Ok(CQL2Expression(r))
}

pub fn to_json(&self) -> Result<String, JsError> {
let r = self.0.to_json()?;
Ok(r)
Expand All @@ -30,8 +81,18 @@ impl CQL2Expression {
Ok(r)
}

pub fn reduce(&self) -> Result<CQL2Expression, JsError> {
let r = self.0.clone().reduce(None)?;
Ok(CQL2Expression(r))
/// Convert the expression to SQL
pub fn to_sql(&self) -> Result<String, JsError> {
Ok(self.0.to_sql()?)
}

/// Add two expressions together (AND operation)
pub fn add(&self, other: &CQL2Expression) -> CQL2Expression {
CQL2Expression(self.0.clone() + other.0.clone())
}

/// Check if two expressions are equal
pub fn equals(&self, other: &CQL2Expression) -> bool {
self.0 == other.0
}
}
137 changes: 137 additions & 0 deletions wasm/tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
//! Shared WASM test suite for both Node.js and browser environments.
//!
//! This module contains tests that run in both environments. The test runner
//! is configured based on which test file includes this module.

#![cfg(target_arch = "wasm32")]

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

#[wasm_bindgen_test]
fn is_valid() {
let expr =
CQL2Expression::new("landsat:scene_id = 'LC82030282019133LGN00'".to_string()).unwrap();
assert!(expr.is_valid());
}

#[wasm_bindgen_test]
fn test_parse_text() {
let expr = parse_text("landsat:scene_id = 'LC82030282019133LGN00'").unwrap();
assert!(expr.is_valid());
}

#[wasm_bindgen_test]
fn test_parse_json() {
let json = r#"{"op":"=","args":[{"property":"landsat:scene_id"},"LC82030282019133LGN00"]}"#;
let expr = parse_json(json).unwrap();
assert!(expr.is_valid());
}

#[wasm_bindgen_test]
fn test_validate_valid_expression() {
let expr =
CQL2Expression::new("landsat:scene_id = 'LC82030282019133LGN00'".to_string()).unwrap();
assert!(expr.validate().is_ok());
}

#[wasm_bindgen_test]
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"));
}

#[wasm_bindgen_test]
fn test_to_json_pretty() {
let expr =
CQL2Expression::new("landsat:scene_id = 'LC82030282019133LGN00'".to_string()).unwrap();
let json = expr.to_json_pretty().unwrap();
assert!(json.contains("landsat:scene_id"));
assert!(json.contains("\n")); // Should have newlines for pretty printing
}

#[wasm_bindgen_test]
fn test_to_text() {
let json = r#"{"op":"=","args":[{"property":"landsat:scene_id"},"LC82030282019133LGN00"]}"#;
let expr = parse_json(json).unwrap();
let text = expr.to_text().unwrap();
assert!(text.contains("landsat:scene_id"));
assert!(text.contains("LC82030282019133LGN00"));
}

#[wasm_bindgen_test]
fn test_to_sql() {
let expr =
CQL2Expression::new("landsat:scene_id = 'LC82030282019133LGN00'".to_string()).unwrap();
let sql = expr.to_sql().unwrap();
assert!(sql.contains("landsat:scene_id"));
assert!(sql.contains("LC82030282019133LGN00"));
}

#[wasm_bindgen_test]
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();
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();
assert!(!result);
}

#[wasm_bindgen_test]
fn test_matches_without_item() {
let expr = CQL2Expression::new("true".to_string()).unwrap();
let result = expr.matches(None).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 text = reduced.to_text().unwrap();
assert_eq!(text, "3");
}

#[wasm_bindgen_test]
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 text = reduced.to_text().unwrap();
assert_eq!(text, "15");
}

#[wasm_bindgen_test]
fn test_add_expressions() {
let expr1 = CQL2Expression::new("id = 1".to_string()).unwrap();
let expr2 = CQL2Expression::new("name = 'test'".to_string()).unwrap();
let combined = expr1.add(&expr2);
let text = combined.to_text().unwrap();
assert!(text.contains("id"));
assert!(text.contains("name"));
assert!(text.contains("AND") || text.contains("and"));
}

#[wasm_bindgen_test]
fn test_equals_same_expressions() {
let expr1 = CQL2Expression::new("id = 1".to_string()).unwrap();
let expr2 = CQL2Expression::new("id = 1".to_string()).unwrap();
assert!(expr1.equals(&expr2));
}

#[wasm_bindgen_test]
fn test_equals_different_expressions() {
let expr1 = CQL2Expression::new("id = 1".to_string()).unwrap();
let expr2 = CQL2Expression::new("id = 2".to_string()).unwrap();
assert!(!expr1.equals(&expr2));
}
Loading
Loading