├── .python-version ├── CODEOWNERS ├── crates ├── wasm │ ├── LICENSE-MIT │ ├── LICENSE-APACHE │ ├── www │ │ ├── .gitignore │ │ ├── bootstrap.js │ │ ├── index.html │ │ ├── webpack.config.js │ │ ├── package.json │ │ └── index.js │ ├── Cargo.toml │ ├── src │ │ └── lib.rs │ ├── CHANGELOG.md │ └── README.md ├── cli │ ├── examples │ ├── src │ │ └── main.rs │ ├── Cargo.toml │ ├── data │ │ └── invalid-item.json │ └── README.md ├── core │ ├── examples │ ├── assets │ │ ├── dataset.tif │ │ └── dataset_geo.tif │ ├── data │ │ ├── extended-item.parquet │ │ ├── bands-v1.1.0.json │ │ ├── items.ndjson │ │ ├── bands-v1.0.0.json │ │ ├── invalid-item.json │ │ └── 20201211_223832_CS2.json │ ├── src │ │ ├── geo.rs │ │ ├── statistics.rs │ │ ├── api │ │ │ ├── collections.rs │ │ │ ├── mod.rs │ │ │ ├── sort.rs │ │ │ └── root.rs │ │ ├── data_type.rs │ │ ├── version.rs │ │ ├── json.rs │ │ ├── datetime.rs │ │ ├── band.rs │ │ └── item_asset.rs │ ├── README.md │ └── Cargo.toml ├── io │ ├── examples │ ├── data │ │ ├── extended-item.parquet │ │ └── items.ndjson │ ├── mocks │ │ └── not-a-collection.json │ ├── tests │ │ └── aws.rs │ ├── src │ │ ├── read.rs │ │ ├── write.rs │ │ ├── realized_href.rs │ │ ├── json.rs │ │ ├── error.rs │ │ └── geoparquet.rs │ ├── README.md │ ├── Cargo.toml │ └── CHANGELOG.md ├── extensions │ ├── examples │ ├── Cargo.toml │ ├── README.md │ ├── CHANGELOG.md │ ├── data │ │ └── auth │ │ │ ├── item.json │ │ │ └── collection.json │ └── src │ │ ├── projection.rs │ │ └── electro_optical.rs ├── validate │ ├── examples │ ├── src │ │ ├── schemas │ │ │ ├── v1.0.0 │ │ │ │ ├── licensing.json │ │ │ │ ├── basics.json │ │ │ │ ├── instrument.json │ │ │ │ ├── provider.json │ │ │ │ ├── datetime.json │ │ │ │ └── catalog.json │ │ │ └── v1.1.0 │ │ │ │ ├── licensing.json │ │ │ │ ├── bands.json │ │ │ │ ├── common.json │ │ │ │ ├── instrument.json │ │ │ │ ├── basics.json │ │ │ │ ├── provider.json │ │ │ │ ├── datetime.json │ │ │ │ ├── catalog.json │ │ │ │ └── data-values.json │ │ ├── lib.rs │ │ └── error.rs │ ├── tests │ │ ├── migrate.rs │ │ └── examples.rs │ ├── Cargo.toml │ ├── README.md │ └── CHANGELOG.md ├── server │ ├── data │ │ ├── 100-sentinel-2-items.parquet │ │ └── joplin │ │ │ ├── collection.json │ │ │ └── feature.geojson │ ├── src │ │ ├── redoc.html │ │ ├── error.rs │ │ └── lib.rs │ ├── Cargo.toml │ └── README.md ├── duckdb │ ├── data │ │ ├── 100-landsat-items.parquet │ │ └── 100-sentinel-2-items.parquet │ ├── scripts │ │ └── generate-test-data │ ├── src │ │ ├── extension.rs │ │ ├── error.rs │ │ └── lib.rs │ ├── Cargo.toml │ └── README.md ├── derive │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── pgstac │ ├── Cargo.toml │ ├── src │ └── page.rs │ └── README.md ├── docs ├── img │ ├── rustac.svg │ ├── rustac-notext.svg │ ├── rustac-small.png │ ├── stac-ferris-2.png │ ├── stac-ferris.png │ ├── stac-ferris-favicon.png │ └── ferris-holding-stac-small.png ├── pronunciation.md ├── history.md ├── formats.md ├── stylesheets │ └── extra.css ├── cli.md └── index.md ├── img ├── rustac.png ├── rustac-small.png ├── stac-ferris.png ├── stac-ferris.xcf ├── stac-ferris-2.png ├── stac-ferris-2.xcf ├── ferris-holding-stac.png ├── ferris-holding-stac.xcf ├── stac-ferris-favicon.png └── ferris-holding-stac-small.png ├── scripts ├── fixtures │ └── 1000-sentinel-2-items.parquet ├── load-pgstac-fixtures ├── validate-stac-server └── validate-stac-geoparquet ├── .gitignore ├── .release-please-manifest.json ├── .markdownlint-cli2.jsonc ├── docker-compose.yml ├── .github ├── workflows │ ├── pr.yml │ ├── release-please.yml │ └── cd.yml ├── dependabot.yml └── pull_request_template.md ├── RELEASING.md ├── release-please-config.json ├── pyproject.toml ├── .pre-commit-config.yaml ├── LICENSE-MIT ├── spec-examples ├── v1.0.0 │ ├── catalog.json │ ├── extensions-collection │ │ └── collection.json │ ├── simple-item.json │ ├── collection.json │ └── core-item.json ├── v1.1.0 │ ├── catalog.json │ ├── extensions-collection │ │ └── collection.json │ ├── simple-item.json │ ├── collection.json │ └── core-item.json └── v1.1.0-beta.1 │ ├── catalog.json │ ├── extensions-collection │ └── collection.json │ ├── simple-item.json │ ├── collection.json │ └── core-item.json ├── mkdocs.yml ├── CONTRIBUTING.md └── Cargo.toml /.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @gadomski 2 | -------------------------------------------------------------------------------- /crates/wasm/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../../LICENSE-MIT -------------------------------------------------------------------------------- /docs/img/rustac.svg: -------------------------------------------------------------------------------- 1 | ../../img/rustac.svg -------------------------------------------------------------------------------- /crates/cli/examples: -------------------------------------------------------------------------------- 1 | ../../spec-examples/v1.1.0 -------------------------------------------------------------------------------- /crates/core/examples: -------------------------------------------------------------------------------- 1 | ../../spec-examples/v1.1.0 -------------------------------------------------------------------------------- /crates/io/examples: -------------------------------------------------------------------------------- 1 | ../../spec-examples/v1.1.0 -------------------------------------------------------------------------------- /crates/wasm/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../../LICENSE-APACHE -------------------------------------------------------------------------------- /crates/extensions/examples: -------------------------------------------------------------------------------- 1 | ../../spec-examples/v1.1.0 -------------------------------------------------------------------------------- /crates/validate/examples: -------------------------------------------------------------------------------- 1 | ../../spec-examples/v1.1.0 -------------------------------------------------------------------------------- /crates/wasm/www/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /docs/img/rustac-notext.svg: -------------------------------------------------------------------------------- 1 | ../../img/rustac-notext.svg -------------------------------------------------------------------------------- /docs/img/rustac-small.png: -------------------------------------------------------------------------------- 1 | ../../img/rustac-small.png -------------------------------------------------------------------------------- /docs/img/stac-ferris-2.png: -------------------------------------------------------------------------------- 1 | ../../img/stac-ferris-2.png -------------------------------------------------------------------------------- /docs/img/stac-ferris.png: -------------------------------------------------------------------------------- 1 | ../../img/stac-ferris.png -------------------------------------------------------------------------------- /docs/img/stac-ferris-favicon.png: -------------------------------------------------------------------------------- 1 | ../../img/stac-ferris-favicon.png -------------------------------------------------------------------------------- /docs/img/ferris-holding-stac-small.png: -------------------------------------------------------------------------------- 1 | ../../img/ferris-holding-stac-small.png -------------------------------------------------------------------------------- /img/rustac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/rustac/HEAD/img/rustac.png -------------------------------------------------------------------------------- /docs/pronunciation.md: -------------------------------------------------------------------------------- 1 | # Pronunciation 2 | 3 | We pronounce **rustac** "ruh-stac". 4 | -------------------------------------------------------------------------------- /crates/server/data/100-sentinel-2-items.parquet: -------------------------------------------------------------------------------- 1 | ../../duckdb/data/100-sentinel-2-items.parquet -------------------------------------------------------------------------------- /img/rustac-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/rustac/HEAD/img/rustac-small.png -------------------------------------------------------------------------------- /img/stac-ferris.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/rustac/HEAD/img/stac-ferris.png -------------------------------------------------------------------------------- /img/stac-ferris.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/rustac/HEAD/img/stac-ferris.xcf -------------------------------------------------------------------------------- /img/stac-ferris-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/rustac/HEAD/img/stac-ferris-2.png -------------------------------------------------------------------------------- /img/stac-ferris-2.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/rustac/HEAD/img/stac-ferris-2.xcf -------------------------------------------------------------------------------- /img/ferris-holding-stac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/rustac/HEAD/img/ferris-holding-stac.png -------------------------------------------------------------------------------- /img/ferris-holding-stac.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/rustac/HEAD/img/ferris-holding-stac.xcf -------------------------------------------------------------------------------- /img/stac-ferris-favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/rustac/HEAD/img/stac-ferris-favicon.png -------------------------------------------------------------------------------- /crates/core/assets/dataset.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/rustac/HEAD/crates/core/assets/dataset.tif -------------------------------------------------------------------------------- /crates/core/assets/dataset_geo.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/rustac/HEAD/crates/core/assets/dataset_geo.tif -------------------------------------------------------------------------------- /img/ferris-holding-stac-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/rustac/HEAD/img/ferris-holding-stac-small.png -------------------------------------------------------------------------------- /crates/io/data/extended-item.parquet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/rustac/HEAD/crates/io/data/extended-item.parquet -------------------------------------------------------------------------------- /crates/core/data/extended-item.parquet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/rustac/HEAD/crates/core/data/extended-item.parquet -------------------------------------------------------------------------------- /crates/duckdb/data/100-landsat-items.parquet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/rustac/HEAD/crates/duckdb/data/100-landsat-items.parquet -------------------------------------------------------------------------------- /crates/duckdb/data/100-sentinel-2-items.parquet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/rustac/HEAD/crates/duckdb/data/100-sentinel-2-items.parquet -------------------------------------------------------------------------------- /scripts/fixtures/1000-sentinel-2-items.parquet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/rustac/HEAD/scripts/fixtures/1000-sentinel-2-items.parquet -------------------------------------------------------------------------------- /crates/io/mocks/not-a-collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": "NotFoundError", 3 | "description": "No collection with id 'not-a-collection' found!" 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | dist/ 4 | pyrightconfig.json 5 | site/ 6 | .cache 7 | crates/wasm/www/package-lock.json 8 | node_modules/ 9 | package.json 10 | package-lock.json 11 | -------------------------------------------------------------------------------- /docs/history.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | Until 2025-04-17, this repository was named **stac-rs**. 4 | See [this RFC](https://github.com/stac-utils/rustac/issues/641) for context on the name change. 5 | -------------------------------------------------------------------------------- /crates/wasm/www/bootstrap.js: -------------------------------------------------------------------------------- 1 | // A dependency graph that contains any wasm must all be imported 2 | // asynchronously. This `bootstrap.js` file does the single async import, so 3 | // that no one else needs to worry about it again. 4 | import("./index.js") 5 | .catch(e => console.error("Error importing `index.js`:", e)); 6 | -------------------------------------------------------------------------------- /scripts/load-pgstac-fixtures: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | scripts=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd) 6 | fixtures="$scripts/fixtures" 7 | dsn=postgresql://username:password@localhost:5432/postgis 8 | 9 | cargo run -- pgstac load "$dsn" "$fixtures/sentinel-2-l2a.json" "$fixtures/1000-sentinel-2-items.parquet" 10 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "crates/cli": "0.2.0", 3 | "crates/core": "0.15.0", 4 | "crates/derive": "0.3.0", 5 | "crates/duckdb": "0.3.0", 6 | "crates/extensions": "0.1.2", 7 | "crates/io": "0.2.0", 8 | "crates/pgstac": "0.4.0", 9 | "crates/server": "0.4.0", 10 | "crates/validate": "0.6.0", 11 | "crates/wasm": "0.1.0" 12 | } 13 | -------------------------------------------------------------------------------- /.markdownlint-cli2.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "MD013": false, 4 | }, 5 | "globs": ["**/*.md"], 6 | "gitignore": true, 7 | "ignores": [ 8 | "spec-examples/*/README.md", 9 | "crates/*/examples/README.md", 10 | "crates/*/CHANGELOG.md", 11 | "target/**/*.md", 12 | "LICENSE-*", 13 | ".github/pull_request_template.md", 14 | ], 15 | } 16 | -------------------------------------------------------------------------------- /crates/cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use rustac::Rustac; 3 | 4 | #[tokio::main] 5 | async fn main() { 6 | let args = Rustac::parse(); 7 | std::process::exit(match args.run(true).await { 8 | Ok(()) => 0, 9 | Err(err) => { 10 | eprintln!("ERROR: {err}"); 11 | 1 // TODO make this more meaningful 12 | } 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /crates/wasm/www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | stac-wasm 6 | 7 | 8 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /crates/validate/src/schemas/v1.0.0/licensing.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/licensing.json#", 4 | "title": "Licensing Fields", 5 | "type": "object", 6 | "properties": { 7 | "license": { 8 | "type": "string", 9 | "pattern": "^[\\w\\-\\.\\+]+$" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /crates/validate/src/schemas/v1.1.0/licensing.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://schemas.stacspec.org/v1.1.0/item-spec/json-schema/licensing.json", 4 | "title": "Licensing Fields", 5 | "type": "object", 6 | "properties": { 7 | "license": { 8 | "type": "string", 9 | "pattern": "^[\\w\\-\\.\\+]+$" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | pgstac: 3 | image: ghcr.io/stac-utils/pgstac:${PGSTAC_VERSION:-v0.9.8} 4 | environment: 5 | - POSTGRES_USER=username 6 | - POSTGRES_PASSWORD=password 7 | - POSTGRES_DB=postgis 8 | - PGUSER=username 9 | - PGPASSWORD=password 10 | - PGDATABASE=postgis 11 | ports: 12 | - "5432:5432" 13 | command: postgres -N 500 14 | -------------------------------------------------------------------------------- /crates/io/tests/aws.rs: -------------------------------------------------------------------------------- 1 | use stac::Catalog; 2 | 3 | #[test] 4 | fn read_from_s3() { 5 | tokio_test::block_on(async { 6 | let (store, path) = stac_io::parse_href_opts( 7 | "s3://nz-elevation/catalog.json", 8 | [("skip_signature", "true"), ("region", "ap-southeast-2")], 9 | ) 10 | .unwrap(); 11 | let _: Catalog = store.get(path).await.unwrap(); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - reopened 9 | 10 | jobs: 11 | lint: 12 | name: Lint 13 | runs-on: ubuntu-latest 14 | permissions: 15 | pull-requests: read 16 | steps: 17 | - uses: amannn/action-semantic-pull-request@v6 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /crates/wasm/www/webpack.config.js: -------------------------------------------------------------------------------- 1 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 2 | const path = require("path"); 3 | 4 | module.exports = { 5 | entry: "./bootstrap.js", 6 | output: { 7 | path: path.resolve(__dirname, "dist"), 8 | filename: "bootstrap.js", 9 | }, 10 | mode: "development", 11 | plugins: [new CopyWebpackPlugin(["index.html"])], 12 | experiments: { 13 | asyncWebAssembly: true, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | We use [release-please](https://github.com/googleapis/release-please) to manage versioning and creating Github releases. 4 | Look for a release [pull request](https://github.com/stac-utils/rustac/pulls) to see what's queued up. 5 | To release, simply merge that pull request, then: 6 | 7 | ```sh 8 | cargo publish --workspace 9 | ``` 10 | 11 | You may need to `--exclude` or `--include` certain packages, depending on what's changed. 12 | -------------------------------------------------------------------------------- /crates/validate/tests/migrate.rs: -------------------------------------------------------------------------------- 1 | use rstest::rstest; 2 | use stac::{Migrate, Value, Version}; 3 | use stac_validate::Validate; 4 | use std::path::PathBuf; 5 | 6 | #[rstest] 7 | #[tokio::test] 8 | async fn v1_0_0_to_v1_1_0(#[files("../../spec-examples/v1.0.0/**/*.json")] path: PathBuf) { 9 | let value: Value = stac::read(path.to_str().unwrap()).unwrap(); 10 | let value = value.migrate(&Version::v1_1_0).unwrap(); 11 | value.validate().await.unwrap(); 12 | } 13 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["cargo-workspace"], 3 | "release-type": "rust", 4 | "bump-minor-pre-major": true, 5 | "bump-patch-for-minor-pre-major": true, 6 | "packages": { 7 | "crates/cli": {}, 8 | "crates/core": {}, 9 | "crates/derive": {}, 10 | "crates/duckdb": {}, 11 | "crates/extensions": {}, 12 | "crates/io": {}, 13 | "crates/pgstac": {}, 14 | "crates/server": {}, 15 | "crates/validate": {}, 16 | "crates/wasm": {} 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /crates/derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stac-derive" 3 | description = "Proc macros for deriving STAC traits. Should usually not be used directly." 4 | version = "0.3.0" 5 | authors.workspace = true 6 | edition.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | license.workspace = true 10 | categories.workspace = true 11 | rust-version.workspace = true 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | quote.workspace = true 18 | syn.workspace = true 19 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "rustac" 3 | version = "0.0.0" 4 | description = "This package should never be released, it's just for uv." 5 | requires-python = ">=3.12" 6 | dependencies = [] 7 | 8 | [dependency-groups] 9 | docs = ["mkdocs-material[imaging]>=9.5.40", "mkdocs-redirects>=1.2.2"] 10 | stac-geoparquet = [ 11 | "deepdiff>=8.0.1", 12 | "pyarrow>=17.0.0", 13 | "stac-geoparquet>=0.6.0", 14 | ] 15 | stac-api-validator = ["setuptools>=75.1.0", "stac-api-validator>=0.6.3"] 16 | 17 | [tool.uv] 18 | default-groups = ["docs", "stac-geoparquet", "stac-api-validator"] 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | labels: [] 8 | - package-ecosystem: "cargo" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | labels: [] 13 | groups: 14 | geoarrow: 15 | patterns: 16 | - "geoarrow-*" 17 | - "geoparquet" 18 | - "arrow-*" 19 | - "parquet" 20 | - package-ecosystem: "pip" 21 | directory: "/" 22 | schedule: 23 | interval: "weekly" 24 | labels: [] 25 | -------------------------------------------------------------------------------- /crates/wasm/www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stac-wasm-test", 3 | "version": "0.0.0", 4 | "description": "STAC WASM test", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack --config webpack.config.js", 8 | "start": "webpack-dev-server" 9 | }, 10 | "keywords": [], 11 | "license": "(MIT OR Apache-2.0)", 12 | "devDependencies": { 13 | "stac-wasm": "file:../pkg", 14 | "@duckdb/duckdb-wasm": "^1.28.0", 15 | "webpack": "^5.99.8", 16 | "webpack-cli": "^6.0.1", 17 | "webpack-dev-server": "^5.2.1", 18 | "copy-webpack-plugin": "^5.0.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/formats.md: -------------------------------------------------------------------------------- 1 | # Formats 2 | 3 | **rustac** "speaks" three forms of STAC: 4 | 5 | - **JSON**: STAC is derived from [GeoJSON](https://geojson.org/) 6 | - **Newline-delimited JSON (ndjson)**: One JSON [item](https://github.com/radiantearth/stac-spec/blob/master/item-spec/item-spec.md) per line, often used for bulk item loading and storage 7 | - **stac-geoparquet**: A newer [specification](https://github.com/radiantearth/stac-geoparquet-spec) for storing STAC items, and optionally collections 8 | 9 | We also have interfaces to other storage backends, e.g. Postgres via [pgstac](https://github.com/stac-utils/pgstac). 10 | -------------------------------------------------------------------------------- /crates/io/src/read.rs: -------------------------------------------------------------------------------- 1 | use crate::{Format, Readable, Result}; 2 | use stac::SelfHref; 3 | 4 | /// Reads a STAC value from an href. 5 | /// 6 | /// The format will be inferred from the href's extension. If you want to 7 | /// specify the format, use [Format::read]. 8 | /// 9 | /// # Examples 10 | /// 11 | /// ``` 12 | /// let item: stac::Item = stac_io::read("examples/simple-item.json").unwrap(); 13 | /// ``` 14 | pub fn read(href: impl ToString) -> Result { 15 | let href = href.to_string(); 16 | let format = Format::infer_from_href(&href).unwrap_or_default(); 17 | format.read(href) 18 | } 19 | -------------------------------------------------------------------------------- /crates/validate/tests/examples.rs: -------------------------------------------------------------------------------- 1 | use rstest::rstest; 2 | use stac::Value; 3 | use stac_validate::Validate; 4 | use std::path::PathBuf; 5 | 6 | #[rstest] 7 | #[tokio::test] 8 | async fn v1_0_0(#[files("../../spec-examples/v1.0.0/**/*.json")] path: PathBuf) { 9 | let value: Value = stac::read(path.to_str().unwrap()).unwrap(); 10 | value.validate().await.unwrap(); 11 | } 12 | 13 | #[rstest] 14 | #[tokio::test] 15 | async fn v1_1_0(#[files("../../spec-examples/v1.1.0/**/*.json")] path: PathBuf) { 16 | let value: Value = stac::read(path.to_str().unwrap()).unwrap(); 17 | value.validate().await.unwrap(); 18 | } 19 | -------------------------------------------------------------------------------- /crates/extensions/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stac-extensions" 3 | description = "Manage STAC extensions (https://stac-extensions.github.io/)" 4 | version = "0.1.2" 5 | keywords = ["geospatial", "stac", "extensions"] 6 | authors.workspace = true 7 | edition.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | license.workspace = true 11 | categories.workspace = true 12 | rust-version.workspace = true 13 | 14 | [dependencies] 15 | geojson.workspace = true 16 | indexmap.workspace = true 17 | serde.workspace = true 18 | serde_json.workspace = true 19 | stac = { version = "0.15.0", path = "../core" } 20 | -------------------------------------------------------------------------------- /docs/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --md-primary-fg-color: rgb(55, 108, 129); 3 | --md-primary-fg-color--light: rgb(228, 246, 251); 4 | --md-primary-fg-color--dark: rgb(26, 78, 99); 5 | --md-accent-fg-color: rgb(78, 180, 174); 6 | --md-accent-fg-color--transparent: rgba(78, 180, 174, 0.5); 7 | } 8 | 9 | [data-md-color-scheme="stac"] { 10 | --md-primary-fg-color: rgb(55, 108, 129); 11 | --md-primary-fg-color--light: rgb(228, 246, 251); 12 | --md-primary-fg-color--dark: rgb(26, 78, 99); 13 | --md-accent-fg-color: rgb(78, 180, 174); 14 | --md-accent-fg-color--transparent: rgba(78, 180, 174, 0.5); 15 | } 16 | -------------------------------------------------------------------------------- /crates/validate/src/schemas/v1.0.0/basics.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/basics.json#", 4 | "title": "Basic Descriptive Fields", 5 | "type": "object", 6 | "properties": { 7 | "title": { 8 | "title": "Item Title", 9 | "description": "A human-readable title describing the Item.", 10 | "type": "string" 11 | }, 12 | "description": { 13 | "title": "Item Description", 14 | "description": "Detailed multi-line description to fully explain the Item.", 15 | "type": "string" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /crates/validate/src/schemas/v1.1.0/bands.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://schemas.stacspec.org/v1.1.0/item-spec/json-schema/bands.json", 4 | "title": "Bands Field", 5 | "type": "object", 6 | "properties": { 7 | "bands": { 8 | "type": "array", 9 | "items": { 10 | "type": "object", 11 | "properties": { 12 | "name": { 13 | "type": "string" 14 | } 15 | }, 16 | "allOf": [ 17 | { 18 | "$ref": "common.json" 19 | } 20 | ] 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | permissions: 7 | contents: write 8 | issues: write 9 | pull-requests: write 10 | 11 | name: release-please 12 | 13 | jobs: 14 | release-please: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/create-github-app-token@v2 18 | id: generate-token 19 | with: 20 | app-id: ${{ vars.RELEASE_BOT_CLIENT_ID }} 21 | private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }} 22 | - uses: googleapis/release-please-action@v4 23 | with: 24 | token: ${{ steps.generate-token.outputs.token }} 25 | -------------------------------------------------------------------------------- /crates/server/src/redoc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | stac-server API documentation 5 | 6 | 7 | 8 | 9 | 10 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | Description of the changes, including any "sidecar" changes that came along (e.g. small bugfixes you found along the way). 4 | 5 | ### Related issues 6 | 7 | - List any issues that this pull request closes or is related to 8 | - Delete this section if it is not applicable 9 | 10 | ### Checklist 11 | 12 | Delete any checklist items that do not apply (e.g. if your change is minor, it may not require documentation updates). 13 | 14 | - [ ] Unit tests 15 | - [ ] Documentation, including doctests 16 | - [ ] Pull request title follows [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) 17 | - [ ] Pre-commit hooks pass (`prek run --all-files`) 18 | -------------------------------------------------------------------------------- /crates/io/src/write.rs: -------------------------------------------------------------------------------- 1 | use crate::{Format, Result, Writeable}; 2 | use std::path::Path; 3 | 4 | /// Writes a STAC value to a path. 5 | /// 6 | /// The format will be inferred from the href's extension. If you want to 7 | /// specify the format, use [Format::write]. 8 | /// 9 | /// # Examples 10 | /// 11 | /// ```no_run 12 | /// use stac::Item; 13 | /// 14 | /// let item = Item::new("an-id"); 15 | /// stac_io::write("an-id.json", item).unwrap(); 16 | /// ``` 17 | pub fn write(path: impl AsRef, value: T) -> Result<()> { 18 | let path = path.as_ref(); 19 | let format = path 20 | .to_str() 21 | .and_then(Format::infer_from_href) 22 | .unwrap_or_default(); 23 | format.write(path, value) 24 | } 25 | -------------------------------------------------------------------------------- /crates/validate/src/schemas/v1.1.0/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://schemas.stacspec.org/v1.1.0/item-spec/json-schema/commonjson", 4 | "title": "STAC Common Metadata", 5 | "type": "object", 6 | "description": "This schema includes all common metadata fields.", 7 | "allOf": [ 8 | { 9 | "$ref": "basics.json" 10 | }, 11 | { 12 | "$ref": "bands.json" 13 | }, 14 | { 15 | "$ref": "datetime.json" 16 | }, 17 | { 18 | "$ref": "data-values.json" 19 | }, 20 | { 21 | "$ref": "instrument.json" 22 | }, 23 | { 24 | "$ref": "licensing.json" 25 | }, 26 | { 27 | "$ref": "provider.json" 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v6.0.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - repo: https://github.com/DavidAnson/markdownlint-cli2 8 | rev: v0.19.0 9 | hooks: 10 | - id: markdownlint-cli2 11 | pass_filenames: false 12 | - repo: local 13 | hooks: 14 | - id: cargo fmt 15 | name: cargo fmt 16 | entry: cargo 17 | language: system 18 | pass_filenames: false 19 | args: ["fmt", "--all"] 20 | - id: cargo clippy 21 | name: cargo clippy 22 | entry: cargo 23 | language: system 24 | pass_filenames: false 25 | args: ["clippy", "--workspace"] 26 | -------------------------------------------------------------------------------- /crates/duckdb/scripts/generate-test-data: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | scripts=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd) 6 | duckdb=$(dirname $scripts) 7 | data="$duckdb/data" 8 | 9 | cargo run -- \ 10 | search https://planetarycomputer.microsoft.com/api/stac/v1 "$data/100-sentinel-2-items.parquet" \ 11 | -c sentinel-2-l2a \ 12 | --max-items 100 \ 13 | --sortby=-datetime \ 14 | --intersects '{"type":"Point","coordinates":[-105.1019,40.1672]}' 15 | 16 | cargo run -- \ 17 | search https://planetarycomputer.microsoft.com/api/stac/v1 "$data/100-landsat-items.parquet" \ 18 | -c landsat-c2-l2 \ 19 | --max-items 100 \ 20 | --sortby=-datetime \ 21 | --intersects '{"type":"Point","coordinates":[-105.1019,40.1672]}' 22 | 23 | -------------------------------------------------------------------------------- /crates/io/src/realized_href.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use url::Url; 3 | 4 | /// An href that has been realized to a path or a url. 5 | #[derive(Debug)] 6 | pub enum RealizedHref { 7 | /// A path buf 8 | PathBuf(PathBuf), 9 | 10 | /// A url 11 | Url(Url), 12 | } 13 | 14 | impl From<&str> for RealizedHref { 15 | fn from(s: &str) -> RealizedHref { 16 | if let Ok(url) = Url::parse(s) { 17 | if url.scheme() == "file" { 18 | url.to_file_path() 19 | .map(RealizedHref::PathBuf) 20 | .unwrap_or_else(|_| RealizedHref::Url(url)) 21 | } else { 22 | RealizedHref::Url(url) 23 | } 24 | } else { 25 | RealizedHref::PathBuf(PathBuf::from(s)) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /crates/core/src/geo.rs: -------------------------------------------------------------------------------- 1 | //! Geometry utilities, enabled by the `geo` feature. 2 | 3 | use crate::{Error, Result}; 4 | use geo::{Rect, coord}; 5 | 6 | /// Creates a two-dimensional rectangle from four coordinates. 7 | /// 8 | /// # Examples 9 | /// 10 | /// ``` 11 | /// let bbox = stac::geo::bbox(&vec![-106.0, 41.0, -105.0, 42.0]).unwrap(); 12 | /// ``` 13 | pub fn bbox(coordinates: &[f64]) -> Result { 14 | if coordinates.len() == 4 { 15 | Ok(Rect::new( 16 | coord! { x: coordinates[0], y: coordinates[1] }, 17 | coord! { x: coordinates[2], y: coordinates[3] }, 18 | )) 19 | } else { 20 | // TODO support three dimensional 21 | Err(Error::InvalidBbox( 22 | coordinates.to_vec(), 23 | "unsupported 3D bbox", 24 | )) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /crates/extensions/README.md: -------------------------------------------------------------------------------- 1 | # stac-extensions 2 | 3 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/stac-utils/rustac/ci.yml?branch=main&style=for-the-badge)](https://github.com/stac-utils/rustac/actions/workflows/ci.yml) 4 | [![docs.rs](https://img.shields.io/docsrs/stac-extensions?style=for-the-badge)](https://docs.rs/stac-extensions/latest/stac_extensions/) 5 | [![Crates.io](https://img.shields.io/crates/v/stac-extensions?style=for-the-badge)](https://crates.io/crates/stac-extensions) 6 | ![Crates.io](https://img.shields.io/crates/l/stac-extensions?style=for-the-badge) 7 | 8 | Rudimentary support for [STAC extensions](https://stac-extensions.github.io/). 9 | 10 | ## Other info 11 | 12 | This crate is part of the [rustac](https://github.com/stac-utils/rustac) monorepo, see its README for contributing and license information. 13 | -------------------------------------------------------------------------------- /crates/validate/src/schemas/v1.0.0/instrument.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/instrument.json#", 4 | "title": "Instrument Fields", 5 | "type": "object", 6 | "properties": { 7 | "platform": { 8 | "title": "Platform", 9 | "type": "string" 10 | }, 11 | "instruments": { 12 | "title": "Instruments", 13 | "type": "array", 14 | "items": { 15 | "type": "string" 16 | } 17 | }, 18 | "constellation": { 19 | "title": "Constellation", 20 | "type": "string" 21 | }, 22 | "mission": { 23 | "title": "Mission", 24 | "type": "string" 25 | }, 26 | "gsd": { 27 | "title": "Ground Sample Distance", 28 | "type": "number", 29 | "exclusiveMinimum": 0 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /crates/validate/src/schemas/v1.1.0/instrument.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://schemas.stacspec.org/v1.1.0/item-spec/json-schema/instrument.json", 4 | "title": "Instrument Fields", 5 | "type": "object", 6 | "properties": { 7 | "platform": { 8 | "title": "Platform", 9 | "type": "string" 10 | }, 11 | "instruments": { 12 | "title": "Instruments", 13 | "type": "array", 14 | "items": { 15 | "type": "string" 16 | } 17 | }, 18 | "constellation": { 19 | "title": "Constellation", 20 | "type": "string" 21 | }, 22 | "mission": { 23 | "title": "Mission", 24 | "type": "string" 25 | }, 26 | "gsd": { 27 | "title": "Ground Sample Distance", 28 | "type": "number", 29 | "exclusiveMinimum": 0 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /crates/wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stac-wasm" 3 | version = "0.1.0" 4 | readme = "README.md" 5 | description = "Converts Arrow arrays to STAC items, via WASM" 6 | authors.workspace = true 7 | edition.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | license.workspace = true 11 | categories.workspace = true 12 | rust-version.workspace = true 13 | publish = false 14 | 15 | [lib] 16 | crate-type = ["cdylib", "rlib"] 17 | 18 | [dependencies] 19 | arrow-array.workspace = true 20 | arrow-schema.workspace = true 21 | arrow-wasm = { git = "https://github.com/kylebarron/arrow-wasm", rev = "6da94ef0a1522a244984a7d3d58a0339d0851d96" } 22 | serde.workspace = true 23 | serde-wasm-bindgen = "0.6.5" 24 | stac = { version = "0.15.0", path = "../core", features = ["geoparquet"] } 25 | thiserror.workspace = true 26 | wasm-bindgen = "0.2.84" 27 | 28 | [dev-dependencies] 29 | wasm-bindgen-test = "0.3.34" 30 | -------------------------------------------------------------------------------- /crates/duckdb/src/extension.rs: -------------------------------------------------------------------------------- 1 | /// A DuckDB extension 2 | // TODO implement aliases ... I don't know how vectors work yet 😢 3 | #[derive(Debug)] 4 | pub struct Extension { 5 | /// The extension name. 6 | pub name: String, 7 | 8 | /// Is the extension loaded? 9 | pub loaded: bool, 10 | 11 | /// Is the extension installed? 12 | pub installed: bool, 13 | 14 | /// The path to the extension. 15 | /// 16 | /// This might be `(BUILT-IN)` for the core extensions. 17 | pub install_path: Option, 18 | 19 | /// The extension description. 20 | pub description: String, 21 | 22 | /// The extension version. 23 | pub version: Option, 24 | 25 | /// The install mode. 26 | /// 27 | /// We don't bother making this an enum, yet. 28 | pub install_mode: Option, 29 | 30 | /// Where the extension was installed from. 31 | pub installed_from: Option, 32 | } 33 | -------------------------------------------------------------------------------- /crates/pgstac/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pgstac" 3 | description = "Rust interface for pgstac" 4 | version = "0.4.0" 5 | keywords = ["geospatial", "stac", "metadata", "raster", "database"] 6 | categories = ["database", "data-structures", "science"] 7 | authors.workspace = true 8 | edition.workspace = true 9 | homepage.workspace = true 10 | repository.workspace = true 11 | license.workspace = true 12 | rust-version.workspace = true 13 | 14 | [dependencies] 15 | serde.workspace = true 16 | serde_json.workspace = true 17 | stac = { version = "0.15.0", path = "../core" } 18 | thiserror.workspace = true 19 | tokio-postgres = { workspace = true, features = ["with-serde_json-1"] } 20 | 21 | [dev-dependencies] 22 | geojson.workspace = true 23 | rstest.workspace = true 24 | tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } 25 | tokio-test.workspace = true 26 | 27 | [package.metadata.docs.rs] 28 | all-features = true 29 | rustdoc-args = ["--cfg", "docsrs"] 30 | -------------------------------------------------------------------------------- /crates/core/src/statistics.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// Statistics of all pixels in the band. 4 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 5 | pub struct Statistics { 6 | /// Mean value of all the pixels in the band 7 | #[serde(skip_serializing_if = "Option::is_none")] 8 | pub mean: Option, 9 | 10 | /// Minimum value of all the pixels in the band 11 | #[serde(skip_serializing_if = "Option::is_none")] 12 | pub minimum: Option, 13 | 14 | /// Maximum value of all the pixels in the band 15 | #[serde(skip_serializing_if = "Option::is_none")] 16 | pub maximum: Option, 17 | 18 | /// Standard deviation value of all the pixels in the band 19 | #[serde(skip_serializing_if = "Option::is_none")] 20 | pub stddev: Option, 21 | 22 | /// Percentage of valid (not nodata) pixel 23 | #[serde(skip_serializing_if = "Option::is_none")] 24 | pub valid_percent: Option, 25 | } 26 | -------------------------------------------------------------------------------- /docs/cli.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: The rustac command-line interface (CLI) 3 | --- 4 | 5 | # Command-line interface (CLI) 6 | 7 | The **rustac** command-line interface can be installed two ways. 8 | If you have Rust, use `cargo`: 9 | 10 | ```sh 11 | cargo install rustac # to use libduckdb on your system 12 | # or 13 | cargo install rustac -F duckdb-bundled # to build libduckdb on install (slow) 14 | ``` 15 | 16 | The CLI is called **rustac**: 17 | 18 | ```shell 19 | rustac --help 20 | ``` 21 | 22 | If you don't have DuckDB on your system, you can also use the Python wheel, which includes **libduckdb**: 23 | 24 | ```shell 25 | python -m pip install rustac 26 | ``` 27 | 28 | To get shell completions, use: 29 | 30 | ```shell 31 | rustac generate-completions > 32 | ``` 33 | 34 | ## History 35 | 36 | The CLI was announced at [@gadomski's](https://github.com/gadomski/) [2024 FOSS4G-NA presentation](https://www.gadom.ski/2024-09-FOSS4G-NA-rustac/). 37 | -------------------------------------------------------------------------------- /crates/validate/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stac-validate" 3 | version = "0.6.0" 4 | readme = "README.md" 5 | description = "json-schema validation for the Rust implementation of the STAC specification" 6 | authors.workspace = true 7 | edition.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | license.workspace = true 11 | categories.workspace = true 12 | rust-version.workspace = true 13 | 14 | [dependencies] 15 | fluent-uri.workspace = true 16 | jsonschema.workspace = true 17 | reqwest = { workspace = true, features = ["blocking", "json"] } 18 | serde.workspace = true 19 | serde_json.workspace = true 20 | stac = { version = "0.15.0", path = "../core" } 21 | thiserror.workspace = true 22 | async-trait.workspace = true 23 | referencing.workspace = true 24 | async-recursion.workspace = true 25 | 26 | [dev-dependencies] 27 | stac-io = { version = "0.2.0", path = "../io" } 28 | rstest.workspace = true 29 | tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } 30 | -------------------------------------------------------------------------------- /crates/core/src/api/collections.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::{Map, Value}; 3 | use stac::{Collection, Link}; 4 | use stac_derive::{Links, SelfHref}; 5 | 6 | /// Object containing an array of collections and an array of links. 7 | #[derive(Debug, Serialize, Deserialize, SelfHref, Links)] 8 | pub struct Collections { 9 | /// The [Collection] objects in the [stac::Catalog]. 10 | pub collections: Vec, 11 | 12 | /// The [stac::Link] relations. 13 | pub links: Vec, 14 | 15 | /// Additional fields. 16 | #[serde(flatten)] 17 | pub additional_fields: Map, 18 | 19 | #[serde(skip)] 20 | self_href: Option, 21 | } 22 | 23 | impl From> for Collections { 24 | fn from(collections: Vec) -> Collections { 25 | Collections { 26 | collections, 27 | links: Vec::new(), 28 | additional_fields: Map::new(), 29 | self_href: None, 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /crates/extensions/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [0.1.2](https://github.com/stac-utils/rustac/compare/stac-extensions-v0.1.1...stac-extensions-v0.1.2) (2025-12-01) 8 | 9 | 10 | ### Dependencies 11 | 12 | * The following workspace dependencies were updated 13 | * dependencies 14 | * stac bumped from 0.14.0 to 0.15.0 15 | 16 | ## [0.1.1] - 2025-11-14 17 | 18 | Update **stac** dependency. 19 | 20 | ## [0.1.0] - 2025-01-31 21 | 22 | Initial release. 23 | 24 | [Unreleased]: https://github.com/stac-utils/rustac/compare/stac-extensions-v0.1.1...main 25 | [0.1.1]: https://github.com/stac-utils/rustac/compare/stac-extensions-v0.1.0...stac-extensions-v0.1.1 26 | [0.1.0]: https://github.com/stac-utils/rustac/releases/tag/v0.1.0 27 | 28 | 29 | -------------------------------------------------------------------------------- /crates/validate/src/schemas/v1.1.0/basics.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://schemas.stacspec.org/v1.1.0/item-spec/json-schema/basics.json", 4 | "title": "Basic Descriptive Fields", 5 | "type": "object", 6 | "properties": { 7 | "title": { 8 | "title": "Title", 9 | "description": "A human-readable title describing the entity.", 10 | "type": "string" 11 | }, 12 | "description": { 13 | "title": "Description", 14 | "description": "Detailed multi-line description to fully explain the entity.", 15 | "type": "string", 16 | "minLength": 1 17 | }, 18 | "keywords": { 19 | "title": "Keywords", 20 | "description": "List of keywords describing the entity.", 21 | "type": "array", 22 | "items": { 23 | "type": "string" 24 | } 25 | }, 26 | "roles": { 27 | "title": "Roles", 28 | "type": "array", 29 | "items": { 30 | "type": "string" 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /crates/validate/README.md: -------------------------------------------------------------------------------- 1 | # stac-validate 2 | 3 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/stac-utils/rustac/ci.yml?branch=main&style=for-the-badge)](https://github.com/stac-utils/rustac/actions/workflows/ci.yml) 4 | [![docs.rs](https://img.shields.io/docsrs/stac-validate?style=for-the-badge)](https://docs.rs/stac-validate/latest/stac_validate/) 5 | [![Crates.io](https://img.shields.io/crates/v/stac-validate?style=for-the-badge)](https://crates.io/crates/stac-validate) 6 | ![Crates.io](https://img.shields.io/crates/l/stac-validate?style=for-the-badge) 7 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg?style=for-the-badge)](./CODE_OF_CONDUCT) 8 | 9 | [json-schema](https://json-schema.org/) validation for the Rust implementation of the [SpatioTemporal Asset Catalog (STAC)](https://stacspec.org/) specification. 10 | 11 | ## Other info 12 | 13 | This crate is part of the [rustac](https://github.com/stac-utils/rustac) monorepo, see its README for contributing and license information. 14 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /crates/io/README.md: -------------------------------------------------------------------------------- 1 | # stac-io 2 | 3 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/stac-utils/rustac/ci.yml?branch=main&style=for-the-badge)](https://github.com/stac-utils/rustac/actions/workflows/ci.yml) 4 | [![docs.rs](https://img.shields.io/docsrs/stac-io?style=for-the-badge)](https://docs.rs/stac-io/latest/stac_io/) 5 | [![Crates.io](https://img.shields.io/crates/v/stac-io?style=for-the-badge)](https://crates.io/crates/stac-io) 6 | ![Crates.io](https://img.shields.io/crates/l/stac-io?style=for-the-badge) 7 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg?style=for-the-badge)](./CODE_OF_CONDUCT) 8 | 9 | Input and output (I/O) for the Rust implementation of the [SpatioTemporal Asset Catalog (STAC)](https://stacspec.org/) specification. 10 | 11 | ## Usage 12 | 13 | To use the library in your project: 14 | 15 | ```toml 16 | [dependencies] 17 | stac-io = "0.1" 18 | ``` 19 | 20 | ## Other info 21 | 22 | This crate is part of the [rustac](https://github.com/stac-utils/rustac) monorepo, see its README for contributing and license information. 23 | -------------------------------------------------------------------------------- /crates/core/data/bands-v1.1.0.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "stac_version": "1.1.0", 4 | "id": "bands-migration", 5 | "geometry": null, 6 | "properties": { 7 | "datetime": "2024-08-11T16:07:32.766181Z" 8 | }, 9 | "links": [], 10 | "assets": { 11 | "example": { 12 | "href": "example.tif", 13 | "data_type": "uint16", 14 | "raster:sampling": "area", 15 | "raster:spatial_resolution": 10, 16 | "bands": [ 17 | { 18 | "name": "r", 19 | "eo:common_name": "red" 20 | }, 21 | { 22 | "name": "g", 23 | "eo:common_name": "green" 24 | }, 25 | { 26 | "name": "b", 27 | "eo:common_name": "blue" 28 | }, 29 | { 30 | "name": "nir", 31 | "eo:common_name": "nir", 32 | "raster:spatial_resolution": 30 33 | } 34 | ] 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /crates/duckdb/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stac-duckdb" 3 | description = "Client for querying stac-geoparquet using DuckDB" 4 | version = "0.3.0" 5 | keywords = ["geospatial", "stac", "metadata", "geo", "raster"] 6 | authors.workspace = true 7 | edition.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | license.workspace = true 11 | categories.workspace = true 12 | rust-version.workspace = true 13 | 14 | [features] 15 | default = [] 16 | bundled = ["duckdb/bundled"] 17 | 18 | [dependencies] 19 | arrow-array.workspace = true 20 | arrow-schema.workspace = true 21 | chrono.workspace = true 22 | cql2.workspace = true 23 | duckdb.workspace = true 24 | geo.workspace = true 25 | geoarrow-schema = { workspace = true } 26 | geojson.workspace = true 27 | getrandom.workspace = true 28 | log.workspace = true 29 | serde_json.workspace = true 30 | stac = { version = "0.15.0", path = "../core", features = ["geoarrow", "geo"] } 31 | thiserror.workspace = true 32 | 33 | [dev-dependencies] 34 | geo.workspace = true 35 | rstest.workspace = true 36 | stac-validate = { version = "0.6.0", path = "../validate" } 37 | tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } 38 | -------------------------------------------------------------------------------- /crates/core/src/data_type.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// The data type gives information about the values in the file. 4 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 5 | #[serde(rename_all = "lowercase")] 6 | pub enum DataType { 7 | /// 8-bit integer 8 | Int8, 9 | 10 | /// 16-bit integer 11 | Int16, 12 | 13 | /// 32-bit integer 14 | Int32, 15 | 16 | /// 64-bit integer 17 | Int64, 18 | 19 | /// Unsigned 8-bit integer (common for 8-bit RGB PNG's) 20 | UInt8, 21 | 22 | /// Unsigned 16-bit integer 23 | UInt16, 24 | 25 | /// Unsigned 32-bit integer 26 | UInt32, 27 | 28 | /// Unsigned 64-bit integer 29 | UInt64, 30 | 31 | /// 16-bit float 32 | Float16, 33 | 34 | /// 32-bit float 35 | Float32, 36 | 37 | /// 64-bit float 38 | Float64, 39 | 40 | /// 16-bit complex integer 41 | CInt16, 42 | 43 | /// 32-bit complex integer 44 | CInt32, 45 | 46 | /// 32-bit complex float 47 | CFloat32, 48 | 49 | /// 64-bit complex float 50 | CFloat64, 51 | 52 | /// Other data type than the ones listed above (e.g. boolean, string, higher precision numbers) 53 | Other, 54 | } 55 | -------------------------------------------------------------------------------- /crates/duckdb/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | /// A crate-specific error enum. 4 | #[derive(Debug, Error)] 5 | #[non_exhaustive] 6 | pub enum Error { 7 | /// [chrono::format::ParseError] 8 | #[error(transparent)] 9 | ChronoParse(#[from] chrono::format::ParseError), 10 | 11 | /// [cql2::Error] 12 | #[error(transparent)] 13 | Cql2(#[from] Box), 14 | 15 | /// [duckdb::Error] 16 | #[error(transparent)] 17 | DuckDB(#[from] duckdb::Error), 18 | 19 | /// [geoarrow_schema::error::GeoArrowError] 20 | #[error(transparent)] 21 | GeoArrow(#[from] geoarrow_schema::error::GeoArrowError), 22 | 23 | /// [serde_json::Error] 24 | #[error(transparent)] 25 | SerdeJson(#[from] serde_json::Error), 26 | 27 | /// [geojson::Error] 28 | #[error(transparent)] 29 | GeoJSON(#[from] Box), 30 | 31 | /// [stac::Error] 32 | #[error(transparent)] 33 | Stac(#[from] stac::Error), 34 | 35 | /// The query search extension is not implemented. 36 | #[error("query is not implemented")] 37 | QueryNotImplemented, 38 | 39 | /// [std::num::TryFromIntError] 40 | #[error(transparent)] 41 | TryFromInt(#[from] std::num::TryFromIntError), 42 | } 43 | -------------------------------------------------------------------------------- /crates/duckdb/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Use [duckdb](https://duckdb.org/) with [STAC](https://stacspec.org). 2 | 3 | #![warn(unused_crate_dependencies)] 4 | 5 | mod client; 6 | mod error; 7 | mod extension; 8 | 9 | pub use {client::Client, error::Error, extension::Extension}; 10 | 11 | use getrandom as _; 12 | 13 | /// Searches a stac-geoparquet file. 14 | /// 15 | /// # Examples 16 | /// 17 | /// ``` 18 | /// let item_collection = stac_duckdb::search("data/100-sentinel-2-items.parquet", Default::default(), None).unwrap(); 19 | /// ``` 20 | pub fn search( 21 | href: &str, 22 | mut search: stac::api::Search, 23 | max_items: Option, 24 | ) -> Result { 25 | if let Some(max_items) = max_items { 26 | search.limit = Some(max_items.try_into()?); 27 | } else { 28 | search.limit = None; 29 | }; 30 | let client = Client::new()?; 31 | client.search(href, search) 32 | } 33 | 34 | /// A crate-specific result type. 35 | pub type Result = std::result::Result; 36 | 37 | /// Return this crate's version. 38 | /// 39 | /// # Examples 40 | /// 41 | /// ``` 42 | /// println!("{}", stac_duckdb::version()); 43 | /// ``` 44 | pub fn version() -> &'static str { 45 | env!("CARGO_PKG_VERSION") 46 | } 47 | -------------------------------------------------------------------------------- /scripts/validate-stac-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # 3 | # Validate stac server using stac-api-validator. 4 | # 5 | # To use this script on macos, you'll need `timeout`, which is provided by Homebrew's coreutils: 6 | # 7 | # brew install coreutils 8 | 9 | set -e 10 | 11 | args="crates/server/data/sentinel-2/*" 12 | build_args="--no-default-features" 13 | conformance="--conformance core --conformance features --conformance item-search" 14 | 15 | if [ $# -eq 1 ]; then 16 | if [ "$1" = "--pgstac" ]; then 17 | args="$args --pgstac postgres://username:password@localhost/postgis" 18 | build_args="$build_args -F pgstac" 19 | conformance="$conformance --conformance filter" 20 | else 21 | echo "Unknown argument: $1" 22 | exit 1 23 | fi 24 | fi 25 | 26 | cargo build -p rustac $build_args 27 | cargo run -p rustac $build_args -- serve $args & 28 | server_pid=$! 29 | echo "server_pid=$server_pid" 30 | set +e 31 | scripts/wait-for-it.sh localhost:7822 && \ 32 | stac-api-validator \ 33 | --root-url http://localhost:7822 \ 34 | $conformance \ 35 | --collection sentinel-2-c1-l2a \ 36 | --geometry '{"type":"Point","coordinates":[-105.07,40.08]}' 37 | status=$? 38 | set -e 39 | kill $server_pid 40 | exit $status 41 | -------------------------------------------------------------------------------- /crates/server/data/joplin/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "joplin", 3 | "description": "This imagery was acquired by the NOAA Remote Sensing Division to support NOAA national security and emergency response requirements. In addition, it will be used for ongoing research efforts for testing and developing standards for airborne digital imagery. Individual images have been combined into a larger mosaic and tiled for distribution. The approximate ground sample distance (GSD) for each pixel is 35 cm (1.14 feet).", 4 | "stac_version": "1.0.0", 5 | "license": "public-domain", 6 | "links": [ 7 | { 8 | "rel": "license", 9 | "href": "https://creativecommons.org/licenses/publicdomain/", 10 | "title": "public domain" 11 | } 12 | ], 13 | "type": "Collection", 14 | "extent": { 15 | "spatial": { 16 | "bbox": [ 17 | [ 18 | -94.6911621, 19 | 37.0332547, 20 | -94.402771, 21 | 37.1077651 22 | ] 23 | ] 24 | }, 25 | "temporal": { 26 | "interval": [ 27 | [ 28 | "2000-02-01T00:00:00Z", 29 | "2000-02-12T00:00:00Z" 30 | ] 31 | ] 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /crates/validate/src/schemas/v1.0.0/provider.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/provider.json#", 4 | "title": "Provider Fields", 5 | "type": "object", 6 | "properties": { 7 | "providers": { 8 | "title": "Providers", 9 | "type": "array", 10 | "items": { 11 | "type": "object", 12 | "required": [ 13 | "name" 14 | ], 15 | "properties": { 16 | "name": { 17 | "title": "Organization name", 18 | "type": "string", 19 | "minLength": 1 20 | }, 21 | "description": { 22 | "title": "Organization description", 23 | "type": "string" 24 | }, 25 | "roles": { 26 | "title": "Organization roles", 27 | "type": "array", 28 | "items": { 29 | "type": "string", 30 | "enum": [ 31 | "producer", 32 | "licensor", 33 | "processor", 34 | "host" 35 | ] 36 | } 37 | }, 38 | "url": { 39 | "title": "Organization homepage", 40 | "type": "string", 41 | "format": "iri" 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /crates/validate/src/schemas/v1.1.0/provider.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://schemas.stacspec.org/v1.1.0/item-spec/json-schema/provider.json", 4 | "title": "Provider Fields", 5 | "type": "object", 6 | "properties": { 7 | "providers": { 8 | "title": "Providers", 9 | "type": "array", 10 | "items": { 11 | "type": "object", 12 | "required": [ 13 | "name" 14 | ], 15 | "properties": { 16 | "name": { 17 | "title": "Organization name", 18 | "type": "string", 19 | "minLength": 1 20 | }, 21 | "description": { 22 | "title": "Organization description", 23 | "type": "string" 24 | }, 25 | "roles": { 26 | "title": "Organization roles", 27 | "type": "array", 28 | "items": { 29 | "type": "string", 30 | "enum": [ 31 | "producer", 32 | "licensor", 33 | "processor", 34 | "host" 35 | ] 36 | } 37 | }, 38 | "url": { 39 | "title": "Organization homepage", 40 | "type": "string", 41 | "format": "iri" 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /crates/wasm/src/lib.rs: -------------------------------------------------------------------------------- 1 | use arrow_array::RecordBatchIterator; 2 | use arrow_schema::ArrowError; 3 | use arrow_wasm::{Table, arrow_js::table::JSTable, error::WasmResult}; 4 | use serde::Serialize; 5 | use serde_wasm_bindgen::Serializer; 6 | use stac::Item; 7 | use std::io::Cursor; 8 | use thiserror::Error; 9 | use wasm_bindgen::prelude::*; 10 | 11 | #[derive(Debug, Error)] 12 | #[non_exhaustive] 13 | pub enum Error { 14 | #[error(transparent)] 15 | Arrow(#[from] ArrowError), 16 | } 17 | 18 | #[wasm_bindgen(js_name = arrowToStacJson)] 19 | pub fn arrow_to_stac_json(table: JSTable) -> WasmResult { 20 | let table = Table::from_js(&table)?; 21 | let reader = RecordBatchIterator::new( 22 | table.record_batches().into_iter().map(From::from).map(Ok), 23 | table.schema().into(), 24 | ); 25 | let items = stac::geoarrow::json::from_record_batch_reader(reader)?; 26 | let serializer = Serializer::json_compatible(); 27 | let items = items.serialize(&serializer)?; 28 | Ok(items) 29 | } 30 | 31 | #[wasm_bindgen(js_name = stacJsonToParquet)] 32 | pub fn stac_json_to_parquet(value: JsValue) -> Result, JsError> { 33 | let items: Vec = serde_wasm_bindgen::from_value(value)?; 34 | let mut cursor = Cursor::new(Vec::new()); 35 | stac::geoparquet::into_writer(&mut cursor, items)?; 36 | Ok(cursor.into_inner()) 37 | } 38 | -------------------------------------------------------------------------------- /crates/wasm/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.0](https://github.com/stac-utils/rustac/compare/stac-wasm-v0.0.4...stac-wasm-v0.1.0) (2025-12-01) 4 | 5 | 6 | ### ⚠ BREAKING CHANGES 7 | 8 | * remove unused error enums ([#868](https://github.com/stac-utils/rustac/issues/868)) 9 | 10 | ### Features 11 | 12 | * stac_wasm.stacJsonToParquet ([#786](https://github.com/stac-utils/rustac/issues/786)) ([6b1971a](https://github.com/stac-utils/rustac/commit/6b1971ae26aa8b80e1a68166cc180f2a6ae7f8ce)) 13 | * wasm ([#744](https://github.com/stac-utils/rustac/issues/744)) ([db5cd21](https://github.com/stac-utils/rustac/commit/db5cd210d769ea04225c8bbbc08173663f584c36)) 14 | 15 | 16 | ### Bug Fixes 17 | 18 | * pin arrow-wasm ([#785](https://github.com/stac-utils/rustac/issues/785)) ([8f9c28b](https://github.com/stac-utils/rustac/commit/8f9c28bb44d8372db8189d3adbf82238d14865fd)) 19 | * remove the package lock ([#745](https://github.com/stac-utils/rustac/issues/745)) ([b3337f6](https://github.com/stac-utils/rustac/commit/b3337f63d3d4402550b0dfef05698f48fafd4077)) 20 | * remove unused error enums ([#868](https://github.com/stac-utils/rustac/issues/868)) ([cf0e815](https://github.com/stac-utils/rustac/commit/cf0e815e03433e8ef219a79a67161174f3e99e84)) 21 | 22 | 23 | ### Dependencies 24 | 25 | * The following workspace dependencies were updated 26 | * dependencies 27 | * stac bumped from 0.14.0 to 0.15.0 28 | -------------------------------------------------------------------------------- /crates/io/data/items.ndjson: -------------------------------------------------------------------------------- 1 | {"type":"Feature","stac_version":"1.0.0","stac_extensions":["https://stac-extensions.github.io/projection/v1.1.0/schema.json","https://stac-extensions.github.io/raster/v1.1.0/schema.json"],"id":"dataset","geometry":null,"properties":{"datetime":"2024-09-07T14:41:31.917359Z","proj:shape":[2667,2658]},"links":[],"assets":{"data":{"href":"/Users/gadomski/Code/rustac/core/assets/dataset.tif","roles":["data"],"raster:bands":[{"data_type":"uint16"}]}}} 2 | {"type":"Feature","stac_version":"1.0.0","stac_extensions":["https://stac-extensions.github.io/projection/v1.1.0/schema.json","https://stac-extensions.github.io/raster/v1.1.0/schema.json"],"id":"dataset_geo","geometry":{"type":"Polygon","coordinates":[[[-61.2876244,72.229798],[-52.3015987,72.229798],[-52.3015987,90.0],[-61.2876244,90.0],[-61.2876244,72.229798]]],"bbox":[-61.2876244,72.229798,-52.3015987,90.0]},"bbox":[-61.2876244,72.229798,-52.3015987,90.0],"properties":{"datetime":"2024-09-07T14:41:31.917358Z","proj:epsg":32621,"proj:bbox":[373185.0,8019284.949381611,639014.9492102272,8286015.0],"proj:centroid":{"lat":73.4675736,"lon":-56.8079473},"proj:shape":[2667,2658],"proj:transform":[100.01126757344893,0.0,373185.0,0.0,-100.01126757344893,8286015.0]},"links":[],"assets":{"data":{"href":"/Users/gadomski/Code/rustac/core/assets/dataset_geo.tif","roles":["data"],"raster:bands":[{"data_type":"uint16","spatial_resolution":100.01126757344893}]}}} 3 | -------------------------------------------------------------------------------- /crates/core/data/items.ndjson: -------------------------------------------------------------------------------- 1 | {"type":"Feature","stac_version":"1.0.0","stac_extensions":["https://stac-extensions.github.io/projection/v1.1.0/schema.json","https://stac-extensions.github.io/raster/v1.1.0/schema.json"],"id":"dataset","geometry":null,"properties":{"datetime":"2024-09-07T14:41:31.917359Z","proj:shape":[2667,2658]},"links":[],"assets":{"data":{"href":"/Users/gadomski/Code/rustac/core/assets/dataset.tif","roles":["data"],"raster:bands":[{"data_type":"uint16"}]}}} 2 | {"type":"Feature","stac_version":"1.0.0","stac_extensions":["https://stac-extensions.github.io/projection/v1.1.0/schema.json","https://stac-extensions.github.io/raster/v1.1.0/schema.json"],"id":"dataset_geo","geometry":{"type":"Polygon","coordinates":[[[-61.2876244,72.229798],[-52.3015987,72.229798],[-52.3015987,90.0],[-61.2876244,90.0],[-61.2876244,72.229798]]],"bbox":[-61.2876244,72.229798,-52.3015987,90.0]},"bbox":[-61.2876244,72.229798,-52.3015987,90.0],"properties":{"datetime":"2024-09-07T14:41:31.917358Z","proj:epsg":32621,"proj:bbox":[373185.0,8019284.949381611,639014.9492102272,8286015.0],"proj:centroid":{"lat":73.4675736,"lon":-56.8079473},"proj:shape":[2667,2658],"proj:transform":[100.01126757344893,0.0,373185.0,0.0,-100.01126757344893,8286015.0]},"links":[],"assets":{"data":{"href":"/Users/gadomski/Code/rustac/core/assets/dataset_geo.tif","roles":["data"],"raster:bands":[{"data_type":"uint16","spatial_resolution":100.01126757344893}]}}} 3 | -------------------------------------------------------------------------------- /crates/core/README.md: -------------------------------------------------------------------------------- 1 | # stac 2 | 3 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/stac-utils/rustac/ci.yml?branch=main&style=for-the-badge)](https://github.com/stac-utils/rustac/actions/workflows/ci.yml) 4 | [![docs.rs](https://img.shields.io/docsrs/stac?style=for-the-badge)](https://docs.rs/stac/latest/stac/) 5 | [![Crates.io](https://img.shields.io/crates/v/stac?style=for-the-badge)](https://crates.io/crates/stac) 6 | ![Crates.io](https://img.shields.io/crates/l/stac?style=for-the-badge) 7 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg?style=for-the-badge)](./CODE_OF_CONDUCT) 8 | 9 | Rust implementation of the [SpatioTemporal Asset Catalog (STAC)](https://stacspec.org/) specification. 10 | 11 | ## Usage 12 | 13 | To use the library in your project: 14 | 15 | ```toml 16 | [dependencies] 17 | stac = "0.13" 18 | ``` 19 | 20 | ## Examples 21 | 22 | ```rust 23 | use stac::Item; 24 | 25 | // Creates an item from scratch. 26 | let item = Item::new("an-id"); 27 | 28 | // Reads an item from the filesystem. 29 | let item: Item = stac::read("examples/simple-item.json").unwrap(); 30 | ``` 31 | 32 | Please see the [documentation](https://docs.rs/stac) for more usage examples. 33 | 34 | ## Other info 35 | 36 | This crate is part of the [rustac](https://github.com/stac-utils/rustac) monorepo, see its README for contributing and license information. 37 | -------------------------------------------------------------------------------- /spec-examples/v1.0.0/catalog.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "examples", 3 | "type": "Catalog", 4 | "title": "Example Catalog", 5 | "stac_version": "1.0.0", 6 | "description": "This catalog is a simple demonstration of an example catalog that is used to organize a hierarchy of collections and their items.", 7 | "links": [ 8 | { 9 | "rel": "root", 10 | "href": "./catalog.json", 11 | "type": "application/json" 12 | }, 13 | { 14 | "rel": "child", 15 | "href": "./extensions-collection/collection.json", 16 | "type": "application/json", 17 | "title": "Collection Demonstrating STAC Extensions" 18 | }, 19 | { 20 | "rel": "child", 21 | "href": "./collection-only/collection.json", 22 | "type": "application/json", 23 | "title": "Collection with no items (standalone)" 24 | }, 25 | { 26 | "rel": "child", 27 | "href": "./collection-only/collection-with-schemas.json", 28 | "type": "application/json", 29 | "title": "Collection with no items (standalone with JSON Schemas)" 30 | }, 31 | { 32 | "rel": "item", 33 | "href": "./collectionless-item.json", 34 | "type": "application/json", 35 | "title": "Collection with no items (standalone)" 36 | }, 37 | { 38 | "rel": "self", 39 | "href": "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.0.0/examples/catalog.json", 40 | "type": "application/json" 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /spec-examples/v1.1.0/catalog.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "examples", 3 | "type": "Catalog", 4 | "title": "Example Catalog", 5 | "stac_version": "1.1.0", 6 | "description": "This catalog is a simple demonstration of an example catalog that is used to organize a hierarchy of collections and their items.", 7 | "links": [ 8 | { 9 | "rel": "root", 10 | "href": "./catalog.json", 11 | "type": "application/json" 12 | }, 13 | { 14 | "rel": "child", 15 | "href": "./extensions-collection/collection.json", 16 | "type": "application/json", 17 | "title": "Collection Demonstrating STAC Extensions" 18 | }, 19 | { 20 | "rel": "child", 21 | "href": "./collection-only/collection.json", 22 | "type": "application/json", 23 | "title": "Collection with no items (standalone)" 24 | }, 25 | { 26 | "rel": "child", 27 | "href": "./collection-only/collection-with-schemas.json", 28 | "type": "application/json", 29 | "title": "Collection with no items (standalone with JSON Schemas)" 30 | }, 31 | { 32 | "rel": "item", 33 | "href": "./collectionless-item.json", 34 | "type": "application/json", 35 | "title": "Item that does not have a collection (not recommended, but allowed by the spec)" 36 | }, 37 | { 38 | "rel": "self", 39 | "href": "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.1.0/examples/catalog.json", 40 | "type": "application/json" 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build-docs: 10 | name: Build docs 11 | runs-on: ubuntu-latest 12 | env: 13 | GIT_COMMITTER_NAME: ci-bot 14 | GIT_COMMITTER_EMAIL: ci-bot@example.com 15 | steps: 16 | - uses: actions/checkout@v6 17 | - uses: astral-sh/setup-uv@v7 18 | - name: Sync 19 | run: uv sync --group docs 20 | - name: Build 21 | run: uv run mkdocs build 22 | - uses: actions/upload-pages-artifact@v4 23 | id: deployment 24 | with: 25 | path: site/ 26 | deploy-docs: 27 | needs: build-docs 28 | name: Deploy docs 29 | permissions: 30 | pages: write 31 | id-token: write 32 | environment: 33 | name: github-pages 34 | url: ${{ steps.deployment.outputs.page_url }} 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/deploy-pages@v4 38 | id: deployment 39 | coverage: 40 | name: Coverage 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v6 44 | - uses: Swatinem/rust-cache@v2 45 | - name: Install tarpaulin 46 | run: cargo install cargo-tarpaulin 47 | - name: Test w/ coverage 48 | run: cargo tarpaulin -p stac --all-features --out xml 49 | - uses: codecov/codecov-action@v5 50 | with: 51 | files: ./cobertura.xml 52 | token: ${{ secrets.CODECOV_TOKEN }} 53 | fail_ci_if_error: true 54 | -------------------------------------------------------------------------------- /spec-examples/v1.1.0-beta.1/catalog.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "examples", 3 | "type": "Catalog", 4 | "title": "Example Catalog", 5 | "stac_version": "1.1.0-beta.1", 6 | "description": "This catalog is a simple demonstration of an example catalog that is used to organize a hierarchy of collections and their items.", 7 | "links": [ 8 | { 9 | "rel": "root", 10 | "href": "./catalog.json", 11 | "type": "application/json" 12 | }, 13 | { 14 | "rel": "child", 15 | "href": "./extensions-collection/collection.json", 16 | "type": "application/json", 17 | "title": "Collection Demonstrating STAC Extensions" 18 | }, 19 | { 20 | "rel": "child", 21 | "href": "./collection-only/collection.json", 22 | "type": "application/json", 23 | "title": "Collection with no items (standalone)" 24 | }, 25 | { 26 | "rel": "child", 27 | "href": "./collection-only/collection-with-schemas.json", 28 | "type": "application/json", 29 | "title": "Collection with no items (standalone with JSON Schemas)" 30 | }, 31 | { 32 | "rel": "item", 33 | "href": "./collectionless-item.json", 34 | "type": "application/json", 35 | "title": "Item that does not have a collection (not recommended, but allowed by the spec)" 36 | }, 37 | { 38 | "rel": "self", 39 | "href": "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.1.0-beta.1/examples/catalog.json", 40 | "type": "application/json" 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /crates/wasm/README.md: -------------------------------------------------------------------------------- 1 | # stac-wasm 2 | 3 | Converts [Arrow](https://arrow.apache.org/) arrays to [SpatioTemporal Asset Catalog (STAC)](https://stacspec.org/) items, via [WebAssembly (WASM)](https://webassembly.org/). 4 | 5 | > [!WARNING] 6 | > This package is in an "alpha" state and will likely break and change a lot. 7 | 8 | ## Usage 9 | 10 | ```shell 11 | npm i stac-wasm 12 | ``` 13 | 14 | We give you two functions: 15 | 16 | ```javascript 17 | import * as stac_wasm from "stac-wasm"; 18 | 19 | const table = loadArrowTable(); // e.g. from DuckDB 20 | const items = stac_wasm.arrowToStacJson(table); 21 | const bytes = stac_wasm.stacJsonToParquet(items); 22 | ``` 23 | 24 | ## Tests 25 | 26 | We don't have automated tests. 27 | If you want to play with the function, modify `www/index.js` and then: 28 | 29 | ```shell 30 | cd www 31 | npm run start 32 | ``` 33 | 34 | This should open a page at that you can use to test out the WASM library. 35 | 36 | ## Contributing 37 | 38 | **stac-wasm** is part of [rustac](https://github.com/stac-utils/rustac), a monorepo that includes the Rust code used to build the WASM module. 39 | See [CONTRIBUTING.md](https://github.com/stac-utils/rustac/blob/main/CONTRIBUTING.md) for instructions on contributing to the monorepo. 40 | If your on MacOS, you might have to use **llvm** as described [in this comment](https://github.com/briansmith/ring/issues/1824#issuecomment-2059955073). 41 | 42 | ## Releasing 43 | 44 | ```shell 45 | wasm-pack build 46 | wasm-pack login 47 | cd pkg 48 | npm publish 49 | ``` 50 | -------------------------------------------------------------------------------- /crates/duckdb/README.md: -------------------------------------------------------------------------------- 1 | # pgstac 2 | 3 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/stac-utils/rustac/ci.yml?branch=main&style=for-the-badge)](https://github.com/stac-utils/rustac/actions/workflows/ci.yml) 4 | [![docs.rs](https://img.shields.io/docsrs/stac-duckdb?style=for-the-badge)](https://docs.rs/stac-duckdb/latest/stac_duckdb/) 5 | [![Crates.io](https://img.shields.io/crates/v/stac-duckdb?style=for-the-badge)](https://crates.io/crates/stac-duckdb) 6 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg?style=for-the-badge)](./CODE_OF_CONDUCT) 7 | 8 | Use [DuckDB](https://duckdb.org/) to search [stac-geoparquet](https://github.com/stac-utils/stac-geoparquet). 9 | 10 | ## Usage 11 | 12 | ```shell 13 | cargo add stac-duckdb 14 | ``` 15 | 16 | See the [documentation](https://docs.rs/stac-duckdb) for more. 17 | 18 | ## Bundling 19 | 20 | By default, DuckDB looks for its shared library on your system. 21 | Use `DUCKDB_LIB_DIR` and `DUCKDB_INCLUDE_DIR` to help it find those resources. 22 | If you want to build the DuckDB library as a part of this (or a downstream's) crate's build process, use the `bundled` feature. 23 | E.g. to test this crate if you don't have DuckDB locally: 24 | 25 | ```shell 26 | cargo test -p stac-duckdb -F bundled 27 | ``` 28 | 29 | See [the duckdb-rs docs](https://github.com/duckdb/duckdb-rs?tab=readme-ov-file#notes-on-building-duckdb-and-libduckdb-sys) for more. 30 | 31 | ## Other info 32 | 33 | This crate is part of the [rustac](https://github.com/stac-utils/rustac) monorepo, see its README for contributing and license information. 34 | -------------------------------------------------------------------------------- /spec-examples/v1.1.0/extensions-collection/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "extensions-collection", 3 | "type": "Collection", 4 | "stac_version": "1.1.0", 5 | "description": "A heterogeneous collection containing deeper examples of various extensions", 6 | "links": [ 7 | { 8 | "rel": "parent", 9 | "href": "../catalog.json", 10 | "type": "application/json", 11 | "title": "Example Catalog" 12 | }, 13 | { 14 | "rel": "root", 15 | "href": "../catalog.json", 16 | "type": "application/json", 17 | "title": "Example Catalog" 18 | }, 19 | { 20 | "rel": "item", 21 | "href": "./proj-example/proj-example.json", 22 | "title": "Proj extension example" 23 | }, 24 | { 25 | "rel": "license", 26 | "href": "https://remotedata.io/license.html", 27 | "title": "Remote Data License Terms" 28 | } 29 | ], 30 | "stac_extensions": [], 31 | "title": "Collection of Extension Items", 32 | "keywords": [ 33 | "examples", 34 | "sar", 35 | "projection" 36 | ], 37 | "providers": [ 38 | { 39 | "name": "Remote Data, Inc.", 40 | "roles": [ 41 | "producer", 42 | "licensor" 43 | ], 44 | "url": "https://remotedata.io" 45 | } 46 | ], 47 | "extent": { 48 | "spatial": { 49 | "bbox": [ 50 | [ 51 | -180, 52 | -56, 53 | 180, 54 | 83 55 | ] 56 | ] 57 | }, 58 | "temporal": { 59 | "interval": [ 60 | [ 61 | "2009-05-20T02:40:01.042784Z", 62 | "2018-11-03T23:59:55.112875Z" 63 | ] 64 | ] 65 | } 66 | }, 67 | "license": "other" 68 | } 69 | -------------------------------------------------------------------------------- /spec-examples/v1.0.0/extensions-collection/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "extensions-collection", 3 | "type": "Collection", 4 | "stac_version": "1.0.0", 5 | "description": "A heterogenous collection containing deeper examples of various extensions", 6 | "links": [ 7 | { 8 | "rel": "parent", 9 | "href": "../catalog.json", 10 | "type": "application/json", 11 | "title": "Example Catalog" 12 | }, 13 | { 14 | "rel": "root", 15 | "href": "../catalog.json", 16 | "type": "application/json", 17 | "title": "Example Catalog" 18 | }, 19 | { 20 | "rel": "item", 21 | "href": "./proj-example/proj-example.json", 22 | "title": "Proj extension example" 23 | }, 24 | { 25 | "rel": "license", 26 | "href": "https://remotedata.io/license.html", 27 | "title": "Remote Data License Terms" 28 | } 29 | ], 30 | "stac_extensions": [], 31 | "title": "Collection of Extension Items", 32 | "keywords": [ 33 | "examples", 34 | "sar", 35 | "projection" 36 | ], 37 | "providers": [ 38 | { 39 | "name": "Remote Data, Inc.", 40 | "roles": [ 41 | "producer", 42 | "licensor" 43 | ], 44 | "url": "https://remotedata.io" 45 | } 46 | ], 47 | "extent": { 48 | "spatial": { 49 | "bbox": [ 50 | [ 51 | -180.0, 52 | -56.0, 53 | 180.0, 54 | 83.0 55 | ] 56 | ] 57 | }, 58 | "temporal": { 59 | "interval": [ 60 | [ 61 | "2009-05-20T02:40:01.042784Z", 62 | "2018-11-03T23:59:55.112875Z" 63 | ] 64 | ] 65 | } 66 | }, 67 | "license": "PDDL-1.0" 68 | } 69 | -------------------------------------------------------------------------------- /spec-examples/v1.1.0-beta.1/extensions-collection/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "extensions-collection", 3 | "type": "Collection", 4 | "stac_version": "1.1.0-beta.1", 5 | "description": "A heterogeneous collection containing deeper examples of various extensions", 6 | "links": [ 7 | { 8 | "rel": "parent", 9 | "href": "../catalog.json", 10 | "type": "application/json", 11 | "title": "Example Catalog" 12 | }, 13 | { 14 | "rel": "root", 15 | "href": "../catalog.json", 16 | "type": "application/json", 17 | "title": "Example Catalog" 18 | }, 19 | { 20 | "rel": "item", 21 | "href": "./proj-example/proj-example.json", 22 | "title": "Proj extension example" 23 | }, 24 | { 25 | "rel": "license", 26 | "href": "https://remotedata.io/license.html", 27 | "title": "Remote Data License Terms" 28 | } 29 | ], 30 | "stac_extensions": [], 31 | "title": "Collection of Extension Items", 32 | "keywords": [ 33 | "examples", 34 | "sar", 35 | "projection" 36 | ], 37 | "providers": [ 38 | { 39 | "name": "Remote Data, Inc.", 40 | "roles": [ 41 | "producer", 42 | "licensor" 43 | ], 44 | "url": "https://remotedata.io" 45 | } 46 | ], 47 | "extent": { 48 | "spatial": { 49 | "bbox": [ 50 | [ 51 | -180, 52 | -56, 53 | 180, 54 | 83 55 | ] 56 | ] 57 | }, 58 | "temporal": { 59 | "interval": [ 60 | [ 61 | "2009-05-20T02:40:01.042784Z", 62 | "2018-11-03T23:59:55.112875Z" 63 | ] 64 | ] 65 | } 66 | }, 67 | "license": "other" 68 | } 69 | -------------------------------------------------------------------------------- /crates/validate/src/schemas/v1.1.0/datetime.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://schemas.stacspec.org/v1.1.0/item-spec/json-schema/datetime.json", 4 | "title": "Date and Time Fields", 5 | "type": "object", 6 | "dependencies": { 7 | "start_datetime": { 8 | "required": [ 9 | "end_datetime" 10 | ] 11 | }, 12 | "end_datetime": { 13 | "required": [ 14 | "start_datetime" 15 | ] 16 | } 17 | }, 18 | "properties": { 19 | "datetime": { 20 | "title": "Date and Time", 21 | "description": "The searchable date/time of the data, in UTC (Formatted in RFC 3339) ", 22 | "type": ["string", "null"], 23 | "format": "date-time", 24 | "pattern": "(\\+00:00|Z)$" 25 | }, 26 | "start_datetime": { 27 | "title": "Start Date and Time", 28 | "description": "The searchable start date/time of the data, in UTC (Formatted in RFC 3339) ", 29 | "type": "string", 30 | "format": "date-time", 31 | "pattern": "(\\+00:00|Z)$" 32 | }, 33 | "end_datetime": { 34 | "title": "End Date and Time", 35 | "description": "The searchable end date/time of the data, in UTC (Formatted in RFC 3339) ", 36 | "type": "string", 37 | "format": "date-time", 38 | "pattern": "(\\+00:00|Z)$" 39 | }, 40 | "created": { 41 | "title": "Creation Time", 42 | "type": "string", 43 | "format": "date-time", 44 | "pattern": "(\\+00:00|Z)$" 45 | }, 46 | "updated": { 47 | "title": "Last Update Time", 48 | "type": "string", 49 | "format": "date-time", 50 | "pattern": "(\\+00:00|Z)$" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /crates/validate/src/schemas/v1.0.0/datetime.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#", 4 | "title": "Date and Time Fields", 5 | "type": "object", 6 | "dependencies": { 7 | "start_datetime": { 8 | "required": [ 9 | "end_datetime" 10 | ] 11 | }, 12 | "end_datetime": { 13 | "required": [ 14 | "start_datetime" 15 | ] 16 | } 17 | }, 18 | "properties": { 19 | "datetime": { 20 | "title": "Date and Time", 21 | "description": "The searchable date/time of the assets, in UTC (Formatted in RFC 3339) ", 22 | "type": ["string", "null"], 23 | "format": "date-time", 24 | "pattern": "(\\+00:00|Z)$" 25 | }, 26 | "start_datetime": { 27 | "title": "Start Date and Time", 28 | "description": "The searchable start date/time of the assets, in UTC (Formatted in RFC 3339) ", 29 | "type": "string", 30 | "format": "date-time", 31 | "pattern": "(\\+00:00|Z)$" 32 | }, 33 | "end_datetime": { 34 | "title": "End Date and Time", 35 | "description": "The searchable end date/time of the assets, in UTC (Formatted in RFC 3339) ", 36 | "type": "string", 37 | "format": "date-time", 38 | "pattern": "(\\+00:00|Z)$" 39 | }, 40 | "created": { 41 | "title": "Creation Time", 42 | "type": "string", 43 | "format": "date-time", 44 | "pattern": "(\\+00:00|Z)$" 45 | }, 46 | "updated": { 47 | "title": "Last Update Time", 48 | "type": "string", 49 | "format": "date-time", 50 | "pattern": "(\\+00:00|Z)$" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /crates/core/data/bands-v1.0.0.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "stac_version": "1.0.0", 4 | "id": "bands-migration", 5 | "geometry": null, 6 | "properties": { 7 | "datetime": "2024-08-11T16:07:32.766181+00:00" 8 | }, 9 | "links": [], 10 | "assets": { 11 | "example": { 12 | "href": "example.tif", 13 | "eo:bands": [ 14 | { 15 | "name": "r", 16 | "common_name": "red" 17 | }, 18 | { 19 | "name": "g", 20 | "common_name": "green" 21 | }, 22 | { 23 | "name": "b", 24 | "common_name": "blue" 25 | }, 26 | { 27 | "name": "nir", 28 | "common_name": "nir" 29 | } 30 | ], 31 | "raster:bands": [ 32 | { 33 | "data_type": "uint16", 34 | "spatial_resolution": 10, 35 | "sampling": "area" 36 | }, 37 | { 38 | "data_type": "uint16", 39 | "spatial_resolution": 10, 40 | "sampling": "area" 41 | }, 42 | { 43 | "data_type": "uint16", 44 | "spatial_resolution": 10, 45 | "sampling": "area" 46 | }, 47 | { 48 | "data_type": "uint16", 49 | "spatial_resolution": 30, 50 | "sampling": "area" 51 | } 52 | ] 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /crates/validate/src/schemas/v1.1.0/catalog.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://schemas.stacspec.org/v1.1.0/catalog-spec/json-schema/catalog.json", 4 | "title": "STAC Catalog Specification", 5 | "description": "This object represents Catalogs in a SpatioTemporal Asset Catalog.", 6 | "allOf": [ 7 | { 8 | "$ref": "#/definitions/catalog" 9 | }, 10 | { 11 | "$ref": "../../item-spec/json-schema/common.json" 12 | } 13 | ], 14 | "definitions": { 15 | "catalog": { 16 | "title": "STAC Catalog", 17 | "type": "object", 18 | "$comment": "title and description is validated through the common metadata.", 19 | "required": [ 20 | "stac_version", 21 | "type", 22 | "id", 23 | "description", 24 | "links" 25 | ], 26 | "properties": { 27 | "stac_version": { 28 | "title": "STAC version", 29 | "type": "string", 30 | "const": "1.1.0" 31 | }, 32 | "stac_extensions": { 33 | "title": "STAC extensions", 34 | "type": "array", 35 | "uniqueItems": true, 36 | "items": { 37 | "title": "Reference to a JSON Schema", 38 | "type": "string", 39 | "format": "iri" 40 | } 41 | }, 42 | "type": { 43 | "title": "Type of STAC entity", 44 | "const": "Catalog" 45 | }, 46 | "id": { 47 | "title": "Identifier", 48 | "type": "string", 49 | "minLength": 1 50 | }, 51 | "links": { 52 | "$ref": "../../item-spec/json-schema/item.json#/definitions/links" 53 | } 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /crates/server/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | /// A crate-specific error type. 4 | #[derive(Debug, Error)] 5 | #[non_exhaustive] 6 | pub enum Error { 7 | /// [bb8::RunError] 8 | #[cfg(feature = "pgstac")] 9 | #[error(transparent)] 10 | Bb8TokioPostgresRun(#[from] bb8::RunError), 11 | 12 | /// [bb8::RunError] 13 | #[cfg(feature = "duckdb")] 14 | #[error(transparent)] 15 | Bb8DuckdbRun(#[from] Box>), 16 | 17 | /// [stac_duckdb::Error] 18 | #[cfg(feature = "duckdb")] 19 | #[error(transparent)] 20 | StacDuckdb(#[from] stac_duckdb::Error), 21 | 22 | /// A memory backend error. 23 | #[error("memory backend error: {0}")] 24 | MemoryBackend(String), 25 | 26 | /// [pgstac::Error] 27 | #[cfg(feature = "pgstac")] 28 | #[error(transparent)] 29 | Pgstac(#[from] pgstac::Error), 30 | 31 | /// [serde_json::Error] 32 | #[error(transparent)] 33 | SerdeJson(#[from] serde_json::Error), 34 | 35 | /// [serde_urlencoded::ser::Error] 36 | #[error(transparent)] 37 | SerdeUrlencodedSer(#[from] serde_urlencoded::ser::Error), 38 | 39 | /// [stac::Error] 40 | #[error(transparent)] 41 | Stac(#[from] stac::Error), 42 | 43 | /// The backend is read-only. 44 | #[error("this backend is read-only")] 45 | ReadOnly, 46 | 47 | /// [tokio_postgres::Error] 48 | #[cfg(feature = "pgstac")] 49 | #[error(transparent)] 50 | TokioPostgres(#[from] tokio_postgres::Error), 51 | 52 | /// [std::num::TryFromIntError] 53 | #[error(transparent)] 54 | TryFromInt(#[from] std::num::TryFromIntError), 55 | 56 | /// [url::ParseError] 57 | #[error(transparent)] 58 | UrlParse(#[from] url::ParseError), 59 | } 60 | -------------------------------------------------------------------------------- /crates/pgstac/src/page.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::{Map, Value}; 3 | use stac::Link; 4 | use stac::api::{Context, Item}; 5 | 6 | /// A page of search results. 7 | #[derive(Debug, Deserialize, Serialize)] 8 | pub struct Page { 9 | /// These are the out features, usually STAC items, but maybe not legal STAC 10 | /// items if fields are excluded. 11 | pub features: Vec, 12 | 13 | /// The next id. 14 | #[serde(skip_serializing_if = "Option::is_none")] 15 | pub next: Option, 16 | 17 | /// The previous id. 18 | #[serde(skip_serializing_if = "Option::is_none")] 19 | pub prev: Option, 20 | 21 | /// The search context. 22 | /// 23 | /// This was removed in pgstac v0.9 24 | #[serde(skip_serializing_if = "Option::is_none")] 25 | pub context: Option, 26 | 27 | /// The number of values returned. 28 | /// 29 | /// Added in pgstac v0.9 30 | #[serde(rename = "numberReturned", skip_serializing_if = "Option::is_none")] 31 | pub number_returned: Option, 32 | 33 | /// Links 34 | /// 35 | /// Added in pgstac v0.9 36 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 37 | pub links: Vec, 38 | 39 | /// Additional fields. 40 | #[serde(flatten)] 41 | pub additional_fields: Map, 42 | } 43 | 44 | impl Page { 45 | /// Returns this page's next token, if it has one. 46 | pub fn next_token(&self) -> Option { 47 | self.next.as_ref().map(|next| format!("next:{next}")) 48 | } 49 | 50 | /// Returns this page's prev token, if it has one. 51 | pub fn prev_token(&self) -> Option { 52 | self.prev.as_ref().map(|prev| format!("prev:{prev}")) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /crates/io/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stac-io" 3 | version = "0.2.0" 4 | description = "Input and output (I/O) for the SpatioTemporal Asset Catalog (STAC)" 5 | authors.workspace = true 6 | edition.workspace = true 7 | homepage.workspace = true 8 | repository.workspace = true 9 | license.workspace = true 10 | categories.workspace = true 11 | rust-version.workspace = true 12 | 13 | [features] 14 | geoparquet = ["stac/geoparquet", "dep:parquet"] 15 | store = ["dep:object_store"] 16 | store-aws = ["store", "object_store/aws"] 17 | store-azure = ["store", "object_store/azure"] 18 | store-gcp = ["store", "object_store/gcp"] 19 | store-http = ["store", "object_store/http"] 20 | store-all = ["store-aws", "store-azure", "store-gcp", "store-http"] 21 | 22 | [dependencies] 23 | async-stream.workspace = true 24 | bytes.workspace = true 25 | fluent-uri = { workspace = true, optional = true } 26 | futures.workspace = true 27 | http.workspace = true 28 | jsonschema = { workspace = true, optional = true } 29 | object_store = { workspace = true, optional = true } 30 | parquet = { workspace = true, optional = true, features = ["arrow", "async", "object_store"] } 31 | reqwest = { workspace = true, features = ["json", "blocking"] } 32 | serde.workspace = true 33 | serde_json = { workspace = true, features = ["preserve_order"] } 34 | stac = { version = "0.15.0", path = "../core" } 35 | thiserror.workspace = true 36 | tokio.workspace = true 37 | tracing.workspace = true 38 | url.workspace = true 39 | 40 | [dev-dependencies] 41 | geojson.workspace = true 42 | mockito.workspace = true 43 | rstest.workspace = true 44 | tempfile.workspace = true 45 | tokio = { workspace = true, features = ["rt", "macros"] } 46 | tokio-test.workspace = true 47 | 48 | [[test]] 49 | name = "aws" 50 | required-features = ["store-aws"] 51 | -------------------------------------------------------------------------------- /crates/pgstac/README.md: -------------------------------------------------------------------------------- 1 | # pgstac 2 | 3 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/stac-utils/rustac/ci.yml?branch=main&style=for-the-badge)](https://github.com/stac-utils/rustac/actions/workflows/ci.yml) 4 | [![docs.rs](https://img.shields.io/docsrs/pgstac?style=for-the-badge)](https://docs.rs/pgstac/latest/pgstac/) 5 | [![Crates.io](https://img.shields.io/crates/v/pgstac?style=for-the-badge)](https://crates.io/crates/pgstac) 6 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg?style=for-the-badge)](./CODE_OF_CONDUCT) 7 | 8 | Rust interface for [pgstac](https://github.com/stac-utils/pgstac). 9 | 10 | ## Usage 11 | 12 | In your `Cargo.toml`: 13 | 14 | ```toml 15 | [dependencies] 16 | pgstac = "0.3" 17 | ``` 18 | 19 | See the [documentation](https://docs.rs/pgstac) for more. 20 | 21 | ## Testing 22 | 23 | **pgstac** needs a blank **pgstac** database for testing, so is not part of the default workspace build. 24 | To test: 25 | 26 | ```shell 27 | docker compose up -d 28 | cargo test -p pgstac 29 | docker compose down 30 | ``` 31 | 32 | Each test is run in its own transaction, which is rolled back after the test. 33 | 34 | ### Customizing the test database connection 35 | 36 | By default, the tests will connect to the database at `postgresql://username:password@localhost:5432/postgis`. 37 | If you need to customize the connection information for whatever reason, set your `PGSTAC_RS_TEST_DB` environment variable: 38 | 39 | ```shell 40 | PGSTAC_RS_TEST_DB=postgresql://otherusername:otherpassword@otherhost:7822/otherdbname cargo test 41 | ``` 42 | 43 | ## Other info 44 | 45 | This crate is part of the [rustac](https://github.com/stac-utils/rustac) monorepo, see its README for contributing and license information. 46 | -------------------------------------------------------------------------------- /crates/server/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A [STAC API](https://github.com/radiantearth/stac-api-spec) server written in Rust. 2 | 3 | #![deny( 4 | elided_lifetimes_in_paths, 5 | explicit_outlives_requirements, 6 | keyword_idents, 7 | macro_use_extern_crate, 8 | meta_variable_misuse, 9 | missing_abi, 10 | missing_debug_implementations, 11 | missing_docs, 12 | non_ascii_idents, 13 | noop_method_call, 14 | rust_2021_incompatible_closure_captures, 15 | rust_2021_incompatible_or_patterns, 16 | rust_2021_prefixes_incompatible_syntax, 17 | rust_2021_prelude_collisions, 18 | single_use_lifetimes, 19 | trivial_casts, 20 | trivial_numeric_casts, 21 | unreachable_pub, 22 | unsafe_code, 23 | unsafe_op_in_unsafe_fn, 24 | unused_crate_dependencies, 25 | unused_extern_crates, 26 | unused_import_braces, 27 | unused_lifetimes, 28 | unused_qualifications, 29 | unused_results, 30 | warnings 31 | )] 32 | 33 | mod api; 34 | mod backend; 35 | mod error; 36 | #[cfg(feature = "axum")] 37 | pub mod routes; 38 | 39 | pub use api::Api; 40 | #[cfg(feature = "duckdb")] 41 | pub use backend::DuckdbBackend; 42 | #[cfg(feature = "pgstac")] 43 | pub use backend::PgstacBackend; 44 | pub use backend::{Backend, MemoryBackend}; 45 | pub use error::Error; 46 | 47 | /// A crate-specific result type. 48 | pub type Result = std::result::Result; 49 | 50 | /// The default catalog id. 51 | pub const DEFAULT_ID: &str = "stac-server-rs"; 52 | 53 | /// The default catalog description. 54 | pub const DEFAULT_DESCRIPTION: &str = "A STAC API server written in Rust"; 55 | 56 | /// The default limit. 57 | pub const DEFAULT_LIMIT: u64 = 10; 58 | 59 | #[cfg(test)] 60 | use tokio_test as _; 61 | 62 | #[cfg(all(test, not(feature = "axum")))] 63 | use tower as _; 64 | -------------------------------------------------------------------------------- /crates/server/data/joplin/feature.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "id": "f2cca2a3-288b-4518-8a3e-a4492bb60b08", 3 | "type": "Feature", 4 | "collection": "joplin", 5 | "links": [], 6 | "geometry": { 7 | "type": "Polygon", 8 | "coordinates": [ 9 | [ 10 | [ 11 | -94.6884155, 12 | 37.0595608 13 | ], 14 | [ 15 | -94.6884155, 16 | 37.0332547 17 | ], 18 | [ 19 | -94.6554565, 20 | 37.0332547 21 | ], 22 | [ 23 | -94.6554565, 24 | 37.0595608 25 | ], 26 | [ 27 | -94.6884155, 28 | 37.0595608 29 | ] 30 | ] 31 | ] 32 | }, 33 | "properties": { 34 | "proj:epsg": 3857, 35 | "orientation": "nadir", 36 | "height": 2500, 37 | "width": 2500, 38 | "datetime": "2000-02-02T00:00:00Z", 39 | "gsd": 0.5971642834779395 40 | }, 41 | "assets": { 42 | "COG": { 43 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 44 | "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C350000e4102500n.tif", 45 | "title": "NOAA STORM COG" 46 | } 47 | }, 48 | "bbox": [ 49 | -94.6884155, 50 | 37.0332547, 51 | -94.6554565, 52 | 37.0595608 53 | ], 54 | "stac_extensions": [ 55 | "https://stac-extensions.github.io/eo/v1.0.0/schema.json", 56 | "https://stac-extensions.github.io/projection/v1.0.0/schema.json" 57 | ], 58 | "stac_version": "1.0.0" 59 | } 60 | -------------------------------------------------------------------------------- /crates/core/src/version.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::{convert::Infallible, fmt::Display, str::FromStr}; 3 | 4 | /// A version of the STAC specification. 5 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq, Hash, PartialOrd)] 6 | #[allow(non_camel_case_types)] 7 | #[non_exhaustive] 8 | pub enum Version { 9 | /// [v1.0.0](https://github.com/radiantearth/stac-spec/releases/tag/v1.0.0) 10 | #[serde(rename = "1.0.0")] 11 | v1_0_0, 12 | 13 | /// [v1.1.0-beta.1](https://github.com/radiantearth/stac-spec/releases/tag/v1.1.0-beta.1) 14 | #[serde(rename = "1.1.0-beta.1")] 15 | v1_1_0_beta_1, 16 | 17 | /// [v1.1.0](https://github.com/radiantearth/stac-spec/releases/tag/v1.1.0) 18 | #[serde(rename = "1.1.0")] 19 | v1_1_0, 20 | 21 | /// An unknown STAC version. 22 | #[serde(untagged)] 23 | Unknown(String), 24 | } 25 | 26 | impl FromStr for Version { 27 | type Err = Infallible; 28 | 29 | fn from_str(s: &str) -> Result { 30 | match s { 31 | "1.0.0" => Ok(Version::v1_0_0), 32 | "1.1.0-beta.1" => Ok(Version::v1_1_0_beta_1), 33 | "1.1.0" => Ok(Version::v1_1_0), 34 | _ => Ok(Version::Unknown(s.to_string())), 35 | } 36 | } 37 | } 38 | 39 | impl Display for Version { 40 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 41 | write!( 42 | f, 43 | "{}", 44 | match self { 45 | Version::v1_0_0 => "1.0.0", 46 | Version::v1_1_0_beta_1 => "1.1.0-beta.1", 47 | Version::v1_1_0 => "1.1.0", 48 | Version::Unknown(v) => v, 49 | } 50 | ) 51 | } 52 | } 53 | 54 | impl Default for Version { 55 | fn default() -> Self { 56 | crate::STAC_VERSION 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /crates/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rustac" 3 | description = "Command line interface for rustac" 4 | version = "0.2.0" 5 | keywords = ["geospatial", "stac", "metadata", "geo", "raster"] 6 | authors.workspace = true 7 | edition.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | license.workspace = true 11 | categories.workspace = true 12 | rust-version.workspace = true 13 | 14 | [features] 15 | default = [] 16 | pgstac = ["stac-server/pgstac"] 17 | duckdb-bundled = ["stac-duckdb/bundled"] 18 | 19 | [dependencies] 20 | anyhow.workspace = true 21 | async-stream.workspace = true 22 | axum.workspace = true 23 | clap = { workspace = true, features = ["derive"] } 24 | clap_complete.workspace = true 25 | futures-core.workspace = true 26 | futures-util.workspace = true 27 | serde_json.workspace = true 28 | stac = { version = "0.15.0", path = "../core" } 29 | stac-duckdb = { version = "0.3.0", path = "../duckdb" } 30 | stac-io = { version = "0.2.0", path = "../io", features = [ 31 | "store-all", 32 | "geoparquet", 33 | ] } 34 | stac-server = { version = "0.4.0", path = "../server", features = ["axum", "duckdb"] } 35 | stac-validate = { version = "0.6.0", path = "../validate" } 36 | tokio = { workspace = true, features = [ 37 | "macros", 38 | "io-std", 39 | "rt-multi-thread", 40 | "fs", 41 | ] } 42 | tracing.workspace = true 43 | tracing-indicatif.workspace = true 44 | tracing-subscriber = { workspace = true, features = ["env-filter"] } 45 | url.workspace = true 46 | 47 | [dev-dependencies] 48 | assert_cmd.workspace = true 49 | rstest.workspace = true 50 | tempfile.workspace = true 51 | 52 | [lib] 53 | crate-type = ["lib", "cdylib"] 54 | 55 | [[bin]] 56 | name = "rustac" 57 | path = "src/main.rs" 58 | doc = false 59 | test = false 60 | 61 | [package.metadata.docs.rs] 62 | all-features = true 63 | rustdoc-args = ["--cfg", "docsrs"] 64 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: rustac 2 | site_description: Command Line Interface (CLI) and Rust crates the SpatioTemporal Asset Catalog (STAC) specification. 3 | site_url: https://stac-utils.github.io/rustac/ 4 | repo_url: https://github.com/stac-utils/rustac 5 | theme: 6 | name: material 7 | logo: img/rustac-notext.svg 8 | icon: 9 | repo: fontawesome/brands/github 10 | favicon: img/rustac-notext.svg 11 | features: 12 | - navigation.indexes 13 | - navigation.footer 14 | palette: 15 | scheme: stac 16 | primary: custom 17 | 18 | nav: 19 | - Home: index.md 20 | - Formats: formats.md 21 | - Command-line interface: cli.md 22 | - History: history.md 23 | - Pronunciation: pronunciation.md 24 | 25 | plugins: 26 | - search 27 | - social: 28 | cards_layout_options: 29 | color: rgb(26, 78, 99) 30 | background_color: rgb(228, 246, 251) 31 | - redirects: 32 | redirect_maps: 33 | python/index.md: https://www.gadom.ski/rustac-py/latest/ 34 | python/example.md: https://www.gadom.ski/rustac-py/latest/example/ 35 | python/api/index.md: https://www.gadom.ski/rustac-py/latest/api/ 36 | python/api/migrate.md: https://www.gadom.ski/rustac-py/latest/api/migrate/ 37 | python/api/read.md: https://www.gadom.ski/rustac-py/latest/api/read/ 38 | python/api/search.md: https://www.gadom.ski/rustac-py/latest/api/search/ 39 | python/api/version.md: https://www.gadom.ski/rustac-py/latest/api/version/ 40 | python/api/write.md: https://www.gadom.ski/rustac-py/latest/api/write/ 41 | 42 | markdown_extensions: 43 | - admonition 44 | - pymdownx.highlight: 45 | anchor_linenums: true 46 | line_spans: __span 47 | pygments_lang_class: true 48 | - pymdownx.inlinehilite 49 | - pymdownx.snippets 50 | - pymdownx.superfences 51 | - pymdownx.details 52 | 53 | extra_css: 54 | - stylesheets/extra.css 55 | -------------------------------------------------------------------------------- /crates/validate/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [0.6.0](https://github.com/stac-utils/rustac/compare/stac-validate-v0.5.1...stac-validate-v0.6.0) (2025-12-01) 8 | 9 | 10 | ### ⚠ BREAKING CHANGES 11 | 12 | * remove unused error enums ([#868](https://github.com/stac-utils/rustac/issues/868)) 13 | 14 | ### Bug Fixes 15 | 16 | * remove circular dev depependency ([#886](https://github.com/stac-utils/rustac/issues/886)) ([dcb9b49](https://github.com/stac-utils/rustac/commit/dcb9b496d4979984178b279c245e897621b9ca76)) 17 | * remove unused error enums ([#868](https://github.com/stac-utils/rustac/issues/868)) ([cf0e815](https://github.com/stac-utils/rustac/commit/cf0e815e03433e8ef219a79a67161174f3e99e84)) 18 | 19 | 20 | ### Dependencies 21 | 22 | * The following workspace dependencies were updated 23 | * dependencies 24 | * stac bumped from 0.14.0 to 0.15.0 25 | * dev-dependencies 26 | * stac-io bumped from 0.1.0 to 0.2.0 27 | 28 | ## [0.5.1] - 2025-11-14 29 | 30 | Update **stac** dependency. 31 | 32 | ## [0.5.0] - 2025-09-08 33 | 34 | ### Changed 35 | 36 | - Validation is now async ([#798](https://github.com/stac-utils/rustac/pull/798)) 37 | 38 | ## [0.4.0] - 2025-07-10 39 | 40 | First release with a changelog. 41 | 42 | [unreleased]: https://github.com/stac-utils/rustac/compare/stac-validate-v0.5.1...main 43 | [0.5.1]: https://github.com/stac-utils/rustac/compare/stac-validate-v0.5.0...stac-validate-v0.5.1 44 | [0.5.0]: https://github.com/stac-utils/rustac/compare/stac-validate-v0.4.0...stac-validate-v0.5.0 45 | [0.4.0]: https://github.com/stac-utils/rustac/releases/tag/stac-validate-v0.4.0 46 | 47 | 48 | -------------------------------------------------------------------------------- /crates/io/src/json.rs: -------------------------------------------------------------------------------- 1 | use crate::Result; 2 | use serde::Serialize; 3 | use stac::{FromJson, SelfHref, ToJson}; 4 | use std::{fs::File, io::Read, path::Path}; 5 | 6 | /// Create a STAC object from JSON. 7 | pub trait FromJsonPath: FromJson + SelfHref { 8 | /// Reads JSON data from a file. 9 | /// 10 | /// # Examples 11 | /// 12 | /// ``` 13 | /// use stac::Item; 14 | /// use stac_io::FromJsonPath; 15 | /// 16 | /// let item = Item::from_json_path("examples/simple-item.json").unwrap(); 17 | /// ``` 18 | fn from_json_path(path: impl AsRef) -> Result { 19 | let path = path.as_ref(); 20 | let mut buf = Vec::new(); 21 | let _ = File::open(path)?.read_to_end(&mut buf)?; 22 | let mut value = Self::from_json_slice(&buf)?; 23 | value.set_self_href(path.to_string_lossy()); 24 | Ok(value) 25 | } 26 | } 27 | 28 | pub trait ToJsonPath: ToJson { 29 | /// Writes a value to a path as JSON. 30 | /// 31 | /// # Examples 32 | /// 33 | /// ```no_run 34 | /// use stac::Item; 35 | /// use stac_io::ToJsonPath; 36 | /// 37 | /// Item::new("an-id").to_json_path("an-id.json", true).unwrap(); 38 | /// ``` 39 | fn to_json_path(&self, path: impl AsRef, pretty: bool) -> Result<()> { 40 | let file = File::create(path)?; 41 | self.to_json_writer(file, pretty)?; 42 | Ok(()) 43 | } 44 | } 45 | 46 | impl FromJsonPath for T {} 47 | impl ToJsonPath for T {} 48 | 49 | #[cfg(test)] 50 | mod tests { 51 | use super::FromJsonPath; 52 | use stac::{Item, SelfHref}; 53 | 54 | #[test] 55 | fn set_href() { 56 | let item = Item::from_json_path("examples/simple-item.json").unwrap(); 57 | assert!( 58 | item.self_href() 59 | .unwrap() 60 | .ends_with("examples/simple-item.json") 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /crates/server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stac-server" 3 | description = "SpatioTemporal Asset Catalog (STAC) API server" 4 | version = "0.4.0" 5 | keywords = ["geospatial", "stac", "metadata", "geo", "server"] 6 | categories = ["science", "data-structures"] 7 | edition.workspace = true 8 | authors.workspace = true 9 | homepage.workspace = true 10 | repository.workspace = true 11 | license.workspace = true 12 | rust-version.workspace = true 13 | 14 | [features] 15 | axum = ["dep:axum", "dep:bytes", "dep:mime", "dep:tower-http"] 16 | duckdb = ["dep:stac-duckdb", "dep:bb8"] 17 | pgstac = [ 18 | "dep:bb8", 19 | "dep:bb8-postgres", 20 | "dep:pgstac", 21 | "dep:rustls", 22 | "dep:tokio-postgres", 23 | "dep:tokio-postgres-rustls", 24 | ] 25 | 26 | [dependencies] 27 | axum = { workspace = true, optional = true } 28 | bb8 = { workspace = true, optional = true } 29 | bb8-postgres = { workspace = true, optional = true } 30 | bytes = { workspace = true, optional = true } 31 | http.workspace = true 32 | mime = { workspace = true, optional = true } 33 | pgstac = { version = "0.4.0", path = "../pgstac", optional = true } 34 | rustls = { workspace = true, optional = true } 35 | serde.workspace = true 36 | serde_json.workspace = true 37 | serde_urlencoded.workspace = true 38 | stac = { version = "0.15.0", path = "../core" } 39 | stac-duckdb = { version = "0.3.0", path = "../duckdb", optional = true } 40 | thiserror.workspace = true 41 | tokio-postgres = { workspace = true, optional = true } 42 | tokio-postgres-rustls = { workspace = true, optional = true } 43 | tower-http = { workspace = true, features = ["cors", "trace"], optional = true } 44 | tracing.workspace = true 45 | url.workspace = true 46 | 47 | [dev-dependencies] 48 | serde_json.workspace = true 49 | tokio = { workspace = true, features = ["macros"] } 50 | tokio-test.workspace = true 51 | tower = { workspace = true, features = ["util"] } 52 | 53 | [package.metadata.docs.rs] 54 | all-features = true 55 | rustdoc-args = ["--cfg", "docsrs"] 56 | -------------------------------------------------------------------------------- /crates/core/src/json.rs: -------------------------------------------------------------------------------- 1 | use crate::{Error, Result}; 2 | use serde::{Serialize, de::DeserializeOwned}; 3 | use std::io::Write; 4 | 5 | /// Create a STAC object from JSON. 6 | pub trait FromJson: DeserializeOwned { 7 | /// Creates an object from JSON bytes. 8 | /// 9 | /// # Examples 10 | /// 11 | /// ``` 12 | /// use std::{fs::File, io::Read}; 13 | /// use stac::{Item, FromJson}; 14 | /// 15 | /// let mut buf = Vec::new(); 16 | /// File::open("examples/simple-item.json").unwrap().read_to_end(&mut buf).unwrap(); 17 | /// let item = Item::from_json_slice(&buf).unwrap(); 18 | /// ``` 19 | fn from_json_slice(slice: &[u8]) -> Result { 20 | serde_json::from_slice(slice).map_err(Error::from) 21 | } 22 | } 23 | 24 | /// Writes a STAC object to JSON bytes. 25 | pub trait ToJson: Serialize { 26 | /// Writes a value as JSON. 27 | /// 28 | /// # Examples 29 | /// 30 | /// ``` 31 | /// use stac::{ToJson, Item}; 32 | /// 33 | /// let mut buf = Vec::new(); 34 | /// Item::new("an-id").to_json_writer(&mut buf, true).unwrap(); 35 | /// ``` 36 | fn to_json_writer(&self, writer: impl Write, pretty: bool) -> Result<()> { 37 | if pretty { 38 | serde_json::to_writer_pretty(writer, self).map_err(Error::from) 39 | } else { 40 | serde_json::to_writer(writer, self).map_err(Error::from) 41 | } 42 | } 43 | 44 | /// Writes a value as JSON bytes. 45 | /// 46 | /// # Examples 47 | /// 48 | /// ``` 49 | /// use stac::{ToJson, Item}; 50 | /// 51 | /// Item::new("an-id").to_json_vec(true).unwrap(); 52 | /// ``` 53 | fn to_json_vec(&self, pretty: bool) -> Result> { 54 | if pretty { 55 | serde_json::to_vec_pretty(self).map_err(Error::from) 56 | } else { 57 | serde_json::to_vec(self).map_err(Error::from) 58 | } 59 | } 60 | } 61 | 62 | impl FromJson for T {} 63 | impl ToJson for T {} 64 | -------------------------------------------------------------------------------- /crates/extensions/data/auth/item.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.0.0", 3 | "stac_extensions": [ 4 | "https://stac-extensions.github.io/authentication/v1.1.0/schema.json" 5 | ], 6 | "type": "Feature", 7 | "id": "item", 8 | "bbox": [ 9 | 172.9, 10 | 1.3, 11 | 173, 12 | 1.4 13 | ], 14 | "geometry": { 15 | "type": "Polygon", 16 | "coordinates": [ 17 | [ 18 | [ 19 | 172.9, 20 | 1.3 21 | ], 22 | [ 23 | 173, 24 | 1.3 25 | ], 26 | [ 27 | 173, 28 | 1.4 29 | ], 30 | [ 31 | 172.9, 32 | 1.4 33 | ], 34 | [ 35 | 172.9, 36 | 1.3 37 | ] 38 | ] 39 | ] 40 | }, 41 | "properties": { 42 | "datetime": "2020-12-11T22:38:32Z", 43 | "auth:schemes": { 44 | "oauth": { 45 | "type": "oauth2", 46 | "description": "requires a login and user token", 47 | "flows": { 48 | "authorizationCode": { 49 | "authorizationUrl": "https://example.com/oauth/authorize", 50 | "tokenUrl": "https://example.com/oauth/token", 51 | "scopes": { 52 | "read:example": "Read the example data", 53 | "write:example": "Write the example data", 54 | "admin:example": "Read/write/delete the example data" 55 | } 56 | } 57 | } 58 | }, 59 | "none": { 60 | "type": "http", 61 | "scheme": "basic", 62 | "description": "Free access without restrictions" 63 | } 64 | } 65 | }, 66 | "links": [ 67 | { 68 | "href": "https://example.com/examples/item.json", 69 | "rel": "self" 70 | } 71 | ], 72 | "assets": { 73 | "data": { 74 | "href": "https://example.com/examples/file.xyz", 75 | "title": "Secure Asset Example", 76 | "type": "application/vnd.example", 77 | "roles": [ 78 | "data" 79 | ], 80 | "auth:refs": [ 81 | "oauth" 82 | ] 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /crates/io/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | /// Crate-specific error enum 4 | #[derive(Error, Debug)] 5 | #[non_exhaustive] 6 | pub enum Error { 7 | /// Returned when unable to read a STAC value from a path. 8 | #[error("{io}: {path}")] 9 | FromPath { 10 | /// The [std::io::Error] 11 | #[source] 12 | io: std::io::Error, 13 | 14 | /// The path. 15 | path: String, 16 | }, 17 | 18 | /// [http::header::InvalidHeaderName] 19 | #[error(transparent)] 20 | InvalidHeaderName(#[from] http::header::InvalidHeaderName), 21 | 22 | /// [http::header::InvalidHeaderValue] 23 | #[error(transparent)] 24 | InvalidHeaderValue(#[from] http::header::InvalidHeaderValue), 25 | 26 | /// [http::method::InvalidMethod] 27 | #[error(transparent)] 28 | InvalidMethod(#[from] http::method::InvalidMethod), 29 | 30 | /// [tokio::task::JoinError] 31 | #[error(transparent)] 32 | Join(#[from] tokio::task::JoinError), 33 | 34 | /// [std::io::Error] 35 | #[error(transparent)] 36 | Io(#[from] std::io::Error), 37 | 38 | #[cfg(feature = "store")] 39 | #[error(transparent)] 40 | /// [object_store::Error] 41 | ObjectStore(#[from] object_store::Error), 42 | 43 | #[cfg(feature = "geoparquet")] 44 | #[error(transparent)] 45 | /// [parquet::errors::ParquetError] 46 | Parquet(#[from] parquet::errors::ParquetError), 47 | 48 | #[error(transparent)] 49 | /// [reqwest::Error] 50 | Reqwest(#[from] reqwest::Error), 51 | 52 | #[error(transparent)] 53 | /// [serde_json::Error] 54 | SerdeJson(#[from] serde_json::Error), 55 | 56 | #[error(transparent)] 57 | /// [stac::Error] 58 | Stac(#[from] stac::Error), 59 | 60 | /// [std::num::TryFromIntError] 61 | #[error(transparent)] 62 | TryFromInt(#[from] std::num::TryFromIntError), 63 | 64 | /// Unsupported file format. 65 | #[error("unsupported format: {0}")] 66 | UnsupportedFormat(String), 67 | 68 | /// [url::ParseError] 69 | #[error(transparent)] 70 | UrlParse(#[from] url::ParseError), 71 | } 72 | -------------------------------------------------------------------------------- /crates/core/src/datetime.rs: -------------------------------------------------------------------------------- 1 | //! Datetime utilities. 2 | 3 | use crate::{Error, Result}; 4 | use chrono::{DateTime, FixedOffset}; 5 | 6 | /// A start and end datetime. 7 | pub type Interval = (Option>, Option>); 8 | 9 | /// Parse a datetime or datetime interval into a start and end datetime. 10 | /// 11 | /// Returns `None` to indicate an open interval. 12 | /// 13 | /// # Examples 14 | /// 15 | /// ``` 16 | /// let (start, end) = stac::datetime::parse("2023-07-11T12:00:00Z/..").unwrap(); 17 | /// assert!(start.is_some()); 18 | /// assert!(end.is_none()); 19 | /// ``` 20 | pub fn parse(datetime: &str) -> Result { 21 | if datetime.contains('/') { 22 | let mut iter = datetime.split('/'); 23 | let start = iter 24 | .next() 25 | .ok_or_else(|| Error::InvalidDatetime(datetime.to_string())) 26 | .and_then(parse_one)?; 27 | let end = iter 28 | .next() 29 | .ok_or_else(|| Error::InvalidDatetime(datetime.to_string())) 30 | .and_then(parse_one)?; 31 | if iter.next().is_some() { 32 | return Err(Error::InvalidDatetime(datetime.to_string())); 33 | } 34 | Ok((start, end)) 35 | } else if datetime == ".." { 36 | Err(Error::InvalidDatetime(datetime.to_string())) 37 | } else { 38 | let datetime = DateTime::parse_from_rfc3339(datetime).map(Some)?; 39 | Ok((datetime, datetime)) 40 | } 41 | } 42 | 43 | fn parse_one(s: &str) -> Result>> { 44 | if s == ".." { 45 | Ok(None) 46 | } else if s.is_empty() { 47 | log::warn!("an empty string in a datetime interval are invalid, converting to \"..\""); 48 | Ok(None) 49 | } else { 50 | DateTime::parse_from_rfc3339(s) 51 | .map(Some) 52 | .map_err(Error::from) 53 | } 54 | } 55 | 56 | mod tests { 57 | #[test] 58 | fn empty_interval() { 59 | let _ = super::parse("2024-04-27T00:00:00Z/").unwrap(); 60 | let _ = super::parse("/2024-04-27T00:00:00Z").unwrap(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /crates/cli/data/invalid-item.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.1.0", 3 | "stac_extensions": [], 4 | "type": "Feature", 5 | "id": "", 6 | "bbox": [ 7 | 172.91173669923782, 8 | 1.3438851951615003, 9 | 172.95469614953714, 10 | 1.3690476620161975 11 | ], 12 | "geometry": { 13 | "type": "Polygon", 14 | "coordinates": [ 15 | [ 16 | [ 17 | 172.91173669923782, 18 | 1.3438851951615003 19 | ], 20 | [ 21 | 172.95469614953714, 22 | 1.3438851951615003 23 | ], 24 | [ 25 | 172.95469614953714, 26 | 1.3690476620161975 27 | ], 28 | [ 29 | 172.91173669923782, 30 | 1.3690476620161975 31 | ], 32 | [ 33 | 172.91173669923782, 34 | 1.3438851951615003 35 | ] 36 | ] 37 | ] 38 | }, 39 | "properties": { 40 | "datetime": "2020-12-11T22:38:32.125000Z" 41 | }, 42 | "collection": "simple-collection", 43 | "links": [ 44 | { 45 | "rel": "collection", 46 | "href": "./collection.json", 47 | "type": "application/json", 48 | "title": "Simple Example Collection" 49 | }, 50 | { 51 | "rel": "root", 52 | "href": "./collection.json", 53 | "type": "application/json", 54 | "title": "Simple Example Collection" 55 | }, 56 | { 57 | "rel": "parent", 58 | "href": "./collection.json", 59 | "type": "application/json", 60 | "title": "Simple Example Collection" 61 | } 62 | ], 63 | "assets": { 64 | "visual": { 65 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 66 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 67 | "title": "3-Band Visual", 68 | "roles": [ 69 | "visual" 70 | ] 71 | }, 72 | "thumbnail": { 73 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 74 | "title": "Thumbnail", 75 | "type": "image/jpeg", 76 | "roles": [ 77 | "thumbnail" 78 | ] 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /crates/core/data/invalid-item.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.1.0", 3 | "stac_extensions": [], 4 | "type": "Feature", 5 | "id": "", 6 | "bbox": [ 7 | 172.91173669923782, 8 | 1.3438851951615003, 9 | 172.95469614953714, 10 | 1.3690476620161975 11 | ], 12 | "geometry": { 13 | "type": "Polygon", 14 | "coordinates": [ 15 | [ 16 | [ 17 | 172.91173669923782, 18 | 1.3438851951615003 19 | ], 20 | [ 21 | 172.95469614953714, 22 | 1.3438851951615003 23 | ], 24 | [ 25 | 172.95469614953714, 26 | 1.3690476620161975 27 | ], 28 | [ 29 | 172.91173669923782, 30 | 1.3690476620161975 31 | ], 32 | [ 33 | 172.91173669923782, 34 | 1.3438851951615003 35 | ] 36 | ] 37 | ] 38 | }, 39 | "properties": { 40 | "datetime": "2020-12-11T22:38:32.125000Z" 41 | }, 42 | "collection": "simple-collection", 43 | "links": [ 44 | { 45 | "rel": "collection", 46 | "href": "./collection.json", 47 | "type": "application/json", 48 | "title": "Simple Example Collection" 49 | }, 50 | { 51 | "rel": "root", 52 | "href": "./collection.json", 53 | "type": "application/json", 54 | "title": "Simple Example Collection" 55 | }, 56 | { 57 | "rel": "parent", 58 | "href": "./collection.json", 59 | "type": "application/json", 60 | "title": "Simple Example Collection" 61 | } 62 | ], 63 | "assets": { 64 | "visual": { 65 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 66 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 67 | "title": "3-Band Visual", 68 | "roles": [ 69 | "visual" 70 | ] 71 | }, 72 | "thumbnail": { 73 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 74 | "title": "Thumbnail", 75 | "type": "image/jpeg", 76 | "roles": [ 77 | "thumbnail" 78 | ] 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /spec-examples/v1.0.0/simple-item.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.0.0", 3 | "stac_extensions": [], 4 | "type": "Feature", 5 | "id": "20201211_223832_CS2", 6 | "bbox": [ 7 | 172.91173669923782, 8 | 1.3438851951615003, 9 | 172.95469614953714, 10 | 1.3690476620161975 11 | ], 12 | "geometry": { 13 | "type": "Polygon", 14 | "coordinates": [ 15 | [ 16 | [ 17 | 172.91173669923782, 18 | 1.3438851951615003 19 | ], 20 | [ 21 | 172.95469614953714, 22 | 1.3438851951615003 23 | ], 24 | [ 25 | 172.95469614953714, 26 | 1.3690476620161975 27 | ], 28 | [ 29 | 172.91173669923782, 30 | 1.3690476620161975 31 | ], 32 | [ 33 | 172.91173669923782, 34 | 1.3438851951615003 35 | ] 36 | ] 37 | ] 38 | }, 39 | "properties": { 40 | "datetime": "2020-12-11T22:38:32.125000Z" 41 | }, 42 | "collection": "simple-collection", 43 | "links": [ 44 | { 45 | "rel": "collection", 46 | "href": "./collection.json", 47 | "type": "application/json", 48 | "title": "Simple Example Collection" 49 | }, 50 | { 51 | "rel": "root", 52 | "href": "./collection.json", 53 | "type": "application/json", 54 | "title": "Simple Example Collection" 55 | }, 56 | { 57 | "rel": "parent", 58 | "href": "./collection.json", 59 | "type": "application/json", 60 | "title": "Simple Example Collection" 61 | } 62 | ], 63 | "assets": { 64 | "visual": { 65 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 66 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 67 | "title": "3-Band Visual", 68 | "roles": [ 69 | "visual" 70 | ] 71 | }, 72 | "thumbnail": { 73 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 74 | "title": "Thumbnail", 75 | "type": "image/jpeg", 76 | "roles": [ 77 | "thumbnail" 78 | ] 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /spec-examples/v1.1.0/simple-item.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.1.0", 3 | "stac_extensions": [], 4 | "type": "Feature", 5 | "id": "20201211_223832_CS2", 6 | "bbox": [ 7 | 172.91173669923782, 8 | 1.3438851951615003, 9 | 172.95469614953714, 10 | 1.3690476620161975 11 | ], 12 | "geometry": { 13 | "type": "Polygon", 14 | "coordinates": [ 15 | [ 16 | [ 17 | 172.91173669923782, 18 | 1.3438851951615003 19 | ], 20 | [ 21 | 172.95469614953714, 22 | 1.3438851951615003 23 | ], 24 | [ 25 | 172.95469614953714, 26 | 1.3690476620161975 27 | ], 28 | [ 29 | 172.91173669923782, 30 | 1.3690476620161975 31 | ], 32 | [ 33 | 172.91173669923782, 34 | 1.3438851951615003 35 | ] 36 | ] 37 | ] 38 | }, 39 | "properties": { 40 | "datetime": "2020-12-11T22:38:32.125000Z" 41 | }, 42 | "collection": "simple-collection", 43 | "links": [ 44 | { 45 | "rel": "collection", 46 | "href": "./collection.json", 47 | "type": "application/json", 48 | "title": "Simple Example Collection" 49 | }, 50 | { 51 | "rel": "root", 52 | "href": "./collection.json", 53 | "type": "application/json", 54 | "title": "Simple Example Collection" 55 | }, 56 | { 57 | "rel": "parent", 58 | "href": "./collection.json", 59 | "type": "application/json", 60 | "title": "Simple Example Collection" 61 | } 62 | ], 63 | "assets": { 64 | "visual": { 65 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 66 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 67 | "title": "3-Band Visual", 68 | "roles": [ 69 | "visual" 70 | ] 71 | }, 72 | "thumbnail": { 73 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 74 | "title": "Thumbnail", 75 | "type": "image/jpeg", 76 | "roles": [ 77 | "thumbnail" 78 | ] 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /spec-examples/v1.1.0-beta.1/simple-item.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.1.0-beta.1", 3 | "stac_extensions": [], 4 | "type": "Feature", 5 | "id": "20201211_223832_CS2", 6 | "bbox": [ 7 | 172.91173669923782, 8 | 1.3438851951615003, 9 | 172.95469614953714, 10 | 1.3690476620161975 11 | ], 12 | "geometry": { 13 | "type": "Polygon", 14 | "coordinates": [ 15 | [ 16 | [ 17 | 172.91173669923782, 18 | 1.3438851951615003 19 | ], 20 | [ 21 | 172.95469614953714, 22 | 1.3438851951615003 23 | ], 24 | [ 25 | 172.95469614953714, 26 | 1.3690476620161975 27 | ], 28 | [ 29 | 172.91173669923782, 30 | 1.3690476620161975 31 | ], 32 | [ 33 | 172.91173669923782, 34 | 1.3438851951615003 35 | ] 36 | ] 37 | ] 38 | }, 39 | "properties": { 40 | "datetime": "2020-12-11T22:38:32.125000Z" 41 | }, 42 | "collection": "simple-collection", 43 | "links": [ 44 | { 45 | "rel": "collection", 46 | "href": "./collection.json", 47 | "type": "application/json", 48 | "title": "Simple Example Collection" 49 | }, 50 | { 51 | "rel": "root", 52 | "href": "./collection.json", 53 | "type": "application/json", 54 | "title": "Simple Example Collection" 55 | }, 56 | { 57 | "rel": "parent", 58 | "href": "./collection.json", 59 | "type": "application/json", 60 | "title": "Simple Example Collection" 61 | } 62 | ], 63 | "assets": { 64 | "visual": { 65 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 66 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 67 | "title": "3-Band Visual", 68 | "roles": [ 69 | "visual" 70 | ] 71 | }, 72 | "thumbnail": { 73 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 74 | "title": "Thumbnail", 75 | "type": "image/jpeg", 76 | "roles": [ 77 | "thumbnail" 78 | ] 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /crates/derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | use syn::{DeriveInput, parse_macro_input}; 4 | 5 | #[proc_macro_derive(SelfHref)] 6 | pub fn self_href_derive(input: TokenStream) -> TokenStream { 7 | let input = parse_macro_input!(input as DeriveInput); 8 | let name = input.ident; 9 | let expanded = quote! { 10 | impl ::stac::SelfHref for #name { 11 | fn self_href(&self) -> Option<&str> { 12 | self.self_href.as_deref() 13 | } 14 | fn self_href_mut(&mut self) -> &mut Option { 15 | &mut self.self_href 16 | } 17 | } 18 | }; 19 | TokenStream::from(expanded) 20 | } 21 | 22 | #[proc_macro_derive(Links)] 23 | pub fn links_derive(input: TokenStream) -> TokenStream { 24 | let input = parse_macro_input!(input as DeriveInput); 25 | let name = input.ident; 26 | let expanded = quote! { 27 | impl ::stac::Links for #name { 28 | fn links(&self) -> &[::stac::Link] { 29 | &self.links 30 | } 31 | fn links_mut(&mut self) -> &mut Vec<::stac::Link> { 32 | &mut self.links 33 | } 34 | } 35 | }; 36 | TokenStream::from(expanded) 37 | } 38 | 39 | #[proc_macro_derive(Migrate)] 40 | pub fn migrate_derive(input: TokenStream) -> TokenStream { 41 | let input = parse_macro_input!(input as DeriveInput); 42 | let name = input.ident; 43 | let expanded = quote! { 44 | impl ::stac::Migrate for #name {} 45 | }; 46 | TokenStream::from(expanded) 47 | } 48 | 49 | #[proc_macro_derive(Fields)] 50 | pub fn fields_derive(input: TokenStream) -> TokenStream { 51 | let input = parse_macro_input!(input as DeriveInput); 52 | let name = input.ident; 53 | let expanded = quote! { 54 | impl ::stac::Fields for #name { 55 | fn fields(&self) -> &serde_json::Map { 56 | &self.additional_fields 57 | } 58 | fn fields_mut(&mut self) -> &mut serde_json::Map { 59 | &mut self.additional_fields 60 | } 61 | } 62 | }; 63 | TokenStream::from(expanded) 64 | } 65 | -------------------------------------------------------------------------------- /crates/core/src/band.rs: -------------------------------------------------------------------------------- 1 | use crate::{DataType, Statistics}; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_json::{Map, Value}; 4 | 5 | /// Bands are used to describe the available bands in a STAC entity or Asset. 6 | /// 7 | /// A band describes the general construct of a band or layer, which doesn't 8 | /// necessarily need to be a spectral band. By adding fields from extensions you 9 | /// can indicate that a band, for example, is 10 | /// 11 | /// - a spectral band (EO extension), 12 | /// - a band with classification results (classification extension), 13 | /// - a band with quality information such as cloud cover probabilities, 14 | /// 15 | /// etc. 16 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 17 | pub struct Band { 18 | /// The name of the band (e.g., "B01", "B8", "band2", "red"), which should 19 | /// be unique across all bands defined in the list of bands. 20 | /// 21 | /// This is typically the name the data provider uses for the band. 22 | #[serde(skip_serializing_if = "Option::is_none")] 23 | pub name: Option, 24 | 25 | /// Description to fully explain the band. 26 | /// 27 | /// CommonMark 0.29 syntax MAY be used for rich text representation. 28 | #[serde(skip_serializing_if = "Option::is_none")] 29 | pub description: Option, 30 | 31 | /// Value used to identify no-data. 32 | /// 33 | /// The extension specifies that this can be a number or a string, but we 34 | /// just use a f64 with a custom (de)serializer. 35 | /// 36 | /// TODO write custom (de)serializer. 37 | #[serde(skip_serializing_if = "Option::is_none")] 38 | pub nodata: Option, 39 | 40 | /// The data type of the values. 41 | #[serde(skip_serializing_if = "Option::is_none")] 42 | pub data_type: Option, 43 | 44 | /// Statistics of all the values. 45 | #[serde(skip_serializing_if = "Option::is_none")] 46 | pub statistics: Option, 47 | 48 | /// Unit of measurement of the value. 49 | #[serde(skip_serializing_if = "Option::is_none")] 50 | pub unit: Option, 51 | 52 | /// Additional fields on the asset. 53 | #[serde(flatten)] 54 | pub additional_fields: Map, 55 | } 56 | -------------------------------------------------------------------------------- /crates/core/data/20201211_223832_CS2.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "stac_version": "1.0.0", 4 | "stac_extensions": [ 5 | "https://stac-extensions.github.io/projection/v1.1.0/schema.json", 6 | "https://stac-extensions.github.io/raster/v1.1.0/schema.json" 7 | ], 8 | "id": "20201211_223832_CS2", 9 | "geometry": { 10 | "type": "Polygon", 11 | "coordinates": [ 12 | [ 13 | [ 14 | 172.9117367, 15 | 1.3438852 16 | ], 17 | [ 18 | 172.9546961, 19 | 1.3438852 20 | ], 21 | [ 22 | 172.9546961, 23 | 90.0 24 | ], 25 | [ 26 | 172.9117367, 27 | 90.0 28 | ], 29 | [ 30 | 172.9117367, 31 | 1.3438852 32 | ] 33 | ] 34 | ], 35 | "bbox": [ 36 | 172.9117367, 37 | 1.3438852, 38 | 172.9546961, 39 | 90.0 40 | ] 41 | }, 42 | "bbox": [ 43 | 172.9117367, 44 | 1.3438852, 45 | 172.9546961, 46 | 90.0 47 | ], 48 | "properties": { 49 | "datetime": "2024-09-05T22:33:45.900997Z", 50 | "proj:epsg": 32659, 51 | "proj:bbox": [ 52 | 712710.0, 53 | 148627.0, 54 | 717489.5, 55 | 151406.0 56 | ], 57 | "proj:centroid": { 58 | "lat": 1.3564664, 59 | "lon": 172.9332164 60 | }, 61 | "proj:shape": [ 62 | 5558, 63 | 9559 64 | ], 65 | "proj:transform": [ 66 | 0.5, 67 | 0.0, 68 | 712710.0, 69 | 0.0, 70 | -0.5, 71 | 151406.0 72 | ] 73 | }, 74 | "links": [], 75 | "assets": { 76 | "data": { 77 | "href": "/Users/gadomski/Downloads/20201211_223832_CS2.tif", 78 | "roles": [ 79 | "data" 80 | ], 81 | "raster:bands": [ 82 | { 83 | "data_type": "uint8", 84 | "spatial_resolution": 0.5 85 | }, 86 | { 87 | "data_type": "uint8", 88 | "spatial_resolution": 0.5 89 | }, 90 | { 91 | "data_type": "uint8", 92 | "spatial_resolution": 0.5 93 | }, 94 | { 95 | "data_type": "uint8", 96 | "spatial_resolution": 0.5 97 | } 98 | ] 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /crates/core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stac" 3 | description = "Rust library for the SpatioTemporal Asset Catalog (STAC) specification" 4 | version = "0.15.0" 5 | keywords = ["geospatial", "stac", "metadata", "geo"] 6 | authors.workspace = true 7 | categories.workspace = true 8 | edition.workspace = true 9 | homepage.workspace = true 10 | license.workspace = true 11 | repository.workspace = true 12 | rust-version.workspace = true 13 | 14 | [features] 15 | geo = ["dep:geo"] 16 | geoarrow = [ 17 | "dep:geoarrow-array", 18 | "dep:geoarrow-schema", 19 | "dep:arrow-array", 20 | "dep:arrow-cast", 21 | "dep:arrow-json", 22 | "dep:arrow-schema", 23 | "dep:geo-traits", 24 | "dep:geo-types", 25 | "dep:wkb", 26 | ] 27 | geoparquet = ["geoarrow", "dep:geoparquet", "dep:parquet"] 28 | 29 | [dependencies] 30 | arrow-array = { workspace = true, optional = true, features = ["chrono-tz"] } 31 | arrow-cast = { workspace = true, optional = true } 32 | arrow-json = { workspace = true, optional = true } 33 | arrow-schema = { workspace = true, optional = true } 34 | bytes.workspace = true 35 | chrono = { workspace = true, features = ["serde"] } 36 | cql2.workspace = true 37 | geo = { workspace = true, optional = true } 38 | geo-traits = { workspace = true, optional = true } 39 | geo-types = { workspace = true, optional = true } 40 | geoarrow-array = { workspace = true, optional = true } 41 | geoarrow-schema = { workspace = true, optional = true } 42 | geojson.workspace = true 43 | geoparquet = { workspace = true, optional = true } 44 | indexmap.workspace = true 45 | log.workspace = true 46 | mime.workspace = true 47 | parquet = { workspace = true, optional = true } 48 | serde = { workspace = true, features = ["derive"] } 49 | serde_json = { workspace = true, features = ["preserve_order"] } 50 | serde_urlencoded.workspace = true 51 | stac-derive = { version = "0.3.0", path = "../derive" } 52 | thiserror.workspace = true 53 | tracing.workspace = true 54 | url = { workspace = true, features = ["serde"] } 55 | wkb = { workspace = true, optional = true } 56 | 57 | [dev-dependencies] 58 | assert-json-diff.workspace = true 59 | bytes.workspace = true 60 | rstest.workspace = true 61 | tokio = { workspace = true, features = ["macros"] } 62 | tokio-test.workspace = true 63 | 64 | [package.metadata.docs.rs] 65 | all-features = true 66 | rustdoc-args = ["--cfg", "docsrs"] 67 | -------------------------------------------------------------------------------- /crates/validate/src/schemas/v1.1.0/data-values.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://schemas.stacspec.org/v1.1.0/item-spec/json-schema/data-values.json#", 4 | "title": "Fields related to data values", 5 | "type": "object", 6 | "properties": { 7 | "data_type": { 8 | "title": "Data type of the values", 9 | "type": "string", 10 | "enum": [ 11 | "int8", 12 | "int16", 13 | "int32", 14 | "int64", 15 | "uint8", 16 | "uint16", 17 | "uint32", 18 | "uint64", 19 | "float16", 20 | "float32", 21 | "float64", 22 | "cint16", 23 | "cint32", 24 | "cfloat32", 25 | "cfloat64", 26 | "other" 27 | ] 28 | }, 29 | "nodata": { 30 | "title": "No data value", 31 | "oneOf": [ 32 | { 33 | "type": "number" 34 | }, 35 | { 36 | "type": "string", 37 | "enum": [ 38 | "nan", 39 | "inf", 40 | "-inf" 41 | ] 42 | } 43 | ] 44 | }, 45 | "statistics": { 46 | "title": "Statistics", 47 | "type": "object", 48 | "minProperties": 1, 49 | "properties": { 50 | "minimum": { 51 | "title": "Minimum value of all the data values", 52 | "type": "number" 53 | }, 54 | "maximum": { 55 | "title": "Maximum value of all the data values", 56 | "type": "number" 57 | }, 58 | "mean": { 59 | "title": "Mean value of all the data values", 60 | "type": "number" 61 | }, 62 | "stddev": { 63 | "title": "Standard deviation value of all the data values", 64 | "type": "number" 65 | }, 66 | "count": { 67 | "title": "Total number of all data values", 68 | "type": "integer", 69 | "minimum": 0 70 | }, 71 | "valid_percent": { 72 | "title": "Percentage of valid (not nodata) values", 73 | "type": "number", 74 | "minimum": 0, 75 | "maximum": 100 76 | } 77 | } 78 | }, 79 | "unit": { 80 | "title": "Unit denomination of the data value", 81 | "type": "string" 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /crates/io/src/geoparquet.rs: -------------------------------------------------------------------------------- 1 | use crate::Result; 2 | use stac::{FromGeoparquet, IntoGeoparquet, geoparquet::WriterOptions}; 3 | use std::{fs::File, io::Read, path::Path}; 4 | 5 | /// Create a STAC object from geoparquet data. 6 | pub trait FromGeoparquetPath: FromGeoparquet { 7 | /// Reads geoparquet data from a file. 8 | /// 9 | /// If the `geoparquet` feature is not enabled, or if `Self` is anything 10 | /// other than an item collection, this function returns an error. 11 | /// 12 | /// # Examples 13 | /// 14 | /// ``` 15 | /// use stac::ItemCollection; 16 | /// use stac_io::FromGeoparquetPath; 17 | /// 18 | /// let item_collection = ItemCollection::from_geoparquet_path("data/extended-item.parquet").unwrap(); 19 | /// ``` 20 | fn from_geoparquet_path(path: impl AsRef) -> Result { 21 | let mut buf = Vec::new(); 22 | let _ = File::open(path)?.read_to_end(&mut buf)?; 23 | let value = Self::from_geoparquet_bytes(buf)?; 24 | Ok(value) 25 | } 26 | } 27 | 28 | /// Write a STAC object to geoparquet. 29 | pub trait IntoGeoparquetPath: IntoGeoparquet { 30 | /// Writes a value to a path as stac-geoparquet. 31 | /// 32 | /// # Examples 33 | /// 34 | /// ```no_run 35 | /// use stac::{ItemCollection, Item}; 36 | /// use stac::geoparquet::WriterOptions; 37 | /// use stac_io::IntoGeoparquetPath; 38 | /// 39 | /// let item_collection: ItemCollection = vec![Item::new("a"), Item::new("b")].into(); 40 | /// item_collection.into_geoparquet_path("items.geoparquet", WriterOptions::default()).unwrap(); 41 | /// ``` 42 | fn into_geoparquet_path( 43 | self, 44 | path: impl AsRef, 45 | writer_options: WriterOptions, 46 | ) -> Result<()> { 47 | let file = File::create(path)?; 48 | self.into_geoparquet_writer(file, writer_options)?; 49 | Ok(()) 50 | } 51 | } 52 | 53 | impl FromGeoparquetPath for T where T: FromGeoparquet {} 54 | impl IntoGeoparquetPath for T where T: IntoGeoparquet {} 55 | 56 | #[cfg(test)] 57 | mod tests { 58 | use super::FromGeoparquetPath; 59 | use stac::{ItemCollection, Value}; 60 | 61 | #[test] 62 | fn read() { 63 | let _ = ItemCollection::from_geoparquet_path("data/extended-item.parquet"); 64 | } 65 | 66 | #[test] 67 | fn read_value() { 68 | let _ = Value::from_geoparquet_path("data/extended-item.parquet").unwrap(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to **rustac** 2 | 3 | First off, thanks for contributing! 4 | We appreciates you. 5 | 6 | ## Testing 7 | 8 | We aim for comprehensive unit testing of this library. 9 | Please provide tests for any new features, or to demonstrate bugs. 10 | Draft pull requests with a failing test to demonstrate a bug are much appreciated. 11 | 12 | To run the tests (for the [default crates](./Cargo.toml#L16)): 13 | 14 | ```bash 15 | cargo test 16 | ``` 17 | 18 | To run the `rustac` CLI using your local changes: 19 | 20 | ```bash 21 | cargo run --help 22 | ``` 23 | 24 | ### DuckDB 25 | 26 | By default, **rustac** will try to find DuckDB on your system, so you need to set `DUCKDB_LIB_DIR` to the directory containing your **libduckdb**. 27 | If you're on macos and using [Homebrew](https://brew.sh/), this might be `export DUCKDB_LIB_DIR=/opt/homebrew/lib` 28 | On linux, you can download the `libduckdb-linux-{platform}.zip` file from the [latest release](https://github.com/duckdb/duckdb/releases/latest) and unzip the contents into a directory on your machine (you will also need to set `LD_LIBRARY_PATH` to include this directory). 29 | 30 | If you don't want to or can't install DuckDB, you can build **rustac** with the `duckdb-bundled` feature to build the bindings from scratch (warning: this takes a while): 31 | 32 | ```sh 33 | cargo build -F duckdb-bundled # or cargo test, cargo run, etc... 34 | ``` 35 | 36 | ## Linting 37 | 38 | We use [prek](https://prek.j178.dev) to run our formatters and linters. 39 | [Install it](https://prek.j178.dev/installation), then: 40 | 41 | ```sh 42 | prek run --all-files 43 | ``` 44 | 45 | To run `prek` on new commits: 46 | 47 | ```sh 48 | prek install 49 | ``` 50 | 51 | ## Submitting changes 52 | 53 | Please open a [pull request](https://docs.github.com/en/pull-requests) with your changes -- make sure to include unit tests. 54 | Please follow standard git commit formatting (subject line 50 characters max, wrap the body at 72 characters). 55 | Run `prek run --all-files` to make sure everything's copacetic. 56 | 57 | We use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). 58 | Your commits do not have to but if you'd like to format them this way, we would be grateful. 59 | 60 | If you can, use `git rebase -i` to create a clean, well-formatted history before opening your pull request. 61 | If you need to make changes after opening your pull request (e.g. to fix CI breakages) we will be grateful if you squash those fixes into their relevant commits. 62 | 63 | Thanks so much! \ 64 | -Pete Gadomski 65 | -------------------------------------------------------------------------------- /crates/cli/README.md: -------------------------------------------------------------------------------- 1 | # rustac 2 | 3 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/stac-utils/rustac/ci.yml?branch=main&style=for-the-badge)](https://github.com/stac-utils/rustac/actions/workflows/ci.yml) 4 | [![docs.rs](https://img.shields.io/docsrs/rustac?style=for-the-badge)](https://docs.rs/rustac/latest/rustac/) 5 | [![Crates.io](https://img.shields.io/crates/v/rustac?style=for-the-badge)](https://crates.io/crates/rustac) 6 | ![Crates.io](https://img.shields.io/crates/l/rustac?style=for-the-badge) 7 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg?style=for-the-badge)](./CODE_OF_CONDUCT) 8 | 9 | Command Line Interface (CLI) for [STAC](https://stacspec.org/), named `rustac`. 10 | 11 | ## Installation 12 | 13 | If you have DuckDB on your system: 14 | 15 | ```sh 16 | cargo install rustac 17 | ``` 18 | 19 | > [!TIP] 20 | > Set `DUCKDB_LIB_DIR` to the directory containing your **libduckdb**. 21 | > If you're on macos and using [Homebrew](https://brew.sh/), this might be `export DUCKDB_LIB_DIR=/opt/homebrew/lib` 22 | 23 | Otherwise: 24 | 25 | ```sh 26 | cargo install rustac -F duckdb-bundled # (slow) 27 | ``` 28 | 29 | Then: 30 | 31 | ```shell 32 | # Search 33 | $ rustac search https://landsatlook.usgs.gov/stac-server \ 34 | --collections landsat-c2l2-sr \ 35 | --intersects '{"type": "Point", "coordinates": [-105.119, 40.173]}' \ 36 | --sortby='-properties.datetime' \ 37 | --max-items 1000 \ 38 | items.parquet 39 | 40 | # Translate formats 41 | $ rustac translate items.parquet items.ndjson 42 | $ rustac translate items.ndjson items.json 43 | 44 | # Migrate STAC versions 45 | $ rustac translate item-v1.0.json item-v1.1.json --migrate 46 | 47 | # Search stac-geoparquet (no API server required) 48 | $ stac search items.parquet 49 | 50 | # Server 51 | $ rustac serve items.parquet # Opens a STAC API server on http://localhost:7822 52 | 53 | # Validate 54 | $ rustac validate item.json 55 | ``` 56 | 57 | ## Usage 58 | 59 | **rustac** provides the following subcommands: 60 | 61 | - `rustac search`: searches STAC APIs and, if the `duckdb` feature is enabled, geoparquet files 62 | - `rustac serve`: serves a STAC API 63 | - `rustac translate`: converts STAC from one format to another 64 | - `rustac validate`: validates a STAC value 65 | 66 | Use the `--help` flag to see all available options for the CLI and the subcommands: 67 | 68 | ## Other info 69 | 70 | This crate is part of the [rustac](https://github.com/stac-utils/rustac) monorepo, see its README for contributing and license information. 71 | -------------------------------------------------------------------------------- /crates/validate/src/schemas/v1.0.0/catalog.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://schemas.stacspec.org/v1.0.0/catalog-spec/json-schema/catalog.json#", 4 | "title": "STAC Catalog Specification", 5 | "description": "This object represents Catalogs in a SpatioTemporal Asset Catalog.", 6 | "allOf": [ 7 | { 8 | "$ref": "#/definitions/catalog" 9 | } 10 | ], 11 | "definitions": { 12 | "catalog": { 13 | "title": "STAC Catalog", 14 | "type": "object", 15 | "required": [ 16 | "stac_version", 17 | "type", 18 | "id", 19 | "description", 20 | "links" 21 | ], 22 | "properties": { 23 | "stac_version": { 24 | "title": "STAC version", 25 | "type": "string", 26 | "const": "1.0.0" 27 | }, 28 | "stac_extensions": { 29 | "title": "STAC extensions", 30 | "type": "array", 31 | "uniqueItems": true, 32 | "items": { 33 | "title": "Reference to a JSON Schema", 34 | "type": "string", 35 | "format": "iri" 36 | } 37 | }, 38 | "type": { 39 | "title": "Type of STAC entity", 40 | "const": "Catalog" 41 | }, 42 | "id": { 43 | "title": "Identifier", 44 | "type": "string", 45 | "minLength": 1 46 | }, 47 | "title": { 48 | "title": "Title", 49 | "type": "string" 50 | }, 51 | "description": { 52 | "title": "Description", 53 | "type": "string", 54 | "minLength": 1 55 | }, 56 | "links": { 57 | "title": "Links", 58 | "type": "array", 59 | "items": { 60 | "$ref": "#/definitions/link" 61 | } 62 | } 63 | } 64 | }, 65 | "link": { 66 | "type": "object", 67 | "required": [ 68 | "rel", 69 | "href" 70 | ], 71 | "properties": { 72 | "href": { 73 | "title": "Link reference", 74 | "type": "string", 75 | "format": "iri-reference", 76 | "minLength": 1 77 | }, 78 | "rel": { 79 | "title": "Link relation type", 80 | "type": "string", 81 | "minLength": 1 82 | }, 83 | "type": { 84 | "title": "Link type", 85 | "type": "string" 86 | }, 87 | "title": { 88 | "title": "Link title", 89 | "type": "string" 90 | } 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /crates/io/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [0.2.0](https://github.com/stac-utils/rustac/compare/stac-io-v0.1.2...stac-io-v0.2.0) (2025-12-01) 8 | 9 | 10 | ### ⚠ BREAKING CHANGES 11 | 12 | * move stac_api crate into stac crate ([#869](https://github.com/stac-utils/rustac/issues/869)) 13 | * remove unused error enums ([#868](https://github.com/stac-utils/rustac/issues/868)) 14 | * move api client to stac-io crate ([#864](https://github.com/stac-utils/rustac/issues/864)) 15 | 16 | ### Features 17 | 18 | * add geoparquet writer encoder and object writing ([#863](https://github.com/stac-utils/rustac/issues/863)) ([ec6e7de](https://github.com/stac-utils/rustac/commit/ec6e7de6bf7c43cff11ba5d7dfd9f7c0654b2db1)) 19 | * specify max_row_group_size in geoparquet WriterBuilder ([#846](https://github.com/stac-utils/rustac/issues/846)) ([2bde538](https://github.com/stac-utils/rustac/commit/2bde538b41e5900b5be2d75587b1f8904520b3a1)) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * remove unused error enums ([#868](https://github.com/stac-utils/rustac/issues/868)) ([cf0e815](https://github.com/stac-utils/rustac/commit/cf0e815e03433e8ef219a79a67161174f3e99e84)) 25 | 26 | 27 | ### Code Refactoring 28 | 29 | * move api client to stac-io crate ([#864](https://github.com/stac-utils/rustac/issues/864)) ([e06de28](https://github.com/stac-utils/rustac/commit/e06de28787f9868f000ccc884979dcede1984f01)), closes [#764](https://github.com/stac-utils/rustac/issues/764) 30 | * move stac_api crate into stac crate ([#869](https://github.com/stac-utils/rustac/issues/869)) ([d0f7405](https://github.com/stac-utils/rustac/commit/d0f7405a811dd2c3b044404b4a6a48cf07926a89)) 31 | 32 | 33 | ### Dependencies 34 | 35 | * The following workspace dependencies were updated 36 | * dependencies 37 | * stac bumped from 0.14.0 to 0.15.0 38 | 39 | ## [0.1.2] - 2025-11-14 40 | 41 | Update **stac** dependency. 42 | 43 | ## [0.1.1] - 2025-09-23 44 | 45 | Bump dependencies. 46 | 47 | ## [0.1.0] - 2025-07-10 48 | 49 | Initial release 50 | 51 | [unreleased]: https://github.com/stac-utils/rustac/compare/stac-io-v0.1.2...main 52 | [0.1.2]: https://github.com/stac-utils/rustac/compare/stac-io-v0.1.1...stac-io-v0.1.2 53 | [0.1.1]: https://github.com/stac-utils/rustac/compare/stac-io-v0.1.0...stac-io-v0.1.1 54 | [0.1.0]: https://github.com/stac-utils/rustac/releases/tag/stac-io-v0.1.0 55 | 56 | 57 | -------------------------------------------------------------------------------- /crates/validate/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Validate STAC objects with [json-schema](https://json-schema.org/). 2 | //! 3 | //! # Examples 4 | //! 5 | //! Validation is provided via the [Validate] trait: 6 | //! 7 | //! ``` 8 | //! use stac::Item; 9 | //! use stac_validate::Validate; 10 | //! 11 | //! #[tokio::main] 12 | //! async fn main() { 13 | //! Item::new("an-id").validate().await.unwrap(); 14 | //! } 15 | //! ``` 16 | //! 17 | //! All fetched schemas are cached, so if you're you're doing multiple 18 | //! validations, you should re-use the same [Validator]: 19 | //! 20 | //! ``` 21 | //! use stac::Item; 22 | //! use stac_validate::Validator; 23 | //! 24 | //! #[tokio::main] 25 | //! async fn main() { 26 | //! let mut items: Vec<_> = (0..10).map(|n| Item::new(format!("item-{}", n))).collect(); 27 | //! let mut validator = Validator::new().await.unwrap(); 28 | //! for item in items { 29 | //! validator.validate(&item).await.unwrap(); 30 | //! } 31 | //! } 32 | //! ``` 33 | //! 34 | //! [Validator] is cheap to clone, so you are encouraged to validate a large 35 | //! number of objects at the same time if that's your use-case. 36 | 37 | use serde::Serialize; 38 | 39 | mod error; 40 | mod validator; 41 | use async_trait::async_trait; 42 | 43 | pub use {error::Error, validator::Validator}; 44 | 45 | /// Public result type. 46 | pub type Result = std::result::Result; 47 | 48 | /// Validate any serializable object with [json-schema](https://json-schema.org/) 49 | #[async_trait] 50 | pub trait Validate: Serialize + Sized { 51 | /// Validates this object. 52 | /// 53 | /// If the object fails validation, this will return an [Error::Validation] 54 | /// which contains a vector of all of the validation errors. 55 | /// 56 | /// If you're doing multiple validations, use [Validator::validate], which 57 | /// will re-use cached schemas. 58 | /// 59 | /// # Examples 60 | /// 61 | /// ``` 62 | /// use stac::Item; 63 | /// use stac_validate::Validate; 64 | /// 65 | /// #[tokio::main] 66 | /// async fn main() { 67 | /// let mut item = Item::new("an-id"); 68 | /// item.validate().await.unwrap(); 69 | /// } 70 | /// ``` 71 | async fn validate(&self) -> Result<()> { 72 | let mut validator = Validator::new().await?; 73 | validator.validate(self).await 74 | } 75 | } 76 | 77 | impl Validate for T {} 78 | 79 | /// Returns a string suitable for use as a HTTP user agent. 80 | pub fn user_agent() -> &'static str { 81 | concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")) 82 | } 83 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # rustac 2 | 3 | ![rustac logo](./img/rustac-small.png) 4 | 5 | Welcome to the home of STAC and Rust. 6 | We're happy you're here. 7 | 8 | ## What is rustac? 9 | 10 | **rustac** is a [Github repository](https://github.com/stac-utils/rustac) that holds the code for several Rust [crates](https://doc.rust-lang.org/book/ch07-01-packages-and-crates.html) for creating, searching, and otherwise working with [STAC](https://stacspec.org). 11 | 12 | ## What is rustac-py? 13 | 14 | **rustac-py** is a Python [package](https://pypi.org/project/rustac/) that provides a simple API for interacting with STAC. 15 | **rustac-py** uses the Rust code in **rustac** under the hood. 16 | 17 | 18 | ```python 19 | import rustac 20 | 21 | items = rustac.search("s3://bucket/items.parquet", ...) 22 | ``` 23 | 24 | 25 | **rustac-py** as its own [docs](https://stac-utils.github.io/rustac-py). 26 | 27 | ## Why are rustac and rustac-py in two separate repos? 28 | 29 | Couple of reasons: 30 | 31 | 1. **rustac** is intended to be useful on its own. 32 | It's not just the engine for some Python bindings. 33 | 2. Care-and-feeding for Python wheels built from Rust is a bit finicky. 34 | By moving **rustac-py** to its own repo, we're able to separate the concerns of keeping a good, clean Rust core, and building Python wheels. 35 | Not everyone agrees with this strategy, but here we are. 36 | 37 | ## docs.rs 38 | 39 | Our Rust documentation is hosted on `docs.rs`: 40 | 41 | - [stac](https://docs.rs/stac): The core Rust crate 42 | - [stac-io](https://docs.rs/stac-io): Input and output 43 | - [stac-server](https://docs.rs/stac-server): A STAC API server with multiple backends 44 | - [pgstac](https://docs.rs/pgstac): Rust bindings for [pgstac](https://github.com/stac-utils/pgstac) 45 | - [stac-duckdb](https://docs.rs/stac-duckdb/latest/stac_duckdb/): A client and methods for querying [stac-geoparquet](https://github.com/stac-utils/stac-geoparquet/blob/main/spec/stac-geoparquet-spec.md) via [DuckDB](https://duckdb.org/) 46 | 47 | ## Acknowledgements 48 | 49 | We'd like to thank [@jkeifer](https://github.com/jkeifer), [@parksjr](https://github.com/parksjr), and [@Xenocide122](https://github.com/Xenocide122) (all from [@Element84](https://github.com/Element84/)) for creating the [rustac logo](./img/rustac.svg) from an AI-generated image from this prompt: 50 | 51 | > There is a library for working with STAC metadata that is written in rust called rustac: . That name sounds like the word "rustic", and is meant to envoke (sic) an image of "a cabin and a glass of neat whisky". 52 | -------------------------------------------------------------------------------- /crates/core/src/item_asset.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::{Map, Value}; 3 | 4 | /// An item asset is an object that contains details about the datafiles that 5 | /// will be included in member items. 6 | /// 7 | /// Assets included at the Collection level do not imply that all assets are 8 | /// available from all Items. However, it is recommended that the Asset 9 | /// Definition is a complete set of all assets that may be available from any 10 | /// member Items. So this should be the union of the available assets, not just 11 | /// the intersection of the available assets. 12 | /// 13 | /// Other custom fields, or fields from other extensions may also be included in the Asset object. 14 | /// 15 | /// Any property that exists for a Collection-level asset object must also exist 16 | /// in the corresponding assets object in each Item. If a collection's asset 17 | /// object contains properties that are not explicitly stated in the Item's 18 | /// asset object then that property does not apply to the item's asset. Item 19 | /// asset objects at the Collection-level can describe any of the properties of 20 | /// an asset, but those assets properties and values must also reside in the 21 | /// item's asset object. To consolidate item-level asset object properties in an 22 | /// API setting, consider storing the STAC Item objects without the larger 23 | /// properties internally as 'invalid' STAC items, and merge in the desired 24 | /// properties at serving time from the Collection-level. 25 | /// 26 | /// At least two fields (e.g. title and type) are required to be provided, in 27 | /// order for it to adequately describe Item assets. The two fields must not 28 | /// necessarily be taken from the defined fields on this struct and may include 29 | /// any custom field. 30 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] 31 | pub struct ItemAsset { 32 | /// The displayed title for clients and users. 33 | #[serde(skip_serializing_if = "Option::is_none")] 34 | pub title: Option, 35 | 36 | /// A description of the Asset providing additional details, such as how it 37 | /// was processed or created. 38 | /// 39 | /// CommonMark 0.29 syntax MAY be used for rich text representation. 40 | #[serde(skip_serializing_if = "Option::is_none")] 41 | pub description: Option, 42 | 43 | /// Media type of the asset. 44 | #[serde(skip_serializing_if = "Option::is_none")] 45 | pub r#type: Option, 46 | 47 | /// The semantic roles of the asset, similar to the use of rel in links. 48 | #[serde(skip_serializing_if = "Vec::is_empty", default)] 49 | pub roles: Vec, 50 | 51 | /// Additional fields. 52 | #[serde(flatten)] 53 | pub additional_fields: Map, 54 | } 55 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "crates/cli", 5 | "crates/core", 6 | "crates/derive", 7 | "crates/duckdb", 8 | "crates/extensions", 9 | "crates/io", 10 | "crates/pgstac", 11 | "crates/server", 12 | "crates/validate", 13 | "crates/wasm", 14 | ] 15 | default-members = [ 16 | "crates/core", 17 | "crates/cli", 18 | "crates/derive", 19 | "crates/duckdb", 20 | "crates/extensions", 21 | "crates/io", 22 | "crates/server", 23 | "crates/validate", 24 | ] 25 | 26 | [workspace.package] 27 | authors = ["Pete Gadomski "] 28 | edition = "2024" 29 | homepage = "https://stac-utils.github.io/rustac" 30 | repository = "https://github.com/stac-utils/rustac" 31 | license = "MIT OR Apache-2.0" 32 | categories = ["science", "data-structures"] 33 | rust-version = "1.85" 34 | 35 | [workspace.dependencies] 36 | anyhow = "1.0" 37 | arrow-array = "56.0.0" 38 | arrow-cast = "56.0.0" 39 | arrow-json = "56.0.0" 40 | arrow-schema = "56.0.0" 41 | assert-json-diff = "2.0" 42 | assert_cmd = "2.0" 43 | async-recursion = "1.1.1" 44 | async-stream = "0.3.6" 45 | async-trait = "0.1.89" 46 | axum = "0.8.1" 47 | bb8 = "0.9.0" 48 | bb8-postgres = "0.9.0" 49 | bytes = "1.7" 50 | chrono = "0.4.39" 51 | clap = "4.5" 52 | clap_complete = "4.5" 53 | cql2 = "0.5.0" 54 | duckdb = "1.4.0" 55 | fluent-uri = "0.4.1" 56 | futures = "0.3.31" 57 | futures-core = "0.3.31" 58 | futures-util = "0.3.31" 59 | geo = "0.32.0" 60 | geo-traits = "0.3.0" 61 | geo-types = "0.7.16" 62 | geoarrow-array = "0.6.0" 63 | geoparquet = "0.6.0" 64 | geoarrow-schema = "0.6.0" 65 | geojson = "0.24.1" 66 | getrandom = { version = "0.3.3", features = ["wasm_js"] } 67 | http = "1.1" 68 | indexmap = { version = "2.10.0", features = ["serde"] } 69 | jsonschema = { version = "0.37.0", default-features = false, features = [ 70 | "resolve-async", 71 | ] } 72 | libduckdb-sys = "1.3.0" 73 | log = "0.4.25" 74 | mime = "0.3.17" 75 | mockito = "1.5" 76 | object_store = "0.12.0" 77 | parquet = { version = "56.0.0" } 78 | quote = "1.0" 79 | reqwest = { version = "0.12.8", default-features = false, features = [ 80 | "rustls-tls", 81 | ] } 82 | referencing = { version = "0.37.0", features = ["retrieve-async"] } 83 | rstest = "0.26.1" 84 | rustls = { version = "0.23.22", default-features = false } 85 | serde = "1.0" 86 | serde_json = "1.0" 87 | serde_urlencoded = "0.7.1" 88 | syn = "2.0" 89 | tempfile = "3.16" 90 | thiserror = "2.0" 91 | tokio = "1.44" 92 | tokio-postgres = "0.7.12" 93 | tokio-postgres-rustls = "0.13.0" 94 | tokio-stream = "0.1.16" 95 | tokio-test = "0.4.4" 96 | tower = "0.5.1" 97 | tower-http = "0.6.1" 98 | tracing = "0.1.40" 99 | tracing-subscriber = { version = "0.3.18", features = [ 100 | "env-filter", 101 | "tracing-log", 102 | ] } 103 | tracing-indicatif = "0.3.9" 104 | url = "2.3" 105 | webpki-roots = "1.0.0" 106 | wkb = "0.9.0" 107 | -------------------------------------------------------------------------------- /crates/core/src/api/mod.rs: -------------------------------------------------------------------------------- 1 | //! Rust implementation of the [STAC API](https://github.com/radiantearth/stac-api-spec) specification. 2 | //! 3 | //! This module **is**: 4 | //! 5 | //! - Data structures 6 | //! 7 | //! This module **is not**: 8 | //! 9 | //! - A server implementation 10 | //! 11 | //! For a STAC API server written in Rust based on this crate, see our 12 | //! [stac-server](https://github.com/stac-utils/rustac/tree/main/stac-server). 13 | //! 14 | //! # Data structures 15 | //! 16 | //! Each API endpoint has its own data structure. In some cases, these are 17 | //! light wrappers around [stac] data structures. In other cases, they can be 18 | //! different -- e.g. the `/search` endpoint may not return [Items](stac::Item) 19 | //! if the [fields](https://github.com/stac-api-extensions/fields) extension is 20 | //! used, so the return type is a crate-specific [Item] struct. 21 | //! 22 | //! For example, here's the root structure (a.k.a the landing page): 23 | //! 24 | //! ``` 25 | //! use stac::Catalog; 26 | //! use stac::api::{Root, Conformance, CORE_URI}; 27 | //! let root = Root { 28 | //! catalog: Catalog::new("an-id", "a description"), 29 | //! conformance: Conformance { 30 | //! conforms_to: vec![CORE_URI.to_string()] 31 | //! }, 32 | //! }; 33 | //! ``` 34 | 35 | #![warn(missing_docs, unused_qualifications)] 36 | 37 | mod collections; 38 | mod conformance; 39 | mod fields; 40 | mod filter; 41 | mod item_collection; 42 | mod items; 43 | mod root; 44 | mod search; 45 | mod sort; 46 | mod url_builder; 47 | 48 | pub use collections::Collections; 49 | pub use conformance::{ 50 | COLLECTIONS_URI, CORE_URI, Conformance, FEATURES_URI, FILTER_URIS, GEOJSON_URI, 51 | ITEM_SEARCH_URI, OGC_API_FEATURES_URI, 52 | }; 53 | pub use fields::Fields; 54 | pub use filter::Filter; 55 | pub use item_collection::{Context, ItemCollection}; 56 | pub use items::{GetItems, Items}; 57 | pub use root::Root; 58 | pub use search::{GetSearch, Search}; 59 | pub use sort::{Direction, Sortby}; 60 | pub use url_builder::UrlBuilder; 61 | 62 | /// Crate-specific result type. 63 | pub type Result = std::result::Result; 64 | 65 | /// A STAC API Item type definition. 66 | /// 67 | /// By default, STAC API endpoints that return [stac::Item] objects return every 68 | /// field of those Items. However, Item objects can have hundreds of fields, or 69 | /// large geometries, and even smaller Item objects can add up when large 70 | /// numbers of them are in results. Frequently, not all fields in an Item are 71 | /// used, so this specification provides a mechanism for clients to request that 72 | /// servers to explicitly include or exclude certain fields. 73 | pub type Item = serde_json::Map; 74 | 75 | /// Return this crate's version. 76 | /// 77 | /// # Examples 78 | /// 79 | /// ``` 80 | /// println!("{}", stac::api::version()); 81 | /// ``` 82 | pub fn version() -> &'static str { 83 | env!("CARGO_PKG_VERSION") 84 | } 85 | 86 | #[cfg(test)] 87 | use geojson as _; 88 | -------------------------------------------------------------------------------- /spec-examples/v1.0.0/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "simple-collection", 3 | "type": "Collection", 4 | "stac_extensions": [ 5 | "https://stac-extensions.github.io/eo/v1.0.0/schema.json", 6 | "https://stac-extensions.github.io/projection/v1.0.0/schema.json", 7 | "https://stac-extensions.github.io/view/v1.0.0/schema.json" 8 | ], 9 | "stac_version": "1.0.0", 10 | "description": "A simple collection demonstrating core catalog fields with links to a couple of items", 11 | "title": "Simple Example Collection", 12 | "providers": [ 13 | { 14 | "name": "Remote Data, Inc", 15 | "description": "Producers of awesome spatiotemporal assets", 16 | "roles": [ 17 | "producer", 18 | "processor" 19 | ], 20 | "url": "http://remotedata.io" 21 | } 22 | ], 23 | "extent": { 24 | "spatial": { 25 | "bbox": [ 26 | [ 27 | 172.91173669923782, 28 | 1.3438851951615003, 29 | 172.95469614953714, 30 | 1.3690476620161975 31 | ] 32 | ] 33 | }, 34 | "temporal": { 35 | "interval": [ 36 | [ 37 | "2020-12-11T22:38:32.125Z", 38 | "2020-12-14T18:02:31.437Z" 39 | ] 40 | ] 41 | } 42 | }, 43 | "license": "CC-BY-4.0", 44 | "summaries": { 45 | "platform": [ 46 | "cool_sat1", 47 | "cool_sat2" 48 | ], 49 | "constellation": [ 50 | "ion" 51 | ], 52 | "instruments": [ 53 | "cool_sensor_v1", 54 | "cool_sensor_v2" 55 | ], 56 | "gsd": { 57 | "minimum": 0.512, 58 | "maximum": 0.66 59 | }, 60 | "eo:cloud_cover": { 61 | "minimum": 1.2, 62 | "maximum": 1.2 63 | }, 64 | "proj:epsg": { 65 | "minimum": 32659, 66 | "maximum": 32659 67 | }, 68 | "view:sun_elevation": { 69 | "minimum": 54.9, 70 | "maximum": 54.9 71 | }, 72 | "view:off_nadir": { 73 | "minimum": 3.8, 74 | "maximum": 3.8 75 | }, 76 | "view:sun_azimuth": { 77 | "minimum": 135.7, 78 | "maximum": 135.7 79 | } 80 | }, 81 | "links": [ 82 | { 83 | "rel": "root", 84 | "href": "./collection.json", 85 | "type": "application/json", 86 | "title": "Simple Example Collection" 87 | }, 88 | { 89 | "rel": "item", 90 | "href": "./simple-item.json", 91 | "type": "application/geo+json", 92 | "title": "Simple Item" 93 | }, 94 | { 95 | "rel": "item", 96 | "href": "./core-item.json", 97 | "type": "application/geo+json", 98 | "title": "Core Item" 99 | }, 100 | { 101 | "rel": "item", 102 | "href": "./extended-item.json", 103 | "type": "application/geo+json", 104 | "title": "Extended Item" 105 | }, 106 | { 107 | "rel": "self", 108 | "href": "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.0.0/examples/collection.json", 109 | "type": "application/json" 110 | } 111 | ] 112 | } 113 | -------------------------------------------------------------------------------- /spec-examples/v1.1.0-beta.1/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "simple-collection", 3 | "type": "Collection", 4 | "stac_extensions": [ 5 | "https://stac-extensions.github.io/eo/v2.0.0-beta.1/schema.json", 6 | "https://stac-extensions.github.io/projection/v2.0.0/schema.json", 7 | "https://stac-extensions.github.io/view/v1.0.0/schema.json" 8 | ], 9 | "stac_version": "1.1.0-beta.1", 10 | "description": "A simple collection demonstrating core catalog fields with links to a couple of items", 11 | "title": "Simple Example Collection", 12 | "keywords": [ 13 | "simple", 14 | "example", 15 | "collection" 16 | ], 17 | "providers": [ 18 | { 19 | "name": "Remote Data, Inc", 20 | "description": "Producers of awesome spatiotemporal assets", 21 | "roles": [ 22 | "producer", 23 | "processor" 24 | ], 25 | "url": "http://remotedata.io" 26 | } 27 | ], 28 | "extent": { 29 | "spatial": { 30 | "bbox": [ 31 | [ 32 | 172.91173669923782, 33 | 1.3438851951615003, 34 | 172.95469614953714, 35 | 1.3690476620161975 36 | ] 37 | ] 38 | }, 39 | "temporal": { 40 | "interval": [ 41 | [ 42 | "2020-12-11T22:38:32.125Z", 43 | "2020-12-14T18:02:31.437Z" 44 | ] 45 | ] 46 | } 47 | }, 48 | "license": "CC-BY-4.0", 49 | "summaries": { 50 | "platform": [ 51 | "cool_sat1", 52 | "cool_sat2" 53 | ], 54 | "constellation": [ 55 | "ion" 56 | ], 57 | "instruments": [ 58 | "cool_sensor_v1", 59 | "cool_sensor_v2" 60 | ], 61 | "gsd": { 62 | "minimum": 0.512, 63 | "maximum": 0.66 64 | }, 65 | "eo:cloud_cover": { 66 | "minimum": 1.2, 67 | "maximum": 1.2 68 | }, 69 | "proj:cpde": [ 70 | "EPSG:32659" 71 | ], 72 | "view:sun_elevation": { 73 | "minimum": 54.9, 74 | "maximum": 54.9 75 | }, 76 | "view:off_nadir": { 77 | "minimum": 3.8, 78 | "maximum": 3.8 79 | }, 80 | "view:sun_azimuth": { 81 | "minimum": 135.7, 82 | "maximum": 135.7 83 | } 84 | }, 85 | "links": [ 86 | { 87 | "rel": "root", 88 | "href": "./collection.json", 89 | "type": "application/json", 90 | "title": "Simple Example Collection" 91 | }, 92 | { 93 | "rel": "item", 94 | "href": "./simple-item.json", 95 | "type": "application/geo+json", 96 | "title": "Simple Item" 97 | }, 98 | { 99 | "rel": "item", 100 | "href": "./core-item.json", 101 | "type": "application/geo+json", 102 | "title": "Core Item" 103 | }, 104 | { 105 | "rel": "item", 106 | "href": "./extended-item.json", 107 | "type": "application/geo+json", 108 | "title": "Extended Item" 109 | }, 110 | { 111 | "rel": "self", 112 | "href": "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.1.0-beta.1/examples/collection.json", 113 | "type": "application/json" 114 | } 115 | ] 116 | } 117 | -------------------------------------------------------------------------------- /crates/wasm/www/index.js: -------------------------------------------------------------------------------- 1 | import * as duckdb from "@duckdb/duckdb-wasm"; 2 | import * as stac_wasm from "stac-wasm"; 3 | 4 | const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles(); 5 | 6 | // Select a bundle based on browser checks 7 | const bundle = await duckdb.selectBundle(JSDELIVR_BUNDLES); 8 | 9 | const worker_url = URL.createObjectURL( 10 | new Blob([`importScripts("${bundle.mainWorker}");`], { 11 | type: "text/javascript", 12 | }) 13 | ); 14 | 15 | // Instantiate the asynchronous version of DuckDB-wasm 16 | const worker = new Worker(worker_url); 17 | const logger = new duckdb.ConsoleLogger(); 18 | const db = new duckdb.AsyncDuckDB(logger, worker); 19 | await db.instantiate(bundle.mainModule, bundle.pthreadWorker); 20 | URL.revokeObjectURL(worker_url); 21 | 22 | const connection = await db.connect(); 23 | const table = await connection.query("select 'an-id' as id"); 24 | console.log(stac_wasm.arrowToStacJson(table)); 25 | 26 | const bytes = stac_wasm.stacJsonToParquet([ 27 | { 28 | stac_version: "1.1.0", 29 | stac_extensions: [], 30 | type: "Feature", 31 | id: "20201211_223832_CS2", 32 | bbox: [ 33 | 172.91173669923782, 1.3438851951615003, 172.95469614953714, 34 | 1.3690476620161975, 35 | ], 36 | geometry: { 37 | type: "Polygon", 38 | coordinates: [ 39 | [ 40 | [172.91173669923782, 1.3438851951615003], 41 | [172.95469614953714, 1.3438851951615003], 42 | [172.95469614953714, 1.3690476620161975], 43 | [172.91173669923782, 1.3690476620161975], 44 | [172.91173669923782, 1.3438851951615003], 45 | ], 46 | ], 47 | }, 48 | properties: { 49 | datetime: "2020-12-11T22:38:32.125000Z", 50 | }, 51 | collection: "simple-collection", 52 | links: [ 53 | { 54 | rel: "collection", 55 | href: "./collection.json", 56 | type: "application/json", 57 | title: "Simple Example Collection", 58 | }, 59 | { 60 | rel: "root", 61 | href: "./collection.json", 62 | type: "application/json", 63 | title: "Simple Example Collection", 64 | }, 65 | { 66 | rel: "parent", 67 | href: "./collection.json", 68 | type: "application/json", 69 | title: "Simple Example Collection", 70 | }, 71 | ], 72 | assets: { 73 | visual: { 74 | href: "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 75 | type: "image/tiff; application=geotiff; profile=cloud-optimized", 76 | title: "3-Band Visual", 77 | roles: ["visual"], 78 | }, 79 | thumbnail: { 80 | href: "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 81 | title: "Thumbnail", 82 | type: "image/jpeg", 83 | roles: ["thumbnail"], 84 | }, 85 | }, 86 | }, 87 | ]); 88 | console.log(bytes); 89 | const url = URL.createObjectURL( 90 | new Blob([bytes], { type: "application/vnd.apache.parquet" }) 91 | ); 92 | const a = document.createElement("a"); 93 | a.href = url; 94 | a.download = "items.parquet"; 95 | a.textContent = "download"; 96 | document.body.appendChild(a); 97 | -------------------------------------------------------------------------------- /crates/core/src/api/sort.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::{ 3 | convert::Infallible, 4 | fmt::{Display, Formatter, Result}, 5 | str::FromStr, 6 | }; 7 | 8 | /// Fields by which to sort results. 9 | #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] 10 | pub struct Sortby { 11 | /// The field to sort by. 12 | pub field: String, 13 | 14 | /// The direction to sort by. 15 | pub direction: Direction, 16 | } 17 | 18 | /// The direction of sorting. 19 | #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] 20 | pub enum Direction { 21 | /// Ascending 22 | #[serde(rename = "asc")] 23 | Ascending, 24 | 25 | /// Descending 26 | #[serde(rename = "desc")] 27 | Descending, 28 | } 29 | 30 | impl Sortby { 31 | /// Creates a new ascending sortby for the field. 32 | /// 33 | /// # Examples 34 | /// 35 | /// ``` 36 | /// # use stac::api::Sortby; 37 | /// let sortby = Sortby::asc("id"); 38 | /// ``` 39 | pub fn asc(field: impl ToString) -> Sortby { 40 | Sortby { 41 | field: field.to_string(), 42 | direction: Direction::Ascending, 43 | } 44 | } 45 | 46 | /// Creates a new descending sortby for the field. 47 | /// 48 | /// # Examples 49 | /// 50 | /// ``` 51 | /// # use stac::api::Sortby; 52 | /// let sortby = Sortby::desc("id"); 53 | /// ``` 54 | pub fn desc(field: impl ToString) -> Sortby { 55 | Sortby { 56 | field: field.to_string(), 57 | direction: Direction::Descending, 58 | } 59 | } 60 | } 61 | 62 | impl FromStr for Sortby { 63 | type Err = Infallible; 64 | 65 | fn from_str(s: &str) -> std::result::Result { 66 | if let Some(s) = s.strip_prefix('+') { 67 | Ok(Sortby::asc(s)) 68 | } else if let Some(s) = s.strip_prefix('-') { 69 | Ok(Sortby::desc(s)) 70 | } else { 71 | Ok(Sortby::asc(s)) 72 | } 73 | } 74 | } 75 | 76 | impl Display for Sortby { 77 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { 78 | match self.direction { 79 | Direction::Ascending => write!(f, "{}", self.field), 80 | Direction::Descending => write!(f, "-{}", self.field), 81 | } 82 | } 83 | } 84 | 85 | #[cfg(test)] 86 | mod tests { 87 | use super::Sortby; 88 | use serde_json::json; 89 | 90 | #[test] 91 | fn optional_plus() { 92 | assert_eq!( 93 | "properties.created".parse::().unwrap(), 94 | "+properties.created".parse().unwrap() 95 | ); 96 | } 97 | 98 | #[test] 99 | fn descending() { 100 | assert_eq!(Sortby::desc("id"), "-id".parse().unwrap()); 101 | } 102 | 103 | #[test] 104 | fn names() { 105 | assert_eq!( 106 | json!({"field": "foo", "direction": "asc"}), 107 | serde_json::to_value(Sortby::asc("foo")).unwrap() 108 | ); 109 | assert_eq!( 110 | json!({"field": "foo", "direction": "desc"}), 111 | serde_json::to_value(Sortby::desc("foo")).unwrap() 112 | ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /crates/extensions/src/projection.rs: -------------------------------------------------------------------------------- 1 | //! The Projection extension. 2 | 3 | use super::Extension; 4 | use geojson::Geometry; 5 | use serde::{Deserialize, Serialize}; 6 | use serde_json::{Map, Value}; 7 | 8 | /// The projection extension fields. 9 | #[derive(Debug, Serialize, Deserialize, Default, PartialEq, Clone)] 10 | pub struct Projection { 11 | /// EPSG code of the datasource 12 | #[serde(skip_serializing_if = "Option::is_none")] 13 | pub code: Option, 14 | 15 | /// WKT2 string representing the Coordinate Reference System (CRS) that the 16 | /// proj:geometry and proj:bbox fields represent 17 | #[serde(skip_serializing_if = "Option::is_none")] 18 | pub wkt2: Option, 19 | 20 | /// PROJJSON object representing the Coordinate Reference System (CRS) that 21 | /// the proj:geometry and proj:bbox fields represent 22 | #[serde(skip_serializing_if = "Option::is_none")] 23 | pub projjson: Option>, 24 | 25 | /// Defines the footprint of this Item. 26 | #[serde(skip_serializing_if = "Option::is_none")] 27 | pub geometry: Option, 28 | 29 | /// Bounding box of the Item in the asset CRS in 2 or 3 dimensions. 30 | #[serde(skip_serializing_if = "Option::is_none")] 31 | pub bbox: Option>, 32 | 33 | /// Coordinates representing the centroid of the Item (in lat/long) 34 | #[serde(skip_serializing_if = "Option::is_none")] 35 | pub centroid: Option, 36 | 37 | /// Number of pixels in Y and X directions for the default grid 38 | #[serde(skip_serializing_if = "Option::is_none")] 39 | pub shape: Option>, 40 | 41 | /// The affine transformation coefficients for the default grid 42 | #[serde(skip_serializing_if = "Option::is_none")] 43 | pub transform: Option>, 44 | } 45 | 46 | /// This object represents the centroid of the Item Geometry. 47 | #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] 48 | pub struct Centroid { 49 | /// The latitude of the centroid. 50 | pub lat: f64, 51 | 52 | /// The longitude of the centroid. 53 | pub lon: f64, 54 | } 55 | 56 | impl Projection { 57 | /// Returns true if this projection structure is empty. 58 | /// 59 | /// # Examples 60 | /// 61 | /// ``` 62 | /// use stac_extensions::Projection; 63 | /// 64 | /// let projection = Projection::default(); 65 | /// assert!(projection.is_empty()); 66 | /// ``` 67 | pub fn is_empty(&self) -> bool { 68 | serde_json::to_value(self) 69 | .map(|v| v == Value::Object(Default::default())) 70 | .unwrap_or(true) 71 | } 72 | } 73 | 74 | impl Extension for Projection { 75 | const IDENTIFIER: &'static str = 76 | "https://stac-extensions.github.io/projection/v2.0.0/schema.json"; 77 | const PREFIX: &'static str = "proj"; 78 | } 79 | 80 | #[cfg(test)] 81 | mod tests { 82 | use super::Projection; 83 | use crate::{Extensions, Item}; 84 | 85 | #[test] 86 | fn example() { 87 | let item: Item = 88 | stac::read("examples/extensions-collection/proj-example/proj-example.json").unwrap(); 89 | let projection = item.extension::().unwrap(); 90 | assert_eq!(projection.code.unwrap(), "EPSG:32614"); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /crates/extensions/src/electro_optical.rs: -------------------------------------------------------------------------------- 1 | //! The [electro-optical](https://github.com/stac-extensions/eo) extension. 2 | 3 | use crate::Extension; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | /// EO data is considered to be data that represents a snapshot of the Earth for 7 | /// a single date and time. 8 | /// 9 | /// It could consist of multiple spectral bands in any part of the 10 | /// electromagnetic spectrum. Examples of EO data include sensors with visible, 11 | /// short-wave and mid-wave IR bands (e.g., the OLI instrument on Landsat-8), 12 | /// long-wave IR bands (e.g. TIRS aboard Landsat-8). 13 | #[derive(Debug, Serialize, Deserialize)] 14 | pub struct ElectroOptical { 15 | /// An array of available bands where each object is a [Band]. 16 | /// 17 | /// If given, requires at least one band. 18 | #[serde(skip_serializing_if = "Vec::is_empty", default)] 19 | pub bands: Vec, 20 | 21 | /// Estimate of cloud cover, in %. 22 | #[serde(skip_serializing_if = "Option::is_none")] 23 | pub cloud_cover: Option, 24 | 25 | /// Estimate of snow and ice cover, in %. 26 | #[serde(skip_serializing_if = "Option::is_none")] 27 | pub snow_cover: Option, 28 | } 29 | 30 | /// [Spectral 31 | /// bands](https://www.sciencedirect.com/topics/earth-and-planetary-sciences/spectral-band) 32 | /// in an [Asset](stac::Asset). 33 | #[derive(Debug, Serialize, Deserialize)] 34 | pub struct Band { 35 | /// The name of the band (e.g., "B01", "B8", "band2", "red"). 36 | #[serde(skip_serializing_if = "Option::is_none")] 37 | pub name: Option, 38 | 39 | /// The name commonly used to refer to the band to make it easier to search for bands across instruments. 40 | /// 41 | /// See the list of [accepted common names](https://github.com/stac-extensions/eo#common-band-names). 42 | #[serde(skip_serializing_if = "Option::is_none")] 43 | pub common_name: Option, 44 | 45 | /// Description to fully explain the band. 46 | /// 47 | /// [CommonMark 0.29](http://commonmark.org/) syntax MAY be used for rich text representation. 48 | #[serde(skip_serializing_if = "Option::is_none")] 49 | pub description: Option, 50 | 51 | /// The center wavelength of the band, in micrometers (μm). 52 | #[serde(skip_serializing_if = "Option::is_none")] 53 | pub center_wavelength: Option, 54 | 55 | /// Full width at half maximum (FWHM). 56 | /// 57 | /// The width of the band, as measured at half the maximum transmission, in 58 | /// micrometers (μm). 59 | #[serde(skip_serializing_if = "Option::is_none")] 60 | pub full_width_half_max: Option, 61 | 62 | /// The solar illumination of the band, as measured at half the maximum transmission, in W/m2/micrometers. 63 | #[serde(skip_serializing_if = "Option::is_none")] 64 | pub solar_illumination: Option, 65 | } 66 | 67 | impl Extension for ElectroOptical { 68 | const IDENTIFIER: &'static str = "https://stac-extensions.github.io/eo/v1.1.0/schema.json"; 69 | const PREFIX: &'static str = "eo"; 70 | } 71 | 72 | #[cfg(test)] 73 | mod tests { 74 | use super::ElectroOptical; 75 | use crate::{Extensions, Item}; 76 | 77 | #[test] 78 | fn item() { 79 | let item: Item = stac::read("data/eo/item.json").unwrap(); 80 | let _: ElectroOptical = item.extension().unwrap(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /crates/validate/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | #[non_exhaustive] 5 | pub enum Error { 6 | /// [fluent_uri::ParseError] 7 | #[error(transparent)] 8 | FluentUriParse(#[from] fluent_uri::ParseError), 9 | 10 | /// [jsonschema::ValidationError] 11 | #[error(transparent)] 12 | JsonschemaValidation(#[from] Box>), 13 | 14 | #[error(transparent)] 15 | /// [reqwest::Error] 16 | Reqwest(#[from] reqwest::Error), 17 | 18 | /// JSON is a scalar when an array or object was expected 19 | #[error("json value is not an object or an array")] 20 | ScalarJson(serde_json::Value), 21 | 22 | #[error(transparent)] 23 | /// [serde_json::Error] 24 | SerdeJson(#[from] serde_json::Error), 25 | 26 | #[error(transparent)] 27 | /// [stac::Error] 28 | Stac(#[from] stac::Error), 29 | 30 | /// A list of validation errors. 31 | #[error("{} validation error(s)", .0.len())] 32 | Validation(Vec), 33 | } 34 | 35 | /// A validation error 36 | #[derive(Debug)] 37 | pub struct Validation { 38 | /// The ID of the STAC object that failed to validate. 39 | id: Option, 40 | 41 | /// The type of the STAC object that failed to validate. 42 | r#type: Option, 43 | 44 | /// The validation error. 45 | error: jsonschema::ValidationError<'static>, 46 | } 47 | 48 | impl Validation { 49 | pub(crate) fn new( 50 | error: jsonschema::ValidationError<'_>, 51 | value: Option<&serde_json::Value>, 52 | ) -> Validation { 53 | let mut id = None; 54 | let mut r#type = None; 55 | if let Some(value) = value.and_then(|v| v.as_object()) { 56 | id = value.get("id").and_then(|v| v.as_str()).map(String::from); 57 | r#type = value 58 | .get("type") 59 | .and_then(|v| v.as_str()) 60 | .and_then(|s| s.parse::().ok()); 61 | } 62 | Validation { 63 | id, 64 | r#type, 65 | error: error.to_owned(), 66 | } 67 | } 68 | 69 | /// Converts this validation error into a [serde_json::Value]. 70 | pub fn into_json(self) -> serde_json::Value { 71 | serde_json::json!({ 72 | "id": self.id, 73 | "type": self.r#type, 74 | "error": self.error.to_string(), 75 | }) 76 | } 77 | } 78 | 79 | impl super::Error { 80 | pub(crate) fn from_validation_errors<'a, I>( 81 | errors: I, 82 | value: Option<&serde_json::Value>, 83 | ) -> super::Error 84 | where 85 | I: Iterator>, 86 | { 87 | super::Error::Validation(errors.map(|error| Validation::new(error, value)).collect()) 88 | } 89 | } 90 | 91 | impl std::fmt::Display for Validation { 92 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 93 | if let Some(r#type) = self.r#type { 94 | if let Some(id) = self.id.as_ref() { 95 | write!(f, "{}[id={id}]: {}", r#type, self.error) 96 | } else { 97 | write!(f, "{}: {}", r#type, self.error) 98 | } 99 | } else if let Some(id) = self.id.as_ref() { 100 | write!(f, "[id={id}]: {}", self.error) 101 | } else { 102 | write!(f, "{}", self.error) 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /crates/core/src/api/root.rs: -------------------------------------------------------------------------------- 1 | use super::Conformance; 2 | use serde::{Deserialize, Serialize}; 3 | use stac::Catalog; 4 | 5 | /// The root landing page of a STAC API. 6 | /// 7 | /// In a STAC API, the root endpoint (Landing Page) has the following characteristics: 8 | /// 9 | /// - The returned JSON is a [STAC 10 | /// Catalog](../stac-spec/catalog-spec/catalog-spec.md), and provides any number 11 | /// of 'child' links to navigate to additional 12 | /// [Catalog](../stac-spec/catalog-spec/catalog-spec.md), 13 | /// [Collection](../stac-spec/collection-spec/README.md), and 14 | /// [Item](../stac-spec/item-spec/README.md) objects. 15 | /// - The `links` attribute is part of a STAC Catalog, and provides a list of 16 | /// relations to API endpoints. Some of these endpoints can exist on any path 17 | /// (e.g., sub-catalogs) and some have a specified path (e.g., `/search`), so 18 | /// the client must inspect the `rel` (relationship) to understand what 19 | /// capabilities are offered at each location. 20 | /// - The `conformsTo` section provides the capabilities of this service. This 21 | /// is the field that indicates to clients that this is a STAC API and how to 22 | /// access conformance classes, including this one. The relevant conformance 23 | /// URIs are listed in each part of the API specification. If a conformance 24 | /// URI is listed then the service must implement all of the required 25 | /// capabilities. 26 | #[derive(Debug, Serialize, Deserialize)] 27 | pub struct Root { 28 | /// The [stac::Catalog]. 29 | #[serde(flatten)] 30 | pub catalog: Catalog, 31 | 32 | /// Provides the capabilities of this service. 33 | /// 34 | /// This is the field that indicates to clients that this is a STAC API and 35 | /// how to access conformance classes, including this one. The relevant 36 | /// conformance URIs are listed in each part of the API specification. If a 37 | /// conformance URI is listed then the service must implement all of the 38 | /// required capabilities. 39 | /// 40 | /// Note the `conformsTo` array follows the same structure of the OGC API - 41 | /// Features [declaration of conformance 42 | /// classes](http://docs.opengeospatial.org/is/17-069r3/17-069r3.html#_declaration_of_conformance_classes), 43 | /// except it is part of the landing page instead of in the JSON response 44 | /// from the `/conformance` endpoint. This is different from how the OGC API 45 | /// advertises conformance, as STAC feels it is important for clients to 46 | /// understand conformance from a single request to the landing page. 47 | /// Implementers who implement the *OGC API - Features* and/or *STAC API - 48 | /// Features* conformance classes must also implement the `/conformance` 49 | /// endpoint. 50 | /// 51 | /// The scope of the conformance classes declared in the 52 | /// `conformsTo` field and the `/conformance` endpoint are limited to the 53 | /// STAC API Catalog that declares them. A STAC API Catalog may link to 54 | /// sub-catalogs within it via `child` links that declare different 55 | /// conformance classes. This is useful when an entire catalog cannot be 56 | /// searched against to support the *STAC API - Item Search* conformance 57 | /// class, perhaps because it uses multiple databases to store items, but 58 | /// sub-catalogs whose items are all in one database can support search. 59 | /// #[serde(rename = "conformsTo")] 60 | #[serde(flatten)] 61 | pub conformance: Conformance, 62 | } 63 | -------------------------------------------------------------------------------- /crates/extensions/data/auth/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.0.0", 3 | "stac_extensions": [ 4 | "https://stac-extensions.github.io/item-assets/v1.0.0/schema.json", 5 | "https://stac-extensions.github.io/authentication/v1.1.0/schema.json" 6 | ], 7 | "type": "Collection", 8 | "id": "collection", 9 | "title": "A title", 10 | "description": "A description", 11 | "license": "Apache-2.0", 12 | "extent": { 13 | "spatial": { 14 | "bbox": [ 15 | [ 16 | 172.9, 17 | 1.3, 18 | 173, 19 | 1.4 20 | ] 21 | ] 22 | }, 23 | "temporal": { 24 | "interval": [ 25 | [ 26 | "2015-06-23T00:00:00Z", 27 | null 28 | ] 29 | ] 30 | } 31 | }, 32 | "auth:schemes": { 33 | "oauth": { 34 | "type": "oauth2", 35 | "description": "requires a login and user token", 36 | "flows": { 37 | "authorizationCode": { 38 | "authorizationUrl": "https://example.com/oauth/authorize", 39 | "tokenUrl": "https://example.com/oauth/token", 40 | "scopes": { 41 | "read:example": "Read the example data", 42 | "write:example": "Write the example data", 43 | "admin:example": "Read/write/delete the example data" 44 | } 45 | } 46 | } 47 | }, 48 | "signed_url_auth": { 49 | "type": "signedUrl", 50 | "description": "Requires an authentication API", 51 | "flows": { 52 | "auth": { 53 | "authorizationApi": "https://example.com/signed_url/authorize", 54 | "method": "POST", 55 | "parameters": { 56 | "bucket": { 57 | "in": "body", 58 | "required": true, 59 | "description": "asset-bucket", 60 | "schema": { 61 | "type": "string", 62 | "examples": [ 63 | "example-bucket" 64 | ] 65 | } 66 | }, 67 | "key": { 68 | "in": "body", 69 | "required": true, 70 | "description": "asset key", 71 | "schema": { 72 | "type": "string", 73 | "examples": [ 74 | "path/to/example/asset.xyz" 75 | ] 76 | } 77 | } 78 | }, 79 | "responseField": "signed_url" 80 | } 81 | } 82 | } 83 | }, 84 | "assets": { 85 | "example": { 86 | "href": "https://example.com/examples/file.xyz", 87 | "title": "Secure Collection Asset Example", 88 | "type": "application/vnd.example", 89 | "roles": [ 90 | "data" 91 | ], 92 | "auth:refs": [ 93 | "signed_url_auth" 94 | ] 95 | } 96 | }, 97 | "item_assets": { 98 | "data": { 99 | "title": "Secure Collection Asset Example", 100 | "type": "application/vnd.example", 101 | "roles": [ 102 | "data" 103 | ], 104 | "auth:refs": [ 105 | "oauth" 106 | ] 107 | } 108 | }, 109 | "summaries": { 110 | "datetime": { 111 | "minimum": "2015-06-23T00:00:00Z", 112 | "maximum": "2019-07-10T13:44:56Z" 113 | } 114 | }, 115 | "links": [ 116 | { 117 | "href": "https://example.com/examples/collection.json", 118 | "rel": "self" 119 | }, 120 | { 121 | "href": "https://example.com/examples/item.json", 122 | "rel": "item", 123 | "auth:refs": [ 124 | "oauth" 125 | ] 126 | } 127 | ] 128 | } 129 | -------------------------------------------------------------------------------- /scripts/validate-stac-geoparquet: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import json 4 | import shutil 5 | import subprocess 6 | import sys 7 | import tempfile 8 | from pathlib import Path 9 | from typing import Any 10 | 11 | import pyarrow 12 | import pyarrow.parquet 13 | import stac_geoparquet.arrow 14 | from deepdiff import DeepDiff 15 | 16 | root = Path(__file__).parents[1] 17 | path = root / "spec-examples" / "v1.1.0" / "extended-item.json" 18 | directory = tempfile.mkdtemp() 19 | parquet_path = Path(directory) / "extended-item.parquet" 20 | 21 | 22 | def clean_item(item: dict[str, Any]) -> None: 23 | if "type" not in item: 24 | item["type"] = "Feature" 25 | if ( 26 | item["geometry"]["type"] == "MultiPolygon" 27 | and len(item["geometry"]["coordinates"]) == 1 28 | ): 29 | item["geometry"]["type"] = "Polygon" 30 | item["geometry"]["coordinates"] = item["geometry"]["coordinates"][0] 31 | 32 | 33 | def clean_report(report: dict[str, Any]) -> dict[str, Any]: 34 | """We expect datetime values to be changed in the report.""" 35 | if report.get("values_changed"): 36 | if report["values_changed"].get("root['properties']['datetime']") == { 37 | "new_value": "2020-12-14T18:02:31.437Z", 38 | "old_value": "2020-12-14T18:02:31.437000Z", 39 | }: 40 | del report["values_changed"]["root['properties']['datetime']"] 41 | if report["values_changed"].get("root['properties']['created']") == { 42 | "new_value": "2020-12-15T01:48:13.725+00:00", 43 | "old_value": "2020-12-15T01:48:13.725Z", 44 | }: 45 | del report["values_changed"]["root['properties']['created']"] 46 | if report["values_changed"].get("root['properties']['updated']") == { 47 | "new_value": "2020-12-15T01:48:13.725+00:00", 48 | "old_value": "2020-12-15T01:48:13.725Z", 49 | }: 50 | del report["values_changed"]["root['properties']['updated']"] 51 | if not report["values_changed"]: 52 | del report["values_changed"] 53 | return report 54 | 55 | 56 | try: 57 | # Writing 58 | subprocess.check_call( 59 | [ 60 | "cargo", 61 | "run", 62 | "--", 63 | "translate", 64 | path, 65 | parquet_path, 66 | ] 67 | ) 68 | table = pyarrow.parquet.read_table(parquet_path) 69 | after = next(stac_geoparquet.arrow.stac_table_to_items(table)) 70 | clean_item(after) 71 | with open(path) as f: 72 | before = json.load(f) 73 | report = DeepDiff(before, after).to_dict() 74 | report = clean_report(report) 75 | if report: 76 | print(json.dumps(report, indent=2)) 77 | sys.exit(1) 78 | else: 79 | parquet_path.unlink() 80 | 81 | # Reading 82 | table = stac_geoparquet.arrow.parse_stac_items_to_arrow([before]) 83 | stac_geoparquet.arrow.to_parquet(table, parquet_path) 84 | item_collection = json.loads( 85 | subprocess.check_output( 86 | [ 87 | "cargo", 88 | "run", 89 | "--", 90 | "translate", 91 | parquet_path, 92 | ] 93 | ) 94 | ) 95 | assert len(item_collection["features"]) == 1 96 | clean_item( 97 | item_collection["features"][0] 98 | ) # stac-geoparquet writes as a multi-polygon 99 | report = DeepDiff(before, item_collection["features"][0]).to_dict() 100 | report = clean_report(report) 101 | if report: 102 | print(json.dumps(report, indent=2)) 103 | sys.exit(1) 104 | 105 | finally: 106 | shutil.rmtree(directory) 107 | -------------------------------------------------------------------------------- /spec-examples/v1.1.0/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "simple-collection", 3 | "type": "Collection", 4 | "stac_extensions": [ 5 | "https://stac-extensions.github.io/eo/v2.0.0/schema.json", 6 | "https://stac-extensions.github.io/projection/v2.0.0/schema.json", 7 | "https://stac-extensions.github.io/view/v1.0.0/schema.json" 8 | ], 9 | "stac_version": "1.1.0", 10 | "description": "A simple collection demonstrating core catalog fields with links to a couple of items", 11 | "title": "Simple Example Collection", 12 | "keywords": [ 13 | "simple", 14 | "example", 15 | "collection" 16 | ], 17 | "providers": [ 18 | { 19 | "name": "Remote Data, Inc", 20 | "description": "Producers of awesome spatiotemporal assets", 21 | "roles": [ 22 | "producer", 23 | "processor" 24 | ], 25 | "url": "http://remotedata.io" 26 | } 27 | ], 28 | "extent": { 29 | "spatial": { 30 | "bbox": [ 31 | [ 32 | 172.91173669923782, 33 | 1.3438851951615003, 34 | 172.95469614953714, 35 | 1.3690476620161975 36 | ] 37 | ] 38 | }, 39 | "temporal": { 40 | "interval": [ 41 | [ 42 | "2020-12-11T22:38:32.125Z", 43 | "2020-12-14T18:02:31.437Z" 44 | ] 45 | ] 46 | } 47 | }, 48 | "license": "CC-BY-4.0", 49 | "summaries": { 50 | "platform": [ 51 | "cool_sat1", 52 | "cool_sat2" 53 | ], 54 | "constellation": [ 55 | "ion" 56 | ], 57 | "instruments": [ 58 | "cool_sensor_v1", 59 | "cool_sensor_v2" 60 | ], 61 | "gsd": { 62 | "minimum": 0.512, 63 | "maximum": 0.66 64 | }, 65 | "eo:cloud_cover": { 66 | "minimum": 1.2, 67 | "maximum": 1.2 68 | }, 69 | "proj:cpde": [ 70 | "EPSG:32659" 71 | ], 72 | "view:sun_elevation": { 73 | "minimum": 54.9, 74 | "maximum": 54.9 75 | }, 76 | "view:off_nadir": { 77 | "minimum": 3.8, 78 | "maximum": 3.8 79 | }, 80 | "view:sun_azimuth": { 81 | "minimum": 135.7, 82 | "maximum": 135.7 83 | }, 84 | "statistics": { 85 | "type": "object", 86 | "properties": { 87 | "vegetation": { 88 | "description": "Percentage of pixels that are detected as vegetation, e.g. forests, grasslands, etc.", 89 | "minimum": 0, 90 | "maximum": 100 91 | }, 92 | "water": { 93 | "description": "Percentage of pixels that are detected as water, e.g. rivers, oceans and ponds.", 94 | "minimum": 0, 95 | "maximum": 100 96 | }, 97 | "urban": { 98 | "description": "Percentage of pixels that detected as urban, e.g. roads and buildings.", 99 | "minimum": 0, 100 | "maximum": 100 101 | } 102 | } 103 | } 104 | }, 105 | "links": [ 106 | { 107 | "rel": "root", 108 | "href": "./collection.json", 109 | "type": "application/json", 110 | "title": "Simple Example Collection" 111 | }, 112 | { 113 | "rel": "item", 114 | "href": "./simple-item.json", 115 | "type": "application/geo+json", 116 | "title": "Simple Item" 117 | }, 118 | { 119 | "rel": "item", 120 | "href": "./core-item.json", 121 | "type": "application/geo+json", 122 | "title": "Core Item" 123 | }, 124 | { 125 | "rel": "item", 126 | "href": "./extended-item.json", 127 | "type": "application/geo+json", 128 | "title": "Extended Item" 129 | }, 130 | { 131 | "rel": "self", 132 | "href": "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.1.0/examples/collection.json", 133 | "type": "application/json" 134 | } 135 | ] 136 | } 137 | -------------------------------------------------------------------------------- /spec-examples/v1.0.0/core-item.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.0.0", 3 | "stac_extensions": [], 4 | "type": "Feature", 5 | "id": "20201211_223832_CS2", 6 | "bbox": [ 7 | 172.91173669923782, 8 | 1.3438851951615003, 9 | 172.95469614953714, 10 | 1.3690476620161975 11 | ], 12 | "geometry": { 13 | "type": "Polygon", 14 | "coordinates": [ 15 | [ 16 | [ 17 | 172.91173669923782, 18 | 1.3438851951615003 19 | ], 20 | [ 21 | 172.95469614953714, 22 | 1.3438851951615003 23 | ], 24 | [ 25 | 172.95469614953714, 26 | 1.3690476620161975 27 | ], 28 | [ 29 | 172.91173669923782, 30 | 1.3690476620161975 31 | ], 32 | [ 33 | 172.91173669923782, 34 | 1.3438851951615003 35 | ] 36 | ] 37 | ] 38 | }, 39 | "properties": { 40 | "title": "Core Item", 41 | "description": "A sample STAC Item that includes examples of all common metadata", 42 | "datetime": null, 43 | "start_datetime": "2020-12-11T22:38:32.125Z", 44 | "end_datetime": "2020-12-11T22:38:32.327Z", 45 | "created": "2020-12-12T01:48:13.725Z", 46 | "updated": "2020-12-12T01:48:13.725Z", 47 | "platform": "cool_sat1", 48 | "instruments": [ 49 | "cool_sensor_v1" 50 | ], 51 | "constellation": "ion", 52 | "mission": "collection 5624", 53 | "gsd": 0.512 54 | }, 55 | "collection": "simple-collection", 56 | "links": [ 57 | { 58 | "rel": "collection", 59 | "href": "./collection.json", 60 | "type": "application/json", 61 | "title": "Simple Example Collection" 62 | }, 63 | { 64 | "rel": "root", 65 | "href": "./collection.json", 66 | "type": "application/json", 67 | "title": "Simple Example Collection" 68 | }, 69 | { 70 | "rel": "parent", 71 | "href": "./collection.json", 72 | "type": "application/json", 73 | "title": "Simple Example Collection" 74 | }, 75 | { 76 | "rel": "alternate", 77 | "type": "text/html", 78 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/index.html", 79 | "title": "HTML version of this STAC Item" 80 | } 81 | ], 82 | "assets": { 83 | "analytic": { 84 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic.tif", 85 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 86 | "title": "4-Band Analytic", 87 | "roles": [ 88 | "data" 89 | ] 90 | }, 91 | "thumbnail": { 92 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 93 | "title": "Thumbnail", 94 | "type": "image/png", 95 | "roles": [ 96 | "thumbnail" 97 | ] 98 | }, 99 | "visual": { 100 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 101 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 102 | "title": "3-Band Visual", 103 | "roles": [ 104 | "visual" 105 | ] 106 | }, 107 | "udm": { 108 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic_udm.tif", 109 | "title": "Unusable Data Mask", 110 | "type": "image/tiff; application=geotiff;" 111 | }, 112 | "json-metadata": { 113 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/extended-metadata.json", 114 | "title": "Extended Metadata", 115 | "type": "application/json", 116 | "roles": [ 117 | "metadata" 118 | ] 119 | }, 120 | "ephemeris": { 121 | "href": "http://cool-sat.com/catalog/20201211_223832_CS2/20201211_223832_CS2.EPH", 122 | "title": "Satellite Ephemeris Metadata" 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /spec-examples/v1.1.0/core-item.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.1.0", 3 | "stac_extensions": [], 4 | "type": "Feature", 5 | "id": "20201211_223832_CS2", 6 | "bbox": [ 7 | 172.91173669923782, 8 | 1.3438851951615003, 9 | 172.95469614953714, 10 | 1.3690476620161975 11 | ], 12 | "geometry": { 13 | "type": "Polygon", 14 | "coordinates": [ 15 | [ 16 | [ 17 | 172.91173669923782, 18 | 1.3438851951615003 19 | ], 20 | [ 21 | 172.95469614953714, 22 | 1.3438851951615003 23 | ], 24 | [ 25 | 172.95469614953714, 26 | 1.3690476620161975 27 | ], 28 | [ 29 | 172.91173669923782, 30 | 1.3690476620161975 31 | ], 32 | [ 33 | 172.91173669923782, 34 | 1.3438851951615003 35 | ] 36 | ] 37 | ] 38 | }, 39 | "properties": { 40 | "title": "Core Item", 41 | "description": "A sample STAC Item that includes examples of all common metadata", 42 | "datetime": null, 43 | "start_datetime": "2020-12-11T22:38:32.125Z", 44 | "end_datetime": "2020-12-11T22:38:32.327Z", 45 | "created": "2020-12-12T01:48:13.725Z", 46 | "updated": "2020-12-12T01:48:13.725Z", 47 | "platform": "cool_sat1", 48 | "instruments": [ 49 | "cool_sensor_v1" 50 | ], 51 | "constellation": "ion", 52 | "mission": "collection 5624", 53 | "gsd": 0.512 54 | }, 55 | "collection": "simple-collection", 56 | "links": [ 57 | { 58 | "rel": "collection", 59 | "href": "./collection.json", 60 | "type": "application/json", 61 | "title": "Simple Example Collection" 62 | }, 63 | { 64 | "rel": "root", 65 | "href": "./collection.json", 66 | "type": "application/json", 67 | "title": "Simple Example Collection" 68 | }, 69 | { 70 | "rel": "parent", 71 | "href": "./collection.json", 72 | "type": "application/json", 73 | "title": "Simple Example Collection" 74 | }, 75 | { 76 | "rel": "alternate", 77 | "type": "text/html", 78 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/index.html", 79 | "title": "HTML version of this STAC Item" 80 | } 81 | ], 82 | "assets": { 83 | "analytic": { 84 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic.tif", 85 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 86 | "title": "4-Band Analytic", 87 | "roles": [ 88 | "data" 89 | ] 90 | }, 91 | "thumbnail": { 92 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 93 | "title": "Thumbnail", 94 | "type": "image/png", 95 | "roles": [ 96 | "thumbnail" 97 | ] 98 | }, 99 | "visual": { 100 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 101 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 102 | "title": "3-Band Visual", 103 | "roles": [ 104 | "visual" 105 | ] 106 | }, 107 | "udm": { 108 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic_udm.tif", 109 | "title": "Unusable Data Mask", 110 | "type": "image/tiff; application=geotiff" 111 | }, 112 | "json-metadata": { 113 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/extended-metadata.json", 114 | "title": "Extended Metadata", 115 | "type": "application/json", 116 | "roles": [ 117 | "metadata" 118 | ] 119 | }, 120 | "ephemeris": { 121 | "href": "http://cool-sat.com/catalog/20201211_223832_CS2/20201211_223832_CS2.EPH", 122 | "title": "Satellite Ephemeris Metadata" 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /spec-examples/v1.1.0-beta.1/core-item.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.1.0-beta.1", 3 | "stac_extensions": [], 4 | "type": "Feature", 5 | "id": "20201211_223832_CS2", 6 | "bbox": [ 7 | 172.91173669923782, 8 | 1.3438851951615003, 9 | 172.95469614953714, 10 | 1.3690476620161975 11 | ], 12 | "geometry": { 13 | "type": "Polygon", 14 | "coordinates": [ 15 | [ 16 | [ 17 | 172.91173669923782, 18 | 1.3438851951615003 19 | ], 20 | [ 21 | 172.95469614953714, 22 | 1.3438851951615003 23 | ], 24 | [ 25 | 172.95469614953714, 26 | 1.3690476620161975 27 | ], 28 | [ 29 | 172.91173669923782, 30 | 1.3690476620161975 31 | ], 32 | [ 33 | 172.91173669923782, 34 | 1.3438851951615003 35 | ] 36 | ] 37 | ] 38 | }, 39 | "properties": { 40 | "title": "Core Item", 41 | "description": "A sample STAC Item that includes examples of all common metadata", 42 | "datetime": null, 43 | "start_datetime": "2020-12-11T22:38:32.125Z", 44 | "end_datetime": "2020-12-11T22:38:32.327Z", 45 | "created": "2020-12-12T01:48:13.725Z", 46 | "updated": "2020-12-12T01:48:13.725Z", 47 | "platform": "cool_sat1", 48 | "instruments": [ 49 | "cool_sensor_v1" 50 | ], 51 | "constellation": "ion", 52 | "mission": "collection 5624", 53 | "gsd": 0.512 54 | }, 55 | "collection": "simple-collection", 56 | "links": [ 57 | { 58 | "rel": "collection", 59 | "href": "./collection.json", 60 | "type": "application/json", 61 | "title": "Simple Example Collection" 62 | }, 63 | { 64 | "rel": "root", 65 | "href": "./collection.json", 66 | "type": "application/json", 67 | "title": "Simple Example Collection" 68 | }, 69 | { 70 | "rel": "parent", 71 | "href": "./collection.json", 72 | "type": "application/json", 73 | "title": "Simple Example Collection" 74 | }, 75 | { 76 | "rel": "alternate", 77 | "type": "text/html", 78 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/index.html", 79 | "title": "HTML version of this STAC Item" 80 | } 81 | ], 82 | "assets": { 83 | "analytic": { 84 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic.tif", 85 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 86 | "title": "4-Band Analytic", 87 | "roles": [ 88 | "data" 89 | ] 90 | }, 91 | "thumbnail": { 92 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 93 | "title": "Thumbnail", 94 | "type": "image/png", 95 | "roles": [ 96 | "thumbnail" 97 | ] 98 | }, 99 | "visual": { 100 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 101 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 102 | "title": "3-Band Visual", 103 | "roles": [ 104 | "visual" 105 | ] 106 | }, 107 | "udm": { 108 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic_udm.tif", 109 | "title": "Unusable Data Mask", 110 | "type": "image/tiff; application=geotiff" 111 | }, 112 | "json-metadata": { 113 | "href": "http://remotedata.io/catalog/20201211_223832_CS2/extended-metadata.json", 114 | "title": "Extended Metadata", 115 | "type": "application/json", 116 | "roles": [ 117 | "metadata" 118 | ] 119 | }, 120 | "ephemeris": { 121 | "href": "http://cool-sat.com/catalog/20201211_223832_CS2/20201211_223832_CS2.EPH", 122 | "title": "Satellite Ephemeris Metadata" 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /crates/server/README.md: -------------------------------------------------------------------------------- 1 | # stac-server 2 | 3 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/stac-utils/rustac/ci.yml?branch=main&style=for-the-badge)](https://github.com/stac-utils/rustac/actions/workflows/ci.yml) 4 | [![docs.rs](https://img.shields.io/docsrs/stac-server?style=for-the-badge)](https://docs.rs/stac-server/latest/stac_server/) 5 | [![Crates.io](https://img.shields.io/crates/v/stac-server?style=for-the-badge)](https://crates.io/crates/stac-server) 6 | ![Crates.io](https://img.shields.io/crates/l/stac-server?style=for-the-badge) 7 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg?style=for-the-badge)](./CODE_OF_CONDUCT) 8 | 9 | A [STAC API](https://github.com/radiantearth/stac-api-spec) server with multiple backends. 10 | 11 | ## Usage 12 | 13 | To run a server from the command-line, use [rustac](../cli/README.md). 14 | Any arguments will be interpreted as hrefs to STAC collections, items, and item collections, and will be loaded into the server on startup. 15 | 16 | ```shell 17 | rustac serve collection.json items.json 18 | ``` 19 | 20 | To use the [pgstac](https://github.com/stac-utils/pgstac) backend: 21 | 22 | ```shell 23 | rustac serve --pgstac postgresql://username:password@localhost:5432/postgis 24 | ``` 25 | 26 | If you'd like to serve your own **pgstac** backend with some sample items: 27 | 28 | ```shell 29 | docker compose up -d pgstac 30 | scripts/load-pgstac-fixtures # This might take a while, e.g. 30 seconds or so 31 | ``` 32 | 33 | ### Library 34 | 35 | To use this library in another application: 36 | 37 | ```toml 38 | [dependencies] 39 | stac-server = "0.3" 40 | ``` 41 | 42 | ### Deploying 43 | 44 | There is currently no infrastructure-as-code for deploying **stac-server**. 45 | We hope to provide this support in the future. 46 | 47 | ### Features 48 | 49 | **stac-server** has two optional features. 50 | 51 | #### axum 52 | 53 | The `axum` feature enables routing and serving using [axum](https://github.com/tokio-rs/axum). 54 | 55 | #### pgstac 56 | 57 | In order to use the [pgstac](https://github.com/stac-utils/pgstac), you need to enable the `pgstac` feature. 58 | 59 | ## Backends 60 | 61 | This table lists the provided backends and their supported conformance classes and extensions: 62 | 63 | | Capability | Memory backend | Pgstac backend | 64 | | -- | -- | -- | 65 | | [STAC API - Core](https://github.com/radiantearth/stac-api-spec/blob/release/v1.0.0/core) | ✅ | ✅ | 66 | | [STAC API - Features](https://github.com/radiantearth/stac-api-spec/blob/release/v1.0.0/ogcapi-features) | ✅ | ✅ | 67 | | [STAC API - Item Search](https://github.com/radiantearth/stac-api-spec/blob/release/v1.0.0/item-search) | ✅ | ✅ | 68 | | [Aggregation extension](https://github.com/stac-api-extensions/aggregation) | ✖️ | ✖️ | 69 | | [Browseable extension](https://github.com/stac-api-extensions/browseable) | ✖️ | ✖️ | 70 | | [Children extension](https://github.com/stac-api-extensions/children) | ✖️ | ✖️ | 71 | | [Collection search extension](https://github.com/stac-api-extensions/collection-search) | ✖️ | ✖️ | 72 | | [Collection transaction extension](https://github.com/stac-api-extensions/collection-transaction) | ✖️ | ✖️ | 73 | | [Fields extension](https://github.com/stac-api-extensions/fields) | ✖️ | ✖️ | 74 | | [Filter extension](https://github.com/stac-api-extensions/filter) | ✖️ | ✅️ | 75 | | [Free-text search extension](https://github.com/stac-api-extensions/freetext-search) | ✖️ | ✖️ | 76 | | [Language (I18N) extension](https://github.com/stac-api-extensions/language) | ✖️ | ✖️ | 77 | | [Query extension](https://github.com/stac-api-extensions/query) | ✖️ | ✖️ | 78 | | [Sort extension](https://github.com/stac-api-extensions/sort) | ✖️ | ✖️ | 79 | | [Transaction extension](https://github.com/stac-api-extensions/transaction) | ✖️ | ✖️ | 80 | 81 | ## Other info 82 | 83 | This crate is part of the [rustac](https://github.com/stac-utils/rustac) monorepo, see its README for contributing and license information. 84 | --------------------------------------------------------------------------------