Merge pull request #11 from Plume-org/feat/async-trait

Feature: async trait
pull/13/head
Gelez 4 years ago committed by GitHub
commit 4e8f12810c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -25,4 +25,7 @@ addons:
- zlib1g-dev - zlib1g-dev
- libiberty-dev - libiberty-dev
script: cargo test && ./coverage.sh script:
- cargo test
- cargo test --features async
- ./coverage.sh

@ -10,11 +10,16 @@ categories = ["web-programming"]
license = "GPL-3.0" license = "GPL-3.0"
edition = "2018" edition = "2018"
[features]
default = []
async = ["async-trait"]
[dependencies] [dependencies]
reqwest = { version = "0.10", features = [ "json" ] } reqwest = { version = "0.10", features = [ "json" ] }
serde = { version = "1.0", features = [ "derive" ] } serde = { version = "1.0", features = [ "derive" ] }
async-trait = {version = "*", optional = true}
[dev-dependencies] [dev-dependencies]
serde_json = "1.0" serde_json = "1.0"
mockito = "0.22" mockito = "0.23"
tokio = "0.2" tokio = "0.2"

@ -0,0 +1,47 @@
use crate::{Prefix, ResolverError, Webfinger};
use async_trait::async_trait;
/// A trait to easily generate a WebFinger endpoint for any resource repository.
///
/// The `R` type is your resource repository (a database for instance) that will be passed to the
/// [`find`](Resolver::find) and [`endpoint`](Resolver::endpoint) functions.
#[async_trait]
pub trait AsyncResolver {
type Repo: Send;
/// Returns the domain name of the current instance.
async fn instance_domain<'a>(&self) -> &'a str;
/// Tries to find a resource, `acct`, in the repository `resource_repo`.
///
/// `acct` is not a complete `acct:` URI, it only contains the identifier of the requested resource
/// (e.g. `test` for `acct:test@example.org`)
///
/// If the resource couldn't be found, you may probably want to return a [`ResolverError::NotFound`].
async fn find(
&self,
prefix: Prefix,
acct: String,
resource_repo: Self::Repo,
) -> Result<Webfinger, ResolverError>;
/// Returns a WebFinger result for a requested resource.
async fn endpoint<R: Into<String> + Send>(
&self,
resource: R,
resource_repo: Self::Repo,
) -> Result<Webfinger, ResolverError> {
let resource = resource.into();
let mut parsed_query = resource.splitn(2, ':');
let res_prefix = Prefix::from(parsed_query.next().ok_or(ResolverError::InvalidResource)?);
let res = parsed_query.next().ok_or(ResolverError::InvalidResource)?;
let mut parsed_res = res.splitn(2, '@');
let user = parsed_res.next().ok_or(ResolverError::InvalidResource)?;
let domain = parsed_res.next().ok_or(ResolverError::InvalidResource)?;
if domain == self.instance_domain().await {
self.find(res_prefix, user.to_string(), resource_repo).await
} else {
Err(ResolverError::WrongDomain)
}
}
}

@ -3,7 +3,15 @@
//! Use [`resolve`] to fetch remote resources, and [`Resolver`] to serve your own resources. //! Use [`resolve`] to fetch remote resources, and [`Resolver`] to serve your own resources.
use reqwest::{header::ACCEPT, Client}; use reqwest::{header::ACCEPT, Client};
use serde::{Serialize, Deserialize}; use serde::{Deserialize, Serialize};
mod resolver;
pub use crate::resolver::*;
#[cfg(feature = "async")]
mod async_resolver;
#[cfg(feature = "async")]
pub use crate::async_resolver::*;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
@ -85,7 +93,7 @@ impl Into<String> for Prefix {
match self { match self {
Prefix::Acct => "acct".into(), Prefix::Acct => "acct".into(),
Prefix::Group => "group".into(), Prefix::Group => "group".into(),
Prefix::Custom(x) => x.clone(), Prefix::Custom(x) => x,
} }
} }
} }
@ -107,7 +115,7 @@ pub fn url_for(
let scheme = if with_https { "https" } else { "http" }; let scheme = if with_https { "https" } else { "http" };
let prefix: String = prefix.into(); let prefix: String = prefix.into();
acct.split("@") acct.split('@')
.nth(1) .nth(1)
.ok_or(WebfingerError::ParseError) .ok_or(WebfingerError::ParseError)
.map(|instance| { .map(|instance| {
@ -139,7 +147,10 @@ pub async fn resolve_with_prefix(
/// Fetches a Webfinger resource. /// Fetches a Webfinger resource.
/// ///
/// If the resource doesn't have a prefix, `acct:` will be used. /// If the resource doesn't have a prefix, `acct:` will be used.
pub async fn resolve(acct: impl Into<String>, with_https: bool) -> Result<Webfinger, WebfingerError> { pub async fn resolve(
acct: impl Into<String>,
with_https: bool,
) -> Result<Webfinger, WebfingerError> {
let acct = acct.into(); let acct = acct.into();
let mut parsed = acct.splitn(2, ':'); let mut parsed = acct.splitn(2, ':');
let first = parsed.next().ok_or(WebfingerError::ParseError)?; let first = parsed.next().ok_or(WebfingerError::ParseError)?;
@ -147,13 +158,11 @@ pub async fn resolve(acct: impl Into<String>, with_https: bool) -> Result<Webfin
if first.contains('@') { if first.contains('@') {
// This : was a port number, not a prefix // This : was a port number, not a prefix
resolve_with_prefix(Prefix::Acct, acct, with_https).await resolve_with_prefix(Prefix::Acct, acct, with_https).await
} else if let Some(other) = parsed.next() {
resolve_with_prefix(Prefix::from(first), other, with_https).await
} else { } else {
if let Some(other) = parsed.next() { // fallback to acct:
resolve_with_prefix(Prefix::from(first), other, with_https).await resolve_with_prefix(Prefix::Acct, first, with_https).await
} else {
// fallback to acct:
resolve_with_prefix(Prefix::Acct, first, with_https).await
}
} }
} }
@ -169,37 +178,3 @@ pub enum ResolverError {
/// The requested resource was not found. /// The requested resource was not found.
NotFound, NotFound,
} }
/// A trait to easily generate a WebFinger endpoint for any resource repository.
///
/// The `R` type is your resource repository (a database for instance) that will be passed to the
/// [`find`](Resolver::find) and [`endpoint`](Resolver::endpoint) functions.
pub trait Resolver<R> {
/// Returns the domain name of the current instance.
fn instance_domain<'a>(&self) -> &'a str;
/// Tries to find a resource, `acct`, in the repository `resource_repo`.
///
/// `acct` is not a complete `acct:` URI, it only contains the identifier of the requested resource
/// (e.g. `test` for `acct:test@example.org`)
///
/// If the resource couldn't be found, you may probably want to return a [`ResolverError::NotFound`].
fn find(&self, prefix: Prefix, acct: String, resource_repo: R) -> Result<Webfinger, ResolverError>;
/// Returns a WebFinger result for a requested resource.
fn endpoint(&self, resource: impl Into<String>, resource_repo: R) -> Result<Webfinger, ResolverError> {
let resource = resource.into();
let mut parsed_query = resource.splitn(2, ':');
let res_prefix = Prefix::from(parsed_query.next().ok_or(ResolverError::InvalidResource)?);
let res = parsed_query.next().ok_or(ResolverError::InvalidResource)?;
let mut parsed_res = res.splitn(2, '@');
let user = parsed_res.next().ok_or(ResolverError::InvalidResource)?;
let domain = parsed_res.next().ok_or(ResolverError::InvalidResource)?;
if domain == self.instance_domain() {
self.find(res_prefix, user.to_string(), resource_repo)
} else {
Err(ResolverError::WrongDomain)
}
}
}

@ -0,0 +1,44 @@
use crate::{Prefix, ResolverError, Webfinger};
/// A trait to easily generate a WebFinger endpoint for any resource repository.
///
/// The `R` type is your resource repository (a database for instance) that will be passed to the
/// [`find`](Resolver::find) and [`endpoint`](Resolver::endpoint) functions.
pub trait Resolver<R> {
/// Returns the domain name of the current instance.
fn instance_domain<'a>(&self) -> &'a str;
/// Tries to find a resource, `acct`, in the repository `resource_repo`.
///
/// `acct` is not a complete `acct:` URI, it only contains the identifier of the requested resource
/// (e.g. `test` for `acct:test@example.org`)
///
/// If the resource couldn't be found, you may probably want to return a [`ResolverError::NotFound`].
fn find(
&self,
prefix: Prefix,
acct: String,
resource_repo: R,
) -> Result<Webfinger, ResolverError>;
/// Returns a WebFinger result for a requested resource.
fn endpoint(
&self,
resource: impl Into<String>,
resource_repo: R,
) -> Result<Webfinger, ResolverError> {
let resource = resource.into();
let mut parsed_query = resource.splitn(2, ':');
let res_prefix = Prefix::from(parsed_query.next().ok_or(ResolverError::InvalidResource)?);
let res = parsed_query.next().ok_or(ResolverError::InvalidResource)?;
let mut parsed_res = res.splitn(2, '@');
let user = parsed_res.next().ok_or(ResolverError::InvalidResource)?;
let domain = parsed_res.next().ok_or(ResolverError::InvalidResource)?;
if domain == self.instance_domain() {
self.find(res_prefix, user.to_string(), resource_repo)
} else {
Err(ResolverError::WrongDomain)
}
}
}

@ -1,5 +1,4 @@
use super::*; use super::*;
use serde_json;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
#[test] #[test]
@ -162,7 +161,43 @@ impl Resolver<&'static str> for MyResolver {
acct: String, acct: String,
resource_repo: &'static str, resource_repo: &'static str,
) -> Result<Webfinger, ResolverError> { ) -> Result<Webfinger, ResolverError> {
if acct == resource_repo.to_string() && prefix == Prefix::Acct { if acct == resource_repo && prefix == Prefix::Acct {
Ok(Webfinger {
subject: acct.clone(),
aliases: vec![acct.clone()],
links: vec![Link {
rel: "http://webfinger.net/rel/profile-page".to_string(),
mime_type: None,
href: Some(format!("https://instance.tld/@{}/", acct)),
template: None,
}],
})
} else {
Err(ResolverError::NotFound)
}
}
}
#[cfg(feature = "async")]
pub struct MyAsyncResolver;
// Only one user, represented by a String
#[cfg(feature = "async")]
#[async_trait::async_trait]
impl AsyncResolver for MyAsyncResolver {
type Repo = &'static str;
async fn instance_domain<'a>(&self) -> &'a str {
"instance.tld"
}
async fn find(
&self,
prefix: Prefix,
acct: String,
resource_repo: &'static str,
) -> Result<Webfinger, ResolverError> {
if acct == resource_repo && prefix == Prefix::Acct {
Ok(Webfinger { Ok(Webfinger {
subject: acct.clone(), subject: acct.clone(),
aliases: vec![acct.clone()], aliases: vec![acct.clone()],
@ -182,7 +217,9 @@ impl Resolver<&'static str> for MyResolver {
#[test] #[test]
fn test_my_resolver() { fn test_my_resolver() {
let resolver = MyResolver; let resolver = MyResolver;
assert!(resolver.endpoint("acct:admin@instance.tld", "admin").is_ok()); assert!(resolver
.endpoint("acct:admin@instance.tld", "admin")
.is_ok());
assert_eq!( assert_eq!(
resolver.endpoint("acct:test@instance.tld", "admin"), resolver.endpoint("acct:test@instance.tld", "admin"),
Err(ResolverError::NotFound) Err(ResolverError::NotFound)
@ -208,3 +245,52 @@ fn test_my_resolver() {
Err(ResolverError::NotFound) Err(ResolverError::NotFound)
); );
} }
#[test]
#[cfg(feature = "async")]
fn test_my_async_resolver() {
let resolver = MyAsyncResolver;
let mut r = Runtime::new().unwrap();
r.block_on(async {
assert!(resolver
.endpoint("acct:admin@instance.tld", "admin")
.await
.is_ok());
});
r.block_on(async {
assert_eq!(
resolver.endpoint("acct:test@instance.tld", "admin").await,
Err(ResolverError::NotFound)
);
});
r.block_on(async {
assert_eq!(
resolver.endpoint("acct:admin@oops.ie", "admin").await,
Err(ResolverError::WrongDomain)
);
});
r.block_on(async {
assert_eq!(
resolver.endpoint("admin@instance.tld", "admin").await,
Err(ResolverError::InvalidResource)
);
});
r.block_on(async {
assert_eq!(
resolver.endpoint("admin", "admin").await,
Err(ResolverError::InvalidResource)
);
});
r.block_on(async {
assert_eq!(
resolver.endpoint("acct:admin", "admin").await,
Err(ResolverError::InvalidResource)
);
});
r.block_on(async {
assert_eq!(
resolver.endpoint("group:admin@instance.tld", "admin").await,
Err(ResolverError::NotFound)
);
});
}

Loading…
Cancel
Save