diff --git a/Cargo.lock b/Cargo.lock index 641e8d0..018b121 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,4 +153,5 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] diff --git a/Cargo.toml b/Cargo.toml index 91391e4..f602295 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,4 +7,4 @@ edition = "2021" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -url = "2.3" \ No newline at end of file +url = { version = "2.3", features = ["serde"] } \ No newline at end of file diff --git a/src/web_model/activity_streams/mod.rs b/src/web_model/activity_streams/mod.rs new file mode 100644 index 0000000..34e7193 --- /dev/null +++ b/src/web_model/activity_streams/mod.rs @@ -0,0 +1,151 @@ +use crate::web_model::ListContaining; +use serde::de::Error; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::str::FromStr; + +#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] +pub struct ContextActivityStreams; + +impl AsRef for ContextActivityStreams { + fn as_ref(&self) -> &'static str { + "https://www.w3.org/ns/activitystreams" + } +} + +impl Serialize for ContextActivityStreams { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(self.as_ref()) + } +} + +impl FromStr for ContextActivityStreams { + type Err = String; + + fn from_str(s: &str) -> Result { + if matches!( + s, + "https://www.w3.org/ns/activitystreams" | "http://www.w3.org/ns/activitystreams" + ) { + Ok(Self) + } else { + Err(format!("Invalid context: {s}")) + } + } +} + +impl<'de> Deserialize<'de> for ContextActivityStreams { + fn deserialize>(deserializer: D) -> Result { + let context = String::deserialize(deserializer)?; + + ContextActivityStreams::from_str(&context).map_err(Error::custom) + } +} + +#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum Context { + String(ContextActivityStreams), + Object { + #[serde(rename = "@vocab")] + ld_vocab: ContextActivityStreams, + }, + List(ListContaining), +} + +impl Default for Context { + fn default() -> Self { + Context::String(ContextActivityStreams) + } +} + +#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] +struct ActivityStreamsDocument { + #[serde(rename = "@context", default)] + ld_context: Context, + #[serde(flatten)] + data: T, +} + +#[cfg(test)] +mod test { + use crate::web_model::activity_streams::{ + ActivityStreamsDocument, Context, ContextActivityStreams, + }; + use crate::web_model::ListContaining; + use serde_json::json; + + #[test] + fn should_parse_context() { + let json = json!({ + "@context": "https://www.w3.org/ns/activitystreams", + "some": "stuff" + }); + + let doc: ActivityStreamsDocument<()> = serde_json::from_value(json).unwrap(); + + assert_eq!(doc.ld_context, Context::String(ContextActivityStreams)); + } + + #[test] + fn should_parse_missing_context() { + let json = json!({ + "some": "stuff" + }); + + let doc: ActivityStreamsDocument<()> = serde_json::from_value(json).unwrap(); + + assert_eq!(doc.ld_context, Context::String(ContextActivityStreams)); + } + + #[test] + fn should_parse_context_http() { + let json = json!({ + "@context": "http://www.w3.org/ns/activitystreams", + "some": "stuff" + }); + + let doc: ActivityStreamsDocument<()> = serde_json::from_value(json).unwrap(); + + assert_eq!(doc.ld_context, Context::String(ContextActivityStreams)); + } + + #[test] + fn should_parse_context_vocab() { + let json = json!({ + "@context": { + "@vocab": "https://www.w3.org/ns/activitystreams", + "foo": "bar" + }, + "some": "stuff" + }); + + let doc: ActivityStreamsDocument<()> = serde_json::from_value(json).unwrap(); + + assert_eq!( + doc.ld_context, + Context::Object { + ld_vocab: ContextActivityStreams + } + ); + } + + #[test] + fn should_parse_context_array() { + let json = json!({ + "@context": [ + { + "foo": "bar" + }, + "https://www.w3.org/ns/activitystreams", + ], + "some": "stuff" + }); + + let doc: ActivityStreamsDocument<()> = serde_json::from_value(json).unwrap(); + + assert_eq!( + doc.ld_context, + Context::List(ListContaining(ContextActivityStreams)) + ); + } +} diff --git a/src/web_model/jsonld/mod.rs b/src/web_model/jsonld/mod.rs deleted file mode 100644 index 4e9eab1..0000000 --- a/src/web_model/jsonld/mod.rs +++ /dev/null @@ -1 +0,0 @@ -struct JsonLD {} diff --git a/src/web_model/mod.rs b/src/web_model/mod.rs index c634f3f..69b3fdb 100644 --- a/src/web_model/mod.rs +++ b/src/web_model/mod.rs @@ -1,6 +1,10 @@ -use serde::Serialize; +use serde::de::Error; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::Value; +use std::fmt::Debug; +use std::str::FromStr; -pub mod jsonld; +pub mod activity_streams; pub mod webfinger; trait ContentType: Serialize { @@ -51,8 +55,11 @@ pub mod content_type { use serde::de::Error; use serde::{Deserialize, Deserializer, Serialize, Serializer}; - content_type!(pub ContentActivityPlusJson, "application/activity+json"); + content_type!(pub ContentActivityStreams, "application/activity+json"); content_type!(pub ContentHtml, "text/html"); + content_type!(pub ContentJson, "application/json"); + content_type!(pub ContentMultipartFormData, "multipart/form-data"); + content_type!(pub ContentUrlEncoded, "application/x-www-form-urlencoded"); } macro_rules! link_rel { @@ -107,3 +114,36 @@ pub mod rel { link_rel!(pub RelSelf, "self"); link_rel!(pub RelOStatusSubscribe, "http://ostatus.org/schema/1.0/subscribe"); } + +#[derive(Clone, Eq, PartialEq, Debug)] +pub struct ListContaining + FromStr>(pub T); + +impl Serialize for ListContaining +where + T: Clone + Eq + PartialEq + Debug + AsRef + FromStr, +{ + fn serialize(&self, serializer: S) -> Result { + vec![AsRef::::as_ref(&self.0)].serialize(serializer) + } +} + +impl<'de, T> Deserialize<'de> for ListContaining +where + T: Clone + Eq + PartialEq + Debug + AsRef + FromStr, +{ + fn deserialize>(deserializer: D) -> Result { + let data = Vec::::deserialize(deserializer)?; + + let dt = data + .iter() + .filter_map(Value::as_str) + .filter_map(|val| T::from_str(val).ok()) + .next(); + + if let Some(value) = dt { + Ok(ListContaining(value)) + } else { + Err(Error::custom("Count not find item in list.".to_string())) + } + } +} diff --git a/src/web_model/webfinger/mod.rs b/src/web_model/webfinger/mod.rs index cafec83..7a4a08a 100644 --- a/src/web_model/webfinger/mod.rs +++ b/src/web_model/webfinger/mod.rs @@ -1,4 +1,4 @@ -use crate::web_model::content_type::{ContentActivityPlusJson, ContentHtml}; +use crate::web_model::content_type::{ContentActivityStreams, ContentHtml}; use crate::web_model::rel::{RelOStatusSubscribe, RelSelf, RelWebFingerProfilePage}; use serde::de::Error; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -30,7 +30,7 @@ enum WebFingerRel { RelSelf { rel: RelSelf, #[serde(rename = "type")] - content_type: ContentActivityPlusJson, + content_type: ContentActivityStreams, href: String, }, RelOStatusSubscribe { @@ -73,7 +73,7 @@ impl<'de> Deserialize<'de> for Acct { #[cfg(test)] mod test { - use crate::web_model::content_type::{ContentActivityPlusJson, ContentHtml}; + use crate::web_model::content_type::{ContentActivityStreams, ContentHtml}; use crate::web_model::rel::{RelOStatusSubscribe, RelSelf, RelWebFingerProfilePage}; use crate::web_model::webfinger::WebFingerSubject::Url; use crate::web_model::webfinger::{Acct, WebFinger, WebFingerRel, WebFingerSubject}; @@ -138,7 +138,7 @@ mod test { }, WebFingerRel::RelSelf { rel: RelSelf, - content_type: ContentActivityPlusJson, + content_type: ContentActivityStreams, href: "https://tech.lgbt/users/natty".to_owned(), }, WebFingerRel::RelOStatusSubscribe {