1use std::fmt;
9use std::str::FromStr;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum ProviderKind {
13 Aws,
14 Azure,
15 DigitalOcean,
16 Gcp,
17 Hetzner,
18 I3d,
19 Leaseweb,
20 Linode,
21 Oracle,
22 Ovh,
23 Proxmox,
24 Scaleway,
25 Tailscale,
26 Transip,
27 UpCloud,
28 Vultr,
29}
30
31impl ProviderKind {
32 pub fn as_str(self) -> &'static str {
33 match self {
34 ProviderKind::Aws => "aws",
35 ProviderKind::Azure => "azure",
36 ProviderKind::DigitalOcean => "digitalocean",
37 ProviderKind::Gcp => "gcp",
38 ProviderKind::Hetzner => "hetzner",
39 ProviderKind::I3d => "i3d",
40 ProviderKind::Leaseweb => "leaseweb",
41 ProviderKind::Linode => "linode",
42 ProviderKind::Oracle => "oracle",
43 ProviderKind::Ovh => "ovh",
44 ProviderKind::Proxmox => "proxmox",
45 ProviderKind::Scaleway => "scaleway",
46 ProviderKind::Tailscale => "tailscale",
47 ProviderKind::Transip => "transip",
48 ProviderKind::UpCloud => "upcloud",
49 ProviderKind::Vultr => "vultr",
50 }
51 }
52
53 pub fn default_auto_sync(self) -> bool {
56 match self {
57 ProviderKind::Proxmox => false,
58 ProviderKind::Aws
59 | ProviderKind::Azure
60 | ProviderKind::DigitalOcean
61 | ProviderKind::Gcp
62 | ProviderKind::Hetzner
63 | ProviderKind::I3d
64 | ProviderKind::Leaseweb
65 | ProviderKind::Linode
66 | ProviderKind::Oracle
67 | ProviderKind::Ovh
68 | ProviderKind::Scaleway
69 | ProviderKind::Tailscale
70 | ProviderKind::Transip
71 | ProviderKind::UpCloud
72 | ProviderKind::Vultr => true,
73 }
74 }
75
76 pub fn alias_prefix(self) -> &'static str {
79 match self {
80 ProviderKind::Aws => "aws",
81 ProviderKind::Azure => "az",
82 ProviderKind::DigitalOcean => "do",
83 ProviderKind::Gcp => "gcp",
84 ProviderKind::Hetzner => "hetzner",
85 ProviderKind::I3d => "i3d",
86 ProviderKind::Leaseweb => "leaseweb",
87 ProviderKind::Linode => "linode",
88 ProviderKind::Oracle => "oci",
89 ProviderKind::Ovh => "ovh",
90 ProviderKind::Proxmox => "pve",
91 ProviderKind::Scaleway => "scw",
92 ProviderKind::Tailscale => "ts",
93 ProviderKind::Transip => "transip",
94 ProviderKind::UpCloud => "uc",
95 ProviderKind::Vultr => "vultr",
96 }
97 }
98
99 pub fn requires_url(self) -> bool {
101 match self {
102 ProviderKind::Proxmox => true,
103 ProviderKind::Aws
104 | ProviderKind::Azure
105 | ProviderKind::DigitalOcean
106 | ProviderKind::Gcp
107 | ProviderKind::Hetzner
108 | ProviderKind::I3d
109 | ProviderKind::Leaseweb
110 | ProviderKind::Linode
111 | ProviderKind::Oracle
112 | ProviderKind::Ovh
113 | ProviderKind::Scaleway
114 | ProviderKind::Tailscale
115 | ProviderKind::Transip
116 | ProviderKind::UpCloud
117 | ProviderKind::Vultr => false,
118 }
119 }
120
121 pub fn accepts_cli_regions(self) -> bool {
124 match self {
125 ProviderKind::Aws
126 | ProviderKind::Azure
127 | ProviderKind::Gcp
128 | ProviderKind::Oracle
129 | ProviderKind::Scaleway => true,
130 ProviderKind::DigitalOcean
131 | ProviderKind::Hetzner
132 | ProviderKind::I3d
133 | ProviderKind::Leaseweb
134 | ProviderKind::Linode
135 | ProviderKind::Ovh
136 | ProviderKind::Proxmox
137 | ProviderKind::Tailscale
138 | ProviderKind::Transip
139 | ProviderKind::UpCloud
140 | ProviderKind::Vultr => false,
141 }
142 }
143
144 pub fn has_regions_field(self) -> bool {
146 match self {
147 ProviderKind::Aws
148 | ProviderKind::Azure
149 | ProviderKind::Gcp
150 | ProviderKind::Oracle
151 | ProviderKind::Ovh
152 | ProviderKind::Scaleway => true,
153 ProviderKind::DigitalOcean
154 | ProviderKind::Hetzner
155 | ProviderKind::I3d
156 | ProviderKind::Leaseweb
157 | ProviderKind::Linode
158 | ProviderKind::Proxmox
159 | ProviderKind::Tailscale
160 | ProviderKind::Transip
161 | ProviderKind::UpCloud
162 | ProviderKind::Vultr => false,
163 }
164 }
165
166 pub fn regions_field_is_mandatory(self) -> bool {
170 match self {
171 ProviderKind::Aws
172 | ProviderKind::Azure
173 | ProviderKind::Ovh
174 | ProviderKind::Scaleway => true,
175 ProviderKind::DigitalOcean
176 | ProviderKind::Gcp
177 | ProviderKind::Hetzner
178 | ProviderKind::I3d
179 | ProviderKind::Leaseweb
180 | ProviderKind::Linode
181 | ProviderKind::Oracle
182 | ProviderKind::Proxmox
183 | ProviderKind::Tailscale
184 | ProviderKind::Transip
185 | ProviderKind::UpCloud
186 | ProviderKind::Vultr => false,
187 }
188 }
189
190 pub fn regions_field_is_picker(self) -> bool {
194 match self {
195 ProviderKind::Aws
196 | ProviderKind::Gcp
197 | ProviderKind::Oracle
198 | ProviderKind::Ovh
199 | ProviderKind::Scaleway => true,
200 ProviderKind::Azure
201 | ProviderKind::DigitalOcean
202 | ProviderKind::Hetzner
203 | ProviderKind::I3d
204 | ProviderKind::Leaseweb
205 | ProviderKind::Linode
206 | ProviderKind::Proxmox
207 | ProviderKind::Tailscale
208 | ProviderKind::Transip
209 | ProviderKind::UpCloud
210 | ProviderKind::Vultr => false,
211 }
212 }
213
214 pub fn has_project_field(self) -> bool {
216 match self {
217 ProviderKind::Gcp | ProviderKind::Ovh => true,
218 ProviderKind::Aws
219 | ProviderKind::Azure
220 | ProviderKind::DigitalOcean
221 | ProviderKind::Hetzner
222 | ProviderKind::I3d
223 | ProviderKind::Leaseweb
224 | ProviderKind::Linode
225 | ProviderKind::Oracle
226 | ProviderKind::Proxmox
227 | ProviderKind::Scaleway
228 | ProviderKind::Tailscale
229 | ProviderKind::Transip
230 | ProviderKind::UpCloud
231 | ProviderKind::Vultr => false,
232 }
233 }
234}
235
236#[derive(Debug, Clone, Copy, PartialEq, Eq)]
238pub struct UnknownProviderKind;
239
240impl fmt::Display for UnknownProviderKind {
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 f.write_str("unknown provider kind")
243 }
244}
245
246impl std::error::Error for UnknownProviderKind {}
247
248impl FromStr for ProviderKind {
249 type Err = UnknownProviderKind;
250
251 fn from_str(s: &str) -> Result<Self, UnknownProviderKind> {
252 match s {
253 "aws" => Ok(ProviderKind::Aws),
254 "azure" => Ok(ProviderKind::Azure),
255 "digitalocean" => Ok(ProviderKind::DigitalOcean),
256 "gcp" => Ok(ProviderKind::Gcp),
257 "hetzner" => Ok(ProviderKind::Hetzner),
258 "i3d" => Ok(ProviderKind::I3d),
259 "leaseweb" => Ok(ProviderKind::Leaseweb),
260 "linode" => Ok(ProviderKind::Linode),
261 "oracle" => Ok(ProviderKind::Oracle),
262 "ovh" => Ok(ProviderKind::Ovh),
263 "proxmox" => Ok(ProviderKind::Proxmox),
264 "scaleway" => Ok(ProviderKind::Scaleway),
265 "tailscale" => Ok(ProviderKind::Tailscale),
266 "transip" => Ok(ProviderKind::Transip),
267 "upcloud" => Ok(ProviderKind::UpCloud),
268 "vultr" => Ok(ProviderKind::Vultr),
269 _ => Err(UnknownProviderKind),
270 }
271 }
272}
273
274impl fmt::Display for ProviderKind {
275 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276 f.write_str(self.as_str())
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 const ALL: &[(&str, ProviderKind)] = &[
285 ("aws", ProviderKind::Aws),
286 ("azure", ProviderKind::Azure),
287 ("digitalocean", ProviderKind::DigitalOcean),
288 ("gcp", ProviderKind::Gcp),
289 ("hetzner", ProviderKind::Hetzner),
290 ("i3d", ProviderKind::I3d),
291 ("leaseweb", ProviderKind::Leaseweb),
292 ("linode", ProviderKind::Linode),
293 ("oracle", ProviderKind::Oracle),
294 ("ovh", ProviderKind::Ovh),
295 ("proxmox", ProviderKind::Proxmox),
296 ("scaleway", ProviderKind::Scaleway),
297 ("tailscale", ProviderKind::Tailscale),
298 ("transip", ProviderKind::Transip),
299 ("upcloud", ProviderKind::UpCloud),
300 ("vultr", ProviderKind::Vultr),
301 ];
302
303 #[test]
308 fn marketing_provider_count_matches_all() {
309 let phrase = format!("{} cloud provider", ALL.len());
310 let llms = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/llms.txt"));
311 let page = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/site/page.html"));
312 assert!(
313 llms.contains(&phrase),
314 "llms.txt must mention \"{phrase}\" (ProviderKind has {} variants); update marketing when adding a provider",
315 ALL.len()
316 );
317 assert!(
318 page.contains(&phrase),
319 "site/page.html must mention \"{phrase}\" (ProviderKind has {} variants); update marketing when adding a provider",
320 ALL.len()
321 );
322 }
323
324 #[test]
325 fn round_trip_string_to_kind_to_string() {
326 for (name, kind) in ALL {
327 assert_eq!(
328 name.parse::<ProviderKind>().ok(),
329 Some(*kind),
330 "parse({name})"
331 );
332 assert_eq!(kind.as_str(), *name, "as_str for {name}");
333 }
334 }
335
336 #[test]
337 fn unknown_returns_err() {
338 assert!("not-a-provider".parse::<ProviderKind>().is_err());
339 assert!("".parse::<ProviderKind>().is_err());
340 assert!("AWS".parse::<ProviderKind>().is_err(), "case sensitive");
341 }
342
343 #[test]
344 fn display_matches_as_str() {
345 assert_eq!(format!("{}", ProviderKind::Hetzner), "hetzner");
346 assert_eq!(format!("{}", ProviderKind::DigitalOcean), "digitalocean");
347 assert_eq!(format!("{}", ProviderKind::UpCloud), "upcloud");
348 }
349
350 #[test]
351 fn requires_url_only_proxmox() {
352 for (_, kind) in ALL {
353 assert_eq!(
354 kind.requires_url(),
355 *kind == ProviderKind::Proxmox,
356 "requires_url for {kind:?}"
357 );
358 }
359 }
360
361 #[test]
362 fn accepts_cli_regions_matches_documented_set() {
363 let expected: &[ProviderKind] = &[
364 ProviderKind::Aws,
365 ProviderKind::Azure,
366 ProviderKind::Gcp,
367 ProviderKind::Oracle,
368 ProviderKind::Scaleway,
369 ];
370 for (_, kind) in ALL {
371 let want = expected.contains(kind);
372 assert_eq!(kind.accepts_cli_regions(), want, "regions cli for {kind:?}");
373 }
374 }
375
376 #[test]
377 fn has_regions_field_is_cli_set_plus_ovh() {
378 for (_, kind) in ALL {
379 let want = kind.accepts_cli_regions() || *kind == ProviderKind::Ovh;
380 assert_eq!(kind.has_regions_field(), want, "regions field for {kind:?}");
381 }
382 }
383
384 #[test]
385 fn regions_mandatory_implies_has_field() {
386 for (_, kind) in ALL {
387 if kind.regions_field_is_mandatory() {
388 assert!(
389 kind.has_regions_field(),
390 "{kind:?} mandates regions but has no field"
391 );
392 }
393 }
394 }
395
396 #[test]
397 fn regions_picker_implies_has_field_excluding_azure() {
398 for (_, kind) in ALL {
399 if kind.regions_field_is_picker() {
400 assert!(kind.has_regions_field(), "{kind:?} picker without field");
401 assert_ne!(*kind, ProviderKind::Azure, "azure regions are free-form");
402 }
403 }
404 }
405
406 #[test]
407 fn project_field_set() {
408 for (_, kind) in ALL {
409 let want = matches!(kind, ProviderKind::Gcp | ProviderKind::Ovh);
410 assert_eq!(kind.has_project_field(), want, "project field for {kind:?}");
411 }
412 }
413}