From de456aa7a704a8d6672af8ee03b32ac81f78a946 Mon Sep 17 00:00:00 2001 From: "Benjamin P. Jung" Date: Thu, 29 Aug 2024 13:57:29 +0200 Subject: [PATCH] Initial commit Signed-off-by: Benjamin P. Jung --- .github/workflows/build.yml | 21 +++++ .gitignore | 1 + Cargo.lock | 157 ++++++++++++++++++++++++++++++++++++ Cargo.toml | 8 ++ README.md | 19 +++++ src/avatar/default.rs | 33 ++++++++ src/avatar/rating.rs | 23 ++++++ src/avatars/default.rs | 76 +++++++++++++++++ src/avatars/mod.rs | 104 ++++++++++++++++++++++++ src/avatars/rating.rs | 50 ++++++++++++ src/common/mod.rs | 22 +++++ src/lib.rs | 15 ++++ src/profiles/mod.rs | 1 + 13 files changed, 530 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/avatar/default.rs create mode 100644 src/avatar/rating.rs create mode 100644 src/avatars/default.rs create mode 100644 src/avatars/mod.rs create mode 100644 src/avatars/rating.rs create mode 100644 src/common/mod.rs create mode 100644 src/lib.rs create mode 100644 src/profiles/mod.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..0d5dc4f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,21 @@ +name: Build + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..aa3262a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,157 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cpufeatures" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +dependencies = [ + "libc", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gravatar_api" +version = "0.1.0" +dependencies = [ + "sha2", + "url", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer", + "cfg-if", + "cpufeatures", + "digest", + "opaque-debug", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +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 = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..fd018af --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "gravatar_api" +version = "0.1.0" +edition = "2021" + +[dependencies] +sha2 = "0.9.1" +url = "2" diff --git a/README.md b/README.md new file mode 100644 index 0000000..7fff85e --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# Gravatar API Client for Rust + +## Example Usage + +```rust +use gravatar_api::avatars; + +fn main() { + println!( + "{}", + avatars::Avatar::builder("john.doe@example.com") + .size(512) + .default(avatars::Default::RoboHash) + .rating(avatars::Rating::G) + .build() + .image_url() + ); +} +`` diff --git a/src/avatar/default.rs b/src/avatar/default.rs new file mode 100644 index 0000000..e92ce68 --- /dev/null +++ b/src/avatar/default.rs @@ -0,0 +1,33 @@ +/// The default image to display if the user's email does not have a Gravatar. +/// +/// See . +#[derive(Clone, Debug)] +pub enum Default { + /// The URL of an image file to display as the default. + ImageUrl(String), + + /// Instead of loading an image, the Gravatar URL will return an HTTP 404 (File Not Found) + /// response if the email is not found. + Http404, + + /// A transparent PNG image. + Blank, + + /// A simple, cartoon-style silhouetted outline of a person that does not vary by email hash. + MysteryPerson, + + /// A geometric pattern based on the email hash. + Identicon, + + /// A "monster" with different colors, faces, etc. that are generated by the email hash. + MonsterId, + + /// A face with different features and backgrounds, generated by the email hash. + Wavatar, + + /// An 8-bit arcade-style pixelated face that is generated by the email hash. + Retro, + + /// A generated robot with different colors, faces, etc. + RoboHash, +} diff --git a/src/avatar/rating.rs b/src/avatar/rating.rs new file mode 100644 index 0000000..42a11db --- /dev/null +++ b/src/avatar/rating.rs @@ -0,0 +1,23 @@ +/// The maximum rating level for which Gravatar will show the user's image instead of the specified +/// default. +/// +/// See . +#[derive(Clone, Debug)] +pub enum Rating { + /// Show "G"-rated images only, + /// suitable for display on all websites with any audience type. + G, + + /// Show "PG"-rated images or lower only, + /// may contain rude gestures, provocatively dressed individuals, the lesser swear words, + /// or mild violence. + Pg, + + /// Show "R"-rated images or lower only, + /// may contain such things as harsh profanity, intense violence, nudity, or hard drug use. + R, + + /// Show all images, up to and including "X"-rated ones, + /// may contain sexual imagery or extremely disturbing violence. + X, +} diff --git a/src/avatars/default.rs b/src/avatars/default.rs new file mode 100644 index 0000000..6bae75a --- /dev/null +++ b/src/avatars/default.rs @@ -0,0 +1,76 @@ +use std::fmt; +use url; + +/// The default image to display if the user's email does not have a Gravatar. +/// +/// See . +#[derive(Clone, Debug)] +pub enum Default { + /// The URL of an image file to display as the default. + ImageUrl(String), + + /// Instead of loading an image, the Gravatar URL will return an HTTP 404 (File Not Found) + /// response if the email is not found. + Http404, + + /// A transparent PNG image. + Blank, + + /// A simple, cartoon-style silhouetted outline of a person that does not vary by email hash. + MysteryPerson, + + /// A geometric pattern based on the email hash. + Identicon, + + /// A "monster" with different colors, faces, etc. that are generated by the email hash. + MonsterId, + + /// A face with different features and backgrounds, generated by the email hash. + Wavatar, + + /// An 8-bit arcade-style pixelated face that is generated by the email hash. + Retro, + + /// A generated robot with different colors, faces, etc. + RoboHash, +} + +impl std::fmt::Display for Default { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let val = match self { + Default::Http404 => "404", + Default::MysteryPerson => "mp", + Default::Identicon => "identicon", + Default::MonsterId => "monsterid", + Default::Wavatar => "wavatar", + Default::Retro => "retro", + Default::RoboHash => "robohash", + Default::Blank => "blank", + Default::ImageUrl(ref u) => { + &url::form_urlencoded::byte_serialize(u.as_bytes()).collect::() + } + }; + f.write_str(val) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_to_string() { + assert_eq!(Default::Http404.to_string(), "404"); + assert_eq!(Default::MysteryPerson.to_string(), "mp"); + assert_eq!(Default::Identicon.to_string(), "identicon"); + assert_eq!(Default::MonsterId.to_string(), "monsterid"); + assert_eq!(Default::Wavatar.to_string(), "wavatar"); + assert_eq!(Default::Retro.to_string(), "retro"); + assert_eq!(Default::RoboHash.to_string(), "robohash"); + assert_eq!(Default::Blank.to_string(), "blank"); + assert_eq!( + Default::ImageUrl("anonymous@example.com".to_string()).to_string(), + "anonymous%40example.com" + ); + } +} diff --git a/src/avatars/mod.rs b/src/avatars/mod.rs new file mode 100644 index 0000000..71ef665 --- /dev/null +++ b/src/avatars/mod.rs @@ -0,0 +1,104 @@ +// See https://docs.gravatar.com/api/avatars/ + +use url::Url; + +use crate::_common::email_hash; + +mod default; +mod rating; + +pub use default::Default as _Default; +pub use rating::Rating; + +const BASE_URL: &str = "https://www.gravatar.com/"; + +/// Representation of a single Gravatar image URL. +#[derive(Clone, Debug)] +pub struct Avatar { + email: String, + pub size: Option, + pub default: Option<_Default>, + pub force_default: Option, + pub rating: Option, +} + +impl Avatar { + pub fn builder(email: &str) -> AvatarBuilder { + AvatarBuilder::new(email) + } + + pub fn image_url(self: &Self) -> Url { + let mut str = format!("{}avatar/{}", BASE_URL, email_hash(&self.email)); + if let Some(size) = self.size { + str.push_str(&format!("?s={}", size)); + } + if let Some(rating) = &self.rating { + str.push_str(&format!("&r={}", rating.to_string())); + } + if let Some(default) = &self.default { + str.push_str(&format!("&d={}", default.to_string())); + } + if let Some(force_default) = self.force_default { + str.push_str(&format!("&f={}", force_default.to_string())); + } + Url::parse(&str).unwrap() + } +} + +#[derive(Default)] +pub struct AvatarBuilder { + email: String, + size: Option, + default: Option<_Default>, + force_default: Option, + rating: Option, +} + +impl AvatarBuilder { + pub fn new(email: &str) -> AvatarBuilder { + AvatarBuilder { + email: email.to_string(), + ..Default::default() + } + } + + pub fn size(mut self, size: u16) -> AvatarBuilder { + self.size = Some(size); + self + } + + /// Gravatar allows users to self-rate their images so that they can indicate + /// if an image is appropriate for a certain audience. + /// By default, only ā€˜Gā€™ rated images are displayed unless you indicate that + /// you would like to see higher ratings. + /// + /// If the requested email hash does not have an image meeting the requested + /// rating level, then the default image is returned (or the specified default, + /// as per above). + pub fn rating(mut self, rating: Rating) -> AvatarBuilder { + self.rating = Some(rating); + self + } + + pub fn default(mut self, default: _Default) -> AvatarBuilder { + self.default = Some(default); + self + } + + /// If for some reason you wanted to force the default image to always load, + /// you can do that by passing `true` to this method. + pub fn force_default(mut self, force_default: bool) -> AvatarBuilder { + self.force_default = Some(force_default); + self + } + + pub fn build(self) -> Avatar { + Avatar { + email: self.email, + size: self.size, + default: self.default, + force_default: self.force_default, + rating: self.rating, + } + } +} diff --git a/src/avatars/rating.rs b/src/avatars/rating.rs new file mode 100644 index 0000000..49d7799 --- /dev/null +++ b/src/avatars/rating.rs @@ -0,0 +1,50 @@ +use std::fmt; + +/// The maximum rating level for which Gravatar will show the user's image instead of the specified +/// default. +/// +/// See . +#[derive(Clone, Debug)] +pub enum Rating { + /// Show "G"-rated images only, + /// suitable for display on all websites with any audience type. + G, + + /// Show "PG"-rated images or lower only, + /// may contain rude gestures, provocatively dressed individuals, the lesser swear words, + /// or mild violence. + Pg, + + /// Show "R"-rated images or lower only, + /// may contain such things as harsh profanity, intense violence, nudity, or hard drug use. + R, + + /// Show all images, up to and including "X"-rated ones, + /// may contain sexual imagery or extremely disturbing violence. + X, +} + +impl std::fmt::Display for Rating { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let val = match self { + Rating::G => "g", + Rating::Pg => "pg", + Rating::R => "r", + Rating::X => "x", + }; + f.write_str(val) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_to_string() { + assert_eq!(Rating::G.to_string(), "g"); + assert_eq!(Rating::Pg.to_string(), "pg"); + assert_eq!(Rating::R.to_string(), "r"); + assert_eq!(Rating::X.to_string(), "x"); + } +} diff --git a/src/common/mod.rs b/src/common/mod.rs new file mode 100644 index 0000000..970271c --- /dev/null +++ b/src/common/mod.rs @@ -0,0 +1,22 @@ +use sha2::{Digest, Sha256}; + +/// Creates a SHA256 hash of the given string and returns +/// it as a hexadecimal string. +pub fn email_hash(email: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update(email); + format!("{:x}", hasher.finalize()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_email_hash() { + assert_eq!( + email_hash("anonymous@example.com"), + "f807b5609eae64257bf4877652ea49fee40ac2451c152c12fa596ffeda647157" + ); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..6535367 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,15 @@ +extern crate sha2; +extern crate url; + +#[path = "avatars/mod.rs"] +mod _avatars; + +#[path = "common/mod.rs"] +mod _common; + +pub use public::*; +pub mod public { + pub mod avatars { + pub use crate::_avatars::{Avatar, AvatarBuilder, Rating, _Default as Default}; + } +} diff --git a/src/profiles/mod.rs b/src/profiles/mod.rs new file mode 100644 index 0000000..f499606 --- /dev/null +++ b/src/profiles/mod.rs @@ -0,0 +1 @@ +const BASE_URL: &str = "https://gravatar.com/";