use std::any;
use std::convert::TryInto;
use std::fmt::{self, Debug};
use async_trait::async_trait;
use bytes::Bytes;
use graphql_client::{GraphQLQuery, QueryBody, Response};
use http::{HeaderMap, Response as HttpResponse};
use itertools::Itertools;
use log::{debug, error, info};
use reqwest::blocking::Client;
use reqwest::Client as AsyncClient;
use serde::de::DeserializeOwned;
use serde::Deserialize;
use thiserror::Error;
use url::Url;
use crate::api::users::CurrentUser;
use crate::api::{self, AsyncQuery, Query};
use crate::auth::{Auth, AuthError};
use crate::types::*;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum GitlabError {
#[error("failed to parse url: {}", source)]
UrlParse {
#[from]
source: url::ParseError,
},
#[error("error setting auth header: {}", source)]
AuthError {
#[from]
source: AuthError,
},
#[error("communication with gitlab: {}", source)]
Communication {
#[from]
source: reqwest::Error,
},
#[error("gitlab HTTP error: {}", status)]
Http { status: reqwest::StatusCode },
#[allow(clippy::upper_case_acronyms)]
#[error("graphql error: [\"{}\"]", message.iter().format("\", \""))]
GraphQL { message: Vec<graphql_client::Error> },
#[error("no response from gitlab")]
NoResponse {},
#[error("could not parse {} data from JSON: {}", typename, source)]
DataType {
#[source]
source: serde_json::Error,
typename: &'static str,
},
#[error("api error: {}", source)]
Api {
#[from]
source: api::ApiError<RestError>,
},
}
impl GitlabError {
fn http(status: reqwest::StatusCode) -> Self {
GitlabError::Http {
status,
}
}
fn graphql(message: Vec<graphql_client::Error>) -> Self {
GitlabError::GraphQL {
message,
}
}
fn no_response() -> Self {
GitlabError::NoResponse {}
}
fn data_type<T>(source: serde_json::Error) -> Self {
GitlabError::DataType {
source,
typename: any::type_name::<T>(),
}
}
}
type GitlabResult<T> = Result<T, GitlabError>;
#[derive(Clone)]
pub struct Gitlab {
client: Client,
rest_url: Url,
graphql_url: Url,
auth: Auth,
}
impl Debug for Gitlab {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Gitlab")
.field("rest_url", &self.rest_url)
.field("graphql_url", &self.graphql_url)
.finish()
}
}
#[derive(Debug, Clone)]
enum CertPolicy {
Default,
Insecure,
}
impl Gitlab {
pub fn new<H, T>(host: H, token: T) -> GitlabResult<Self>
where
H: AsRef<str>,
T: Into<String>,
{
Self::new_impl(
"https",
host.as_ref(),
Auth::Token(token.into()),
CertPolicy::Default,
)
}
pub fn new_insecure<H, T>(host: H, token: T) -> GitlabResult<Self>
where
H: AsRef<str>,
T: Into<String>,
{
Self::new_impl(
"http",
host.as_ref(),
Auth::Token(token.into()),
CertPolicy::Insecure,
)
}
pub fn with_oauth2<H, T>(host: H, token: T) -> GitlabResult<Self>
where
H: AsRef<str>,
T: Into<String>,
{
Self::new_impl(
"https",
host.as_ref(),
Auth::OAuth2(token.into()),
CertPolicy::Default,
)
}
pub fn with_oauth2_insecure<H, T>(host: H, token: T) -> GitlabResult<Self>
where
H: AsRef<str>,
T: Into<String>,
{
Self::new_impl(
"http",
host.as_ref(),
Auth::OAuth2(token.into()),
CertPolicy::Default,
)
}
fn new_impl(
protocol: &str,
host: &str,
auth: Auth,
cert_validation: CertPolicy,
) -> GitlabResult<Self> {
let rest_url = Url::parse(&format!("{}://{}/api/v4/", protocol, host))?;
let graphql_url = Url::parse(&format!("{}://{}/api/graphql", protocol, host))?;
let client = match cert_validation {
CertPolicy::Insecure => {
Client::builder()
.danger_accept_invalid_certs(true)
.build()?
},
CertPolicy::Default => Client::new(),
};
let api = Gitlab {
client,
rest_url,
graphql_url,
auth,
};
let _: UserPublic = CurrentUser::builder().build().unwrap().query(&api)?;
Ok(api)
}
pub fn builder<H, T>(host: H, token: T) -> GitlabBuilder
where
H: Into<String>,
T: Into<String>,
{
GitlabBuilder::new(host, token)
}
pub fn graphql<Q>(&self, query: &QueryBody<Q::Variables>) -> GitlabResult<Q::ResponseData>
where
Q: GraphQLQuery,
Q::Variables: Debug,
for<'d> Q::ResponseData: Deserialize<'d>,
{
info!(
target: "gitlab",
"sending GraphQL query '{}' {:?}",
query.operation_name,
query.variables,
);
let req = self.client.post(self.graphql_url.clone()).json(query);
let rsp: Response<Q::ResponseData> = self.send(req)?;
if let Some(errs) = rsp.errors {
return Err(GitlabError::graphql(errs));
}
rsp.data.ok_or_else(GitlabError::no_response)
}
fn send<T>(&self, req: reqwest::blocking::RequestBuilder) -> GitlabResult<T>
where
T: DeserializeOwned,
{
let auth_headers = {
let mut headers = HeaderMap::default();
self.auth.set_header(&mut headers)?;
headers
};
let rsp = req.headers(auth_headers).send()?;
let status = rsp.status();
if status.is_server_error() {
return Err(GitlabError::http(status));
}
serde_json::from_reader::<_, T>(rsp).map_err(GitlabError::data_type::<T>)
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum RestError {
#[error("error setting auth header: {}", source)]
AuthError {
#[from]
source: AuthError,
},
#[error("communication with gitlab: {}", source)]
Communication {
#[from]
source: reqwest::Error,
},
#[error("`http` error: {}", source)]
Http {
#[from]
source: http::Error,
},
}
impl api::Client for Gitlab {
type Error = RestError;
fn rest_endpoint(&self, endpoint: &str) -> Result<Url, api::ApiError<Self::Error>> {
debug!(target: "gitlab", "REST api call {}", endpoint);
Ok(self.rest_url.join(endpoint)?)
}
fn rest(
&self,
mut request: http::request::Builder,
body: Vec<u8>,
) -> Result<HttpResponse<Bytes>, api::ApiError<Self::Error>> {
let call = || -> Result<_, RestError> {
self.auth.set_header(request.headers_mut().unwrap())?;
let http_request = request.body(body)?;
let request = http_request.try_into()?;
let rsp = self.client.execute(request)?;
let mut http_rsp = HttpResponse::builder()
.status(rsp.status())
.version(rsp.version());
let headers = http_rsp.headers_mut().unwrap();
for (key, value) in rsp.headers() {
headers.insert(key, value.clone());
}
Ok(http_rsp.body(rsp.bytes()?)?)
};
call().map_err(api::ApiError::client)
}
}
pub struct GitlabBuilder {
protocol: &'static str,
host: String,
token: Auth,
cert_validation: CertPolicy,
}
impl GitlabBuilder {
pub fn new<H, T>(host: H, token: T) -> Self
where
H: Into<String>,
T: Into<String>,
{
Self {
protocol: "https",
host: host.into(),
token: Auth::Token(token.into()),
cert_validation: CertPolicy::Default,
}
}
pub fn insecure(&mut self) -> &mut Self {
self.protocol = "http";
self
}
pub fn cert_insecure(&mut self) -> &mut Self {
self.cert_validation = CertPolicy::Insecure;
self
}
pub fn oauth2_token(&mut self) -> &mut Self {
if let Auth::Token(token) = self.token.clone() {
self.token = Auth::OAuth2(token);
}
self
}
pub fn build(&self) -> GitlabResult<Gitlab> {
Gitlab::new_impl(
self.protocol,
&self.host,
self.token.clone(),
self.cert_validation.clone(),
)
}
pub async fn build_async(&self) -> GitlabResult<AsyncGitlab> {
AsyncGitlab::new_impl(
self.protocol,
&self.host,
self.token.clone(),
self.cert_validation.clone(),
)
.await
}
}
#[derive(Clone)]
pub struct AsyncGitlab {
client: reqwest::Client,
rest_url: Url,
graphql_url: Url,
auth: Auth,
}
impl Debug for AsyncGitlab {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("AsyncGitlab")
.field("rest_url", &self.rest_url)
.field("graphql_url", &self.graphql_url)
.finish()
}
}
#[async_trait]
impl api::AsyncClient for AsyncGitlab {
type Error = RestError;
fn rest_endpoint(&self, endpoint: &str) -> Result<Url, api::ApiError<Self::Error>> {
debug!(target: "gitlab", "REST api call {}", endpoint);
Ok(self.rest_url.join(endpoint)?)
}
async fn rest_async(
&self,
mut request: http::request::Builder,
body: Vec<u8>,
) -> Result<HttpResponse<Bytes>, api::ApiError<Self::Error>> {
use futures_util::TryFutureExt;
let call = || {
async {
self.auth.set_header(request.headers_mut().unwrap())?;
let http_request = request.body(body)?;
let request = http_request.try_into()?;
let rsp = self.client.execute(request).await?;
let mut http_rsp = HttpResponse::builder()
.status(rsp.status())
.version(rsp.version());
let headers = http_rsp.headers_mut().unwrap();
for (key, value) in rsp.headers() {
headers.insert(key, value.clone());
}
Ok(http_rsp.body(rsp.bytes().await?)?)
}
};
call().map_err(api::ApiError::client).await
}
}
impl AsyncGitlab {
async fn new_impl(
protocol: &str,
host: &str,
auth: Auth,
cert_validation: CertPolicy,
) -> GitlabResult<Self> {
let rest_url = Url::parse(&format!("{}://{}/api/v4/", protocol, host))?;
let graphql_url = Url::parse(&format!("{}://{}/api/graphql", protocol, host))?;
let client = match cert_validation {
CertPolicy::Insecure => {
AsyncClient::builder()
.danger_accept_invalid_certs(true)
.build()?
},
CertPolicy::Default => AsyncClient::new(),
};
let api = AsyncGitlab {
client,
rest_url,
graphql_url,
auth,
};
let _: UserPublic = CurrentUser::builder()
.build()
.unwrap()
.query_async(&api)
.await?;
Ok(api)
}
}