Skip to main content

mz_cloud_api/
client.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! # Materialize cloud API client
11//!
12//! This module provides an API client with typed methods for
13//! interacting with the Materialize cloud API. This client includes,
14//! token management, and basic requests against the API.
15//!
16//! The [`Client`] requires an [`mz_frontegg_client::client::Client`] as a parameter. The
17//! Frontegg client is used to request and manage the access token.
18use std::sync::Arc;
19
20use reqwest::{Method, RequestBuilder, StatusCode, Url};
21use serde::Deserialize;
22use serde::de::DeserializeOwned;
23
24use crate::error::{ApiError, Error};
25
26/// Represents the structure for the client.
27pub struct Client {
28    pub(crate) inner: reqwest::Client,
29    pub(crate) auth_client: Arc<mz_frontegg_client::client::Client>,
30    pub(crate) endpoint: Url,
31}
32
33pub mod cloud_provider;
34pub mod region;
35
36impl Client {
37    /// Builds a request towards the `Client`'s endpoint
38    async fn build_global_request<P>(
39        &self,
40        method: Method,
41        path: P,
42        query: Option<&[(&str, &str)]>,
43    ) -> Result<RequestBuilder, Error>
44    where
45        P: IntoIterator,
46        P::Item: AsRef<str>,
47    {
48        let mut domain = self.endpoint.clone();
49        domain
50            .path_segments_mut()
51            .or(Err(Error::UrlBaseError))?
52            .clear()
53            .extend(path);
54
55        let mut req = self.inner.request(method, domain);
56        if let Some(query_params) = query {
57            req = req.query(&query_params);
58        }
59        let token = self.auth_client.auth().await?;
60
61        Ok(req.bearer_auth(token))
62    }
63
64    async fn send_request<T>(&self, req: RequestBuilder) -> Result<T, Error>
65    where
66        T: DeserializeOwned,
67    {
68        #[derive(Deserialize)]
69        #[serde(rename_all = "camelCase")]
70        struct ErrorResponse {
71            #[serde(default)]
72            message: Option<String>,
73            #[serde(default)]
74            errors: Vec<String>,
75        }
76
77        let res = req.send().await?;
78        let status_code = res.status();
79        if status_code.is_success() {
80            if status_code == StatusCode::NO_CONTENT {
81                Err(Error::SuccesfullButNoContent)
82            } else {
83                Ok(res.json().await?)
84            }
85        } else {
86            match res.json::<ErrorResponse>().await {
87                Ok(e) => {
88                    let mut messages = e.errors;
89                    messages.extend(e.message);
90                    Err(Error::Api(ApiError {
91                        status_code,
92                        messages,
93                    }))
94                }
95                Err(_) => Err(Error::Api(ApiError {
96                    status_code,
97                    messages: vec!["unable to decode error details".into()],
98                })),
99            }
100        }
101    }
102}