Added basic ActivityStreams context parsing

This commit is contained in:
Natty 2023-02-18 00:31:30 +01:00
parent 322619e82d
commit 3e2849929d
Signed by: natty
GPG Key ID: BF6CB659ADEE60EC
6 changed files with 200 additions and 9 deletions

1
Cargo.lock generated
View File

@ -153,4 +153,5 @@ dependencies = [
"form_urlencoded", "form_urlencoded",
"idna", "idna",
"percent-encoding", "percent-encoding",
"serde",
] ]

View File

@ -7,4 +7,4 @@ edition = "2021"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
url = "2.3" url = { version = "2.3", features = ["serde"] }

View File

@ -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<str> for ContextActivityStreams {
fn as_ref(&self) -> &'static str {
"https://www.w3.org/ns/activitystreams"
}
}
impl Serialize for ContextActivityStreams {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_ref())
}
}
impl FromStr for ContextActivityStreams {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
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<ContextActivityStreams>),
}
impl Default for Context {
fn default() -> Self {
Context::String(ContextActivityStreams)
}
}
#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)]
struct ActivityStreamsDocument<T> {
#[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))
);
}
}

View File

@ -1 +0,0 @@
struct JsonLD {}

View File

@ -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; pub mod webfinger;
trait ContentType: Serialize { trait ContentType: Serialize {
@ -51,8 +55,11 @@ pub mod content_type {
use serde::de::Error; use serde::de::Error;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; 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 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 { macro_rules! link_rel {
@ -107,3 +114,36 @@ pub mod rel {
link_rel!(pub RelSelf, "self"); link_rel!(pub RelSelf, "self");
link_rel!(pub RelOStatusSubscribe, "http://ostatus.org/schema/1.0/subscribe"); link_rel!(pub RelOStatusSubscribe, "http://ostatus.org/schema/1.0/subscribe");
} }
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct ListContaining<T: Clone + Eq + PartialEq + Debug + AsRef<str> + FromStr>(pub T);
impl<T> Serialize for ListContaining<T>
where
T: Clone + Eq + PartialEq + Debug + AsRef<str> + FromStr,
{
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
vec![AsRef::<str>::as_ref(&self.0)].serialize(serializer)
}
}
impl<'de, T> Deserialize<'de> for ListContaining<T>
where
T: Clone + Eq + PartialEq + Debug + AsRef<str> + FromStr,
{
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let data = Vec::<Value>::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()))
}
}
}

View File

@ -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 crate::web_model::rel::{RelOStatusSubscribe, RelSelf, RelWebFingerProfilePage};
use serde::de::Error; use serde::de::Error;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
@ -30,7 +30,7 @@ enum WebFingerRel {
RelSelf { RelSelf {
rel: RelSelf, rel: RelSelf,
#[serde(rename = "type")] #[serde(rename = "type")]
content_type: ContentActivityPlusJson, content_type: ContentActivityStreams,
href: String, href: String,
}, },
RelOStatusSubscribe { RelOStatusSubscribe {
@ -73,7 +73,7 @@ impl<'de> Deserialize<'de> for Acct {
#[cfg(test)] #[cfg(test)]
mod 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::rel::{RelOStatusSubscribe, RelSelf, RelWebFingerProfilePage};
use crate::web_model::webfinger::WebFingerSubject::Url; use crate::web_model::webfinger::WebFingerSubject::Url;
use crate::web_model::webfinger::{Acct, WebFinger, WebFingerRel, WebFingerSubject}; use crate::web_model::webfinger::{Acct, WebFinger, WebFingerRel, WebFingerSubject};
@ -138,7 +138,7 @@ mod test {
}, },
WebFingerRel::RelSelf { WebFingerRel::RelSelf {
rel: RelSelf, rel: RelSelf,
content_type: ContentActivityPlusJson, content_type: ContentActivityStreams,
href: "https://tech.lgbt/users/natty".to_owned(), href: "https://tech.lgbt/users/natty".to_owned(),
}, },
WebFingerRel::RelOStatusSubscribe { WebFingerRel::RelOStatusSubscribe {