Skip to main content

geoparquet_validator/
lib.rs

1//! Validate GeoParquet files: the abstract tests of the OGC GeoParquet 2.0 draft, and the 1.0
2//! and 1.1 community rules, on top of the Apache Arrow Rust `parquet` crate. No DuckDB, GDAL or
3//! PROJ.
4//!
5//! The simplest entry points are [`validate`] for a path or URL and [`validate_bytes`] for a file
6//! already in memory; both return a [`checks::Report`], which serialises to the JSON described by
7//! `schemas/report.schema.json`. [`checks::run`] takes any [`source::Source`] for finer control.
8//!
9//! ```no_run
10//! let report = geoparquet_validator::validate("example.parquet", None)?;
11//! for o in &report.outcomes {
12//!     println!("{:?} {} {}", o.status, o.id, o.message);
13//! }
14//! println!("{}", serde_json::to_string_pretty(&report)?);
15//! # Ok::<(), anyhow::Error>(())
16//! ```
17//!
18//! The same code is the `geoparquet-validator` command (`cli`, the default feature), the Python
19//! package (`python`), a C library (`capi`) and the browser build (`wasm`).
20
21pub mod checks;
22pub mod corpus;
23pub mod crs;
24pub mod source;
25pub mod spatial;
26pub mod verify;
27pub mod wkb;
28
29#[cfg(feature = "capi")]
30pub mod capi;
31#[cfg(feature = "cli")]
32pub mod cli;
33#[cfg(feature = "python")]
34mod python;
35#[cfg(feature = "wasm")]
36pub mod wasm;
37
38use checks::{Options, Report, Schemas};
39
40/// Check a local path or, with the `remote` feature, an `s3://`, `gs://`, `az://` or `https://`
41/// URL. `max_rows` reads only the first row groups holding that many rows; the report then says
42/// the data tests were sampled.
43pub fn validate(target: &str, max_rows: Option<usize>) -> anyhow::Result<Report> {
44    let schemas = Schemas::load()?;
45    let options = Options { max_rows };
46    #[cfg(feature = "remote")]
47    if source::is_remote(target) {
48        let url = url::Url::parse(target)?;
49        let opts = source::RemoteOptions {
50            s3_region: None,
51            extra: Vec::new(),
52        };
53        let src = source::open_remote(&url, &opts)?;
54        return checks::run(&src, &schemas, &options);
55    }
56    let src = source::Local(std::path::PathBuf::from(target));
57    checks::run(&src, &schemas, &options)
58}
59
60/// Check a file already in memory; `name` only labels the report.
61pub fn validate_bytes(
62    name: &str,
63    bytes: Vec<u8>,
64    max_rows: Option<usize>,
65) -> anyhow::Result<Report> {
66    let schemas = Schemas::load()?;
67    let src = source::InMemory {
68        name: name.to_string(),
69        bytes: bytes::Bytes::from(bytes),
70    };
71    checks::run(&src, &schemas, &Options { max_rows })
72}
73
74#[cfg(test)]
75mod tests {
76    #[test]
77    fn report_matches_its_schema() {
78        let schema: serde_json::Value =
79            serde_json::from_str(include_str!("../schemas/report.schema.json")).unwrap();
80        let validator = jsonschema::validator_for(&schema).unwrap();
81        // the corpus submodule is present in a checkout, not in the published crate
82        let path = "corpus/data/bbox/bbox-present.parquet";
83        if !std::path::Path::new(path).exists() {
84            eprintln!("skipped: {path} not present");
85            return;
86        }
87        let report = super::validate(path, None).unwrap();
88        let value = serde_json::to_value(&report).unwrap();
89        let errors: Vec<String> = validator
90            .iter_errors(&value)
91            .map(|e| e.to_string())
92            .collect();
93        assert!(errors.is_empty(), "{errors:?}");
94        assert_eq!(value["report_version"], 1);
95    }
96}