oparl-types 0.8.5

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 crate::{
    AgendaItemListUrl, BodyClassification, BodyUrl, ConsultationListUrl, DateTime, EmailAddress,
    FileListUrl, Keyword, LegislativeTerm, LegislativeTermListUrl, Location, LocationListUrl,
    MeetingListUrl, MembershipListUrl, Name, OrganizationListUrl, PaperListUrl, PersonListUrl,
    SystemUrl, Url, namespace::BodyNamespaceUrl,
};

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

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

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

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

    pub name: Name,

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

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

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

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

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

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

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

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

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

    pub organization: OrganizationListUrl,

    pub person: PersonListUrl,

    pub meeting: MeetingListUrl,

    pub paper: PaperListUrl,

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

    pub agenda_item: AgendaItemListUrl,

    pub consultation: ConsultationListUrl,

    pub file: FileListUrl,

    pub location_list: LocationListUrl,

    pub legislative_term_list: LegislativeTermListUrl,

    pub membership: MembershipListUrl,

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

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

    #[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::Body;
    use crate::{
        LegislativeTerm, Location,
        namespace::{BodyNamespaceUrl, LegislativeTermNamespaceUrl, LocationNamespaceUrl},
    };

    fn example_body() -> Body {
        let geojson_feature = {
            let mut f =
                geojson::Feature::from(geojson::Geometry::new(geojson::GeometryValue::Point {
                    coordinates: [50.1234, 10.4321].into(),
                }));
            f.set_property("name", "Rathausplatz");
            f
        };

        Body {
            id: "https://oparl.example.org/body/0"
                .parse()
                .expect("value must be parseable as id"),
            namespace: BodyNamespaceUrl::Identifier,
            system: Some(
                "https://oparl.example.org/"
                    .parse()
                    .expect("value must be parseable as url"),
            ),
            short_name: Some("Köln".into()),
            name: "Stadt Köln, kreisfreie Stadt".into(),
            website: Some(
                "http://www.beispielstadt.de/"
                    .parse()
                    .expect("value must be parseable as url"),
            ),
            license: Some(
                "http://creativecommons.org/licenses/by/4.0/"
                    .parse()
                    .expect("value must be parseable as url"),
            ),
            license_valid_since: Some(datetime!(2014-01-01 00:00:00 +02:00).into()),
            oparl_since: None,
            ags: Some("05315000".to_string()),
            rgs: Some("053150000000".to_string()),
            equivalent: vec![
                "http://d-nb.info/gnd/2015732-0"
                    .parse()
                    .expect("value must be parseable as url"),
                "http://dbpedia.org/resource/Cologne"
                    .parse()
                    .expect("value must be parseable as url"),
            ],
            contact_email: Some(
                "ris@beispielstadt.de"
                    .parse()
                    .expect("value must be parseable as email"),
            ),
            contact_name: Some(
                "RIS-Betreuung"
                    .parse()
                    .expect("value must be parseable as name"),
            ),
            organization: "https://oparl.example.org/body/0/organizations/"
                .parse()
                .expect("value must be parseable as url"),
            person: "https://oparl.example.org/body/0/persons/"
                .parse()
                .expect("value must be parseable as url"),
            meeting: "https://oparl.example.org/body/0/meetings/"
                .parse()
                .expect("value must be parseable as url"),
            paper: "https://oparl.example.org/body/0/papers/"
                .parse()
                .expect("value must be parseable as url"),
            legislative_term: vec![LegislativeTerm {
                id: "https://oparl.example.org/term/21"
                    .parse()
                    .expect("value must be parseable as id"),
                namespace: LegislativeTermNamespaceUrl::Identifier,
                body: Some(
                    "https://oparl.example.org/body/0"
                        .parse()
                        .expect("value must be parseable as url"),
                ),
                name: Some(
                    "21. Wahlperiode"
                        .parse()
                        .expect("value must be parseable as name"),
                ),
                start_date: Some(
                    "2010-12-03"
                        .parse()
                        .expect("value must be parseable as date"),
                ),
                end_date: Some(
                    "2013-12-03"
                        .parse()
                        .expect("value must be parseable as date"),
                ),
                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(),
            }],
            agenda_item: "https://oparl.example.org/body/0/agendaItems/"
                .parse()
                .expect("value must be parseable as url"),
            consultation: "https://oparl.example.org/body/0/consultations/"
                .parse()
                .expect("value must be parseable as url"),
            file: "https://oparl.example.org/body/0/files/"
                .parse()
                .expect("value must be parseable as url"),
            location_list: "https://oparl.example.org/body/0/location_list/"
                .parse()
                .expect("value must be parseable as url"),
            legislative_term_list: "https://oparl.example.org/body/0/legislative_term_list/"
                .parse()
                .expect("value must be parseable as url"),
            membership: "https://oparl.example.org/body/0/memberships/"
                .parse()
                .expect("value must be parseable as url"),
            classification: Some("Kreisfreie Stadt".into()),
            location: Some(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(),
            }),
            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_body_json() -> serde_json::Value {
        json!({
            "id": "https://oparl.example.org/body/0",
            "type": "https://schema.oparl.org/1.1/Body",
            "system": "https://oparl.example.org/",
            "contactEmail": "ris@beispielstadt.de",
            "contactName": "RIS-Betreuung",
            "ags": "05315000",
            "rgs": "053150000000",
            "equivalent": [
                "http://d-nb.info/gnd/2015732-0",
                "http://dbpedia.org/resource/Cologne"
            ],
            "shortName": "Köln",
            "name": "Stadt Köln, kreisfreie Stadt",
            "website": "http://www.beispielstadt.de/",
            "license": "http://creativecommons.org/licenses/by/4.0/",
            "licenseValidSince": "2014-01-01T00:00:00+02:00",
            "organization": "https://oparl.example.org/body/0/organizations/",
            "person": "https://oparl.example.org/body/0/persons/",
            "meeting": "https://oparl.example.org/body/0/meetings/",
            "paper": "https://oparl.example.org/body/0/papers/",
            "agendaItem": "https://oparl.example.org/body/0/agendaItems/",
            "consultation": "https://oparl.example.org/body/0/consultations/",
            "file": "https://oparl.example.org/body/0/files/",
            "locationList": "https://oparl.example.org/body/0/location_list/",
            "membership": "https://oparl.example.org/body/0/memberships/",
            "legislativeTermList": "https://oparl.example.org/body/0/legislative_term_list/",
            "legislativeTerm": [
                {
                    "id": "https://oparl.example.org/term/21",
                    "type": "https://schema.oparl.org/1.1/LegislativeTerm",
                    "body": "https://oparl.example.org/body/0",
                    "name": "21. Wahlperiode",
                    "startDate": "2010-12-03",
                    "endDate": "2013-12-03",
                    "created": "2014-01-08T14:28:31+01:00",
                    "modified": "2014-01-08T14:28:31+01:00"
                }
            ],
            "location": {
                "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"
                    }
                }
            },
            "classification": "Kreisfreie Stadt",
            "created": "2014-01-08T14:28:31+01:00",
            "modified": "2014-01-08T14:28:31+01:00"
        })
    }

    #[test]
    fn serialize() {
        assert_eq!(json!(example_body()), example_body_json());
    }

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

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