1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
// Copyright Materialize, Inc. and contributors. All rights reserved.
//
// Use of this software is governed by the Business Source License
// included in the LICENSE file.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0.
//! # Materialize cloud API client
//!
//! This module provides an API client with typed methods for
//! interacting with the Materialize cloud API. This client includes,
//! token management, and basic requests against the API.
//!
//! The [`Client`] requires an [`mz_frontegg_client::client::Client`] as a parameter. The
//! Frontegg client is used to request and manage the access token.
use std::sync::Arc;
use reqwest::{header::HeaderMap, Method, RequestBuilder, StatusCode, Url};
use serde::de::DeserializeOwned;
use serde::Deserialize;
use crate::config::API_VERSION_HEADER;
use crate::error::{ApiError, Error};
use self::cloud_provider::CloudProvider;
/// Represents the structure for the client.
pub struct Client {
pub(crate) inner: reqwest::Client,
pub(crate) auth_client: Arc<mz_frontegg_client::client::Client>,
pub(crate) endpoint: Url,
}
pub mod cloud_provider;
pub mod region;
/// Cloud endpoints architecture:
///
/// (CloudProvider) (Region)
/// --------- --------------------------------------
/// | | | Region API |
/// | Cloud | url | ---------- ------------- |
/// | Sync | ---------------> | | Provider | ---- | Region | |
/// | | | | (aws..) | | (east-1..) | |
/// | | | ---------- ------------- |
/// ---------- --------------------------------------
///
impl Client {
/// Builds a request towards the `Client`'s endpoint
async fn build_global_request<P>(
&self,
method: Method,
path: P,
query: Option<&[(&str, &str)]>,
) -> Result<RequestBuilder, Error>
where
P: IntoIterator,
P::Item: AsRef<str>,
{
self.build_request(method, path, query, self.endpoint.clone(), None)
.await
}
/// Builds a request towards the `Client`'s endpoint
/// The function requires a [CloudProvider] as parameter
/// since it contains the api url (Region API url)
/// to interact with the region.
/// Specify an api_version corresponding to the request/response
/// schema your code will handle. Refer to the Region API docs
/// for schema information.
async fn build_region_request<P>(
&self,
method: Method,
path: P,
query: Option<&[(&str, &str)]>,
cloud_provider: &CloudProvider,
api_version: Option<u16>,
) -> Result<RequestBuilder, Error>
where
P: IntoIterator,
P::Item: AsRef<str>,
{
self.build_request(
method,
path,
query,
cloud_provider.url.clone(),
api_version.and_then(|api_ver| {
let mut headers = HeaderMap::with_capacity(1);
headers.insert(API_VERSION_HEADER, api_ver.into());
Some(headers)
}),
)
.await
}
/// Builds a request towards the `Client`'s endpoint
async fn build_request<P>(
&self,
method: Method,
path: P,
query: Option<&[(&str, &str)]>,
mut domain: Url,
headers: Option<HeaderMap>,
) -> Result<RequestBuilder, Error>
where
P: IntoIterator,
P::Item: AsRef<str>,
{
domain
.path_segments_mut()
.or(Err(Error::UrlBaseError))?
.clear()
.extend(path);
let mut req = self.inner.request(method, domain);
if let Some(header_map) = headers {
req = req.headers(header_map);
}
if let Some(query_params) = query {
req = req.query(&query_params);
}
let token = self.auth_client.auth().await?;
Ok(req.bearer_auth(token))
}
async fn send_request<T>(&self, req: RequestBuilder) -> Result<T, Error>
where
T: DeserializeOwned,
{
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ErrorResponse {
#[serde(default)]
message: Option<String>,
#[serde(default)]
errors: Vec<String>,
}
let res = req.send().await?;
let status_code = res.status();
if status_code.is_success() {
if status_code == StatusCode::NO_CONTENT {
Err(Error::SuccesfullButNoContent)
} else {
Ok(res.json().await?)
}
} else {
match res.json::<ErrorResponse>().await {
Ok(e) => {
let mut messages = e.errors;
messages.extend(e.message);
Err(Error::Api(ApiError {
status_code,
messages,
}))
}
Err(_) => Err(Error::Api(ApiError {
status_code,
messages: vec!["unable to decode error details".into()],
})),
}
}
}
}