use std::error::Error;
use async_trait::async_trait;
use bytes::Bytes;
use http::{request::Builder as RequestBuilder, Response};
use url::Url;
use crate::{auth::Scope, ApiError};
pub trait RestClient {
type Error: Error + Send + Sync + 'static;
type AccessLevel: Scope;
fn rest_endpoint(&self, endpoint: &str) -> Result<Url, ApiError<Self::Error>>;
}
pub trait Client: RestClient {
fn rest(
&self,
request: RequestBuilder,
body: Vec<u8>,
) -> Result<Response<Bytes>, ApiError<Self::Error>>;
}
#[async_trait]
pub trait AsyncClient: RestClient {
async fn rest_async(
&self,
request: RequestBuilder,
body: Vec<u8>,
) -> Result<Response<Bytes>, ApiError<Self::Error>>;
}
#[doc(hidden)]
pub mod doctests {
use std::marker::PhantomData;
use super::*;
use crate::{
auth::{Authenticated, Unauthenticated},
traduora::RestError,
ApiError, Login, TraduoraError,
};
use http::{Method, Response};
use super::RestClient;
fn generate_response(method: &Method, endpoint: &str) -> Response<Bytes> {
let is_match = |wildcard_str: &str| {
let actual_parts: Vec<_> = endpoint.split('/').collect();
let expected_parts: Vec<_> = wildcard_str.split('/').collect();
actual_parts.len() == expected_parts.len()
&& actual_parts
.into_iter()
.zip(expected_parts)
.all(|(a, e)| e == "*" || a == e)
};
let body = Bytes::from_static(match (method, endpoint) {
(&Method::POST, "/api/v1/auth/change-password") => b"",
(&Method::GET, "/api/v1/auth/providers") => include_bytes!("../data/providers.json"),
(&Method::POST, "/api/v1/auth/signup") => include_bytes!("../data/signup_user.json"),
(&Method::POST, "/api/v1/auth/token") => include_bytes!("../data/access_token.json"),
(&Method::GET, _) if is_match("/api/v1/projects/*/terms") => {
include_bytes!("../data/terms.json")
}
(&Method::POST, _) if is_match("/api/v1/projects/*/terms") => {
include_bytes!("../data/new_term.json")
}
(&Method::PATCH, _) if is_match("/api/v1/projects/*/terms/*") => {
include_bytes!("../data/edit_term.json")
}
(&Method::DELETE, _) if is_match("/api/v1/projects/*/terms/*") => b"",
(&Method::GET, _) if is_match("/api/v1/projects/*/translations") => {
include_bytes!("../data/project_locales.json")
}
(&Method::POST, _) if is_match("/api/v1/projects/*/translations") => {
include_bytes!("../data/create_project_locale.json")
}
(&Method::PATCH, _) if is_match("/api/v1/projects/*/translations/*") => {
include_bytes!("../data/edit_translation.json")
}
(&Method::DELETE, _) if is_match("/api/v1/projects/*/translations/*") => b"",
(&Method::GET, _) if is_match("/api/v1/projects/*/translations/*") => {
include_bytes!("../data/translations.json")
}
(&Method::GET, "/api/v1/projects") => include_bytes!("../data/projects.json"),
(&Method::POST, "/api/v1/projects") => include_bytes!("../data/create_project.json"),
(&Method::GET, _) if is_match("/api/v1/projects/*") => {
include_bytes!("../data/project.json")
}
(&Method::PATCH, _) if is_match("/api/v1/projects/*") => {
include_bytes!("../data/edit_project.json")
}
(&Method::DELETE, _) if is_match("/api/v1/projects/*") => b"",
(&Method::GET, "/api/v1/users/me") => include_bytes!("../data/user_info.json"),
(&Method::DELETE, "/api/v1/users/me") => b"",
(&Method::PATCH, "/api/v1/users/me") => include_bytes!("../data/edit_me.json"),
(&Method::GET, "/api/v1/locales") => include_bytes!("../data/locales.json"),
_ => panic!(
"Failed to find appropriate response body for {} {}",
method, endpoint
),
});
Response::builder()
.body(body)
.expect("Failed to build dummy response")
}
#[doc(hidden)]
pub struct TestClient<T: Scope> {
url: String,
phantom: PhantomData<T>,
}
impl TestClient<Unauthenticated> {
pub fn new(host: &str) -> Result<Self, TraduoraError> {
Ok(Self {
url: host.into(),
phantom: PhantomData::default(),
})
}
}
impl TestClient<Authenticated> {
pub fn with_auth(host: &str, _: Login) -> Result<Self, TraduoraError> {
Ok(Self {
url: host.into(),
phantom: PhantomData::default(),
})
}
}
impl<T: Scope> Client for TestClient<T> {
fn rest(
&self,
builder: RequestBuilder,
_: Vec<u8>,
) -> Result<Response<Bytes>, ApiError<Self::Error>> {
let request = builder.body(()).map_err(|e| ApiError::client(e.into()))?;
Ok(generate_response(request.method(), request.uri().path()))
}
}
impl<T: Scope> RestClient for TestClient<T> {
type Error = RestError;
type AccessLevel = Authenticated;
fn rest_endpoint(&self, endpoint: &str) -> Result<reqwest::Url, ApiError<Self::Error>> {
Ok(format!("http://{}/api/v1/{}", self.url, endpoint).parse()?)
}
}
#[async_trait]
impl<T: Scope + Send + Sync + 'static> AsyncClient for TestClient<T> {
async fn rest_async(
&self,
builder: RequestBuilder,
_: Vec<u8>,
) -> Result<Response<Bytes>, ApiError<Self::Error>> {
let request = builder.body(()).map_err(|e| ApiError::client(e.into()))?;
Ok(generate_response(request.method(), request.uri().path()))
}
}
}