Skip to content
Draft
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
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,45 @@ jobs:
- uses: taiki-e/install-action@cargo-hack
- name: Check msrv
run: cargo hack check --rust-version --workspace --all-targets --ignore-private
changes-r:
name: Check R changes
runs-on: ubuntu-latest
outputs:
r: ${{ steps.filter.outputs.r }}
steps:
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
r:
- 'crates/r/**'
test-r:
name: Test R
needs: changes-r
if: needs.changes-r.outputs.r == 'true'
runs-on: ubuntu-latest
defaults:
run:
working-directory: crates/r
steps:
- uses: actions/checkout@v6
- uses: Swatinem/rust-cache@v2
- uses: r-lib/actions/setup-r@v2
with:
use-public-rspm: true
- uses: r-lib/actions/setup-r-dependencies@v2
with:
working-directory: crates/r
- name: Build and check
run: |
R CMD build .
R CMD check --output=/tmp --no-manual --no-clean rustac_*.tar.gz
- uses: actions/upload-artifact@v4
if: failure()
with:
name: r-check-results
path: /tmp/rustac.Rcheck/
doc:
name: Docs
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ members = [
"crates/validate",
"crates/wasm",
]
exclude = [
"crates/r/*",
]
default-members = [
"crates/core",
"crates/cli",
Expand Down
3 changes: 3 additions & 0 deletions crates/r/.Rbuildignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
^src/rust/target$
^src/rust/.cargo$
^README\.md$
53 changes: 53 additions & 0 deletions crates/r/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# History files
.Rhistory
.Rapp.history

# Session Data files
.RData
.RDataTmp

# User-specific files
.Ruserdata

# Example code in package build process
*-Ex.R

# Output files from R CMD build
/*.tar.gz

# Output files from R CMD check
/*.Rcheck/

# RStudio files
.Rproj.user/

# produced vignettes
vignettes/*.html
vignettes/*.pdf

# OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3
.httr-oauth

# knitr and R markdown default cache directories
*_cache/
/cache/

# Temporary files created by R markdown
*.utf8.md
*.knit.md

# R Environment Variables
.Renviron

# pkgdown site
docs/

# translation temp files
po/*~

# RStudio Connect folder
rsconnect/

src/rust/target/
src/*.so
src/*.o
20 changes: 20 additions & 0 deletions crates/r/DESCRIPTION
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Package: rustac
Title: SpatioTemporal Asset Catalog (STAC) Client
Version: 0.1.0
Authors@R:
person("Pete", "Gadomski", , "pete.gadomski@gmail.com", role = c("aut", "cre"))
Description: Read and write SpatioTemporal Asset Catalog (STAC) data in JSON,
NDJSON, and geoparquet formats. Powered by Rust via the 'extendr' framework.
License: MIT + file LICENSE
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.2
Imports:
arrow,
jsonlite,
sf
Suggests:
geojsonsf,
testthat (>= 3.0.0)
Config/testthat/edition: 3
SystemRequirements: Cargo (Rust's package manager), rustc >= 1.88
23 changes: 23 additions & 0 deletions crates/r/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
4 changes: 4 additions & 0 deletions crates/r/NAMESPACE
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
useDynLib(rustac, .registration = TRUE)

export(stac_read)
export(stac_write)
58 changes: 58 additions & 0 deletions crates/r/R/rustac.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#' Read a STAC value from a file or URL
#'
#' Reads STAC data from JSON, NDJSON, or geoparquet formats. The format is
#' inferred from the file extension. Geoparquet files are returned as sf data
#' frames via Arrow; all other types are returned as R lists.
#'
#' @param path Path to a file or a URL.
#' @return An sf data frame (for geoparquet/FeatureCollections) or an R list.
#' @export
stac_read <- function(path) {
if (is_geoparquet(path)) {
ipc_bytes <- .Call(wrap__stac_read_geoparquet, path)
table <- arrow::read_ipc_stream(ipc_bytes, as_data_frame = FALSE)
df <- as.data.frame(table)
geom <- sf::st_as_sfc(structure(df$geometry, class = "WKB"), EWKB = TRUE)
df$geometry <- NULL
sf::st_sf(df, geometry = geom)
} else {
json <- .Call(wrap__stac_read_json, path)
value <- jsonlite::fromJSON(json, simplifyVector = FALSE)
if (identical(value$type, "FeatureCollection")) {
sf::read_sf(json)
} else {
value
}
}
}

#' Write a STAC value to a file
#'
#' Writes STAC data to JSON, NDJSON, or geoparquet formats. The format is
#' inferred from the file extension. sf data frames are written via Arrow for
#' geoparquet output; all other values are serialized with jsonlite.
#'
#' @param x An sf data frame or an R list representing a STAC value.
#' @param path Output file path.
#' @export
stac_write <- function(x, path) {
if (is_geoparquet(path)) {
table <- arrow::as_arrow_table(sf::st_as_sf(x))
buf <- arrow::write_ipc_stream(table, raw())
invisible(.Call(wrap__stac_write_geoparquet, buf, path))
} else {
if (inherits(x, "sf")) {
if (!requireNamespace("geojsonsf", quietly = TRUE)) {
stop("Package 'geojsonsf' is required to write sf objects to non-parquet formats")
}
json <- geojsonsf::sf_geojson(x)
} else {
json <- jsonlite::toJSON(x, auto_unbox = TRUE, null = "null")
}
invisible(.Call(wrap__stac_write_json, as.character(json), path))
}
}

is_geoparquet <- function(path) {
grepl("\\.(parquet|geoparquet)$", path, ignore.case = TRUE)
}
84 changes: 84 additions & 0 deletions crates/r/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# rustac

R package for reading and writing [SpatioTemporal Asset Catalog (STAC)](https://stacspec.org/) data in JSON, NDJSON, and [stac-geoparquet](https://github.com/stac-utils/stac-geoparquet) formats.
Powered by Rust via [extendr](https://extendr.github.io/extendr/extendr_api/).

## Prerequisites

- **Rust** toolchain (rustc >= 1.88): <https://rustup.rs/>

## Install

Install directly from GitHub using [pak](https://pak.r-lib.org/) (recommended):

```r
# install.packages("pak")
pak::pak("stac-utils/rustac/crates/r")
```

Or with `devtools`:

```r
# install.packages("devtools")
devtools::install_github("stac-utils/rustac", subdir = "crates/r")
```

### From source

From the repository root:

```bash
R CMD build crates/r
R CMD INSTALL rustac_0.1.0.tar.gz
```

### Troubleshooting

If `arrow` or `sf` fail to install, try [r-universe](https://r-universe.dev/) binaries first:

```r
install.packages(
c("arrow", "sf"),
repos = c("https://apache.r-universe.dev", "https://r-spatial.r-universe.dev", "https://cloud.r-project.org")
)
```

Then retry the install.

## Development

For iterative development, use `devtools`:

```r
devtools::load_all("crates/r")
devtools::test("crates/r")
```

To run the full R CMD check:

```bash
R CMD build crates/r
R CMD check rustac_0.1.0.tar.gz
```

## Usage

```r
library(rustac)

# Read a STAC item (returns an R list)
item <- stac_read("spec-examples/v1.1.0/simple-item.json")
item$id
#> [1] "20201211_223832_CS2"

# Read stac-geoparquet (returns an sf data frame)
sf <- stac_read("data/extended-item.parquet")
class(sf)
#> [1] "sf" "data.frame"

# Write to JSON
stac_write(item, "output.json")

# Write to stac-geoparquet
stac_write(sf, "output.parquet")
```
23 changes: 23 additions & 0 deletions crates/r/src/Makevars
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
TARGET_DIR = ./rust/target
LIBDIR = $(TARGET_DIR)/release
STATLIB = $(LIBDIR)/librustac_r.a
PKG_LIBS = -L$(LIBDIR) -lrustac_r

all: C_clean

$(SHLIB): $(STATLIB)

CARGOTMP = $(CURDIR)/.cargo

$(STATLIB):
# In some environments, ~/.cargo/bin might not be included in PATH, so we need
# to set it here to ensure cargo can be found.
export PATH="$(PATH):$(HOME)/.cargo/bin" && \
export CARGO_TARGET_DIR=$(TARGET_DIR) && \
cargo build --lib --release --manifest-path=./rust/Cargo.toml

C_clean:
rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS)

clean:
rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(TARGET_DIR)
20 changes: 20 additions & 0 deletions crates/r/src/Makevars.win
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
TARGET_DIR = ./rust/target
LIBDIR = $(TARGET_DIR)/release
STATLIB = $(LIBDIR)/rustac_r.lib
PKG_LIBS = -L$(LIBDIR) -lrustac_r -lws2_32 -ladvapi32 -luserenv -lbcrypt -lntdll

all: C_clean

$(SHLIB): $(STATLIB)

CARGOTMP = $(CURDIR)/.cargo

$(STATLIB):
export CARGO_TARGET_DIR=$(TARGET_DIR) && \
cargo build --lib --release --manifest-path=./rust/Cargo.toml

C_clean:
rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS)

clean:
rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(TARGET_DIR)
8 changes: 8 additions & 0 deletions crates/r/src/entrypoint.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// We need to forward routine registration from C to Rust
// to avoid the linker removing the bytes from the final library.
// See <https://github.com/pola-rs/r-polars/issues/1292> for more details.
void R_init_rustac_extendr(void *dll);

void R_init_rustac(void *dll) {
R_init_rustac_extendr(dll);
}
22 changes: 22 additions & 0 deletions crates/r/src/rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "rustac-r"
version = "0.1.0"
# authors.workspace = true
edition = "2024"
# license.workspace = true
publish = false

[lib]
crate-type = ["staticlib"]

[dependencies]
arrow-array = "57.0.0"
arrow-ipc = { version = "57.0.0", features = ["lz4", "zstd"] }
bytes = "1"
extendr-api = "0.8"
geoparquet = "0.7.0"
parquet = "57.0.0"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] }
serde_json = "1.0.80"
stac = { git = "https://github.com/stac-utils/rustac", branch = "main", features = ["geoparquet"] }
stac-io = { git = "https://github.com/stac-utils/rustac", branch = "main", features = ["geoparquet"] }
Loading
Loading