pivotal_tracker/
client.rs

1use core::fmt;
2use reqwest::header::HeaderMap;
3use serde::{de::DeserializeOwned, Deserialize, Serialize};
4use std::{error::Error, fmt::Display};
5use url::ParseError;
6
7#[derive(Debug)]
8pub struct Client {
9	pub api_version: u8,
10	pub token: String,
11}
12
13impl Client {
14	pub fn new(options: ClientNewOptions) -> Self {
15		Client {
16			api_version: options.api_version,
17			token: options.token,
18		}
19	}
20
21	pub async fn request<TSuccess, TGetBuilder>(
22		&self,
23		func: TGetBuilder,
24	) -> Result<TSuccess, RequestError>
25	where
26		TSuccess: DeserializeOwned,
27		TGetBuilder: Fn(&reqwest::Client, String) -> reqwest::RequestBuilder,
28	{
29		let mut headers = HeaderMap::new();
30
31		headers.append("X-TrackerToken", self.token.parse().unwrap());
32
33		let client = reqwest::Client::builder()
34			.default_headers(headers)
35			.build()?;
36		let base_url = format!(
37			"https://www.pivotaltracker.com/services/v{}",
38			self.api_version
39		);
40		let res = func(&client, base_url).send().await?;
41
42		if res.status().as_u16() >= 400 {
43			let result = res.json::<ResponseError>().await?;
44
45			return Err(RequestError::Response(result));
46		}
47
48		let result = res.json::<TSuccess>().await?;
49
50		Ok(result)
51	}
52}
53
54#[derive(Debug)]
55pub struct ClientNewOptions {
56	pub token: String,
57	pub api_version: u8,
58}
59
60#[derive(Debug)]
61pub enum RequestError {
62	Reqwest(reqwest::Error),
63	Parse(ParseError),
64	Response(ResponseError),
65}
66
67impl Display for RequestError {
68	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69		write!(f, "RequestError")
70	}
71}
72
73impl Error for RequestError {}
74
75impl From<reqwest::Error> for RequestError {
76	fn from(err: reqwest::Error) -> Self {
77		Self::Reqwest(err)
78	}
79}
80
81impl From<ParseError> for RequestError {
82	fn from(err: ParseError) -> Self {
83		Self::Parse(err)
84	}
85}
86
87impl From<ResponseError> for RequestError {
88	fn from(err: ResponseError) -> Self {
89		Self::Response(err)
90	}
91}
92
93#[derive(Debug, Serialize, Deserialize)]
94pub struct ResponseError {
95	/// Class of error that has occurred or been detected.
96	pub code: ResponseErrorCode,
97	pub error: String,
98
99	/// An English string with a broader/alternative description of the problem.
100	pub general_problem: Option<String>,
101	pub kind: String,
102
103	/// An English string with hints to the API client developer. Consider this
104	/// a micro-FAQ answer specific to the particular error case. Very unlikely
105	/// to be suitable for direct presentation to a user.
106	pub possible_fix: Option<String>,
107
108	/// An English string describing the constraint on the API that wasn't met
109	/// by the current request.
110	pub requirement: Option<String>,
111
112	/// In the case where the server detected one or more errors with regard to the value of specific individual parameters, rather than with the request as a whole, the error response hash may be augmented with parameter-specific error messages. These messages are packaged in an array of hashes, stored under the validation_errors key. Each validation error hash contains a field key with the name of a request parameter (resource attribute), and a problem key with an English string describing the error(s) detected in the value of that parameter.
113	pub validation_errors: Option<Vec<ValidationError>>,
114}
115
116#[derive(Debug, Serialize, Deserialize)]
117#[serde(rename_all = "snake_case")]
118pub enum ResponseErrorCode {
119	#[serde(rename = "api_external_error")]
120	APIExternalError,
121
122	#[serde(rename = "api_internal_error")]
123	APIInternalError,
124	CantParseJSON,
125	CapabilityAccessDenied,
126	ComponentUnavailable,
127	ContentFormatNotFound,
128	CouldNotParseBody,
129	Deadlock,
130	DuplicateEntry,
131	EnterpriseSignupError,
132
133	#[serde(rename = "http_method_not_supported")]
134	HTTPMethodNotSupported,
135
136	#[serde(rename = "https_required")]
137	HTTPSRequired,
138	IntegrationError,
139	InvalidAuthentication,
140	InvalidParameter,
141	InvalidUpload,
142	NotAcceptable,
143	RequestEntityTooLarge,
144	RequiresGet,
145	RequiresPost,
146	RouteNotFound,
147	ServerUnderLoad,
148	Timeout,
149	Unauthenticated,
150	UnauthorizedOperation,
151	UnfoundResource,
152	UnhandledCondition,
153
154	#[serde(rename = "xhr_required")]
155	XHRRequired,
156}
157
158#[derive(Debug, Serialize, Deserialize)]
159pub struct ValidationError {
160	/// Name of request parameter
161	pub field: String,
162
163	/// The error(s) detected in the field
164	pub problem: String,
165}