Add a public-aware deserializer for recipient URLs (#165)
* Accept Public aliases in URL deserializer Update deserialize_one_or_many to deserialize recipient URL fields while accepting `Public` and `as:Public` as aliases for the canonical ActivityStreams public URL. Add focused tests for single and array inputs, and verify that unrelated string fields such as `content` are left unchanged. https://github.com/LemmyNet/lemmy/issues/6465 * Deduplicate deserialized recipients Drop repeated recipient URLs after deserialization so equivalent public aliases such as `Public`, `as:Public`, and the canonical public URL do not produce duplicate entries. Update the helper documentation and tests to match the deduplicated result.
This commit is contained in:
parent
279d29d350
commit
838dd9e501
1 changed files with 107 additions and 12 deletions
|
|
@ -1,13 +1,22 @@
|
||||||
//! Serde deserialization functions which help to receive differently shaped data
|
//! Serde deserialization functions which help to receive differently shaped data
|
||||||
|
|
||||||
use serde::{Deserialize, Deserializer};
|
use activitystreams_kinds::public;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use serde::{de::Error, Deserialize, Deserializer};
|
||||||
|
use serde_json::Value;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
/// Deserialize JSON single value or array into Vec.
|
/// Deserialize JSON single value or array into `Vec<Url>`.
|
||||||
///
|
///
|
||||||
/// Useful if your application can handle multiple values for a field, but another federated
|
/// Useful if your application can handle multiple values for a field, but another federated
|
||||||
/// platform only sends a single one.
|
/// platform only sends a single one.
|
||||||
///
|
///
|
||||||
|
/// Also accepts common `Public` aliases for recipient fields. Some implementations send `Public`
|
||||||
|
/// or `as:Public` instead of the canonical `https://www.w3.org/ns/activitystreams#Public` URL
|
||||||
|
/// in fields such as `to` and `cc`.
|
||||||
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
/// # use activitypub_federation::kinds::public;
|
||||||
/// # use activitypub_federation::protocol::helpers::deserialize_one_or_many;
|
/// # use activitypub_federation::protocol::helpers::deserialize_one_or_many;
|
||||||
/// # use url::Url;
|
/// # use url::Url;
|
||||||
/// #[derive(serde::Deserialize)]
|
/// #[derive(serde::Deserialize)]
|
||||||
|
|
@ -25,24 +34,39 @@ use serde::{Deserialize, Deserializer};
|
||||||
/// "https://lemmy.ml/u/bob"
|
/// "https://lemmy.ml/u/bob"
|
||||||
/// ]}"#)?;
|
/// ]}"#)?;
|
||||||
/// assert_eq!(multiple.to.len(), 2);
|
/// assert_eq!(multiple.to.len(), 2);
|
||||||
/// Ok::<(), anyhow::Error>(())
|
///
|
||||||
pub fn deserialize_one_or_many<'de, T, D>(deserializer: D) -> Result<Vec<T>, D::Error>
|
/// let note: Note = serde_json::from_str(r#"{"to": ["Public", "as:Public"]}"#)?;
|
||||||
|
/// assert_eq!(note.to, vec![public()]);
|
||||||
|
/// # Ok::<(), anyhow::Error>(())
|
||||||
|
/// ```
|
||||||
|
pub fn deserialize_one_or_many<'de, D>(deserializer: D) -> Result<Vec<Url>, D::Error>
|
||||||
where
|
where
|
||||||
T: Deserialize<'de>,
|
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
{
|
{
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
enum OneOrMany<T> {
|
enum OneOrMany {
|
||||||
One(T),
|
Many(Vec<Value>),
|
||||||
Many(Vec<T>),
|
One(Value),
|
||||||
}
|
}
|
||||||
|
|
||||||
let result: OneOrMany<T> = Deserialize::deserialize(deserializer)?;
|
let result: OneOrMany = Deserialize::deserialize(deserializer)?;
|
||||||
Ok(match result {
|
let values = match result {
|
||||||
OneOrMany::Many(list) => list,
|
|
||||||
OneOrMany::One(value) => vec![value],
|
OneOrMany::One(value) => vec![value],
|
||||||
})
|
OneOrMany::Many(values) => values,
|
||||||
|
};
|
||||||
|
|
||||||
|
values
|
||||||
|
.into_iter()
|
||||||
|
.map(|value| match value {
|
||||||
|
Value::String(value) if matches!(value.as_str(), "Public" | "as:Public") => {
|
||||||
|
Ok(public())
|
||||||
|
}
|
||||||
|
Value::String(value) => Url::parse(&value).map_err(D::Error::custom),
|
||||||
|
value => Url::deserialize(value).map_err(D::Error::custom),
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map(|values| values.into_iter().unique().collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deserialize JSON single value or single element array into single value.
|
/// Deserialize JSON single value or single element array into single value.
|
||||||
|
|
@ -140,6 +164,11 @@ where
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use super::deserialize_one_or_many;
|
||||||
|
use activitystreams_kinds::public;
|
||||||
|
use anyhow::Result;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn deserialize_one_multiple_values() {
|
fn deserialize_one_multiple_values() {
|
||||||
use crate::protocol::helpers::deserialize_one;
|
use crate::protocol::helpers::deserialize_one;
|
||||||
|
|
@ -155,4 +184,70 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert!(note.is_err());
|
assert!(note.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_one_or_many_single_public_aliases() -> Result<()> {
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Note {
|
||||||
|
#[serde(deserialize_with = "deserialize_one_or_many")]
|
||||||
|
to: Vec<Url>,
|
||||||
|
}
|
||||||
|
|
||||||
|
for alias in ["Public", "as:Public"] {
|
||||||
|
let note = serde_json::from_str::<Note>(&format!(r#"{{"to": "{alias}"}}"#))?;
|
||||||
|
assert_eq!(note.to, vec![public()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_one_or_many_array() -> Result<()> {
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Note {
|
||||||
|
#[serde(deserialize_with = "deserialize_one_or_many")]
|
||||||
|
to: Vec<Url>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let note = serde_json::from_str::<Note>(
|
||||||
|
r#"{
|
||||||
|
"to": [
|
||||||
|
"https://example.com/c/main",
|
||||||
|
"Public",
|
||||||
|
"as:Public",
|
||||||
|
"https://www.w3.org/ns/activitystreams#Public"
|
||||||
|
]
|
||||||
|
}"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
note.to,
|
||||||
|
vec![Url::parse("https://example.com/c/main")?, public(),]
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_one_or_many_leaves_other_strings_unchanged() -> Result<()> {
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Note {
|
||||||
|
#[serde(deserialize_with = "deserialize_one_or_many")]
|
||||||
|
to: Vec<Url>,
|
||||||
|
content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let note = serde_json::from_str::<Note>(r#"{"to": "Public", "content": "Public"}"#)?;
|
||||||
|
|
||||||
|
assert_eq!(note.to, vec![public()]);
|
||||||
|
assert_eq!(note.content, "Public");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue