commit 19471c125f1f858e39285126610adb2db8321d9d Author: Natty Date: Tue Feb 14 01:59:15 2023 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2674b3d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +.idea +.vscode \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..641e8d0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,156 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "itoa" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" + +[[package]] +name = "magnetar" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "url", +] + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "unicode-bidi" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58" + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..91391e4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "magnetar" +version = "0.1.0" +edition = "2021" + +[dependencies] + +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +url = "2.3" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..82994b3 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,6 @@ +#[forbid(unsafe_code)] +mod web_model; + +fn main() { + println!("Hello, world!"); +} diff --git a/src/web_model/jsonld/mod.rs b/src/web_model/jsonld/mod.rs new file mode 100644 index 0000000..4e9eab1 --- /dev/null +++ b/src/web_model/jsonld/mod.rs @@ -0,0 +1 @@ +struct JsonLD {} diff --git a/src/web_model/mod.rs b/src/web_model/mod.rs new file mode 100644 index 0000000..c634f3f --- /dev/null +++ b/src/web_model/mod.rs @@ -0,0 +1,109 @@ +use serde::Serialize; + +pub mod jsonld; +pub mod webfinger; + +trait ContentType: Serialize { + fn mime_type(&self) -> &'static str; +} + +macro_rules! content_type { + ($visib:vis $typ:ident, $expression:expr) => { + #[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] + $visib struct $typ; + + impl AsRef for $typ { + fn as_ref(&self) -> &'static str { + $expression + } + } + + impl Serialize for $typ { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str($expression) + } + } + + impl ContentType for $typ { + fn mime_type(&self) -> &'static str { + $expression + } + } + + impl<'de> Deserialize<'de> for $typ { + fn deserialize>(deserializer: D) -> Result { + let content_type = String::deserialize(deserializer)?; + + if matches!(content_type.as_ref(), $expression) { + Ok(Self) + } else { + Err(Error::custom(format!( + "Invalid content type: {content_type}" + ))) + } + } + } + }; +} + +pub mod content_type { + use crate::web_model::ContentType; + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + content_type!(pub ContentActivityPlusJson, "application/activity+json"); + content_type!(pub ContentHtml, "text/html"); +} + +macro_rules! link_rel { + ($visib:vis $typ:ident, $expression:expr) => { + #[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] + $visib struct $typ; + + impl AsRef for $typ { + fn as_ref(&self) -> &'static str { + $expression + } + } + + impl Serialize for $typ { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str($expression) + } + } + + impl Rel for $typ { + fn rel(&self) -> &'static str { + $expression + } + } + + impl<'de> Deserialize<'de> for $typ { + fn deserialize>(deserializer: D) -> Result { + let rel = String::deserialize(deserializer)?; + + if matches!(rel.as_ref(), $expression) { + Ok(Self) + } else { + Err(Error::custom(format!( + "Invalid rel: {rel}" + ))) + } + } + } + }; +} + +trait Rel: Serialize { + fn rel(&self) -> &'static str; +} + +pub mod rel { + use crate::web_model::Rel; + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + link_rel!(pub RelWebFingerProfilePage, "http://webfinger.net/rel/profile-page"); + link_rel!(pub RelSelf, "self"); + link_rel!(pub RelOStatusSubscribe, "http://ostatus.org/schema/1.0/subscribe"); +} diff --git a/src/web_model/webfinger/mod.rs b/src/web_model/webfinger/mod.rs new file mode 100644 index 0000000..cafec83 --- /dev/null +++ b/src/web_model/webfinger/mod.rs @@ -0,0 +1,153 @@ +use crate::web_model::content_type::{ContentActivityPlusJson, ContentHtml}; +use crate::web_model::rel::{RelOStatusSubscribe, RelSelf, RelWebFingerProfilePage}; +use serde::de::Error; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +struct WebFinger { + subject: WebFingerSubject, + aliases: Vec, + links: Vec, +} + +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +enum WebFingerSubject { + Acct(Acct), + Url(String), +} + +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +#[allow(clippy::enum_variant_names)] +enum WebFingerRel { + RelWebFingerProfilePage { + rel: RelWebFingerProfilePage, + #[serde(rename = "type")] + content_type: ContentHtml, + href: String, + }, + RelSelf { + rel: RelSelf, + #[serde(rename = "type")] + content_type: ContentActivityPlusJson, + href: String, + }, + RelOStatusSubscribe { + rel: RelOStatusSubscribe, + template: String, + }, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Acct(String); + +impl AsRef for Acct { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Serialize for Acct { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&format!("acct:{}", self.0)) + } +} + +impl<'de> Deserialize<'de> for Acct { + fn deserialize>(deserializer: D) -> Result { + let acct = String::deserialize(deserializer)?; + + if let Some(rem) = acct.strip_prefix("acct:") { + Ok(Acct(rem.to_owned())) + } else { + Err(Error::custom( + "Missing acct protocol for account!".to_owned(), + )) + } + } +} + +#[cfg(test)] +mod test { + use crate::web_model::content_type::{ContentActivityPlusJson, 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}; + use serde_json::json; + + #[test] + fn should_remove_acct_prefix() { + let json = json!("acct:natty@tech.lgbt"); + + let acct: Acct = serde_json::from_value(json).unwrap(); + + assert_eq!(acct, Acct("natty@tech.lgbt".to_owned())) + } + + #[test] + fn should_add_acct_prefix() { + let acct = Acct("natty@tech.lgbt".to_owned()); + let json = serde_json::to_value(acct).unwrap(); + + assert_eq!(json, json!("acct:natty@tech.lgbt")); + } + + #[test] + fn should_parse_webfinger() { + let json = json!({ + "subject": "acct:natty@tech.lgbt", + "aliases": [ + "https://tech.lgbt/@natty", + "https://tech.lgbt/users/natty" + ], + "links": [ + { + "rel": "http://webfinger.net/rel/profile-page", + "type": "text/html", + "href": "https://tech.lgbt/@natty" + }, + { + "rel": "self", + "type": "application/activity+json", + "href": "https://tech.lgbt/users/natty" + }, + { + "rel": "http://ostatus.org/schema/1.0/subscribe", + "template": "https://tech.lgbt/authorize_interaction?uri={uri}" + } + ] + }); + + let webfinger: WebFinger = serde_json::from_value(json).unwrap(); + + let real = WebFinger { + subject: WebFingerSubject::Acct(Acct("natty@tech.lgbt".to_owned())), + aliases: vec![ + Url("https://tech.lgbt/@natty".to_owned()), + Url("https://tech.lgbt/users/natty".to_owned()), + ], + links: vec![ + WebFingerRel::RelWebFingerProfilePage { + rel: RelWebFingerProfilePage, + content_type: ContentHtml, + href: "https://tech.lgbt/@natty".to_owned(), + }, + WebFingerRel::RelSelf { + rel: RelSelf, + content_type: ContentActivityPlusJson, + href: "https://tech.lgbt/users/natty".to_owned(), + }, + WebFingerRel::RelOStatusSubscribe { + rel: RelOStatusSubscribe, + template: "https://tech.lgbt/authorize_interaction?uri={uri}".to_owned(), + }, + ], + }; + + assert_eq!(webfinger, real) + } +}