jsonfg 0.1.0

Types for OGC Features and Geometries JSON (JSON-FG), a GeoJSON superset with support for arbitrary coordinate reference systems, solids, and curved geometries.
Documentation
//! Round-trip tests: parse a JSON-FG document, re-serialize it, and assert the result is
//! semantically identical (no members dropped, none spuriously added). Fixtures cover the
//! JSON-FG 1.0 members and geometry types, including geometry in a non-WGS84 CRS.

use jsonfg::{CoordRefSys, Feature, FeatureCollection, Geometry, conformance};
use serde_json::{Value, json};

fn roundtrip_feature(v: Value) {
    let feature: Feature = serde_json::from_value(v.clone()).expect("deserialize Feature");
    let back = serde_json::to_value(&feature).expect("serialize Feature");
    assert_eq!(v, back, "feature did not round-trip");
}

fn roundtrip_collection(v: Value) {
    let fc: FeatureCollection =
        serde_json::from_value(v.clone()).expect("deserialize FeatureCollection");
    let back = serde_json::to_value(&fc).expect("serialize FeatureCollection");
    assert_eq!(v, back, "feature collection did not round-trip");
}

#[test]
fn wgs84_point_feature() {
    // Plain GeoJSON-compatible feature: geometry present, place absent.
    roundtrip_feature(json!({
        "type": "Feature",
        "id": 1,
        "geometry": { "type": "Point", "coordinates": [5.0, 52.0] },
        "properties": { "name": "example" }
    }));
}

#[test]
fn prism_feature() {
    // A prism (3D solid) in a projected CRS: the geometry is in `place`, `geometry` is
    // null, with an unbounded time interval and a top-level `conformsTo`.
    let v = json!({
        "type": "Feature",
        "id": "prism-1",
        "conformsTo": [conformance::CORE, conformance::PRISMS],
        "featureType": "example",
        "time": { "interval": ["2022-07-12T16:55:18Z", ".."] },
        "coordRefSys": "http://www.opengis.net/def/crs/EPSG/0/3857",
        "place": {
            "type": "Prism",
            "base": {
                "type": "LineString",
                "coordinates": [[10.0, 20.0], [13.0, 23.0]]
            },
            "lower": 2.02,
            "upper": 3.22
        },
        "geometry": null,
        "properties": null
    });
    roundtrip_feature(v.clone());

    // Verify the place/geometry split concretely.
    let f: Feature = serde_json::from_value(v).unwrap();
    assert!(f.geometry.is_none());
    assert!(matches!(f.place, Some(Geometry::Prism { upper, .. }) if upper == 3.22));
    assert!(!f.coord_ref_sys.as_ref().unwrap().is_wgs84());
}

#[test]
fn curve_polygon_feature() {
    // A curved geometry in a projected CRS: a CurvePolygon whose ring is a CompoundCurve
    // of LineString + CircularString segments.
    roundtrip_feature(json!({
        "type": "Feature",
        "id": "curve-1",
        "conformsTo": [conformance::CORE, conformance::CIRCULAR_ARCS],
        "featureType": "example",
        "coordRefSys": "http://www.opengis.net/def/crs/EPSG/0/3857",
        "place": {
            "type": "CurvePolygon",
            "geometries": [{
                "type": "CompoundCurve",
                "geometries": [
                    { "type": "LineString", "coordinates": [[1.0, 1.0], [2.0, 1.0]] },
                    { "type": "CircularString", "coordinates": [[2.0, 1.0], [2.5, 1.5], [2.0, 2.0]] },
                    { "type": "LineString", "coordinates": [[2.0, 2.0], [1.0, 1.0]] }
                ]
            }]
        },
        "geometry": null,
        "properties": { "year": 1975 }
    }));
}

#[test]
fn polyhedron_place() {
    // A Polyhedron (3D solid): coordinates nest shell -> polygon -> ring -> 3D position.
    roundtrip_feature(json!({
        "type": "Feature",
        "conformsTo": [conformance::CORE, conformance::POLYHEDRA],
        "coordRefSys": "http://www.opengis.net/def/crs/EPSG/0/3857",
        "place": {
            "type": "Polyhedron",
            "coordinates": [[
                [[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 0.0, 0.0]]],
                [[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, 0.0]]]
            ]]
        },
        "geometry": null,
        "properties": null
    }));
}

#[test]
fn geometry_with_bbox_and_coordrefsys() {
    // A standalone geometry object carrying the optional `coordRefSys` and `bbox`
    // members (exercises the flattened GeometryMeta).
    let v = json!({
        "type": "Polygon",
        "coordRefSys": "http://www.opengis.net/def/crs/EPSG/0/3857",
        "bbox": [0.0, 0.0, 10.0, 10.0],
        "coordinates": [[[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 0.0]]]
    });
    let g: Geometry = serde_json::from_value(v.clone()).unwrap();
    assert_eq!(g.meta().bbox.as_ref().unwrap(), &vec![0.0, 0.0, 10.0, 10.0]);
    assert_eq!(serde_json::to_value(&g).unwrap(), v);
}

#[test]
fn feature_collection_with_ogc_api_members() {
    // A collection with collection-level CRS and OGC API foreign members (links, paging
    // counters) that are not part of core JSON-FG but must be preserved.
    roundtrip_collection(json!({
        "type": "FeatureCollection",
        "conformsTo": [conformance::CORE],
        "coordRefSys": "http://www.opengis.net/def/crs/EPSG/0/3857",
        "features": [
            {
                "type": "Feature",
                "id": 1,
                "place": { "type": "Point", "coordinates": [100.0, 200.0] },
                "geometry": null,
                "properties": null
            }
        ],
        "links": [
            { "href": "https://example.com/items?f=jsonfg", "rel": "self",
              "type": "application/vnd.ogc.fg+json" }
        ],
        "timeStamp": "2026-01-01T12:00:00Z",
        "numberMatched": 1,
        "numberReturned": 1
    }));
}

#[test]
fn empty_collection_serializes_features_array() {
    let fc = FeatureCollection::new();
    let v = serde_json::to_value(&fc).unwrap();
    assert_eq!(v["type"], "FeatureCollection");
    assert_eq!(v["features"], json!([]));
}

#[test]
fn feature_always_emits_geometry_and_properties() {
    // Even a default feature must serialize `geometry` and `properties` as null.
    let v = serde_json::to_value(Feature::new()).unwrap();
    assert!(v.get("geometry").is_some() && v["geometry"].is_null());
    assert!(v.get("properties").is_some() && v["properties"].is_null());
    // Optional members are omitted.
    assert!(v.get("place").is_none());
    assert!(v.get("time").is_none());
}

#[test]
fn coord_ref_sys_helpers() {
    assert_eq!(
        serde_json::to_value(CoordRefSys::from_epsg(3857)).unwrap(),
        json!("http://www.opengis.net/def/crs/EPSG/0/3857")
    );
    assert!(CoordRefSys::crs84().is_wgs84());
    assert!(!CoordRefSys::from_epsg(3857).is_wgs84());
}

#[test]
fn coord_ref_sys_reference_object() {
    let v = json!({ "type": "Reference", "href": "http://www.opengis.net/def/crs/EPSG/0/4979", "epoch": 2017.23 });
    let crs: CoordRefSys = serde_json::from_value(v.clone()).unwrap();
    assert_eq!(serde_json::to_value(&crs).unwrap(), v);
}

#[cfg(feature = "geojson")]
#[test]
fn geojson_interop() {
    // A GeoJSON geometry lifts into a JSON-FG geometry and back.
    let gj: geojson::Geometry = serde_json::from_value(json!({
        "type": "Polygon",
        "coordinates": [[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 0.0]]]
    }))
    .unwrap();
    let fg: Geometry = gj.clone().into();
    assert!(matches!(fg, Geometry::Polygon { .. }));
    let back: geojson::Geometry = fg.try_into().unwrap();
    assert_eq!(back.value, gj.value);

    // A curved geometry has no GeoJSON equivalent.
    let arc = Geometry::CircularString {
        coordinates: vec![vec![0.0, 0.0], vec![1.0, 1.0], vec![2.0, 0.0]],
        meta: Default::default(),
    };
    assert!(geojson::Geometry::try_from(arc).is_err());
}