diff --git a/entities/src/filter.rs b/entities/src/filter.rs index 5d4c470..751e45c 100644 --- a/entities/src/filter.rs +++ b/entities/src/filter.rs @@ -1,11 +1,159 @@ -use serde::{Deserialize, Serialize}; +use serde::{de::Visitor, Deserialize, Deserializer, Serialize}; +use time::{serde::iso8601, OffsetDateTime}; + +/// Represents a user-defined filter for determining which statuses should not +/// be shown to the user. +/// +/// ## Example +/// ```rust +/// use mastodon_async::entities::filter::Filter; +/// let subject = r#"{ +/// "id": "19972", +/// "title": "Test filter", +/// "context": [ +/// "home" +/// ], +/// "expires_at": "2022-09-20T17:27:39.296Z", +/// "filter_action": "warn", +/// "keywords": [ +/// { +/// "id": "1197", +/// "keyword": "bad word", +/// "whole_word": false +/// } +/// ], +/// "statuses": [ +/// { +/// "id": "1", +/// "status_id": "109031743575371913" +/// } +/// ] +/// }"#; +/// let subject: Filter = serde_json::from_str(subject).expect("deserialize"); +/// assert_eq!(subject.id, "19972"); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Filter { + /// The ID of the Filter in the database. + pub id: String, + /// A title given by the user to name the filter. + pub title: String, + /// The contexts in which the filter should be applied. + pub context: Vec, + /// When the filter should no longer be applied. + #[serde(with = "iso8601::option")] + pub expires_at: Option, + /// The action to be taken when a status matches this filter. + pub filter_action: Action, + /// The keywords grouped under this filter. + pub keywords: Vec, + /// The statuses grouped under this filter. + pub statuses: Vec, +} + +/// Represents the various types of Filter contexts +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum FilterContext { + /// Represents the "home" context + Home, + /// Represents the "notifications" context + Notifications, + /// Represents the "public" context + Public, + /// Represents the "thread" context + Thread, + /// Represents the "account" context + Account, +} + +/// The action the filter should take +/// +/// Please note that the spec requests that any unknown value be interpreted +/// as "warn". +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum Action { + /// Indicates filtered toots should show up, but with a warning + Warn, + /// Indicates filtered toots should be hidden. + Hide, +} + +impl<'de> Deserialize<'de> for Action { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct FilterActionDeserializer; + + impl<'v> Visitor<'v> for FilterActionDeserializer { + type Value = Action; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str(r#""warn" or "hide" (or really any string; any string other than "hide" will deserialize to "warn")"#) + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Ok(if v == "hide" { + Action::Hide + } else { + Action::Warn + }) + } + } + + deserializer.deserialize_str(FilterActionDeserializer) + } +} + +/// Represents a keyword that, if matched, should cause the filter action to be taken. +/// +/// ## Example +/// ```json +/// { +/// "id": "1197", +/// "keyword": "bad word", +/// "whole_word": false +/// } +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Keyword { + /// The ID of the FilterKeyword in the database. + id: String, + /// The phrase to be matched against. + keyword: String, + /// Should the filter consider word boundaries? See [implementation guidelines + /// for filters](https://docs.joinmastodon.org/api/guidelines/#filters). + whole_word: bool, +} + +/// Represents a status ID that, if matched, should cause the filter action to be taken. +/// +/// ## Example +/// ```json +/// { +/// "id": "1", +/// "status_id": "109031743575371913" +/// } +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Status { + /// The ID of the FilterStatus in the database. + id: String, + /// The ID of the filtered Status in the database. + status_id: String, +} mod v1 { pub use super::FilterContext; use serde::{Deserialize, Serialize}; use time::{serde::iso8601, OffsetDateTime}; - /// Represents a single Filter + /// Represents a single v1 Filter #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Filter { /// The ID of the Filter in the database. @@ -26,19 +174,37 @@ mod v1 { } } -/// Represents the various types of Filter contexts -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum FilterContext { - /// Represents the "home" context - #[serde(rename = "home")] - Home, - /// Represents the "notifications" context - #[serde(rename = "notifications")] - Notifications, - /// Represents the "public" context - #[serde(rename = "public")] - Public, - /// Represents the "thread" context - #[serde(rename = "thread")] - Thread, +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_filter_action_serialize_and_deserialize() { + use Action::*; + let hide = r#""hide""#; + let warn = r#""warn""#; + let subject = serde_json::to_string(&Hide).expect("serialize hide"); + assert_eq!(subject, hide); + let subject = serde_json::to_string(&Warn).expect("serialize warn"); + assert_eq!(subject, warn); + let subject: Action = serde_json::from_str(hide).expect("deserialize hide"); + assert_eq!(subject, Hide); + let subject: Action = serde_json::from_str(warn).expect("deserialize warn"); + assert_eq!(subject, Warn); + let subject: Action = + serde_json::from_str(r#""something else""#).expect("deserialize something else"); + assert_eq!(subject, Warn); + // This 👆 further implies... + let subject: Action = serde_json::from_str(r#""Hide""#).expect("deserialize Hide"); + assert_eq!(subject, Warn /* 👈 ! capital H */); + // This behavior is specified by the spec https://docs.joinmastodon.org/entities/Filter/#filter_action + + // improper value + let subject: Result = serde_json::from_str("[1, 2, 3]"); + let subject = subject.expect_err("value was not expected to be valid"); + assert_eq!( + subject.to_string(), + r#"invalid type: sequence, expected "warn" or "hide" (or really any string; any string other than "hide" will deserialize to "warn") at line 1 column 0"# + ); + } }