consulrs/
api.rs

1use std::str::FromStr;
2
3use crate::api::features::FeaturedEndpoint;
4use crate::client::Client;
5use crate::error::ClientError;
6use derive_builder::Builder;
7use rustify::endpoint::{Endpoint, EndpointResult, MiddleWare};
8use rustify::errors::ClientError as RestClientError;
9use serde::de::DeserializeOwned;
10
11pub use crate::api::features::Features;
12
13pub mod catalog;
14pub mod check;
15pub mod connect;
16pub mod features;
17pub mod kv;
18pub mod service;
19pub mod session;
20
21#[derive(Builder, Debug)]
22#[builder(pattern = "owned")]
23pub struct ApiResponse<T> {
24    #[builder(setter(into, strip_option), default)]
25    pub cache: Option<String>,
26    #[builder(setter(into, strip_option), default)]
27    pub content_hash: Option<String>,
28    #[builder(setter(into, strip_option), default)]
29    pub default_acl_policy: Option<String>,
30    #[builder(setter(into, strip_option), default)]
31    pub index: Option<String>,
32    #[builder(setter(into, strip_option), default)]
33    pub known_leader: Option<String>,
34    #[builder(setter(into, strip_option), default)]
35    pub last_contact: Option<String>,
36    #[builder(setter(into, strip_option), default)]
37    pub query_backend: Option<String>,
38    pub response: T,
39}
40
41impl<T> ApiResponse<T> {
42    pub fn builder() -> ApiResponseBuilder<T> {
43        ApiResponseBuilder::default()
44    }
45}
46
47/// A [MiddleWare] for adding version and token information to all requests.
48///
49/// Implements [MiddleWare] to provide support for prepending API version
50/// information to all requests and adding an ACL token to the header of all
51/// requests. Additionally, any API features specified in the endpoint are
52/// appended to the request. This is passed by the API functions when an
53/// endpoint is executed.
54#[derive(Debug, Clone)]
55pub struct EndpointMiddleware {
56    pub features: Option<Features>,
57    pub token: Option<String>,
58    pub version: String,
59}
60impl MiddleWare for EndpointMiddleware {
61    #[instrument(skip(self, req), err)]
62    fn request<E: Endpoint>(
63        &self,
64        _: &E,
65        req: &mut http::Request<Vec<u8>>,
66    ) -> Result<(), rustify::errors::ClientError> {
67        // Prepend API version to all requests
68        debug!(
69            "Middleware: prepending {} version to URL",
70            self.version.as_str()
71        );
72        let url = url::Url::parse(req.uri().to_string().as_str()).unwrap();
73        let mut url_c = url.clone();
74        let mut segs: Vec<&str> = url.path_segments().unwrap().collect();
75        segs.insert(0, self.version.as_str());
76        url_c.path_segments_mut().unwrap().clear().extend(segs);
77        *req.uri_mut() = http::Uri::from_str(url_c.as_str()).unwrap();
78        debug!("Middleware: final URL is {}", url_c.as_str());
79
80        // Add ACL token to header if present
81        if let Some(token) = &self.token {
82            debug!("Middleware: adding ACL token to header");
83            req.headers_mut().append(
84                "X-Consul-Token",
85                http::HeaderValue::from_str(token).unwrap(),
86            );
87        }
88
89        // Add optional API features
90        if let Some(f) = &self.features {
91            f.process(req);
92        }
93
94        Ok(())
95    }
96
97    fn response<E: Endpoint>(
98        &self,
99        _: &E,
100        _: &mut http::Response<Vec<u8>>,
101    ) -> Result<(), rustify::errors::ClientError> {
102        Ok(())
103    }
104}
105
106/// Executes an [Endpoint] and returns the raw response body.
107///
108/// Any errors which occur in execution are wrapped in a
109/// [ClientError::RestClientError] and propagated.
110pub async fn exec_with_empty<E>(
111    client: &impl Client,
112    endpoint: E,
113) -> Result<ApiResponse<()>, ClientError>
114where
115    E: Endpoint<Response = ()> + FeaturedEndpoint,
116{
117    info!("Executing {} and expecting no response", endpoint.path());
118    let features = endpoint.features();
119    endpoint
120        .with_middleware(&client.middle(features))
121        .exec(client.http())
122        .await
123        .map_err(parse_err)
124        .map(parse_empty)?
125}
126
127/// Executes an [Endpoint] and returns the raw response body.
128///
129/// Any errors which occur in execution are wrapped in a
130/// [ClientError::RestClientError] and propagated.
131pub async fn exec_with_raw<E>(
132    client: &impl Client,
133    endpoint: E,
134) -> Result<ApiResponse<Vec<u8>>, ClientError>
135where
136    E: Endpoint + FeaturedEndpoint,
137{
138    info!("Executing {} and expecting a response", endpoint.path());
139    let features = endpoint.features();
140    endpoint
141        .with_middleware(&client.middle(features))
142        .exec(client.http())
143        .await
144        .map_err(parse_err)
145        .map(parse_raw)?
146}
147
148/// Executes an [Endpoint] and returns the result.
149///
150/// The result from the executed endpoint has a few operations performed on it:
151///
152/// * Any potential API error responses from the execution are searched for and,
153///   if found, converted to a [ClientError::APIError]
154/// * All other errors are mapped from [rustify::errors::ClientError] to
155///   [ClientError::RestClientError]
156pub async fn exec_with_result<E>(
157    client: &impl Client,
158    endpoint: E,
159) -> Result<ApiResponse<E::Response>, ClientError>
160where
161    E: Endpoint + FeaturedEndpoint,
162{
163    info!("Executing {} and expecting a response", endpoint.path());
164    let features = endpoint.features();
165    endpoint
166        .with_middleware(&client.middle(features))
167        .exec(client.http())
168        .await
169        .map_err(parse_err)
170        .map(parse)?
171}
172
173/// Parses an [EndpointResult], turning it into an [ApiResponse].
174fn parse<T>(result: EndpointResult<T>) -> Result<ApiResponse<T>, ClientError>
175where
176    T: DeserializeOwned + Send + Sync,
177{
178    let mut builder = parse_headers(result.response.headers());
179
180    let response = result.parse().map_err(ClientError::from)?;
181    builder = builder.response(response);
182    Ok(builder.build().unwrap())
183}
184
185/// Parses an [EndpointResult], turning it into an [ApiResponse].
186fn parse_empty(result: EndpointResult<()>) -> Result<ApiResponse<()>, ClientError> {
187    let mut builder = parse_headers(result.response.headers());
188
189    builder = builder.response(());
190    Ok(builder.build().unwrap())
191}
192
193/// Parses an [EndpointResult], turning it into an [ApiResponse].
194fn parse_raw<T>(result: EndpointResult<T>) -> Result<ApiResponse<Vec<u8>>, ClientError>
195where
196    T: DeserializeOwned + Send + Sync,
197{
198    let mut builder = parse_headers(result.response.headers());
199
200    let response = result.raw();
201    builder = builder.response(response);
202    Ok(builder.build().unwrap())
203}
204
205/// Parses commonly found header fields out of response headers.
206fn parse_headers<T>(headers: &http::HeaderMap) -> ApiResponseBuilder<T>
207where
208    T: DeserializeOwned + Send + Sync,
209{
210    let mut builder = ApiResponse::builder();
211
212    if headers.contains_key("X-Cache") {
213        builder = builder.cache(headers["X-Cache"].to_str().unwrap());
214    }
215    if headers.contains_key("X-Consul-ContentHash") {
216        builder = builder.content_hash(headers["X-Consul-ContentHash"].to_str().unwrap())
217    }
218    if headers.contains_key("X-Consul-Default-ACL-Policy") {
219        builder =
220            builder.default_acl_policy(headers["X-Consul-Default-ACL-Policy"].to_str().unwrap())
221    }
222    if headers.contains_key("X-Consul-Index") {
223        builder = builder.index(headers["X-Consul-Index"].to_str().unwrap())
224    }
225    if headers.contains_key("X-Consul-KnownLeader") {
226        builder = builder.known_leader(headers["X-Consul-KnownLeader"].to_str().unwrap())
227    }
228    if headers.contains_key("X-Consul-LastContact") {
229        builder = builder.last_contact(headers["X-Consul-LastContact"].to_str().unwrap())
230    }
231    if headers.contains_key("X-Consul-Query-Backend") {
232        builder = builder.query_backend(headers["X-Consul-Query-Backend"].to_str().unwrap())
233    }
234
235    builder
236}
237
238/// Extracts any API errors found and converts them to [ClientError::APIError].
239fn parse_err(e: RestClientError) -> ClientError {
240    if let RestClientError::ServerResponseError { code, content } = &e {
241        ClientError::APIError {
242            code: *code,
243            message: content.clone(),
244        }
245    } else {
246        ClientError::from(e)
247    }
248}