oparl-types 0.1.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 url::Url;

use crate::{
    namespace::PaperNamespaceUrl, BodyId, Consultation, Date, DateTime, File, Keyword, Location,
    Name, OrganizationId, PaperId, PersonId,
};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    #[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>,
}

#[cfg(test)]
mod serde_tests {
    use super::Paper;
    use crate::{
        namespace::{
            ConsultationNamespaceUrl, FileNamespaceUrl, LocationNamespaceUrl, PaperNamespaceUrl,
        },
        Consultation, File, Location, Sha1Sum,
    };

    use pretty_assertions::assert_eq;
    use serde_json::json;
    use time::macros::{date, datetime};

    fn example_paper() -> Paper {
        let geojson_feature = geojson::Value::Point(vec![7.03291, 50.98249]);

        Paper {
            id: "https://oparl.example.org/paper/749"
                .parse()
                .expect("value must be parseable as id"),
            namespace: PaperNamespaceUrl::Identifier,
            body: Some(
                "https://oparl.example.org/bodies/1"
                    .parse()
                    .expect("value must be parseable as url"),
            ),
            name: Some("Antwort auf Anfrage 1200/2014".into()),
            reference: Some("1234/2014".into()),
            date: Some(date!(2014 - 04 - 04).into()),
            paper_type: Some("Beantwortung einer Anfrage".into()),
            related_paper: vec!["https://oparl.example.org/paper/699"
                .parse()
                .expect("value must be parseable as url")],
            superordinated_paper: vec![],
            subordinated_paper: vec![],
            main_file: Some(File {
                id: "https://oparl.example.org/files/57737"
                    .parse()
                    .expect("value must be parseable as id"),
                namespace: FileNamespaceUrl::Identifier,
                file_name: Some("anlage_1_zur_anfrage.pdf".into()),
                name: Some("Anlage 1 zur Anfrage".into()),
                mime_type: Some("application/pdf".into()),
                date: Some(date!(2013 - 01 - 04).into()),
                size: Some(82930),
                sha1_checksum: Some(Sha1Sum::from([
                    0xd7, 0x49, 0x75, 0x1a, 0xf4, 0x4a, 0x32, 0xc8, 0x18, 0xb9, 0xb1, 0xe1, 0x51,
                    0x52, 0x51, 0xc6, 0x77, 0x34, 0xf5, 0xd2,
                ])),
                sha512_checksum: None,
                text: None,
                access_url: "https://oparl.example.org/files/57737.pdf"
                    .parse()
                    .expect("value must be parseable as url"),
                download_url: Some(
                    "https://oparl.example.org/files/download/57737.pdf"
                        .parse()
                        .expect("value must be parseable as url"),
                ),
                external_service_url: None,
                master_file: None,
                derivative_file: vec![],
                file_license: None,
                meeting: vec![],
                agenda_item: vec![],
                paper: vec![],
                license: Some(
                    "http://www.opendefinition.org/licenses/cc-by"
                        .parse()
                        .expect("value must be parseable as url"),
                ),
                keyword: vec![],
                created: datetime!(2013-01-04 07:54:13 +01:00).into(),
                modified: datetime!(2013-01-04 07:54:13 +01:00).into(),
                web: None,
                deleted: None,
            }),
            auxiliary_file: vec![File {
                id: "https://oparl.example.org/files/57739"
                    .parse()
                    .expect("value must be parseable as id"),
                namespace: FileNamespaceUrl::Identifier,
                file_name: Some("anlage.pdf".into()),
                name: Some("Anlage 1 zur Anfrage".into()),
                mime_type: Some("application/pdf".into()),
                date: Some(date!(2013 - 01 - 04).into()),
                size: Some(82930),
                sha1_checksum: Some(Sha1Sum::from([
                    0xda, 0x39, 0xa3, 0xee, 0x5e, 0x6b, 0x4b, 0x0d, 0x32, 0x55, 0xbf, 0xef, 0x95,
                    0x60, 0x18, 0x90, 0xaf, 0xd8, 0x07, 0x09,
                ])),
                sha512_checksum: None,
                text: Some("Der Übersichtsplan zeigt alle Ebenen des ...".into()),
                access_url: "https://oparl.example.org/files/57739.pdf"
                    .parse()
                    .expect("value must be parseable as url"),
                download_url: Some(
                    "https://oparl.example.org/files/download/57739.pdf"
                        .parse()
                        .expect("value must be parseable as url"),
                ),
                external_service_url: None,
                master_file: Some(
                    "https://oparl.example.org/files/57738"
                        .parse()
                        .expect("value must be parseable as id"),
                ),
                derivative_file: vec![],
                file_license: None,
                meeting: vec![],
                agenda_item: vec![],
                paper: vec![],
                license: Some(
                    "http://www.opendefinition.org/licenses/cc-by"
                        .parse()
                        .expect("value must be parseable as url"),
                ),
                keyword: vec![],
                created: datetime!(2013-01-04 07:54:13 +01:00).into(),
                modified: datetime!(2013-01-04 07:54:13 +01:00).into(),
                web: None,
                deleted: None,
            }],
            location: vec![Location {
                id: "https://oparl.example.org/locations/29856"
                    .parse()
                    .expect("value must be parseable as id"),
                namespace: LocationNamespaceUrl::Identifier,
                description: Some("Honschaftsstraße 312, Köln".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-08 14:05:27 +01:00).into(),
                modified: datetime!(2012-01-08 14:05:27 +01:00).into(),
                web: None,
                deleted: None,
            }],
            originator_person: vec![
                "https://oparl.example.org/person/2000"
                    .parse()
                    .expect("value must be parseable as url"),
                "https://oparl.example.org/person/1000"
                    .parse()
                    .expect("value must be parseable as url"),
            ],
            under_direction_of: vec!["https://oparl.example.org/organization/2000"
                .parse()
                .expect("value must be parseable as url")],
            originator_organization: vec![
                "https://oparl.example.org/organization/2000"
                    .parse()
                    .expect("value must be parseable as url"),
                "https://oparl.example.org/organization/1000"
                    .parse()
                    .expect("value must be parseable as url"),
            ],
            consultation: vec![Consultation {
                id: "https://oparl.example.org/consultation/47594"
                    .parse()
                    .expect("value must be parseable as id"),
                namespace: ConsultationNamespaceUrl::Identifier,
                paper: None,
                agenda_item: Some(
                    "https://oparl.example.org/agendaitem/15569"
                        .parse()
                        .expect("value must be parseable as url"),
                ),
                meeting: Some(
                    "https://oparl.example.org/meeting/243"
                        .parse()
                        .expect("value must be parseable as url"),
                ),
                organization: vec!["https://oparl.example.org/organization/96"
                    .parse()
                    .expect("value must be parseable as url")],
                authoritative: Some(false),
                role: Some("Beschlussfassung".into()),
                license: None,
                keyword: vec![],
                created: datetime!(2012-01-08 14:05:27 +01:00).into(),
                modified: datetime!(2012-01-08 14:05:27 +01:00).into(),
                web: None,
                deleted: None,
            }],
            license: None,
            keyword: vec![],
            created: datetime!(2013-01-08 12:05:27 +01:00).into(),
            modified: datetime!(2013-01-08 12:05:27 +01:00).into(),
            web: None,
            deleted: None,
        }
    }

    fn example_paper_json() -> serde_json::Value {
        json!({
    "id": "https://oparl.example.org/paper/749",
    "type": "https://schema.oparl.org/1.1/Paper",
    "body": "https://oparl.example.org/bodies/1",
    "name": "Antwort auf Anfrage 1200/2014",
    "reference": "1234/2014",
    "date": "2014-04-04",
    "paperType": "Beantwortung einer Anfrage",
    "relatedPaper": [
        "https://oparl.example.org/paper/699"
    ],
    "mainFile": {
        "id": "https://oparl.example.org/files/57737",
        "type": "https://schema.oparl.org/1.1/File",
        "name": "Anlage 1 zur Anfrage",
        "fileName": "anlage_1_zur_anfrage.pdf",
        "mimeType": "application/pdf",
        "date": "2013-01-04",
        "sha1Checksum": "d749751af44a32c818b9b1e1515251c67734f5d2",
        "size": 82930,
        "accessUrl": "https://oparl.example.org/files/57737.pdf",
        "downloadUrl": "https://oparl.example.org/files/download/57737.pdf",
        "license": "http://www.opendefinition.org/licenses/cc-by",
        "created": "2013-01-04T07:54:13+01:00",
        "modified": "2013-01-04T07:54:13+01:00"
    },
    "auxiliaryFile": [
        {
            "id": "https://oparl.example.org/files/57739",
            "type": "https://schema.oparl.org/1.1/File",
            "name": "Anlage 1 zur Anfrage",
            "fileName": "anlage.pdf",
            "mimeType": "application/pdf",
            "date": "2013-01-04",
            "sha1Checksum": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
            "size": 82930,
            "accessUrl": "https://oparl.example.org/files/57739.pdf",
            "downloadUrl": "https://oparl.example.org/files/download/57739.pdf",
            "text": "Der Übersichtsplan zeigt alle Ebenen des ...",
            "masterFile": "https://oparl.example.org/files/57738",
            "license": "http://www.opendefinition.org/licenses/cc-by",
            "created": "2013-01-04T07:54:13+01:00",
            "modified": "2013-01-04T07:54:13+01:00"
        }
    ],
    "location": [
        {
            "id": "https://oparl.example.org/locations/29856",
            "type": "https://schema.oparl.org/1.1/Location",
            "description": "Honschaftsstraße 312, Köln",
            "created": "2012-01-08T14:05:27+01:00",
            "modified": "2012-01-08T14:05:27+01:00",
            "geojson": {
                "type": "Point",
                "coordinates": [
                    7.03291,
                    50.98249
                ]
            }
        }
    ],
    "originatorPerson": [
        "https://oparl.example.org/person/2000",
        "https://oparl.example.org/person/1000"
    ],
    "originatorOrganization": [
        "https://oparl.example.org/organization/2000",
        "https://oparl.example.org/organization/1000"
    ],
    "consultation": [
        {
            "id": "https://oparl.example.org/consultation/47594",
            "type": "https://schema.oparl.org/1.1/Consultation",
            "agendaItem": "https://oparl.example.org/agendaitem/15569",
            "meeting": "https://oparl.example.org/meeting/243",
            "organization": [
                "https://oparl.example.org/organization/96"
            ],
            "authoritative": false,
            "role": "Beschlussfassung",
            "created": "2012-01-08T14:05:27+01:00",
            "modified": "2012-01-08T14:05:27+01:00"
        }
    ],
    "underDirectionOf": [
        "https://oparl.example.org/organization/2000"
    ],
    "created": "2013-01-08T12:05:27+01:00",
    "modified": "2013-01-08T12:05:27+01:00"})
    }

    #[test]
    fn serialize() {
        assert_eq!(json!(example_paper()), example_paper_json());
    }

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

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