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::{
    BodyUrl, ConsultationListUrl, DateTime, Keyword, Location, MeetingListUrl, MembershipUrl, Name,
    OrganizationClassification, OrganizationType, OrganizationUrl, Post, Url, date::Date,
    namespace::OrganizationNamespaceUrl,
};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    #[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::{date, datetime};

    use super::Organization;
    use crate::{
        Location, OrganizationType,
        namespace::{LocationNamespaceUrl, OrganizationNamespaceUrl},
    };

    fn example_organization() -> Organization {
        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
        };

        Organization {
            id: "https://oparl.example.org/organization/34"
                .parse()
                .expect("value must be parseable as id"),
            namespace: OrganizationNamespaceUrl::Identifier,
            body: Some(
                "https://oparl.example.org/bodies/1"
                    .parse()
                    .expect("value must be parseable as url"),
            ),
            name: Some("Ausschuss für Haushalt und Finanzen".into()),
            membership: vec![
                "https://oparl.example.org/membership/27"
                    .parse()
                    .expect("value must be parseable as url"),
                "https://oparl.example.org/membership/48"
                    .parse()
                    .expect("value must be parseable as url"),
                "https://oparl.example.org/membership/57"
                    .parse()
                    .expect("value must be parseable as url"),
            ],
            meeting: Some(
                "https://oparl.example.org/organization/34/meetings"
                    .parse()
                    .expect("value must be parseable as url"),
            ),
            consultation: None,
            short_name: Some("Finanzausschuss".into()),
            post: vec![
                "Vorsitzender".into(),
                "1. Stellvertreter".into(),
                "Mitglied".into(),
            ],
            sub_organization_of: None,
            organization_type: Some(OrganizationType::Council),
            classification: Some("Ausschuss".into()),
            start_date: Some(date!(2012 - 07 - 17).into()),
            end_date: None,
            website: None,
            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!(2012-01-06 12:01:00 +01:00).into(),
                modified: datetime!(2012-01-08 14:05:27 +01:00).into(),
                web: None,
                deleted: None,
                extensions: serde_json::Map::new(),
            }),
            external_body: None,
            license: None,
            keyword: vec!["finanzen".into(), "haushalt".into()],
            created: datetime!(2012-07-16 00:00:00 +02:00).into(),
            modified: datetime!(2012-08-16 12:34:56 +02:00).into(),
            web: None,
            deleted: None,
            extensions: serde_json::Map::new(),
        }
    }

    fn example_organization_json() -> serde_json::Value {
        json!({
            "id": "https://oparl.example.org/organization/34",
            "type": "https://schema.oparl.org/1.1/Organization",
            "body": "https://oparl.example.org/bodies/1",
            "name": "Ausschuss für Haushalt und Finanzen",
            "shortName": "Finanzausschuss",
            "startDate": "2012-07-17",
            "organizationType": "Gremium",
            "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": "2012-01-06T12:01:00+01:00",
                "modified": "2012-01-08T14:05:27+01:00",
                "geojson": {
                    "type": "Feature",
                    "geometry": {
                        "type": "Point",
                        "coordinates": [
                            50.1234,
                            10.4321
                        ]
                    },
                    "properties": {
                        "name": "Rathausplatz"
                    }
                }
            },
            "post": [
                "Vorsitzender",
                "1. Stellvertreter",
                "Mitglied"
            ],
            "meeting": "https://oparl.example.org/organization/34/meetings",
            "membership": [
                "https://oparl.example.org/membership/27",
                "https://oparl.example.org/membership/48",
                "https://oparl.example.org/membership/57"
            ],
            "classification": "Ausschuss",
            "keyword": [
                "finanzen",
                "haushalt"
            ],
            "created": "2012-07-16T00:00:00+02:00",
            "modified": "2012-08-16T12:34:56+02:00"
        })
    }

    #[test]
    fn serialize() {
        assert_eq!(json!(example_organization()), example_organization_json());
    }

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

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