Skip to content

Commit b3536a0

Browse files
alukachgadomski
andauthored
Expand JS functionality, add tests (#115)
* Expand JS functionality, add tests * lint: cargo fmt * fix: reorder WASM build steps for clarity * Update ci for better parallelism * fix: reorder WASM build steps for Node.js and web * Build before tests * Build to right dir --------- Co-authored-by: Pete Gadomski <pete.gadomski@gmail.com>
1 parent 420c3e6 commit b3536a0

File tree

11 files changed

+589
-16
lines changed

11 files changed

+589
-16
lines changed

.github/workflows/ci.yml

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ jobs:
5252
run: uv run pytest
5353
- name: CLI smoke test
5454
run: uv run cql2 < examples/text/example01.txt
55-
build-wasm:
56-
name: Build wasm
55+
wasm-nodejs:
56+
name: WASM (Node.js)
5757
runs-on: ubuntu-latest
5858
steps:
5959
- uses: actions/checkout@v5
@@ -62,5 +62,34 @@ jobs:
6262
uses: jetli/wasm-pack-action@v0.4.0
6363
with:
6464
version: "latest"
65-
- name: Build WASM
66-
run: scripts/buildwasm
65+
- name: Build for Node.js
66+
run: wasm-pack build --target nodejs wasm
67+
- name: Test (Rust unit tests in Node.js)
68+
run: wasm-pack test --node wasm
69+
- name: Setup Node.js
70+
uses: actions/setup-node@v4
71+
with:
72+
node-version: "20"
73+
- name: Test (JavaScript integration tests)
74+
run: npm --prefix wasm test
75+
76+
wasm-web:
77+
name: WASM (web)
78+
runs-on: ubuntu-latest
79+
steps:
80+
- uses: actions/checkout@v5
81+
- uses: Swatinem/rust-cache@v2
82+
- name: Install wasm-pack
83+
uses: jetli/wasm-pack-action@v0.4.0
84+
with:
85+
version: "latest"
86+
- name: Build for web
87+
run: wasm-pack build --target web wasm
88+
- name: Setup Node.js
89+
uses: actions/setup-node@v4
90+
with:
91+
node-version: "20"
92+
- name: Test (JavaScript integration tests with web target)
93+
run: npm --prefix wasm test
94+
- name: Test (Rust unit tests in web)
95+
run: wasm-pack test --headless --firefox wasm

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ debug/
33
**/*.rs.bk
44
.cache
55
site/
6+
wasm/pkg/

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

wasm/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ crate-type = ["cdylib", "rlib"]
1818
cql2 = { path = ".." }
1919
wasm-bindgen = "0.2"
2020
getrandom = { version = "0.3.3", features = ["wasm_js"] }
21+
serde_json = "1.0"
2122

2223
[dependencies.web-sys]
2324
version = "0.3.82"

wasm/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,44 @@ There is a live version available at <http://developmentseed.org/cql2-rs/latest/
4545

4646
## Testing
4747

48+
This package includes two types of tests:
49+
50+
### Rust Unit Tests
51+
52+
Unit tests are written in `tests/common/mod.rs` and support execution in various environments.
53+
54+
> [!NOTE]
55+
> All demonstrated commands are to be run from the root of the repository
56+
57+
#### Firefox
58+
59+
Run `tests/web.rs` browser tests in a WASM environment using `wasm-bindgen-test`:
60+
4861
```shell
4962
wasm-pack test --firefox wasm
5063
```
5164

5265
Then, open <http://127.0.0.1:8000/> to see the test(s) run.
66+
67+
#### Node
68+
69+
Run `tests/node.rs` browser tests in a WASM environment using `wasm-bindgen-test`:
70+
71+
```shell
72+
wasm-pack test --node wasm
73+
```
74+
75+
### JavaScript Integration Tests
76+
77+
These tests are written in JavaScript and verify the actual JavaScript API surface that developers will use.
78+
The tests work with both nodejs and web targets:
79+
80+
```shell
81+
# Test with nodejs target
82+
wasm-pack build --target nodejs wasm
83+
npm --prefix wasm test
84+
85+
# Test with web target
86+
wasm-pack build --target web wasm
87+
npm --prefix wasm test
88+
```

wasm/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "cql2-wasm-tests",
3+
"version": "0.4.1",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"test": "node tests/integration.mjs"
8+
},
9+
"devDependencies": {}
10+
}

wasm/src/lib.rs

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
1+
use cql2::ToSqlAst;
12
use wasm_bindgen::prelude::*;
23

4+
/// Parse CQL2 text format
5+
#[wasm_bindgen(js_name = parseText)]
6+
pub fn parse_text(s: &str) -> Result<CQL2Expression, JsError> {
7+
let expr = cql2::parse_text(s)?;
8+
Ok(CQL2Expression(expr))
9+
}
10+
11+
/// Parse CQL2 JSON format
12+
#[wasm_bindgen(js_name = parseJson)]
13+
pub fn parse_json(s: &str) -> Result<CQL2Expression, JsError> {
14+
let expr = cql2::parse_json(s)?;
15+
Ok(CQL2Expression(expr))
16+
}
17+
318
#[wasm_bindgen(js_name = CQL2)]
419
pub struct CQL2Expression(cql2::Expr);
520

@@ -11,10 +26,46 @@ impl CQL2Expression {
1126
Ok(CQL2Expression(e))
1227
}
1328

29+
/// Validate the CQL2 expression against the JSON schema
30+
pub fn validate(&self) -> Result<(), JsError> {
31+
let validator = cql2::Validator::new()?;
32+
let value = self.0.to_value()?;
33+
validator.validate(&value)?;
34+
Ok(())
35+
}
36+
37+
/// Check if the expression is valid (deprecated, use validate() instead)
1438
pub fn is_valid(&self) -> bool {
1539
self.0.is_valid()
1640
}
1741

42+
/// Check if the expression matches the given item
43+
///
44+
/// # Arguments
45+
/// * `item` - JSON string representing the item to match against
46+
pub fn matches(&self, item: Option<String>) -> Result<bool, JsError> {
47+
let value = if let Some(item_str) = item {
48+
Some(serde_json::from_str(&item_str)?)
49+
} else {
50+
None
51+
};
52+
Ok(self.0.clone().matches(value.as_ref())?)
53+
}
54+
55+
/// Reduce the expression, optionally with an item context
56+
///
57+
/// # Arguments
58+
/// * `item` - Optional JSON string representing the item context for reduction
59+
pub fn reduce(&self, item: Option<String>) -> Result<CQL2Expression, JsError> {
60+
let value = if let Some(item_str) = item {
61+
Some(serde_json::from_str(&item_str)?)
62+
} else {
63+
None
64+
};
65+
let r = self.0.clone().reduce(value.as_ref())?;
66+
Ok(CQL2Expression(r))
67+
}
68+
1869
pub fn to_json(&self) -> Result<String, JsError> {
1970
let r = self.0.to_json()?;
2071
Ok(r)
@@ -30,8 +81,18 @@ impl CQL2Expression {
3081
Ok(r)
3182
}
3283

33-
pub fn reduce(&self) -> Result<CQL2Expression, JsError> {
34-
let r = self.0.clone().reduce(None)?;
35-
Ok(CQL2Expression(r))
84+
/// Convert the expression to SQL
85+
pub fn to_sql(&self) -> Result<String, JsError> {
86+
Ok(self.0.to_sql()?)
87+
}
88+
89+
/// Add two expressions together (AND operation)
90+
pub fn add(&self, other: &CQL2Expression) -> CQL2Expression {
91+
CQL2Expression(self.0.clone() + other.0.clone())
92+
}
93+
94+
/// Check if two expressions are equal
95+
pub fn equals(&self, other: &CQL2Expression) -> bool {
96+
self.0 == other.0
3697
}
3798
}

wasm/tests/common/mod.rs

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
//! Shared WASM test suite for both Node.js and browser environments.
2+
//!
3+
//! This module contains tests that run in both environments. The test runner
4+
//! is configured based on which test file includes this module.
5+
6+
#![cfg(target_arch = "wasm32")]
7+
8+
use cql2_wasm::{parse_json, parse_text, CQL2Expression};
9+
use wasm_bindgen_test::*;
10+
11+
#[wasm_bindgen_test]
12+
fn is_valid() {
13+
let expr =
14+
CQL2Expression::new("landsat:scene_id = 'LC82030282019133LGN00'".to_string()).unwrap();
15+
assert!(expr.is_valid());
16+
}
17+
18+
#[wasm_bindgen_test]
19+
fn test_parse_text() {
20+
let expr = parse_text("landsat:scene_id = 'LC82030282019133LGN00'").unwrap();
21+
assert!(expr.is_valid());
22+
}
23+
24+
#[wasm_bindgen_test]
25+
fn test_parse_json() {
26+
let json = r#"{"op":"=","args":[{"property":"landsat:scene_id"},"LC82030282019133LGN00"]}"#;
27+
let expr = parse_json(json).unwrap();
28+
assert!(expr.is_valid());
29+
}
30+
31+
#[wasm_bindgen_test]
32+
fn test_validate_valid_expression() {
33+
let expr =
34+
CQL2Expression::new("landsat:scene_id = 'LC82030282019133LGN00'".to_string()).unwrap();
35+
assert!(expr.validate().is_ok());
36+
}
37+
38+
#[wasm_bindgen_test]
39+
fn test_to_json() {
40+
let expr =
41+
CQL2Expression::new("landsat:scene_id = 'LC82030282019133LGN00'".to_string()).unwrap();
42+
let json = expr.to_json().unwrap();
43+
assert!(json.contains("landsat:scene_id"));
44+
assert!(json.contains("LC82030282019133LGN00"));
45+
}
46+
47+
#[wasm_bindgen_test]
48+
fn test_to_json_pretty() {
49+
let expr =
50+
CQL2Expression::new("landsat:scene_id = 'LC82030282019133LGN00'".to_string()).unwrap();
51+
let json = expr.to_json_pretty().unwrap();
52+
assert!(json.contains("landsat:scene_id"));
53+
assert!(json.contains("\n")); // Should have newlines for pretty printing
54+
}
55+
56+
#[wasm_bindgen_test]
57+
fn test_to_text() {
58+
let json = r#"{"op":"=","args":[{"property":"landsat:scene_id"},"LC82030282019133LGN00"]}"#;
59+
let expr = parse_json(json).unwrap();
60+
let text = expr.to_text().unwrap();
61+
assert!(text.contains("landsat:scene_id"));
62+
assert!(text.contains("LC82030282019133LGN00"));
63+
}
64+
65+
#[wasm_bindgen_test]
66+
fn test_to_sql() {
67+
let expr =
68+
CQL2Expression::new("landsat:scene_id = 'LC82030282019133LGN00'".to_string()).unwrap();
69+
let sql = expr.to_sql().unwrap();
70+
assert!(sql.contains("landsat:scene_id"));
71+
assert!(sql.contains("LC82030282019133LGN00"));
72+
}
73+
74+
#[wasm_bindgen_test]
75+
fn test_matches_with_matching_item() {
76+
let expr = CQL2Expression::new("id = 1".to_string()).unwrap();
77+
let item = r#"{"id": 1, "name": "test"}"#;
78+
let result = expr.matches(Some(item.to_string())).unwrap();
79+
assert!(result);
80+
}
81+
82+
#[wasm_bindgen_test]
83+
fn test_matches_with_non_matching_item() {
84+
let expr = CQL2Expression::new("id = 1".to_string()).unwrap();
85+
let item = r#"{"id": 2, "name": "test"}"#;
86+
let result = expr.matches(Some(item.to_string())).unwrap();
87+
assert!(!result);
88+
}
89+
90+
#[wasm_bindgen_test]
91+
fn test_matches_without_item() {
92+
let expr = CQL2Expression::new("true".to_string()).unwrap();
93+
let result = expr.matches(None).unwrap();
94+
assert!(result);
95+
}
96+
97+
#[wasm_bindgen_test]
98+
fn test_reduce_without_item() {
99+
let expr = CQL2Expression::new("1 + 2".to_string()).unwrap();
100+
let reduced = expr.reduce(None).unwrap();
101+
let text = reduced.to_text().unwrap();
102+
assert_eq!(text, "3");
103+
}
104+
105+
#[wasm_bindgen_test]
106+
fn test_reduce_with_item() {
107+
let expr = CQL2Expression::new("id + 10".to_string()).unwrap();
108+
let item = r#"{"id": 5}"#;
109+
let reduced = expr.reduce(Some(item.to_string())).unwrap();
110+
let text = reduced.to_text().unwrap();
111+
assert_eq!(text, "15");
112+
}
113+
114+
#[wasm_bindgen_test]
115+
fn test_add_expressions() {
116+
let expr1 = CQL2Expression::new("id = 1".to_string()).unwrap();
117+
let expr2 = CQL2Expression::new("name = 'test'".to_string()).unwrap();
118+
let combined = expr1.add(&expr2);
119+
let text = combined.to_text().unwrap();
120+
assert!(text.contains("id"));
121+
assert!(text.contains("name"));
122+
assert!(text.contains("AND") || text.contains("and"));
123+
}
124+
125+
#[wasm_bindgen_test]
126+
fn test_equals_same_expressions() {
127+
let expr1 = CQL2Expression::new("id = 1".to_string()).unwrap();
128+
let expr2 = CQL2Expression::new("id = 1".to_string()).unwrap();
129+
assert!(expr1.equals(&expr2));
130+
}
131+
132+
#[wasm_bindgen_test]
133+
fn test_equals_different_expressions() {
134+
let expr1 = CQL2Expression::new("id = 1".to_string()).unwrap();
135+
let expr2 = CQL2Expression::new("id = 2".to_string()).unwrap();
136+
assert!(!expr1.equals(&expr2));
137+
}

0 commit comments

Comments
 (0)