From 5349a0c4320949d803df0dd8596ba8aa5497c81c Mon Sep 17 00:00:00 2001 From: Harald Eilertsen Date: Mon, 8 Jan 2024 19:12:02 +0100 Subject: Add high level stream API. --- src/stream.rs | 231 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 src/stream.rs (limited to 'src/stream.rs') diff --git a/src/stream.rs b/src/stream.rs new file mode 100644 index 0000000..475c886 --- /dev/null +++ b/src/stream.rs @@ -0,0 +1,231 @@ +/** + * High level stream API for ZotApi. + * + * The stream API will parse the raw json response from a stream (network + * or channel stream) into proper types, that should be easier to use from + * a client application. + * + * SPDX-FileCopyrightText: 2023 Eilertsens Kodeknekkeri + * SPDX-FileCopyrightText: 2023 Harald Eilertsen + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +use serde::Deserialize; +use std::{ + error::Error, + result::Result +}; + +#[derive(Debug, PartialEq)] +pub struct Stream { + pub items: Vec, +} + +#[derive(Debug, Deserialize, PartialEq)] +enum StreamItemType { + #[serde(rename="activity")] + Activity, +} + +#[derive(Debug, Deserialize, PartialEq)] +enum StreamItemEncoding { + #[serde(rename="zot")] + Zot, +} + +#[derive(Debug, PartialEq)] +enum Verb { + Post, + Like, + Update, +} + +impl<'de> Deserialize<'de> for Verb { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + deserializer.deserialize_str(VerbVisitor) + } +} + +struct VerbVisitor; + +impl<'de> serde::de::Visitor<'de> for VerbVisitor { + type Value = Verb; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a valid activity streams verb URI") + } + + fn visit_str(self, s: &str) -> Result + where + E: serde::de::Error, + { + match s { + "http://activitystrea.ms/schema/1.0/post" => + Ok(Verb::Post), + "http://activitystrea.ms/schema/1.0/like" => + Ok(Verb::Like), + "http://activitystrea.ms/schema/1.0/update" => + Ok(Verb::Update), + _ => + Err(E::custom(format!("unknown activity streams verb"))) + } + } +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct StreamItem { + #[serde(rename="type")] + item_type: StreamItemType, + encoding: StreamItemEncoding, + + verb: Verb, + + pub title: String, + pub summary: String, + pub body: String, +} + +impl StreamItem { + pub fn is_post(&self) -> bool { + self.verb == Verb::Post + } +} + +impl Stream { + pub fn from_json(json: &str) -> Result> { + let items: Vec = serde_json::from_str(&json)?; + Ok(Self { items }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn construct_stream_from_empty_string_should_fail() { + let s = Stream::from_json(""); + assert!(s.is_err()); + } + + #[test] + fn construct_stream_from_empty_json() { + let s = Stream::from_json("[]"); + assert!(s.is_ok()); + } + + #[test] + fn construct_stream_from_json_with_one_activity() { + let json = r#"[ + { + "type": "activity", + "encoding": "zot", + "uuid": "2c102ec3-676b-4d84-b2b4-9141467a254f", + "message_id": "https://example.com/item/2c102ec3-676b-4d84-b2b4-9141467a254f", + "message_top": "https://example.com/item/2c102ec3-676b-4d84-b2b4-9141467a254f", + "message_parent": "https://example.com/item/2c102ec3-676b-4d84-b2b4-9141467a254f", + "created": "2023-12-12 17:00:42", + "edited": "2023-12-12 17:00:42", + "expires": "0000-00-00 00:00:00", + "commented": "2023-12-19 09:01:15", + "mimetype": "text/bbcode", + "title": "The item title", + "summary": "The summary of the post", + "body": "The body of the post", + "app": "", + "verb": "http://activitystrea.ms/schema/1.0/post", + "object_type": "http://activitystrea.ms/schema/1.0/note", + "target_type": "", + "permalink": "https://example.com/item/2c102ec3-676b-4d84-b2b4-9141467a254f", + "location": "", + "longlat": "", + "signature": "sha256.ADzsjr5uFUpcwVkUT9liHimr1n2TJmGpTtm0SGSody_6vTbfYhHK3OJJhGknImHYwMradSwVcA9dNAAXzhSoAY5hCmSbnThLun30HVIA3E0ZSDBbt1RyKNomqBCvCBmyyFKhxYoPk34UQisCH6gIQ3eZL-m5PxE9t2oMO_CnpPvLWESoezY3CAZaEIIRj3KKozwC8DxibQmsnCeA32C-2Ejzv8PdZCX1skbIdc5d8Jj_ykyUTqc2DZKMSl9osua3esEMwYZLbxlRQruDfgyjJExdvz57zpeqf1WKzqdIZ5kmguZxch6NLVMiRitooT-sIZOvo9JzgP1ogBQW34W-kG1hHzuGJQkDZKOAA1rLD8qrfwugbbg9wDpjtZY-WHv265KMMsFhctIIupNPVVkuSgs0jMKQmjJTeDE0IxBKx2dCsxiqMWlJll_LSu7b6BtTa7jcuhfMPaKqdMOCRRnGNwehj6qeOXPV7zEZsc4Mzym6N_jRTV556OmUwaCHouEFqTG1ARE5EquYTMk2wXIteC5lT2d0WpEGTnlL1WpqWLx9DCp76Z2JsxMBxjZicKvJSm1gX3ng2ENG2cvECww0IF_zPy2DmHcWOqm795-11uLBNt_60bpP9-sgjrNSj0q6MHgAsFGnpW42M4sFe-6gb-W-HjyHwY2B2yjErYC9KDo", + "route": "", + "owner": { + "name": "Benjamin Franklin", + "address": "ben@example.com", + "url": "https://example.com/channel/ben", + "network": "zot6", + "photo": { + "mimetype": "image/jpeg", + "src": "https://example.com/photo/profile/m/2" + }, + "id": "rUpgk2qbvnWLoKIXOlZlwlqI5vk8C4NgudFNjbcmnOBjFSXU34TObkZEClaPSfKnpFZpg87tANtko7WGs7QRvA", + "id_sig": "sha256.ZD8uwYmUEG_d02Y...", + "key": "-----BEGIN PUBLIC KEY-----\n....\n-----END PUBLIC KEY-----\n" + }, + "author": { + "name": "Benjamin Franklin", + "address": "ben@example.com", + "url": "https://example.com/channel/ben", + "network": "zot6", + "photo": { + "mimetype": "image/jpeg", + "src": "https://example.com/photo/profile/m/2" + }, + "id": "rUpgk2qbvnWLoKIXOlZlwlqI5vk8C4NgudFNjbcmnOBjFSXU34TObkZEClaPSfKnpFZpg87tANtko7WGs7QRvA", + "id_sig": "sha256.ZD8uwYmUEG_d02Y...", + "key": "-----BEGIN PUBLIC KEY-----\n....\n-----END PUBLIC KEY-----\n" + }, + "flags": [ + "thread_parent" + ], + "public_scope": "", + "comment_scope": "authenticated", + "tags": [ + { + "tag": "a-tag", + "url": "https://example.com/search?tag=a-tag", + "type": "hashtag" + }, + { + "tag": "fediverse", + "url": "https://example.com/search?tag=fediverse", + "type": "hashtag" + }, + { + "tag": "hubzilla", + "url": "https://example.com/search?tag=hubzilla", + "type": "hashtag" + } + ] + }]"#; + let s = Stream::from_json(&json).unwrap(); + assert_eq!(1, s.items.len()); + + let item = &s.items[0]; + + assert_eq!(StreamItemType::Activity, item.item_type); + assert_eq!(StreamItemEncoding::Zot, item.encoding); + assert_eq!(Verb::Post, item.verb); + assert_eq!("The item title", &item.title); + assert_eq!("The summary of the post", &item.summary); + assert_eq!("The body of the post", &item.body); + } + + #[test] + fn deserialize_activitustreams_verb_from_json() { + #[derive(Debug, Deserialize, PartialEq)] + struct VerbTest { + verb: Verb + } + + let verbs = vec![ + (Verb::Post, "post"), + (Verb::Like, "like"), + (Verb::Update, "update") + ]; + + for v in verbs { + let verb: VerbTest = serde_json::from_str( + &format!(r#"{{"verb": "http://activitystrea.ms/schema/1.0/{}"}}"#, v.1) + ).unwrap(); + assert_eq!(v.0, verb.verb); + } + + } +} -- cgit v1.2.3