Browse Source

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

Feature: async trait
main
Gelez 11 months ago
committed by GitHub
parent
commit
4e8f12810c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 209 additions and 49 deletions
  1. +4
    -1
      .travis.yml
  2. +6
    -1
      Cargo.toml
  3. +47
    -0
      src/async_resolver.rs
  4. +19
    -44
      src/lib.rs
  5. +44
    -0
      src/resolver.rs
  6. +89
    -3
      src/tests.rs

+ 4
- 1
.travis.yml View File

@@ -25,4 +25,7 @@ addons:
- zlib1g-dev
- libiberty-dev

script: cargo test && ./coverage.sh
script:
- cargo test
- cargo test --features async
- ./coverage.sh

+ 6
- 1
Cargo.toml View File

@@ -10,11 +10,16 @@ categories = ["web-programming"]
license = "GPL-3.0"
edition = "2018"

[features]
default = []
async = ["async-trait"]

[dependencies]
reqwest = { version = "0.10", features = [ "json" ] }
serde = { version = "1.0", features = [ "derive" ] }
async-trait = {version = "*", optional = true}

[dev-dependencies]
serde_json = "1.0"
mockito = "0.22"
mockito = "0.23"
tokio = "0.2"

+ 47
- 0
src/async_resolver.rs View File

@@ -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)
}
}
}

+ 19
- 44
src/lib.rs View File

@@ -3,7 +3,15 @@
//! Use [`resolve`] to fetch remote resources, and [`Resolver`] to serve your own resources.

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)]
mod tests;
@@ -85,7 +93,7 @@ impl Into<String> for Prefix {
match self {
Prefix::Acct => "acct".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 prefix: String = prefix.into();
acct.split("@")
acct.split('@')
.nth(1)
.ok_or(WebfingerError::ParseError)
.map(|instance| {
@@ -139,7 +147,10 @@ pub async fn resolve_with_prefix(
/// Fetches a Webfinger resource.
///
/// 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 mut parsed = acct.splitn(2, ':');
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('@') {
// This : was a port number, not a prefix
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 {
if let Some(other) = parsed.next() {
resolve_with_prefix(Prefix::from(first), other, with_https).await
} else {
// fallback to acct:
resolve_with_prefix(Prefix::Acct, first, with_https).await
}
// 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.
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)
}
}
}

+ 44
- 0
src/resolver.rs View File

@@ -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)
}
}
}

+ 89
- 3
src/tests.rs View File

@@ -1,5 +1,4 @@
use super::*;
use serde_json;
use tokio::runtime::Runtime;

#[test]
@@ -162,7 +161,43 @@ impl Resolver<&'static str> for MyResolver {
acct: String,
resource_repo: &'static str,
) -> 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 {
subject: acct.clone(),
aliases: vec![acct.clone()],
@@ -182,7 +217,9 @@ impl Resolver<&'static str> for MyResolver {
#[test]
fn test_my_resolver() {
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!(
resolver.endpoint("acct:test@instance.tld", "admin"),
Err(ResolverError::NotFound)
@@ -208,3 +245,52 @@ fn test_my_resolver() {
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