oparl-types 0.6.0

Type definitions for the OParl protocol
Documentation
// SPDX-FileCopyrightText: Politik im Blick developers
// SPDX-FileCopyrightText: Wolfgang Silbermayr <wolfgang@silbermayr.at>
//
// SPDX-License-Identifier: AGPL-3.0-or-later OR EUPL-1.2

use geojson::GeoJson;

use crate::{
    DateTime, Keyword, LocationUrl, MeetingUrl, OrganizationUrl, PaperUrl, PersonUrl, Url,
    namespace::LocationNamespaceUrl,
};

#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Location {
    pub id: LocationUrl,

    #[serde(rename = "type")]
    pub namespace: LocationNamespaceUrl,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub geojson: Option<GeoJson>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub street_address: Option<String>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub room: Option<String>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub postal_code: Option<String>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub sub_locality: Option<String>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub locality: Option<String>,

    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub bodies: Vec<Url>,

    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub organizations: Vec<OrganizationUrl>,

    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub persons: Vec<PersonUrl>,

    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub meetings: Vec<MeetingUrl>,

    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub papers: Vec<PaperUrl>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub license: Option<Url>,

    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub keyword: Vec<Keyword>,

    pub created: DateTime,

    pub modified: DateTime,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub web: Option<Url>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub deleted: Option<bool>,

    #[serde(default, flatten)]
    pub extensions: serde_json::Map<String, serde_json::Value>,
}

#[cfg(test)]
mod serde_tests {
    use pretty_assertions::assert_eq;
    use serde_json::json;
    use time::macros::datetime;

    use super::Location;
    use crate::namespace::LocationNamespaceUrl;

    fn example_location() -> Location {
        let geojson_feature = {
            let mut f =
                geojson::Feature::from(geojson::Geometry::new(geojson::Value::Point(vec![
                    50.1234, 10.4321,
                ])));
            f.set_property("name", "Rathausplatz");
            f
        };

        Location {
            id: "https://oparl.example.org/location/0"
                .parse()
                .expect("value must be parseable as id"),
            namespace: LocationNamespaceUrl::Identifier,
            description: Some(
                "Rathaus der Beispielstadt, Ratshausplatz 1, 12345 Beispielstadt".to_string(),
            ),
            geojson: Some(geojson_feature.into()),
            street_address: None,
            room: None,
            postal_code: None,
            sub_locality: None,
            locality: None,
            bodies: vec![],
            organizations: vec![],
            persons: vec![],
            meetings: vec![],
            papers: vec![],
            license: None,
            keyword: vec![],
            created: datetime!(2014-01-08 14:28:31 +01:00).into(),
            modified: datetime!(2014-01-08 14:28:31 +01:00).into(),
            web: None,
            deleted: None,
            extensions: serde_json::Map::new(),
        }
    }

    fn example_location_json() -> serde_json::Value {
        json!({
            "id": "https://oparl.example.org/location/0",
            "type": "https://schema.oparl.org/1.1/Location",
            "description": "Rathaus der Beispielstadt, Ratshausplatz 1, 12345 Beispielstadt",
            "created": "2014-01-08T14:28:31+01:00",
            "modified": "2014-01-08T14:28:31+01:00",
            "geojson": {
                "type": "Feature",
                "geometry": {
                    "type": "Point",
                    "coordinates": [
                        50.1234,
                        10.4321
                    ]
                },
                "properties": {
                    "name": "Rathausplatz"
                }
            }
        })
    }

    #[test]
    fn serialize() {
        assert_eq!(json!(example_location()), example_location_json());
    }

    #[test]
    fn deserialize_good() {
        let deserialized: Location = serde_json::from_value(example_location_json())
            .expect("value must be deserializable as Location");
        assert_eq!(deserialized, example_location());
    }

    #[test]
    fn deserialize_bad() {
        assert!(serde_json::from_value::<Location>(json!("xyzabcd")).is_err());
        assert!(serde_json::from_value::<Location>(json!(true)).is_err());
        assert!(serde_json::from_value::<Location>(json!(123)).is_err());
    }
}