Commit 30e5048b authored by Ben Boeckel's avatar Ben Boeckel
Browse files

api: move error types to be API-specific

This drops all the oddball error cases currently in `GitlabError`.
parent a6d9dac7
......@@ -17,6 +17,7 @@
mod client;
mod endpoint;
mod error;
mod ignore;
mod paged;
mod query;
......@@ -31,6 +32,8 @@ pub use self::client::Client;
pub use self::endpoint::Endpoint;
pub use self::endpoint::Pairs;
pub use self::error::ApiError;
pub use self::ignore::ignore;
pub use self::ignore::Ignore;
......@@ -39,5 +42,6 @@ pub use self::paged::LinkHeaderParseError;
pub use self::paged::Pageable;
pub use self::paged::Paged;
pub use self::paged::Pagination;
pub use self::paged::PaginationError;
pub use self::query::Query;
......@@ -4,22 +4,27 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use std::error::Error;
use reqwest::blocking::{RequestBuilder, Response};
use reqwest::Method;
use url::Url;
use crate::gitlab::GitlabError;
use crate::api::ApiError;
/// A trait representing a client which can communicate with a GitLab instance.
pub trait Client {
/// The errors which may occur for this client.
type Error: Error + Send + Sync + 'static;
/// Get the URL for the endpoint for the client.
///
/// This method adds the hostname for the client's target instance.
fn rest_endpoint(&self, endpoint: &str) -> Result<Url, GitlabError>;
fn rest_endpoint(&self, endpoint: &str) -> Result<Url, ApiError<Self::Error>>;
/// Build a REST query from a URL and a given method.
fn build_rest(&self, method: Method, url: Url) -> RequestBuilder;
/// Send a REST query.
fn rest(&self, request: RequestBuilder) -> Result<Response, GitlabError>;
fn rest(&self, request: RequestBuilder) -> Result<Response, ApiError<Self::Error>>;
}
......@@ -11,8 +11,7 @@ use serde::de::DeserializeOwned;
use url::form_urlencoded::Serializer;
use url::UrlQuery;
use crate::api::{Client, Query};
use crate::gitlab::GitlabError;
use crate::api::{ApiError, Client, Query};
/// A type for managing query parameters.
pub type Pairs<'a> = Serializer<'a, UrlQuery<'a>>;
......@@ -34,12 +33,13 @@ pub trait Endpoint {
}
}
impl<E, T> Query<T> for E
impl<E, T, C> Query<T, C> for E
where
E: Endpoint,
T: DeserializeOwned,
C: Client,
{
fn query(&self, client: &dyn Client) -> Result<T, GitlabError> {
fn query(&self, client: &C) -> Result<T, ApiError<C::Error>> {
let mut url = client.rest_endpoint(&self.endpoint())?;
self.add_parameters(url.query_pairs_mut());
......@@ -48,11 +48,11 @@ where
.form(&self.form_data());
let rsp = client.rest(req)?;
let status = rsp.status();
let v = serde_json::from_reader(rsp).map_err(GitlabError::json)?;
let v = serde_json::from_reader(rsp)?;
if !status.is_success() {
return Err(GitlabError::from_gitlab(v));
return Err(ApiError::from_gitlab(v));
}
serde_json::from_value::<T>(v).map_err(GitlabError::data_type::<T>)
serde_json::from_value::<T>(v).map_err(ApiError::data_type::<T>)
}
}
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use std::any;
use std::error::Error;
use thiserror::Error;
use crate::api::PaginationError;
/// Errors which may occur when using API endpoints.
#[derive(Debug, Error)]
// TODO #[non_exhaustive]
pub enum ApiError<E>
where
E: Error + Send + Sync + 'static,
{
/// The client encountered an error.
#[error("client error: {}", source)]
Client {
/// The client error.
source: E,
},
/// The URL failed to parse.
#[error("failed to parse url: {}", source)]
UrlParse {
/// The source of the error.
#[from]
source: url::ParseError,
},
/// JSON deserialization from GitLab failed.
#[error("could not parse JSON response: {}", source)]
Json {
/// The source of the error.
#[from]
source: serde_json::Error,
},
/// GitLab returned an error message.
#[error("gitlab server error: {}", msg)]
Gitlab {
/// The error message from GitLab.
msg: String,
},
/// Failed to parse an expected data type from JSON.
#[error("could not parse {} data from JSON: {}", typename, source)]
DataType {
/// The source of the error.
source: serde_json::Error,
/// The name of the type that could not be deserialized.
typename: &'static str,
},
/// An error with pagination occurred.
#[error("failed to handle for pagination: {}", source)]
Pagination {
/// The source of the error.
#[from]
source: PaginationError,
},
/// This is here to force `_` matching right now.
///
/// **DO NOT USE**
#[doc(hidden)]
#[error("unreachable...")]
_NonExhaustive,
}
impl<E> ApiError<E>
where
E: Error + Send + Sync + 'static,
{
/// Create an API error in a client error.
pub fn client(source: E) -> Self {
ApiError::Client {
source,
}
}
pub(crate) fn from_gitlab(value: serde_json::Value) -> Self {
let msg = value
.pointer("/message")
.or_else(|| value.pointer("/error"))
.and_then(|s| s.as_str())
.unwrap_or_else(|| "<unknown error>");
ApiError::Gitlab {
msg: msg.into(),
}
}
pub(crate) fn data_type<T>(source: serde_json::Error) -> Self {
ApiError::DataType {
source,
typename: any::type_name::<T>(),
}
}
}
......@@ -4,8 +4,7 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use crate::api::{Client, Endpoint, Query};
use crate::gitlab::GitlabError;
use crate::api::{ApiError, Client, Endpoint, Query};
/// A query modifier that ignores the data returned from an endpoint.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
......@@ -20,11 +19,12 @@ pub fn ignore<E>(endpoint: E) -> Ignore<E> {
}
}
impl<E> Query<()> for Ignore<E>
impl<E, C> Query<(), C> for Ignore<E>
where
E: Endpoint,
C: Client,
{
fn query(&self, client: &dyn Client) -> Result<(), GitlabError> {
fn query(&self, client: &C) -> Result<(), ApiError<C::Error>> {
let mut url = client.rest_endpoint(&self.endpoint.endpoint())?;
self.endpoint.add_parameters(url.query_pairs_mut());
......@@ -33,8 +33,8 @@ where
.form(&self.endpoint.form_data());
let rsp = client.rest(req)?;
if !rsp.status().is_success() {
let v = serde_json::from_reader(rsp).map_err(GitlabError::json)?;
return Err(GitlabError::from_gitlab(v));
let v = serde_json::from_reader(rsp)?;
return Err(ApiError::from_gitlab(v));
}
Ok(())
......
......@@ -10,8 +10,33 @@ use serde::de::DeserializeOwned;
use thiserror::Error;
use url::Url;
use crate::api::{Client, Endpoint, Query};
use crate::gitlab::{GitlabError, PaginationError};
use crate::api::{ApiError, Client, Endpoint, Query};
/// Errors which may occur with pagination.
#[derive(Debug, Error)]
// TODO #[non_exhaustive]
pub enum PaginationError {
/// A `Link` HTTP header can fail to parse.
#[error("failed to parse a Link HTTP header: {}", source)]
LinkHeader {
/// The source of the error.
#[from]
source: LinkHeaderParseError,
},
/// An invalid URL can be returned.
#[error("failed to parse a Link HTTP header URL: {}", source)]
InvalidUrl {
/// The source of the error.
#[from]
source: url::ParseError,
},
/// This is here to force `_` matching right now.
///
/// **DO NOT USE**
#[doc(hidden)]
#[error("unreachable...")]
_NonExhaustive,
}
struct LinkHeader<'a> {
url: &'a str,
......@@ -151,13 +176,14 @@ pub trait Pageable {
}
}
impl<E, T> Query<Vec<T>> for Paged<E>
impl<E, T, C> Query<Vec<T>, C> for Paged<E>
where
E: Endpoint,
E: Pageable,
T: DeserializeOwned,
C: Client,
{
fn query(&self, client: &dyn Client) -> Result<Vec<T>, GitlabError> {
fn query(&self, client: &C) -> Result<Vec<T>, ApiError<C::Error>> {
let url = {
let mut url = client.rest_endpoint(&self.endpoint.endpoint())?;
self.endpoint.add_parameters(url.query_pairs_mut());
......@@ -201,13 +227,13 @@ where
next_url = next_page_from_headers(rsp.headers())?;
}
let v = serde_json::from_reader(rsp).map_err(GitlabError::json)?;
let v = serde_json::from_reader(rsp)?;
if !status.is_success() {
return Err(GitlabError::from_gitlab(v));
return Err(ApiError::from_gitlab(v));
}
let page =
serde_json::from_value::<Vec<T>>(v).map_err(GitlabError::data_type::<Vec<T>>)?;
serde_json::from_value::<Vec<T>>(v).map_err(ApiError::data_type::<Vec<T>>)?;
let page_len = page.len();
results.extend(page);
......
......@@ -4,11 +4,13 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use crate::api::Client;
use crate::gitlab::GitlabError;
use crate::api::{ApiError, Client};
/// A trait which represents a query which may be made to a GitLab client.
pub trait Query<T> {
pub trait Query<T, C>
where
C: Client,
{
/// Perform the query against the client.
fn query(&self, client: &dyn Client) -> Result<T, GitlabError>;
fn query(&self, client: &C) -> Result<T, ApiError<C::Error>>;
}
......@@ -46,27 +46,6 @@ const PATH_SEGMENT_ENCODE_SET: &AsciiSet = &CONTROLS
.add(b'%')
.add(b'/');
#[derive(Debug, Error)]
// TODO #[non_exhaustive]
pub enum PaginationError {
#[error("failed to parse a Link HTTP header: {}", source)]
LinkHeader {
#[from]
source: api::LinkHeaderParseError,
},
#[error("failed to parse a Link HTTP header URL: {}", source)]
InvalidUrl {
#[from]
source: url::ParseError,
},
/// This is here to force `_` matching right now.
///
/// **DO NOT USE**
#[doc(hidden)]
#[error("unreachable...")]
_NonExhaustive,
}
#[derive(Debug, Error)]
// TODO #[non_exhaustive]
pub enum GitlabError {
......@@ -108,10 +87,10 @@ pub enum GitlabError {
source: serde_json::Error,
typename: &'static str,
},
#[error("failed to handle for pagination: {}", source)]
Pagination {
#[error("api error: {}", source)]
Api {
#[from]
source: PaginationError,
source: api::ApiError<RestError>,
},
/// This is here to force `_` matching right now.
///
......@@ -371,7 +350,7 @@ impl Gitlab {
note = "use `gitlab::api::users::CurrentUser.query()` instead"
)]
pub fn current_user(&self) -> GitlabResult<UserPublic> {
CurrentUser::builder().build().unwrap().query(self)
Ok(CurrentUser::builder().build().unwrap().query(self)?)
}
/// Get all user accounts
......@@ -403,11 +382,11 @@ impl Gitlab {
K: AsRef<str>,
V: AsRef<str>,
{
User::builder()
Ok(User::builder()
.user(user.value())
.build()
.unwrap()
.query(self)
.query(self)?)
}
/// Find a user by username.
......@@ -548,11 +527,11 @@ impl Gitlab {
note = "use `gitlab::api::projects::Projects.query()` instead"
)]
pub fn owned_projects(&self) -> GitlabResult<Vec<Project>> {
api::paged(
Ok(api::paged(
Projects::builder().owned(true).build().unwrap(),
api::Pagination::All,
)
.query(self)
.query(self)?)
}
/// Find a project by id.
......@@ -1539,12 +1518,12 @@ impl Gitlab {
note = "use `gitlab::api::projects::pipeline::Pipeline.query()` instead"
)]
pub fn pipeline(&self, project: ProjectId, id: PipelineId) -> GitlabResult<Pipeline> {
pipelines::Pipeline::builder()
Ok(pipelines::Pipeline::builder()
.project(project.value())
.pipeline(id.value())
.build()
.unwrap()
.query(self)
.query(self)?)
}
/// Get variables of a pipeline.
......@@ -1571,7 +1550,7 @@ impl Gitlab {
ref_: ObjectId,
variables: &[PipelineVariable],
) -> GitlabResult<Pipeline> {
pipelines::CreatePipeline::builder()
Ok(pipelines::CreatePipeline::builder()
.project(project.value())
.ref_(ref_.value().as_str())
.variables(variables.iter().map(|variable| {
......@@ -1587,7 +1566,7 @@ impl Gitlab {
}))
.build()
.unwrap()
.query(self)
.query(self)?)
}
/// Retry jobs in a pipeline.
......@@ -1596,12 +1575,12 @@ impl Gitlab {
note = "use `gitlab::api::projects::pipelines::RetryPipeline.query()` instead"
)]
pub fn retry_pipeline(&self, project: ProjectId, id: PipelineId) -> GitlabResult<Pipeline> {
pipelines::RetryPipeline::builder()
Ok(pipelines::RetryPipeline::builder()
.project(project.value())
.pipeline(id.value())
.build()
.unwrap()
.query(self)
.query(self)?)
}
/// Cancel a pipeline.
......@@ -1610,12 +1589,12 @@ impl Gitlab {
note = "use `gitlab::api::projects::pipelines::CancelPipeline.query()` instead"
)]
pub fn cancel_pipeline(&self, project: ProjectId, id: PipelineId) -> GitlabResult<Pipeline> {
pipelines::CancelPipeline::builder()
Ok(pipelines::CancelPipeline::builder()
.project(project.value())
.pipeline(id.value())
.build()
.unwrap()
.query(self)
.query(self)?)
}
/// Get a list of jobs for a pipeline.
......@@ -2271,8 +2250,31 @@ impl Gitlab {
}
}
#[derive(Debug, Error)]
// TODO #[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,
},
/// This is here to force `_` matching right now.
///
/// **DO NOT USE**
#[doc(hidden)]
#[error("unreachable...")]
_NonExhaustive,
}
impl api::Client for Gitlab {
fn rest_endpoint(&self, endpoint: &str) -> GitlabResult<Url> {
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)?)
}
......@@ -2281,8 +2283,14 @@ impl api::Client for Gitlab {
self.client.request(method, url)
}
fn rest(&self, request: RequestBuilder) -> GitlabResult<HttpResponse> {
Ok(self.auth.set_header(request)?.send()?)
fn rest(&self, request: RequestBuilder) -> Result<HttpResponse, api::ApiError<Self::Error>> {
self.auth
.set_header(request)
.map_err(RestError::from)
.map_err(api::ApiError::client)?
.send()
.map_err(RestError::from)
.map_err(api::ApiError::client)
}
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment