diff --git a/CHANGELOG.md b/CHANGELOG.md index d812c73..edd792d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Unreleased +# 0.7.0-alpha.17 +- add `XsdBoolean` for `xsd:boolean` serde compatibility +- add `closed` field for `Question` (range Object | Link | xsd:datetime | xsd:boolean), and related + getters and setters + # 0.7.0-alpha.16 - implement `IntoIterator` for `&OneOrMany` and `&mut OneOrMany` - add `check` function for verifying an IRI's authority diff --git a/Cargo.toml b/Cargo.toml index 3372e5e..168e2c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "activitystreams" description = "A set of core types and traits for activitystreams data" -version = "0.7.0-alpha.16" +version = "0.7.0-alpha.17" license = "GPL-3.0" authors = ["asonix "] repository = "https://git.asonix.dog/Aardwolf/activitystreams" @@ -13,14 +13,17 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace] -members = [ - "activitystreams-ext", - "activitystreams-kinds" -] +members = ["activitystreams-ext", "activitystreams-kinds"] [dependencies] -activitystreams-kinds = { version = "0.2.0", path = "./activitystreams-kinds/", default-features = false, features = ["iri-string"] } -iri-string = { version = "0.5.0-beta.1", features = ["serde", "serde-std", "std"] } +activitystreams-kinds = { version = "0.2.0", path = "./activitystreams-kinds/", default-features = false, features = [ + "iri-string", +] } +iri-string = { version = "0.5.0-beta.1", features = [ + "serde", + "serde-std", + "std", +] } mime = "0.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/src/activity.rs b/src/activity.rs index 10d2bfb..c6b95ff 100644 --- a/src/activity.rs +++ b/src/activity.rs @@ -25,14 +25,16 @@ use crate::{ base::{AnyBase, AsBase, Base, Extends}, checked::CheckError, + either::Either, markers, object::{ApObject, AsObject, Object}, prelude::BaseExt, - primitives::OneOrMany, + primitives::{OneOrMany, XsdBoolean, XsdDateTime}, unparsed::{Unparsed, UnparsedMut, UnparsedMutExt}, }; use iri_string::types::IriString; use std::convert::TryFrom; +use time::OffsetDateTime; pub use activitystreams_kinds::activity as kind; @@ -1418,6 +1420,185 @@ pub trait QuestionExt: AsQuestion { self.question_mut().any_of = None; self } + + /// Fetch the closed field for the current activity + /// + /// ```rust + /// # use activitystreams::activity::Question; + /// # let mut question = Question::new(); + /// # + /// use activitystreams::prelude::*; + /// + /// if let Some(closed) = question.closed() { + /// println!("{:?}", closed); + /// } + /// ``` + fn closed(&self) -> Option, Either>> { + self.question_ref().closed.as_ref().map(|either| { + either + .as_ref() + .map(|l| l, |r| r.as_ref().map(|l| *l.as_datetime(), |r| r.0)) + }) + } + + /// Set the closed field for the current activity + /// + /// This overwrites the contents of any_of + /// + /// ```rust + /// # fn main() -> Result<(), anyhow::Error> { + /// use activitystreams::prelude::*; + /// # use activitystreams::{activity::Question, iri}; + /// # let mut question = Question::new(); + /// + /// question.set_closed_base(iri!("https://example.com/one")); + /// # Ok(()) + /// # } + /// ``` + fn set_closed_base(&mut self, closed: T) -> &mut Self + where + T: Into, + { + self.question_mut().closed = Some(Either::Left(OneOrMany::from_one(closed.into()))); + self + } + + /// Set many closed items for the current activity + /// + /// This overwrites the contents of any_of + /// + /// ```rust + /// # fn main() -> Result<(), anyhow::Error> { + /// use activitystreams::prelude::*; + /// # use activitystreams::{activity::Question, iri}; + /// # let mut question = Question::new(); + /// + /// question.set_many_closed_bases(vec![ + /// iri!("https://example.com/one"), + /// iri!("https://example.com/two"), + /// ]); + /// # Ok(()) + /// # } + /// ``` + fn set_many_closed_bases(&mut self, closed: I) -> &mut Self + where + I: IntoIterator, + T: Into, + { + let many = OneOrMany::from_many(closed.into_iter().map(|t| t.into()).collect()); + self.question_mut().closed = Some(Either::Left(many)); + self + } + + /// Set the closed field as a date + /// + /// This overwrites the contents of any_of + /// + /// ```rust + /// # fn main() -> Result<(), anyhow::Error> { + /// use activitystreams::prelude::*; + /// # use activitystreams::{activity::Question, iri}; + /// # let mut question = Question::new(); + /// + /// question.set_closed_date(time::OffsetDateTime::now_utc()); + /// # Ok(()) + /// # } + /// ``` + fn set_closed_date(&mut self, closed: OffsetDateTime) -> &mut Self { + self.question_mut().closed = Some(Either::Right(Either::Left(closed.into()))); + self + } + + /// Set the closed field as a boolean + /// + /// This overwrites the contents of any_of + /// + /// ```rust + /// # fn main() -> Result<(), anyhow::Error> { + /// use activitystreams::prelude::*; + /// # use activitystreams::{activity::Question, iri}; + /// # let mut question = Question::new(); + /// + /// question.set_closed_bool(true); + /// # Ok(()) + /// # } + /// ``` + fn set_closed_bool(&mut self, closed: bool) -> &mut Self { + self.question_mut().closed = Some(Either::Right(Either::Right(closed.into()))); + self + } + + /// Add an object or link to the closed field + /// + /// This overwrites the contents of any_of + /// + /// ```rust + /// # fn main() -> Result<(), anyhow::Error> { + /// use activitystreams::prelude::*; + /// # use activitystreams::{activity::Question, iri}; + /// # let mut question = Question::new(); + /// + /// question + /// .add_closed_base(iri!("https://example.com/one")) + /// .add_closed_base(iri!("https://example.com/two")); + /// # Ok(()) + /// # } + /// ``` + fn add_closed_base(&mut self, closed: T) -> &mut Self + where + T: Into, + { + let one_or_many = match self.question_mut().closed.take() { + Some(Either::Left(mut one_or_many)) => { + one_or_many.add(closed.into()); + one_or_many + } + _ => OneOrMany::from_one(closed.into()), + }; + + self.question_mut().closed = Some(Either::Left(one_or_many)); + self + } + + /// Take the closed field from the current activity + /// + /// ```rust + /// # use activitystreams::activity::Question; + /// # let mut question = Question::new(); + /// # + /// use activitystreams::prelude::*; + /// + /// if let Some(closed) = question.take_closed() { + /// println!("{:?}", closed); + /// } + /// ``` + fn take_closed(&mut self) -> Option, Either>> { + self.question_mut() + .closed + .take() + .map(|either| either.map(|l| l, |r| r.map(|date| date.into(), |b| b.into()))) + } + + /// Remove the closed field from the current activity + /// + /// ```rust + /// # fn main() -> Result<(), anyhow::Error> { + /// # use activitystreams::{activity::Question, iri}; + /// # let mut question = Question::new(); + /// # question.set_closed_bool(true); + /// # + /// use activitystreams::prelude::*; + /// + /// assert!(question.closed().is_some()); + /// question.delete_closed(); + /// assert!(question.closed().is_none()); + /// # Ok(()) + /// # } + /// ``` + fn delete_closed(&mut self) -> &mut Self { + self.question_mut().closed = None; + self + } } /// Indicates that the actor accepts the object. @@ -1940,6 +2121,13 @@ pub struct Question { #[serde(skip_serializing_if = "Option::is_none")] any_of: Option>, + /// Indicates that a question has been closed, and answers are no longer accepted. + /// + /// - Range: Object | Link | xsd:datetime | xsd:boolean + /// - Functional: false + #[serde(skip_serializing_if = "Option::is_none")] + closed: Option, Either>>, + /// base fields and unparsed json ends up here #[serde(flatten)] inner: Activity, @@ -2619,6 +2807,7 @@ impl Question { Question { one_of: None, any_of: None, + closed: None, inner: Activity::new(), } } @@ -2647,10 +2836,12 @@ impl Question { let one_of = inner.remove("oneOf")?; let any_of = inner.remove("anyOf")?; + let closed = inner.remove("closed")?; Ok(Question { one_of, any_of, + closed, inner, }) } @@ -2659,10 +2850,14 @@ impl Question { let Question { one_of, any_of, + closed, mut inner, } = self; - inner.insert("oneOf", one_of)?.insert("anyOf", any_of)?; + inner + .insert("oneOf", one_of)? + .insert("anyOf", any_of)? + .insert("closed", closed)?; inner.retracting() } diff --git a/src/primitives/mod.rs b/src/primitives/mod.rs index c1c53f6..33e2b3d 100644 --- a/src/primitives/mod.rs +++ b/src/primitives/mod.rs @@ -18,6 +18,7 @@ mod one_or_many; mod rdf_lang_string; mod serde_parse; mod unit; +mod xsd_boolean; mod xsd_datetime; mod xsd_duration; @@ -26,6 +27,7 @@ pub use self::{ one_or_many::OneOrMany, rdf_lang_string::RdfLangString, unit::Unit, + xsd_boolean::XsdBoolean, xsd_datetime::XsdDateTime, xsd_duration::{XsdDuration, XsdDurationError}, }; diff --git a/src/primitives/xsd_boolean.rs b/src/primitives/xsd_boolean.rs new file mode 100644 index 0000000..78cffad --- /dev/null +++ b/src/primitives/xsd_boolean.rs @@ -0,0 +1,169 @@ +use crate::either::Either; +use serde::{Deserialize, Serialize}; +use std::ops::{Deref, DerefMut}; + +/// The type xsd:boolean represents logical yes/no values. The valid values for xsd:boolean are +/// true, false, 0, and 1. Values that are capitalized (e.g. TRUE) or abbreviated (e.g. T) are not +/// valid. +#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct XsdBoolean(pub bool); + +impl XsdBoolean { + /// Construct a new XsdBoolean + pub fn new(b: bool) -> Self { + Self(b) + } + + /// Retreive the inner bool + pub fn into_inner(self) -> bool { + self.0 + } +} + +impl PartialEq for XsdBoolean { + fn eq(&self, other: &bool) -> bool { + self.0 == *other + } +} + +impl PartialEq for bool { + fn eq(&self, other: &XsdBoolean) -> bool { + *self == other.0 + } +} + +impl Deref for XsdBoolean { + type Target = bool; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for XsdBoolean { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl AsRef for XsdBoolean { + fn as_ref(&self) -> &bool { + &self.0 + } +} + +impl AsMut for XsdBoolean { + fn as_mut(&mut self) -> &mut bool { + &mut self.0 + } +} + +impl From for XsdBoolean { + fn from(b: bool) -> Self { + Self(b) + } +} + +impl From for bool { + fn from(b: XsdBoolean) -> Self { + b.0 + } +} + +impl<'de> Deserialize<'de> for XsdBoolean { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let helper: Either = Deserialize::<'de>::deserialize(deserializer)?; + + match helper { + Either::Left(u @ 0 | u @ 1) => Ok(XsdBoolean(u == 1)), + Either::Right(b) => Ok(XsdBoolean(b)), + _ => Err(serde::de::Error::custom("Invalid boolean")), + } + } +} + +impl Serialize for XsdBoolean { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.serialize(serializer) + } +} + +#[cfg(test)] +mod tests { + use super::XsdBoolean; + + #[derive(Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)] + struct MyStruct { + field: XsdBoolean, + } + + #[test] + fn deserialize_bool() { + let json = r#"[{"field":true},{"field":false}]"#; + + let structs: Vec = serde_json::from_str(json).unwrap(); + + assert_eq!(structs[0].field, true); + assert_eq!(structs[1].field, false); + } + + #[test] + fn deserialize_number() { + let json = r#"[{"field":1},{"field":0}]"#; + + let structs: Vec = serde_json::from_str(json).unwrap(); + + assert_eq!(structs[0].field, true); + assert_eq!(structs[1].field, false); + } + + #[test] + fn dont_deserialize_invalid_number() { + let invalids = [ + r#"{"field":2}"#, + r#"{"field":3}"#, + r#"{"field":4}"#, + r#"{"field":-1}"#, + ]; + + for case in invalids { + assert!(serde_json::from_str::(case).is_err()); + } + } + + #[test] + fn dont_deserialize_strings() { + let invalids = [ + r#"{"field":"1"}"#, + r#"{"field":"0"}"#, + r#"{"field":"true"}"#, + r#"{"field":"false"}"#, + ]; + + for case in invalids { + assert!(serde_json::from_str::(case).is_err()); + } + } + + #[test] + fn round_trip() { + let structs = vec![ + MyStruct { + field: XsdBoolean(false), + }, + MyStruct { + field: XsdBoolean(true), + }, + ]; + let string = serde_json::to_string(&structs).unwrap(); + let new_structs: Vec = serde_json::from_str(&string).unwrap(); + + assert_eq!(structs, new_structs); + } +}