cityjson-arrow 0.6.2

Arrow IPC and Parquet transport for CityJSON 2.0 city models
Documentation
#[path = "support/shared_corpus.rs"]
mod shared_corpus;

use cityjson_arrow::{
    ExportOptions, ImportOptions, export_reader, import_batches, read_stream, write_stream,
};

macro_rules! conformance_roundtrip_tests {
    ($($case_id:ident),+ $(,)?) => {
        mod stream {
            use super::*;
            $(
                #[test]
                fn $case_id() {
                    assert_stream_roundtrip(stringify!($case_id));
                }
            )+
        }
        mod batch {
            use super::*;
            $(
                #[test]
                fn $case_id() {
                    assert_batch_roundtrip(stringify!($case_id));
                }
            )+
        }
    };
}

fn assert_stream_roundtrip(case_id: &str) {
    let case = shared_corpus::load_named_conformance_case(case_id);
    let mut bytes = Vec::new();
    write_stream(&mut bytes, &case.model, &ExportOptions::default())
        .unwrap_or_else(|err| panic!("{case_id}: stream write failed: {err}"));
    let decoded = read_stream(bytes.as_slice(), &ImportOptions::default())
        .unwrap_or_else(|err| panic!("{case_id}: stream read failed: {err}"));
    let expected = shared_corpus::transport_roundtrip_json(&case.model, &case.model);
    let actual = shared_corpus::transport_roundtrip_json(&decoded, &case.model);
    assert_eq!(actual, expected, "{case_id}: stream roundtrip mismatch");
}

fn assert_batch_roundtrip(case_id: &str) {
    let case = shared_corpus::load_named_conformance_case(case_id);
    let reader = export_reader(&case.model, &ExportOptions::default())
        .unwrap_or_else(|err| panic!("{case_id}: export failed: {err}"));
    let header = reader.header().clone();
    let projection = reader.projection().clone();
    let batches = reader.collect::<Vec<_>>();
    let decoded = import_batches(header, projection, batches, &ImportOptions::default())
        .unwrap_or_else(|err| panic!("{case_id}: import failed: {err}"));
    let expected = shared_corpus::transport_roundtrip_json(&case.model, &case.model);
    let actual = shared_corpus::transport_roundtrip_json(&decoded, &case.model);
    assert_eq!(actual, expected, "{case_id}: batch roundtrip mismatch");
}

#[test]
fn removed_transform_stream_tag_is_rejected() {
    let case = shared_corpus::load_named_conformance_case("cityjson_minimal");
    let mut bytes = Vec::new();
    write_stream(&mut bytes, &case.model, &ExportOptions::default()).expect("stream write");
    let first_tag = first_stream_frame_tag_offset(&bytes);
    bytes[first_tag] = 1;

    let Err(err) = read_stream(bytes.as_slice(), &ImportOptions::default()) else {
        panic!("stream tag 1 should be rejected");
    };
    assert!(
        err.to_string().contains("tag 1"),
        "unexpected error for removed transform tag: {err}"
    );
}

fn first_stream_frame_tag_offset(bytes: &[u8]) -> usize {
    let magic_len = b"CITYJSON_ARROW_STREAM_V3\0".len();
    let prelude_len_start = magic_len;
    let prelude_len_end = prelude_len_start + 8;
    let prelude_len = u64::from_le_bytes(
        bytes[prelude_len_start..prelude_len_end]
            .try_into()
            .expect("prelude length bytes"),
    );
    prelude_len_end + usize::try_from(prelude_len).expect("prelude length fits usize")
}

conformance_roundtrip_tests!(
    appearance_complete,
    cityobject_building_address,
    cityobject_complete,
    cityobject_extended,
    cityobject_all_types,
    coordinates_precision_ecef,
    coordinates_precision_local,
    coordinates_precision_stateplane,
    coordinates_precision_utm,
    coordinates_precision_wgs84,
    coordinates_precision_worst,
    geometry_instance,
    geometry_material_solid,
    geometry_material_multisolid,
    geometry_material_multisurface,
    geometry_texture_solid,
    geometry_texture_multisolid,
    geometry_texture_multisurface,
    geometry_semantics_solid,
    geometry_semantics_multisolid,
    geometry_semantics_multisurface,
    geometry_semantics_multilinestring,
    geometry_semantics_multipoint,
    cityjson_extended,
    cityjsonfeature_minimal,
    cityjson_fake_complete,
    cityjson_minimal,
    metadata_complete,
    metadata_extra_properties,
    semantic_all_types,
    semantic_complete,
    semantic_extended,
    vertices,
    extension,
    spec_geometry_matrix,
);