diff options
Diffstat (limited to 'indieauth/src')
-rw-r--r-- | indieauth/src/lib.rs | 252 | ||||
-rw-r--r-- | indieauth/src/pkce.rs | 27 | ||||
-rw-r--r-- | indieauth/src/scopes.rs | 78 |
3 files changed, 212 insertions, 145 deletions
diff --git a/indieauth/src/lib.rs b/indieauth/src/lib.rs index 459d943..1582318 100644 --- a/indieauth/src/lib.rs +++ b/indieauth/src/lib.rs @@ -20,13 +20,13 @@ //! [`axum`]: https://github.com/tokio-rs/axum use std::borrow::Cow; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; use url::Url; mod scopes; pub use self::scopes::{Scope, Scopes}; mod pkce; -pub use self::pkce::{PKCEMethod, PKCEVerifier, PKCEChallenge}; +pub use self::pkce::{PKCEChallenge, PKCEMethod, PKCEVerifier}; // Re-export rand crate just to be sure. pub use rand; @@ -48,7 +48,7 @@ pub enum IntrospectionEndpointAuthMethod { /// TLS client auth with a certificate signed by a valid CA. TlsClientAuth, /// TLS client auth with a self-signed certificate. - SelfSignedTlsClientAuth + SelfSignedTlsClientAuth, } /// Authentication methods supported by the revocation endpoint. @@ -64,7 +64,7 @@ pub enum IntrospectionEndpointAuthMethod { pub enum RevocationEndpointAuthMethod { /// No authentication is required to access an endpoint declaring /// this value. - None + None, } /// The response types supported by the authorization endpoint. @@ -80,7 +80,7 @@ pub enum ResponseType { /// This response type requires a valid access token. /// /// [AutoAuth spec]: https://github.com/sknebel/AutoAuth/blob/master/AutoAuth.md#allowing-external-clients-to-obtain-tokens - ExternalToken + ExternalToken, } // TODO serde_variant impl ResponseType { @@ -108,7 +108,7 @@ pub enum GrantType { /// The refresh token grant, allowing to exchange a refresh token /// for a fresh access token and a new refresh token, to /// facilitate long-term access. - RefreshToken + RefreshToken, } /// OAuth 2.0 Authorization Server Metadata in application to the IndieAuth protocol. @@ -220,7 +220,7 @@ pub struct Metadata { /// registration. #[serde(skip_serializing_if = "ref_identity")] #[serde(default = "Default::default")] - pub client_id_metadata_document_supported: bool + pub client_id_metadata_document_supported: bool, } impl std::fmt::Debug for Metadata { @@ -230,31 +230,59 @@ impl std::fmt::Debug for Metadata { .field("authorization_endpoint", &self.issuer.as_str()) .field("token_endpoint", &self.issuer.as_str()) .field("introspection_endpoint", &self.issuer.as_str()) - .field("introspection_endpoint_auth_methods_supported", &self.introspection_endpoint_auth_methods_supported) - .field("revocation_endpoint", &self.revocation_endpoint.as_ref().map(Url::as_str)) - .field("revocation_endpoint_auth_methods_supported", &self.revocation_endpoint_auth_methods_supported) + .field( + "introspection_endpoint_auth_methods_supported", + &self.introspection_endpoint_auth_methods_supported, + ) + .field( + "revocation_endpoint", + &self.revocation_endpoint.as_ref().map(Url::as_str), + ) + .field( + "revocation_endpoint_auth_methods_supported", + &self.revocation_endpoint_auth_methods_supported, + ) .field("scopes_supported", &self.scopes_supported) .field("response_types_supported", &self.response_types_supported) .field("grant_types_supported", &self.grant_types_supported) - .field("service_documentation", &self.service_documentation.as_ref().map(Url::as_str)) - .field("code_challenge_methods_supported", &self.code_challenge_methods_supported) - .field("authorization_response_iss_parameter_supported", &self.authorization_response_iss_parameter_supported) - .field("userinfo_endpoint", &self.userinfo_endpoint.as_ref().map(Url::as_str)) - .field("client_id_metadata_document_supported", &self.client_id_metadata_document_supported) + .field( + "service_documentation", + &self.service_documentation.as_ref().map(Url::as_str), + ) + .field( + "code_challenge_methods_supported", + &self.code_challenge_methods_supported, + ) + .field( + "authorization_response_iss_parameter_supported", + &self.authorization_response_iss_parameter_supported, + ) + .field( + "userinfo_endpoint", + &self.userinfo_endpoint.as_ref().map(Url::as_str), + ) + .field( + "client_id_metadata_document_supported", + &self.client_id_metadata_document_supported, + ) .finish() } } -fn ref_identity(v: &bool) -> bool { *v } +fn ref_identity(v: &bool) -> bool { + *v +} #[cfg(feature = "axum")] impl axum_core::response::IntoResponse for Metadata { fn into_response(self) -> axum_core::response::Response { use http::StatusCode; - (StatusCode::OK, - [("Content-Type", "application/json")], - serde_json::to_vec(&self).unwrap()) + ( + StatusCode::OK, + [("Content-Type", "application/json")], + serde_json::to_vec(&self).unwrap(), + ) .into_response() } } @@ -306,7 +334,7 @@ pub struct ClientMetadata { pub software_version: Option<Cow<'static, str>>, /// URI for the homepage of this client's owners #[serde(skip_serializing_if = "Option::is_none")] - pub homepage_uri: Option<Url> + pub homepage_uri: Option<Url>, } /// Error that occurs when creating [`ClientMetadata`] with mismatched `client_id` and `client_uri`. @@ -328,12 +356,15 @@ impl ClientMetadata { /// Returns `()` if the `client_uri` is not a prefix of `client_id` as required by the IndieAuth /// spec. pub fn new(client_id: url::Url, client_uri: url::Url) -> Result<Self, ClientIdMismatch> { - if client_id.as_str().as_bytes()[..client_uri.as_str().len()] != *client_uri.as_str().as_bytes() { + if client_id.as_str().as_bytes()[..client_uri.as_str().len()] + != *client_uri.as_str().as_bytes() + { return Err(ClientIdMismatch); } Ok(Self { - client_id, client_uri, + client_id, + client_uri, client_name: None, logo_uri: None, redirect_uris: None, @@ -363,14 +394,15 @@ impl axum_core::response::IntoResponse for ClientMetadata { fn into_response(self) -> axum_core::response::Response { use http::StatusCode; - (StatusCode::OK, - [("Content-Type", "application/json")], - serde_json::to_vec(&self).unwrap()) + ( + StatusCode::OK, + [("Content-Type", "application/json")], + serde_json::to_vec(&self).unwrap(), + ) .into_response() } } - /// User profile to be returned from the userinfo endpoint and when /// the `profile` scope was requested. #[derive(Clone, Debug, Serialize, Deserialize)] @@ -387,7 +419,7 @@ pub struct Profile { /// User's email, if they've chosen to reveal it. This is guarded /// by the `email` scope. #[serde(skip_serializing_if = "Option::is_none")] - pub email: Option<String> + pub email: Option<String>, } #[cfg(feature = "axum")] @@ -395,9 +427,11 @@ impl axum_core::response::IntoResponse for Profile { fn into_response(self) -> axum_core::response::Response { use http::StatusCode; - (StatusCode::OK, - [("Content-Type", "application/json")], - serde_json::to_vec(&self).unwrap()) + ( + StatusCode::OK, + [("Content-Type", "application/json")], + serde_json::to_vec(&self).unwrap(), + ) .into_response() } } @@ -422,13 +456,13 @@ impl State { /// Generate a random state string of 128 bytes in length, using /// the provided random number generator. pub fn from_rng(rng: &mut (impl rand::CryptoRng + rand::Rng)) -> Self { - use rand::{Rng, distributions::Alphanumeric}; + use rand::{distributions::Alphanumeric, Rng}; - let bytes = rng.sample_iter(&Alphanumeric) + let bytes = rng + .sample_iter(&Alphanumeric) .take(128) .collect::<Vec<u8>>(); Self(String::from_utf8(bytes).unwrap()) - } } impl AsRef<str> for State { @@ -511,21 +545,23 @@ impl AuthorizationRequest { ("response_type", Cow::Borrowed(self.response_type.as_str())), ("client_id", Cow::Borrowed(self.client_id.as_str())), ("redirect_uri", Cow::Borrowed(self.redirect_uri.as_str())), - ("code_challenge", Cow::Borrowed(self.code_challenge.as_str())), - ("code_challenge_method", Cow::Borrowed(self.code_challenge.method().as_str())), - ("state", Cow::Borrowed(self.state.as_ref())) + ( + "code_challenge", + Cow::Borrowed(self.code_challenge.as_str()), + ), + ( + "code_challenge_method", + Cow::Borrowed(self.code_challenge.method().as_str()), + ), + ("state", Cow::Borrowed(self.state.as_ref())), ]; if let Some(ref scope) = self.scope { - v.push( - ("scope", Cow::Owned(scope.to_string())) - ); + v.push(("scope", Cow::Owned(scope.to_string()))); } if let Some(ref me) = self.me { - v.push( - ("me", Cow::Borrowed(me.as_str())) - ); + v.push(("me", Cow::Borrowed(me.as_str()))); } v @@ -558,17 +594,22 @@ pub struct AutoAuthRequest { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AutoAuthCallbackData { state: State, - callback_url: Url + callback_url: Url, } #[inline(always)] -fn deserialize_secs<'de, D: serde::de::Deserializer<'de>>(d: D) -> Result<std::time::Duration, D::Error> { +fn deserialize_secs<'de, D: serde::de::Deserializer<'de>>( + d: D, +) -> Result<std::time::Duration, D::Error> { use serde::Deserialize; Ok(std::time::Duration::from_secs(u64::deserialize(d)?)) } #[inline(always)] -fn serialize_secs<S: serde::ser::Serializer>(d: &std::time::Duration, s: S) -> Result<S::Ok, S::Error> { +fn serialize_secs<S: serde::ser::Serializer>( + d: &std::time::Duration, + s: S, +) -> Result<S::Ok, S::Error> { s.serialize_u64(std::time::Duration::as_secs(d)) } @@ -578,7 +619,7 @@ pub struct AutoAuthPollingResponse { request_id: State, #[serde(serialize_with = "serialize_secs")] #[serde(deserialize_with = "deserialize_secs")] - interval: std::time::Duration + interval: std::time::Duration, } /// The authorization response that must be appended to the @@ -610,10 +651,9 @@ pub struct AuthorizationResponse { /// authorization server. /// /// [oauth2-iss]: https://www.ietf.org/archive/id/draft-ietf-oauth-iss-auth-resp-02.html - pub iss: Url + pub iss: Url, } - /// A special grant request that is used in the AutoAuth ceremony. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct AutoAuthCodeGrant { @@ -636,7 +676,7 @@ pub struct AutoAuthCodeGrant { callback_url: Url, /// The user's URL. Will be used to confirm the authorization /// endpoint's authority. - me: Url + me: Url, } /// A grant request that continues the IndieAuth ceremony. @@ -655,7 +695,7 @@ pub enum GrantRequest { redirect_uri: Url, /// The PKCE code verifier that was used to create the code /// challenge. - code_verifier: PKCEVerifier + code_verifier: PKCEVerifier, }, /// Use a refresh token to get a fresh access token and a new /// matching refresh token. @@ -670,8 +710,8 @@ pub enum GrantRequest { /// /// This cannot be used to gain new scopes -- you need to /// start over if you need new scopes from the user. - scope: Option<Scopes> - } + scope: Option<Scopes>, + }, } /// Token type, as described in [RFC6749][]. @@ -685,7 +725,7 @@ pub enum TokenType { /// IndieAuth uses. /// /// [RFC6750]: https://www.rfc-editor.org/rfc/rfc6750 - Bearer + Bearer, } /// The response to a successful [`GrantRequest`]. @@ -722,14 +762,14 @@ pub enum GrantResponse { profile: Option<Profile>, /// The refresh token, if it was issued. #[serde(skip_serializing_if = "Option::is_none")] - refresh_token: Option<String> + refresh_token: Option<String>, }, /// A profile URL response, that only contains the profile URL and /// the profile, if it was requested. /// /// This is suitable for confirming the identity of the user, but /// no more than that. - ProfileUrl(ProfileUrl) + ProfileUrl(ProfileUrl), } /// The contents of a profile URL response. @@ -739,7 +779,7 @@ pub struct ProfileUrl { pub me: Url, /// The user's profile information, if it was requested. #[serde(skip_serializing_if = "Option::is_none")] - pub profile: Option<Profile> + pub profile: Option<Profile>, } #[cfg(feature = "axum")] @@ -747,12 +787,15 @@ impl axum_core::response::IntoResponse for GrantResponse { fn into_response(self) -> axum_core::response::Response { use http::StatusCode; - (StatusCode::OK, - [("Content-Type", "application/json"), - ("Cache-Control", "no-store"), - ("Pragma", "no-cache") - ], - serde_json::to_vec(&self).unwrap()) + ( + StatusCode::OK, + [ + ("Content-Type", "application/json"), + ("Cache-Control", "no-store"), + ("Pragma", "no-cache"), + ], + serde_json::to_vec(&self).unwrap(), + ) .into_response() } } @@ -766,7 +809,7 @@ impl axum_core::response::IntoResponse for GrantResponse { pub enum RequestMaybeAuthorizationEndpoint { Authorization(AuthorizationRequest), Grant(GrantRequest), - AutoAuth(AutoAuthCodeGrant) + AutoAuth(AutoAuthCodeGrant), } /// A token introspection request that can be handled by the token @@ -778,7 +821,7 @@ pub enum RequestMaybeAuthorizationEndpoint { #[derive(Debug, Serialize, Deserialize)] pub struct TokenIntrospectionRequest { /// The token for which data was requested. - pub token: String + pub token: String, } /// Data for a token that will be returned by the introspection @@ -800,7 +843,7 @@ pub struct TokenData { /// The issue date, represented in the same format as the /// [`exp`][TokenData::exp] field. #[serde(skip_serializing_if = "Option::is_none")] - pub iat: Option<u64> + pub iat: Option<u64>, } impl TokenData { @@ -809,24 +852,25 @@ impl TokenData { use std::time::{Duration, SystemTime, UNIX_EPOCH}; self.exp - .map(|exp| SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or(Duration::ZERO) - .as_secs() >= exp) + .map(|exp| { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or(Duration::ZERO) + .as_secs() + >= exp + }) .unwrap_or_default() } /// Return a timestamp at which the token is not considered valid anymore. pub fn expires_at(&self) -> Option<std::time::SystemTime> { - self.exp.map(|time| { - std::time::UNIX_EPOCH + std::time::Duration::from_secs(time) - }) + self.exp + .map(|time| std::time::UNIX_EPOCH + std::time::Duration::from_secs(time)) } /// Return a timestamp describing when the token was issued. pub fn issued_at(&self) -> Option<std::time::SystemTime> { - self.iat.map(|time| { - std::time::UNIX_EPOCH + std::time::Duration::from_secs(time) - }) + self.iat + .map(|time| std::time::UNIX_EPOCH + std::time::Duration::from_secs(time)) } /// Check if a certain scope is allowed for this token. @@ -849,18 +893,24 @@ pub struct TokenIntrospectionResponse { active: bool, #[serde(flatten)] #[serde(skip_serializing_if = "Option::is_none")] - data: Option<TokenData> + data: Option<TokenData>, } // These wrappers and impls should take care of making use of this // type as painless as possible. impl TokenIntrospectionResponse { /// Indicate that this token is not valid. pub fn inactive() -> Self { - Self { active: false, data: None } + Self { + active: false, + data: None, + } } /// Indicate that this token is valid, and provide data about it. pub fn active(data: TokenData) -> Self { - Self { active: true, data: Some(data) } + Self { + active: true, + data: Some(data), + } } /// Check if the endpoint reports this token as valid. pub fn is_active(&self) -> bool { @@ -870,7 +920,7 @@ impl TokenIntrospectionResponse { /// Get data contained in the response, if the token is valid. pub fn data(&self) -> Option<&TokenData> { if !self.active { - return None + return None; } self.data.as_ref() } @@ -882,7 +932,10 @@ impl Default for TokenIntrospectionResponse { } impl From<Option<TokenData>> for TokenIntrospectionResponse { fn from(data: Option<TokenData>) -> Self { - Self { active: data.is_some(), data } + Self { + active: data.is_some(), + data, + } } } impl From<TokenIntrospectionResponse> for Option<TokenData> { @@ -896,9 +949,11 @@ impl axum_core::response::IntoResponse for TokenIntrospectionResponse { fn into_response(self) -> axum_core::response::Response { use http::StatusCode; - (StatusCode::OK, - [("Content-Type", "application/json")], - serde_json::to_vec(&self).unwrap()) + ( + StatusCode::OK, + [("Content-Type", "application/json")], + serde_json::to_vec(&self).unwrap(), + ) .into_response() } } @@ -908,7 +963,7 @@ impl axum_core::response::IntoResponse for TokenIntrospectionResponse { #[derive(Debug, Serialize, Deserialize)] pub struct TokenRevocationRequest { /// The token that needs to be revoked in case it is valid. - pub token: String + pub token: String, } /// Types of errors that a resource server (IndieAuth consumer) can @@ -969,7 +1024,6 @@ pub enum ErrorKind { /// AutoAuth/OAuth2 Device Flow: Access was denied by the /// authorization endpoint. AccessDenied, - } // TODO consider relying on serde_variant for these conversions impl AsRef<str> for ErrorKind { @@ -1005,13 +1059,15 @@ pub struct Error { pub msg: Option<String>, /// An URL to documentation describing what went wrong and how to /// fix it. - pub error_uri: Option<url::Url> + pub error_uri: Option<url::Url>, } impl From<ErrorKind> for Error { fn from(kind: ErrorKind) -> Error { Error { - kind, msg: None, error_uri: None + kind, + msg: None, + error_uri: None, } } } @@ -1037,9 +1093,11 @@ impl axum_core::response::IntoResponse for self::Error { fn into_response(self) -> axum_core::response::Response { use http::StatusCode; - (StatusCode::BAD_REQUEST, - [("Content-Type", "application/json")], - serde_json::to_vec(&self).unwrap()) + ( + StatusCode::BAD_REQUEST, + [("Content-Type", "application/json")], + serde_json::to_vec(&self).unwrap(), + ) .into_response() } } @@ -1052,17 +1110,23 @@ mod tests { fn test_serialize_deserialize_grant_request() { let authorization_code: GrantRequest = GrantRequest::AuthorizationCode { client_id: "https://kittybox.fireburn.ru/".parse().unwrap(), - redirect_uri: "https://kittybox.fireburn.ru/.kittybox/login/redirect".parse().unwrap(), + redirect_uri: "https://kittybox.fireburn.ru/.kittybox/login/redirect" + .parse() + .unwrap(), code_verifier: PKCEVerifier("helloworld".to_string()), - code: "hithere".to_owned() + code: "hithere".to_owned(), }; let serialized = serde_urlencoded::to_string([ ("grant_type", "authorization_code"), ("code", "hithere"), ("client_id", "https://kittybox.fireburn.ru/"), - ("redirect_uri", "https://kittybox.fireburn.ru/.kittybox/login/redirect"), + ( + "redirect_uri", + "https://kittybox.fireburn.ru/.kittybox/login/redirect", + ), ("code_verifier", "helloworld"), - ]).unwrap(); + ]) + .unwrap(); let deserialized = serde_urlencoded::from_str(&serialized).unwrap(); diff --git a/indieauth/src/pkce.rs b/indieauth/src/pkce.rs index 8dcf9b1..6233016 100644 --- a/indieauth/src/pkce.rs +++ b/indieauth/src/pkce.rs @@ -1,6 +1,6 @@ -use serde::{Serialize, Deserialize}; -use sha2::{Sha256, Digest}; use data_encoding::BASE64URL; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; /// Methods to use for PKCE challenges. #[derive(PartialEq, Eq, Copy, Clone, Debug, Serialize, Deserialize, Default)] @@ -10,7 +10,7 @@ pub enum PKCEMethod { S256, /// Plain string by itself. Please don't use this. #[serde(rename = "snake_case")] - Plain + Plain, } impl PKCEMethod { @@ -18,7 +18,7 @@ impl PKCEMethod { pub fn as_str(&self) -> &'static str { match self { PKCEMethod::S256 => "S256", - PKCEMethod::Plain => "plain" + PKCEMethod::Plain => "plain", } } } @@ -57,7 +57,7 @@ impl PKCEVerifier { /// Generate a new PKCE string of 128 bytes in length, using /// the provided random number generator. pub fn from_rng(rng: &mut (impl rand::CryptoRng + rand::Rng)) -> Self { - use rand::{Rng, distributions::Alphanumeric}; + use rand::{distributions::Alphanumeric, Rng}; let bytes = rng .sample_iter(&Alphanumeric) @@ -65,7 +65,6 @@ impl PKCEVerifier { .collect::<Vec<u8>>(); Self(String::from_utf8(bytes).unwrap()) } - } /// A PKCE challenge as described in [RFC7636]. @@ -75,7 +74,7 @@ impl PKCEVerifier { pub struct PKCEChallenge { code_challenge: String, #[serde(rename = "code_challenge_method")] - method: PKCEMethod + method: PKCEMethod, } impl PKCEChallenge { @@ -92,10 +91,10 @@ impl PKCEChallenge { challenge.retain(|c| c != '='); challenge - }, + } PKCEMethod::Plain => code_verifier.to_string(), }, - method + method, } } @@ -130,17 +129,21 @@ impl PKCEChallenge { #[cfg(test)] mod tests { - use super::{PKCEMethod, PKCEVerifier, PKCEChallenge}; + use super::{PKCEChallenge, PKCEMethod, PKCEVerifier}; #[test] /// A snapshot test generated using [Aaron Parecki's PKCE /// tools](https://example-app.com/pkce) that checks for a /// conforming challenge. fn test_pkce_challenge_verification() { - let verifier = PKCEVerifier("ec03310e4e90f7bc988af05384060c3c1afeae4bb4d0f648c5c06b63".to_owned()); + let verifier = + PKCEVerifier("ec03310e4e90f7bc988af05384060c3c1afeae4bb4d0f648c5c06b63".to_owned()); let challenge = PKCEChallenge::new(&verifier, PKCEMethod::S256); - assert_eq!(challenge.as_str(), "aB8OG20Rh8UoQ9gFhI0YvPkx4dDW2MBspBKGXL6j6Wg"); + assert_eq!( + challenge.as_str(), + "aB8OG20Rh8UoQ9gFhI0YvPkx4dDW2MBspBKGXL6j6Wg" + ); } } diff --git a/indieauth/src/scopes.rs b/indieauth/src/scopes.rs index 1157996..295b0c8 100644 --- a/indieauth/src/scopes.rs +++ b/indieauth/src/scopes.rs @@ -1,12 +1,8 @@ use std::str::FromStr; use serde::{ - Serialize, Serializer, - Deserialize, - de::{ - Deserializer, Visitor, - Error as DeserializeError - } + de::{Deserializer, Error as DeserializeError, Visitor}, + Deserialize, Serialize, Serializer, }; /// Various scopes that can be requested through IndieAuth. @@ -36,7 +32,7 @@ pub enum Scope { /// Allows to receive email in the profile information. Email, /// Custom scope not included above. - Custom(String) + Custom(String), } impl Scope { /// Create a custom scope from a string slice. @@ -61,25 +57,25 @@ impl AsRef<str> for Scope { Channels => "channels", Profile => "profile", Email => "email", - Custom(s) => s.as_ref() + Custom(s) => s.as_ref(), } } } impl From<&str> for Scope { fn from(scope: &str) -> Self { match scope { - "create" => Scope::Create, - "update" => Scope::Update, - "delete" => Scope::Delete, - "media" => Scope::Media, - "read" => Scope::Read, - "follow" => Scope::Follow, - "mute" => Scope::Mute, - "block" => Scope::Block, + "create" => Scope::Create, + "update" => Scope::Update, + "delete" => Scope::Delete, + "media" => Scope::Media, + "read" => Scope::Read, + "follow" => Scope::Follow, + "mute" => Scope::Mute, + "block" => Scope::Block, "channels" => Scope::Channels, - "profile" => Scope::Profile, - "email" => Scope::Email, - other => Scope::custom(other) + "profile" => Scope::Profile, + "email" => Scope::Email, + other => Scope::custom(other), } } } @@ -106,7 +102,8 @@ impl Scopes { } /// Ensure all of the requested scopes are in the list. pub fn has_all(&self, scopes: &[Scope]) -> bool { - scopes.iter() + scopes + .iter() .map(|s1| self.iter().any(|s2| s1 == s2)) .all(|s| s) } @@ -123,8 +120,7 @@ impl AsRef<[Scope]> for Scopes { impl std::fmt::Display for Scopes { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut iter = self.0.iter() - .peekable(); + let mut iter = self.0.iter().peekable(); while let Some(scope) = iter.next() { f.write_str(scope.as_ref())?; if iter.peek().is_some() { @@ -139,15 +135,18 @@ impl FromStr for Scopes { type Err = std::convert::Infallible; fn from_str(value: &str) -> Result<Self, Self::Err> { - Ok(Self(value.split_ascii_whitespace() + Ok(Self( + value + .split_ascii_whitespace() .map(Scope::from) - .collect::<Vec<Scope>>())) + .collect::<Vec<Scope>>(), + )) } } impl Serialize for Scopes { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where - S: Serializer + S: Serializer, { serializer.serialize_str(&self.to_string()) } @@ -163,16 +162,15 @@ impl<'de> Visitor<'de> for ScopeVisitor { fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> where - E: DeserializeError + E: DeserializeError, { Ok(Scopes::from_str(value).unwrap()) } } impl<'de> Deserialize<'de> for Scopes { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where - D: Deserializer<'de> + D: Deserializer<'de>, { deserializer.deserialize_str(ScopeVisitor) } @@ -185,29 +183,31 @@ mod tests { #[test] fn test_serde_vec_scope() { let scopes = vec![ - Scope::Create, Scope::Update, Scope::Delete, + Scope::Create, + Scope::Update, + Scope::Delete, Scope::Media, - Scope::custom("kittybox_internal_access") + Scope::custom("kittybox_internal_access"), ]; - let scope_serialized = serde_json::to_value( - Scopes::new(scopes.clone()) - ).unwrap(); + let scope_serialized = serde_json::to_value(Scopes::new(scopes.clone())).unwrap(); let scope_str = scope_serialized.as_str().unwrap(); - assert_eq!(scope_str, "create update delete media kittybox_internal_access"); + assert_eq!( + scope_str, + "create update delete media kittybox_internal_access" + ); - assert!(serde_json::from_value::<Scopes>(scope_serialized).unwrap().has_all(&scopes)) + assert!(serde_json::from_value::<Scopes>(scope_serialized) + .unwrap() + .has_all(&scopes)) } #[test] fn test_scope_has_all() { - let scopes = Scopes(vec![ - Scope::Create, Scope::Update, Scope::custom("draft") - ]); + let scopes = Scopes(vec![Scope::Create, Scope::Update, Scope::custom("draft")]); assert!(scopes.has_all(&[Scope::Create, Scope::custom("draft")])); assert!(!scopes.has_all(&[Scope::Read, Scope::custom("kittybox_internal_access")])); } - } |