Skip to content

Commit 4accf42

Browse files
mpiannucciclaude
andcommitted
Add comprehensive TypeScript/WASM API for OpenDAP with xarray-style selection
- Implement URL builder system with coordinate-aware constraints in readap/src/url.rs - Add 20 comprehensive tests for URL building and constraint generation - Create WASM bindings with automatic fetch integration for browser/Node.js usage - Implement xarray-style isel/sel API with nearest neighbor coordinate lookup - Add high-level OpenDAPDataset with automatic metadata and coordinate loading - Support both object-based and array-based selection syntax for flexibility - Include comprehensive error handling for network failures and parsing errors - Enable efficient typed array data transfer between Rust and JavaScript 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 9be7d89 commit 4accf42

20 files changed

+2754
-1
lines changed

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
[workspace]
2+
members = [
3+
"readap",
4+
"readap-wasm",
5+
]
26
resolver = "3"
3-
members = ["readap"]

readap-wasm/.appveyor.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
install:
2+
- appveyor-retry appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe
3+
- if not defined RUSTFLAGS rustup-init.exe -y --default-host x86_64-pc-windows-msvc --default-toolchain nightly
4+
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
5+
- rustc -V
6+
- cargo -V
7+
8+
build: false
9+
10+
test_script:
11+
- cargo test --locked

readap-wasm/.github/dependabot.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: cargo
4+
directory: "/"
5+
schedule:
6+
interval: daily
7+
time: "08:00"
8+
open-pull-requests-limit: 10

readap-wasm/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/target
2+
**/*.rs.bk
3+
Cargo.lock
4+
bin/
5+
pkg/
6+
wasm-pack.log

readap-wasm/.travis.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
language: rust
2+
sudo: false
3+
4+
cache: cargo
5+
6+
matrix:
7+
include:
8+
9+
# Builds with wasm-pack.
10+
- rust: beta
11+
env: RUST_BACKTRACE=1
12+
addons:
13+
firefox: latest
14+
chrome: stable
15+
before_script:
16+
- (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
17+
- (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate)
18+
- cargo install-update -a
19+
- curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f
20+
script:
21+
- cargo generate --git . --name testing
22+
# Having a broken Cargo.toml (in that it has curlies in fields) anywhere
23+
# in any of our parent dirs is problematic.
24+
- mv Cargo.toml Cargo.toml.tmpl
25+
- cd testing
26+
- wasm-pack build
27+
- wasm-pack test --chrome --firefox --headless
28+
29+
# Builds on nightly.
30+
- rust: nightly
31+
env: RUST_BACKTRACE=1
32+
before_script:
33+
- (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
34+
- (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate)
35+
- cargo install-update -a
36+
- rustup target add wasm32-unknown-unknown
37+
script:
38+
- cargo generate --git . --name testing
39+
- mv Cargo.toml Cargo.toml.tmpl
40+
- cd testing
41+
- cargo check
42+
- cargo check --target wasm32-unknown-unknown
43+
- cargo check --no-default-features
44+
- cargo check --target wasm32-unknown-unknown --no-default-features
45+
- cargo check --no-default-features --features console_error_panic_hook
46+
- cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook
47+
- cargo check --no-default-features --features "console_error_panic_hook wee_alloc"
48+
- cargo check --target wasm32-unknown-unknown --no-default-features --features "console_error_panic_hook wee_alloc"
49+
50+
# Builds on beta.
51+
- rust: beta
52+
env: RUST_BACKTRACE=1
53+
before_script:
54+
- (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
55+
- (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate)
56+
- cargo install-update -a
57+
- rustup target add wasm32-unknown-unknown
58+
script:
59+
- cargo generate --git . --name testing
60+
- mv Cargo.toml Cargo.toml.tmpl
61+
- cd testing
62+
- cargo check
63+
- cargo check --target wasm32-unknown-unknown
64+
- cargo check --no-default-features
65+
- cargo check --target wasm32-unknown-unknown --no-default-features
66+
- cargo check --no-default-features --features console_error_panic_hook
67+
- cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook
68+
# Note: no enabling the `wee_alloc` feature here because it requires
69+
# nightly for now.

readap-wasm/API_SUMMARY.md

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# ReadAP WASM Library - API Summary
2+
3+
A powerful TypeScript/WebAssembly library for OpenDAP data access with automatic fetching, xarray-style selection, and efficient typed array handling.
4+
5+
## Features
6+
7+
### **Complete Implementation**
8+
9+
- **Automatic Data Fetching**: Uses JavaScript fetch API for seamless data access
10+
- **Network-free Core**: The underlying readap library remains portable across platforms
11+
- **xarray-style Selection**: Intuitive `isel` (index) and `sel` (value) selection patterns
12+
- **Nearest Neighbor Lookup**: Automatic coordinate value → index mapping
13+
- **Typed Arrays**: Efficient JavaScript typed arrays (Float64Array, Int32Array, etc.)
14+
- **Lazy Loading**: Coordinates and data loaded only when needed
15+
- **Error Handling**: Comprehensive HTTP and parsing error handling
16+
17+
## High-Level API
18+
19+
### Dataset Creation
20+
21+
```typescript
22+
// Automatic metadata loading
23+
const dataset = await OpenDAPDataset.fromURL("http://example.com/data.nc");
24+
25+
// Manual control (lazy loading)
26+
const dataset = OpenDAPDataset.fromURLLazy("http://example.com/data.nc");
27+
await dataset.parseDAS(await fetch(dataset.dasUrl()).then(r => r.text()));
28+
await dataset.parseDDS(await fetch(dataset.ddsUrl()).then(r => r.text()));
29+
```
30+
31+
### Data Access
32+
33+
```typescript
34+
// Simple variable access
35+
const tempData = await dataset.getVariable("temperature");
36+
console.log(tempData.data); // Float64Array or appropriate typed array
37+
38+
// Multiple variables
39+
const data = await dataset.getVariables(["temperature", "pressure"]);
40+
41+
// With constraints
42+
const selection = dataset.isel({ time: 0, lat: [10, 20] });
43+
const slicedData = await dataset.getVariable("temperature", selection);
44+
```
45+
46+
### Selection API
47+
48+
```typescript
49+
// Index-based selection (isel)
50+
const indexSel = dataset.isel({
51+
time: { type: "single", value: 0 },
52+
lat: { type: "range", start: 10, end: 20 },
53+
lon: { type: "multiple", values: [0, 5, 10] }
54+
});
55+
56+
// Value-based selection (sel) with nearest neighbor
57+
const valueSel = dataset.sel({
58+
time: "2023-01-15", // nearest neighbor to time coordinate
59+
lat: [40.0, 50.0], // range between coordinate values
60+
lon: -74.0 // single coordinate value (nearest neighbor)
61+
});
62+
63+
// Chained selections
64+
const combined = dataset
65+
.isel({ time: 0 })
66+
.sel({ lat: [40, 50] });
67+
```
68+
69+
### Coordinate Management
70+
71+
```typescript
72+
// Automatic coordinate loading for sel operations
73+
await dataset.loadCoordinates("time");
74+
await dataset.loadCoordinates("lat");
75+
76+
// Manual coordinate addition
77+
const timeCoords = new Float64Array([0, 6, 12, 18, 24]); // hours
78+
dataset.addCoordinates("time", timeCoords);
79+
```
80+
81+
## Low-Level API
82+
83+
### URL Building
84+
85+
```typescript
86+
const urlBuilder = new OpenDAPUrlBuilder("http://example.com/data");
87+
console.log(urlBuilder.dasUrl()); // http://example.com/data.das
88+
console.log(urlBuilder.ddsUrl()); // http://example.com/data.dds
89+
console.log(urlBuilder.dodsUrl("temperature[0:10]")); // with constraints
90+
```
91+
92+
### Constraint Building
93+
94+
```typescript
95+
const builder = new ConstraintBuilder()
96+
.isel({ time: { type: "single", value: 0 } })
97+
.sel({ lat: { type: "range", min: 40.0, max: 50.0 } });
98+
99+
console.log(builder.build()); // "time[0],lat[nearest_indices]"
100+
```
101+
102+
### Direct Parsing
103+
104+
```typescript
105+
// Parse OpenDAP formats directly
106+
const dasResult = OpenDAPDataset.fromDAS(dasText);
107+
const ddsResult = OpenDAPDataset.fromDDS(ddsText);
108+
const dodsResult = dataset.parseDODS(dodsBytes); // Uint8Array → Object with typed arrays
109+
```
110+
111+
## Example Usage
112+
113+
```typescript
114+
import init, { OpenDAPDataset } from '@readap-wasm/readap-wasm';
115+
116+
async function main() {
117+
await init();
118+
119+
// Load dataset with automatic metadata fetching
120+
const dataset = await OpenDAPDataset.fromURL('http://example.com/ocean.nc');
121+
122+
// Check available variables
123+
console.log('Variables:', dataset.getVariableNames());
124+
125+
// Load coordinates for value-based selection
126+
await dataset.loadCoordinates('time');
127+
await dataset.loadCoordinates('lat');
128+
await dataset.loadCoordinates('lon');
129+
130+
// Select data using coordinate values (nearest neighbor)
131+
const selection = dataset.sel({
132+
time: "2023-01-15T12:00:00Z",
133+
lat: [40.0, 45.0], // latitude range
134+
lon: -70.0 // single longitude
135+
});
136+
137+
// Fetch temperature data with selection
138+
const tempData = await dataset.getVariable('temperature', selection);
139+
console.log('Temperature:', tempData.data); // Float32Array or Float64Array
140+
141+
// Get multiple variables efficiently
142+
const oceanData = await dataset.getVariables(['temperature', 'salinity', 'velocity']);
143+
144+
// Chain selections for complex queries
145+
const surface = dataset
146+
.sel({ depth: 0 }) // surface level
147+
.isel({ time: [0, 1, 2] }) // first 3 time steps
148+
.sel({ lat: [35, 45], lon: [-80, -60] }); // geographic subset
149+
150+
const surfaceTemp = await dataset.getVariable('temperature', surface);
151+
}
152+
```
153+
154+
## Architecture Benefits
155+
156+
1. **Portable Core**: The readap library has no network dependencies, making it usable in any Rust environment
157+
2. **Efficient WASM**: Direct memory transfer between Rust and JavaScript using typed arrays
158+
3. **Smart Caching**: Coordinates are cached to avoid redundant network requests
159+
4. **Error Resilience**: Comprehensive error handling for network failures and data parsing issues
160+
5. **TypeScript Ready**: Full type safety with appropriate typed array selection based on data types
161+
162+
## Performance Features
163+
164+
- **Zero-copy data transfer** where possible between WASM and JavaScript
165+
- **Lazy coordinate loading** - coordinates fetched only when needed for `sel` operations
166+
- **Efficient nearest neighbor** lookup using binary search algorithms
167+
- **Minimal network requests** through intelligent constraint building
168+
- **Browser optimization** with proper fetch API usage and CORS support
169+
170+
This implementation provides both the low-level control needed for advanced users and the high-level convenience required for typical scientific data workflows.

readap-wasm/CLAUDE.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# readap-wasm
2+
3+
This is a WebAssembly port of [readap](../readap), a library for parsing OpenDAP binary data.
4+
5+
The goal of this crate is to build a simple OpenDAP client that can be used in the browser.
6+
7+
## Development
8+
9+
This create uses `wasm-pack` to build the WASM version of readap. It was initialized from the [wasm-pack template](https://github.com/rustwasm/wasm-pack-template) using `cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name readap-wasm`.
10+
11+
To build the WASM version of readap, run:
12+
13+
```bash
14+
$ wasm-pack build
15+
```
16+

readap-wasm/Cargo.toml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
[package]
2+
name = "readap-wasm"
3+
version = "0.1.0"
4+
authors = ["Matthew Iannucci <mpiannucci@gmail.com>"]
5+
edition = "2018"
6+
7+
[lib]
8+
crate-type = ["cdylib", "rlib"]
9+
10+
[features]
11+
default = ["console_error_panic_hook"]
12+
13+
[dependencies]
14+
readap = { path = "../readap" }
15+
wasm-bindgen = "0.2.84"
16+
wasm-bindgen-futures = "0.4"
17+
js-sys = "0.3"
18+
serde = { version = "1.0", features = ["derive"] }
19+
serde-wasm-bindgen = "0.4"
20+
serde_json = "1.0"
21+
console_error_panic_hook = { version = "0.1.7", optional = true }
22+
23+
[dependencies.web-sys]
24+
version = "0.3"
25+
features = [
26+
"console",
27+
"Request",
28+
"RequestInit",
29+
"RequestMode",
30+
"Response",
31+
"Window",
32+
"Headers",
33+
"AbortController",
34+
"AbortSignal",
35+
]
36+
37+
# The `console_error_panic_hook` crate provides better debugging of panics by
38+
# logging them with `console.error`. This is great for development, but requires
39+
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
40+
# code size when deploying.
41+
console_error_panic_hook = { version = "0.1.7", optional = true }
42+
43+
[dev-dependencies]
44+
wasm-bindgen-test = "0.3.34"
45+
46+
[profile.release]
47+
# Tell `rustc` to optimize for small code size.
48+
opt-level = "s"

readap-wasm/LICENSE

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Copyright (c) 2018 Matthew Iannucci <mpiannucci@gmail.com>
2+
3+
Permission is hereby granted, free of charge, to any
4+
person obtaining a copy of this software and associated
5+
documentation files (the "Software"), to deal in the
6+
Software without restriction, including without
7+
limitation the rights to use, copy, modify, merge,
8+
publish, distribute, sublicense, and/or sell copies of
9+
the Software, and to permit persons to whom the Software
10+
is furnished to do so, subject to the following
11+
conditions:
12+
13+
The above copyright notice and this permission notice
14+
shall be included in all copies or substantial portions
15+
of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
18+
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
19+
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
20+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
21+
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
24+
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25+
DEALINGS IN THE SOFTWARE.

0 commit comments

Comments
 (0)