mz_cloud_api/client/
cloud_provider.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//! This module implements Materialize cloud API sync endpoint to
11//! list all the availables cloud providers and regions, represented
12//! by [`CloudProvider`].
13//! As utilities, the module implements the enum [`CloudProviderRegion`].
14//! It is useful to validate and compare external input (e.g. user input)
15//! with the results from the cloud API represented by [`CloudProvider`].
16//!
17//! Example:
18//! ```ignore
19//! // List all the available providers
20//! let cloud_providers = client.list_cloud_regions().await?;
21//!
22//! // Search the cloud provider
23//! let user_input = "aws/us-east-1";
24//! let cloud_provider_region_selected: CloudProviderRegion = CloudProviderRegion::from_str(user_input)?;
25//! let cloud_provider: CloudProvider = cloud_providers
26//!    .iter()
27//!    .find(|cp| cp.as_cloud_provider_region().unwrap() == cloud_provider_region_selected)
28//!    .unwrap()
29//!    .to_owned();
30//! ```
31
32use std::fmt::Display;
33use std::str::FromStr;
34
35use reqwest::Method;
36use serde::{Deserialize, Serialize};
37use url::Url;
38
39use crate::client::Client;
40use crate::error::Error;
41
42/// Holds all the information related to a cloud provider and a particular region.
43#[derive(Debug, Deserialize, Clone)]
44#[serde(rename_all = "camelCase")]
45pub struct CloudProvider {
46    /// Contains the concatenation between cloud provider name and region:
47    ///
48    /// E.g.: `aws/us-east-1` or `aws/eu-west-1`
49    pub id: String,
50    /// Contains the region name:
51    ///
52    /// E.g.: `us-east-1` or `eu-west-1`
53    pub name: String,
54    /// Contains the complete Region API url.
55    ///
56    /// E..g: `https://api.eu-west-1.aws.cloud.materialize.com`
57    pub url: Url,
58    /// Contains the cloud provider name.
59    ///
60    /// E.g.: `aws` or `gcp`
61    pub cloud_provider: String,
62}
63
64impl CloudProvider {
65    /// Converts the CloudProvider object to a CloudProviderRegion.
66    pub fn as_cloud_provider_region(&self) -> Result<CloudProviderRegion, Error> {
67        match self.id.as_str() {
68            "aws/us-east-1" => Ok(CloudProviderRegion::AwsUsEast1),
69            "aws/eu-west-1" => Ok(CloudProviderRegion::AwsEuWest1),
70            "aws/us-west-2" => Ok(CloudProviderRegion::AwsUsWest2),
71            _ => Err(Error::CloudProviderRegionParseError),
72        }
73    }
74}
75
76/// Represents a cloud provider and a region.
77/// Useful to transform a user input selecting a
78/// cloud provider region to an enum and vice-versa.
79#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
80pub enum CloudProviderRegion {
81    /// Represents `aws/us-east-1` cloud provider and region
82    #[serde(rename = "aws/us-east-1")]
83    AwsUsEast1,
84    /// Represents `aws/eu-west-1` cloud provider and region
85    #[serde(rename = "aws/eu-west-1")]
86    AwsEuWest1,
87    /// Represents `aws/us-west-2` cloud provider and region
88    #[serde(rename = "aws/us-west-2")]
89    AwsUsWest2,
90}
91
92impl CloudProviderRegion {
93    /// Converts a CloudProvider object to a CloudProviderRegion.
94    pub fn from_cloud_provider(cloud_provider: CloudProvider) -> Result<Self, Error> {
95        match cloud_provider.id.as_str() {
96            "aws/us-east-1" => Ok(CloudProviderRegion::AwsUsEast1),
97            "aws/eu-west-1" => Ok(CloudProviderRegion::AwsEuWest1),
98            "aws/us-west-2" => Ok(CloudProviderRegion::AwsUsWest2),
99            _ => Err(Error::CloudProviderRegionParseError),
100        }
101    }
102}
103
104impl Display for CloudProviderRegion {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        match self {
107            CloudProviderRegion::AwsUsEast1 => write!(f, "aws/us-east-1"),
108            CloudProviderRegion::AwsEuWest1 => write!(f, "aws/eu-west-1"),
109            CloudProviderRegion::AwsUsWest2 => write!(f, "aws/us-west-2"),
110        }
111    }
112}
113
114impl FromStr for CloudProviderRegion {
115    type Err = Error;
116
117    fn from_str(s: &str) -> Result<Self, Self::Err> {
118        match s.to_lowercase().as_str() {
119            "aws/us-east-1" => Ok(CloudProviderRegion::AwsUsEast1),
120            "aws/eu-west-1" => Ok(CloudProviderRegion::AwsEuWest1),
121            "aws/us-west-2" => Ok(CloudProviderRegion::AwsUsWest2),
122            _ => Err(Error::CloudProviderRegionParseError),
123        }
124    }
125}
126
127impl Client {
128    /// List all the available cloud regions.
129    ///
130    /// E.g.: [us-east-1, eu-west-1]
131    pub async fn list_cloud_regions(&self) -> Result<Vec<CloudProvider>, Error> {
132        #[derive(Deserialize)]
133        #[serde(rename_all = "camelCase")]
134        struct CloudProviderResponse {
135            data: Vec<CloudProvider>,
136            #[serde(skip_serializing_if = "Option::is_none")]
137            next_cursor: Option<String>,
138        }
139        let mut cloud_providers: Vec<CloudProvider> = vec![];
140        let mut cursor: String = String::new();
141
142        loop {
143            let req = self
144                .build_global_request(Method::GET, ["api", "cloud-regions"], None)
145                .await?;
146
147            let req = req.query(&[("limit", "50"), ("cursor", &cursor)]);
148
149            let response: CloudProviderResponse = self.send_request(req).await?;
150            cloud_providers.extend(response.data);
151
152            if let Some(next_cursor) = response.next_cursor {
153                cursor = next_cursor;
154            } else {
155                break;
156            }
157        }
158
159        Ok(cloud_providers)
160    }
161}