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
// 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.

//! This module implements Materialize cloud API sync endpoint to
//! list all the availables cloud providers and regions, represented
//! by [`CloudProvider`].
//! As utilities, the module implements the enum [`CloudProviderRegion`].
//! It is useful to validate and compare external input (e.g. user input)
//! with the results from the cloud API represented by [`CloudProvider`].
//!
//! Example:
//! ```ignore
//! // List all the available providers
//! let cloud_providers = client.list_cloud_regions().await?;
//!
//! // Search the cloud provider
//! let user_input = "aws/us-east-1";
//! let cloud_provider_region_selected: CloudProviderRegion = CloudProviderRegion::from_str(user_input)?;
//! let cloud_provider: CloudProvider = cloud_providers
//!    .iter()
//!    .find(|cp| cp.as_cloud_provider_region().unwrap() == cloud_provider_region_selected)
//!    .unwrap()
//!    .to_owned();
//! ```

use std::fmt::Display;
use std::str::FromStr;

use reqwest::Method;
use serde::{Deserialize, Serialize};
use url::Url;

use crate::client::Client;
use crate::error::Error;

/// Holds all the information related to a cloud provider and a particular region.
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct CloudProvider {
    /// Contains the concatenation between cloud provider name and region:
    ///
    /// E.g.: `aws/us-east-1` or `aws/eu-west-1`
    pub id: String,
    /// Contains the region name:
    ///
    /// E.g.: `us-east-1` or `eu-west-1`
    pub name: String,
    /// Contains the complete Region API url.
    ///
    /// E..g: `https://api.eu-west-1.aws.cloud.materialize.com`
    pub url: Url,
    /// Contains the cloud provider name.
    ///
    /// E.g.: `aws` or `gcp`
    pub cloud_provider: String,
}

impl CloudProvider {
    /// Converts the CloudProvider object to a CloudProviderRegion.
    pub fn as_cloud_provider_region(&self) -> Result<CloudProviderRegion, Error> {
        match self.id.as_str() {
            "aws/us-east-1" => Ok(CloudProviderRegion::AwsUsEast1),
            "aws/eu-west-1" => Ok(CloudProviderRegion::AwsEuWest1),
            "aws/us-west-2" => Ok(CloudProviderRegion::AwsUsWest2),
            _ => Err(Error::CloudProviderRegionParseError),
        }
    }
}

/// Represents a cloud provider and a region.
/// Useful to transform a user input selecting a
/// cloud provider region to an enum and vice-versa.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub enum CloudProviderRegion {
    /// Represents `aws/us-east-1` cloud provider and region
    #[serde(rename = "aws/us-east-1")]
    AwsUsEast1,
    /// Represents `aws/eu-west-1` cloud provider and region
    #[serde(rename = "aws/eu-west-1")]
    AwsEuWest1,
    /// Represents `aws/us-west-2` cloud provider and region
    #[serde(rename = "aws/us-west-2")]
    AwsUsWest2,
}

impl CloudProviderRegion {
    /// Converts a CloudProvider object to a CloudProviderRegion.
    pub fn from_cloud_provider(cloud_provider: CloudProvider) -> Result<Self, Error> {
        match cloud_provider.id.as_str() {
            "aws/us-east-1" => Ok(CloudProviderRegion::AwsUsEast1),
            "aws/eu-west-1" => Ok(CloudProviderRegion::AwsEuWest1),
            "aws/us-west-2" => Ok(CloudProviderRegion::AwsUsWest2),
            _ => Err(Error::CloudProviderRegionParseError),
        }
    }
}

impl Display for CloudProviderRegion {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            CloudProviderRegion::AwsUsEast1 => write!(f, "aws/us-east-1"),
            CloudProviderRegion::AwsEuWest1 => write!(f, "aws/eu-west-1"),
            CloudProviderRegion::AwsUsWest2 => write!(f, "aws/us-west-2"),
        }
    }
}

impl FromStr for CloudProviderRegion {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "aws/us-east-1" => Ok(CloudProviderRegion::AwsUsEast1),
            "aws/eu-west-1" => Ok(CloudProviderRegion::AwsEuWest1),
            "aws/us-west-2" => Ok(CloudProviderRegion::AwsUsWest2),
            _ => Err(Error::CloudProviderRegionParseError),
        }
    }
}

impl Client {
    /// List all the available cloud regions.
    ///
    /// E.g.: [us-east-1, eu-west-1]
    pub async fn list_cloud_regions(&self) -> Result<Vec<CloudProvider>, Error> {
        #[derive(Deserialize)]
        #[serde(rename_all = "camelCase")]
        struct CloudProviderResponse {
            data: Vec<CloudProvider>,
            #[serde(skip_serializing_if = "Option::is_none")]
            next_cursor: Option<String>,
        }
        let mut cloud_providers: Vec<CloudProvider> = vec![];
        let mut cursor: String = String::new();

        loop {
            let req = self
                .build_global_request(Method::GET, ["api", "cloud-regions"])
                .await?;

            let req = req.query(&[("limit", "50"), ("cursor", &cursor)]);

            let response: CloudProviderResponse = self.send_request(req).await?;
            cloud_providers.extend(response.data);

            if let Some(next_cursor) = response.next_cursor {
                cursor = next_cursor;
            } else {
                break;
            }
        }

        Ok(cloud_providers)
    }
}