1use std::{fmt, ops::Deref, str::FromStr};
2
3use enum_map::{enum_map, Enum, EnumMap};
4use geo_types::Point;
5use lazy_static::lazy_static;
6use noisy_float::types::R32;
7
8#[derive(PartialEq, Eq, Copy, Clone, Debug)]
20#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
21#[cfg_attr(feature = "serde", serde(untagged))]
22pub enum Location {
23 Region(Region),
24 Unknown(RegionCode),
25}
26
27impl Location {
28 #[inline]
31 pub const fn region(&self) -> Option<Region> {
32 match *self {
33 Location::Region(region) => Some(region),
34 Location::Unknown(_) => None,
35 }
36 }
37
38 #[inline]
39 fn key(&self) -> RegionKey<'_> {
40 match self {
41 Location::Region(region) => region.key(),
42 Location::Unknown(code) => code.key(),
43 }
44 }
45}
46
47impl FromStr for Location {
48 type Err = RegionError;
49
50 fn from_str(s: &str) -> Result<Self, Self::Err> {
51 if let Ok(region) = s.parse::<Region>() {
52 Ok(Self::Region(region))
53 } else if let Ok(code) = s.parse::<RegionCode>() {
54 Ok(Self::Unknown(code))
55 } else {
56 Err(RegionError::Invalid)
57 }
58 }
59}
60
61impl fmt::Display for Location {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 match self {
64 Location::Region(region) => write!(f, "{region}"),
65 Location::Unknown(code) => write!(f, "{code}"),
66 }
67 }
68}
69
70impl Ord for Location {
71 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
72 self.key().cmp(&other.key())
73 }
74}
75
76impl PartialOrd for Location {
77 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
78 Some(self.cmp(other))
79 }
80}
81
82impl From<Region> for Location {
83 fn from(value: Region) -> Self {
84 Self::Region(value)
85 }
86}
87
88impl From<RegionCode> for Location {
89 fn from(value: RegionCode) -> Self {
90 Self::Unknown(value)
91 }
92}
93
94impl From<Location> for Option<Region> {
95 fn from(value: Location) -> Self {
96 value.region()
97 }
98}
99
100#[derive(Enum, PartialEq, Eq, Copy, Clone, Debug)]
124#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
125#[repr(u32)]
126pub enum Region {
127 #[cfg_attr(feature = "serde", serde(rename = "ams"))]
129 Amsterdam = 0x616d7300,
130 #[cfg_attr(feature = "serde", serde(rename = "iad"))]
132 Ashburn = 0x69616400,
133 #[cfg_attr(feature = "serde", serde(rename = "atl"))]
135 Atlanta = 0x61746c00,
136 #[cfg_attr(feature = "serde", serde(rename = "bog"))]
138 Bogota = 0x626f6700,
139 #[cfg_attr(feature = "serde", serde(rename = "bos"))]
141 Boston = 0x626f7300,
142 #[cfg_attr(feature = "serde", serde(rename = "otp"))]
144 Bucharest = 0x6f747000,
145 #[cfg_attr(feature = "serde", serde(rename = "maa"))]
147 Chennai = 0x6d616100,
148 #[cfg_attr(feature = "serde", serde(rename = "ord"))]
150 Chicago = 0x6f726400,
151 #[cfg_attr(feature = "serde", serde(rename = "dfw"))]
153 Dallas = 0x64667700,
154 #[cfg_attr(feature = "serde", serde(rename = "den"))]
156 Denver = 0x64656e00,
157 #[cfg_attr(feature = "serde", serde(rename = "eze"))]
159 Ezeiza = 0x657a6500,
160 #[cfg_attr(feature = "serde", serde(rename = "fra"))]
162 Frankfurt = 0x66726100,
163 #[cfg_attr(feature = "serde", serde(rename = "gdl"))]
165 Guadalajara = 0x67646c00,
166 #[cfg_attr(feature = "serde", serde(rename = "hkg"))]
168 HongKong = 0x686b6700,
169 #[cfg_attr(feature = "serde", serde(rename = "jnb"))]
171 Johannesburg = 0x6a6e6200,
172 #[cfg_attr(feature = "serde", serde(rename = "lhr"))]
174 London = 0x6c687200,
175 #[cfg_attr(feature = "serde", serde(rename = "lax"))]
177 LosAngeles = 0x6c617800,
178 #[cfg_attr(feature = "serde", serde(rename = "mad"))]
180 Madrid = 0x6d616400,
181 #[cfg_attr(feature = "serde", serde(rename = "mia"))]
183 Miami = 0x6d696100,
184 #[cfg_attr(feature = "serde", serde(rename = "yul"))]
186 Montreal = 0x79756c00,
187 #[cfg_attr(feature = "serde", serde(rename = "bom"))]
189 Mumbai = 0x626f6d00,
190 #[cfg_attr(feature = "serde", serde(rename = "cdg"))]
192 Paris = 0x63646700,
193 #[cfg_attr(feature = "serde", serde(rename = "phx"))]
195 Phoenix = 0x70687800,
196 #[cfg_attr(feature = "serde", serde(rename = "qro"))]
198 Queretaro = 0x71726f00,
199 #[cfg_attr(feature = "serde", serde(rename = "gig"))]
201 RioDeJaneiro = 0x67696700,
202 #[cfg_attr(feature = "serde", serde(rename = "sjc"))]
204 SanJose = 0x736a6300,
205 #[cfg_attr(feature = "serde", serde(rename = "scl"))]
207 Santiago = 0x73636c00,
208 #[cfg_attr(feature = "serde", serde(rename = "gru"))]
210 SaoPaulo = 0x67727500,
211 #[cfg_attr(feature = "serde", serde(rename = "sea"))]
213 Seattle = 0x73656100,
214 #[cfg_attr(feature = "serde", serde(rename = "ewr"))]
216 Secaucus = 0x65777200,
217 #[cfg_attr(feature = "serde", serde(rename = "sin"))]
219 Singapore = 0x73696e00,
220 #[cfg_attr(feature = "serde", serde(rename = "arn"))]
222 Stockholm = 0x61726e00,
223 #[cfg_attr(feature = "serde", serde(rename = "syd"))]
225 Sydney = 0x73796400,
226 #[cfg_attr(feature = "serde", serde(rename = "nrt"))]
228 Tokyo = 0x6e727400,
229 #[cfg_attr(feature = "serde", serde(rename = "yyz"))]
231 Toronto = 0x79797a00,
232 #[cfg_attr(feature = "serde", serde(rename = "waw"))]
234 Warsaw = 0x77617700,
235}
236
237impl Region {
238 pub fn details(&self) -> RegionDetails<'static> {
240 DETAILS[*self]
241 }
242
243 pub fn all() -> impl Iterator<Item = (Region, RegionDetails<'static>)> {
245 DETAILS.iter().map(|(r, d)| (r, *d))
246 }
247
248 fn key(&self) -> RegionKey<'_> {
249 (self.city.geo.x(), self.city.geo.y(), &self.code)
250 }
251}
252
253type RegionKey<'a> = (R32, R32, &'a str);
255
256impl Ord for Region {
257 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
258 self.key().cmp(&other.key())
259 }
260}
261
262impl PartialOrd for Region {
263 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
264 Some(self.cmp(other))
265 }
266}
267
268impl Deref for Region {
269 type Target = RegionDetails<'static>;
270
271 fn deref(&self) -> &Self::Target {
272 &DETAILS[*self]
273 }
274}
275
276impl fmt::Display for Region {
277 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
278 write!(f, "{}", self.code)
279 }
280}
281
282impl FromStr for Region {
283 type Err = RegionError;
284
285 fn from_str(s: &str) -> Result<Self, Self::Err> {
286 match s {
287 "ams" => Ok(Self::Amsterdam),
288 "arn" => Ok(Self::Stockholm),
289 "atl" => Ok(Self::Atlanta),
290 "bog" => Ok(Self::Bogota),
291 "bom" => Ok(Self::Mumbai),
292 "bos" => Ok(Self::Boston),
293 "cdg" => Ok(Self::Paris),
294 "den" => Ok(Self::Denver),
295 "dfw" => Ok(Self::Dallas),
296 "ewr" => Ok(Self::Secaucus),
297 "eze" => Ok(Self::Ezeiza),
298 "fra" => Ok(Self::Frankfurt),
299 "gdl" => Ok(Self::Guadalajara),
300 "gig" => Ok(Self::RioDeJaneiro),
301 "gru" => Ok(Self::SaoPaulo),
302 "hkg" => Ok(Self::HongKong),
303 "iad" => Ok(Self::Ashburn),
304 "jnb" => Ok(Self::Johannesburg),
305 "lax" => Ok(Self::LosAngeles),
306 "lhr" => Ok(Self::London),
307 "maa" => Ok(Self::Chennai),
308 "mad" => Ok(Self::Madrid),
309 "mia" => Ok(Self::Miami),
310 "nrt" => Ok(Self::Tokyo),
311 "ord" => Ok(Self::Chicago),
312 "otp" => Ok(Self::Bucharest),
313 "phx" => Ok(Self::Phoenix),
314 "qro" => Ok(Self::Queretaro),
315 "scl" => Ok(Self::Santiago),
316 "sea" => Ok(Self::Seattle),
317 "sin" => Ok(Self::Singapore),
318 "sjc" => Ok(Self::SanJose),
319 "syd" => Ok(Self::Sydney),
320 "waw" => Ok(Self::Warsaw),
321 "yul" => Ok(Self::Montreal),
322 "yyz" => Ok(Self::Toronto),
323 _ => Err(RegionError::Unrecognized),
324 }
325 }
326}
327
328#[derive(Copy, Clone, Debug, PartialEq, Eq)]
339#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
340pub struct RegionDetails<'l> {
341 pub code: &'l str,
342 pub name: &'l str,
343 pub city: City<'l>,
344}
345
346impl RegionDetails<'static> {
347 pub(crate) const fn new(
348 code: &'static str,
349 name: &'static str,
350 city: &'static str,
351 country: &'static str,
352 geo: [f32; 2],
353 ) -> Self {
354 Self {
355 code,
356 name,
357 city: City {
358 name: city,
359 country,
360 geo: point(geo[0], geo[1]),
361 },
362 }
363 }
364}
365
366#[derive(Copy, Clone, Debug, PartialEq, Eq)]
368#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
369pub struct City<'l> {
370 pub name: &'l str,
371 pub country: &'l str,
372 pub geo: Point<R32>,
373}
374
375lazy_static! {
376 static ref DETAILS: EnumMap<Region, RegionDetails<'static>> = enum_map! {
377 Region::Amsterdam => RegionDetails::new("ams", "Amsterdam, Netherlands", "Amsterdam", "NL", [52.374342, 4.895439]),
378 Region::Stockholm => RegionDetails::new("arn", "Stockholm, Sweden", "Stockholm", "SE", [59.6512, 17.9178]),
379 Region::Atlanta => RegionDetails::new("atl", "Atlanta, Georgia (US)", "Atlanta", "US", [33.6407, -84.4277]),
380 Region::Bogota => RegionDetails::new("bog", "Bogotá, Colombia", "Bogotá", "CO", [4.70159, -74.1469]),
381 Region::Mumbai => RegionDetails::new("bom", "Mumbai, India", "Mumbai", "IN", [19.097403, 72.874245]),
382 Region::Boston => RegionDetails::new("bos", "Boston, Massachusetts (US)", "Boston", "US", [42.366978, -71.022_36]),
383 Region::Paris => RegionDetails::new("cdg", "Paris, France", "Paris", "FR", [48.860875, 2.353477]),
384 Region::Denver => RegionDetails::new("den", "Denver, Colorado (US)", "Denver", "US", [39.7392, -104.9847]),
385 Region::Dallas => RegionDetails::new("dfw", "Dallas, Texas (US)", "Dallas", "US", [32.778287, -96.7984]),
386 Region::Secaucus => RegionDetails::new("ewr", "Secaucus, NJ (US)", "Secaucus", "US", [40.789543, -74.056_53]),
387 Region::Ezeiza => RegionDetails::new("eze", "Ezeiza, Argentina", "Ezeiza", "AR", [-34.8222, -58.5358]),
388 Region::Frankfurt => RegionDetails::new("fra", "Frankfurt, Germany", "Frankfurt", "DE", [50.1167, 8.6833]),
389 Region::Guadalajara => RegionDetails::new("gdl", "Guadalajara, Mexico", "Guadalajara", "MX", [20.5217, -103.3109]),
390 Region::RioDeJaneiro => RegionDetails::new("gig", "Rio de Janeiro, Brazil", "Rio de Janeiro", "BR", [-22.8099, -43.2505]),
391 Region::SaoPaulo => RegionDetails::new("gru", "Sao Paulo, Brazil", "Sao Paulo", "BR", [-23.549664, -46.654_35]),
392 Region::HongKong => RegionDetails::new("hkg", "Hong Kong, Hong Kong", "Hong Kong", "HK", [22.250_97, 114.203224]),
393 Region::Ashburn => RegionDetails::new("iad", "Ashburn, Virginia (US)", "Ashburn", "US", [39.02214, -77.462556]),
394 Region::Johannesburg => RegionDetails::new("jnb", "Johannesburg, South Africa", "Johannesburg", "ZA", [-26.13629, 28.20298]),
395 Region::LosAngeles => RegionDetails::new("lax", "Los Angeles, California (US)", "Los Angeles", "US", [33.9416, -118.4085]),
396 Region::London => RegionDetails::new("lhr", "London, United Kingdom", "London", "GB", [51.516434, -0.125656]),
397 Region::Chennai => RegionDetails::new("maa", "Chennai (Madras), India", "Chennai", "IN", [13.064429, 80.253_07]),
398 Region::Madrid => RegionDetails::new("mad", "Madrid, Spain", "Madrid", "ES", [40.4381, -3.82]),
399 Region::Miami => RegionDetails::new("mia", "Miami, Florida (US)", "Miami", "US", [25.7877, -80.2241]),
400 Region::Tokyo => RegionDetails::new("nrt", "Tokyo, Japan", "Tokyo", "JP", [35.621_61, 139.741_85]),
401 Region::Chicago => RegionDetails::new("ord", "Chicago, Illinois (US)", "Chicago", "US", [41.891544, -87.630_39]),
402 Region::Bucharest => RegionDetails::new("otp", "Bucharest, Romania", "Bucharest", "RO", [44.4325, 26.1039]),
403 Region::Phoenix => RegionDetails::new("phx", "Phoenix, Arizona (US)", "Phoenix", "US", [33.416084, -112.009_48]),
404 Region::Queretaro => RegionDetails::new("qro", "Querétaro, Mexico", "Querétaro", "MX", [20.62, -100.1863]),
405 Region::Santiago => RegionDetails::new("scl", "Santiago, Chile", "Santiago", "CL", [-33.36572, -70.64292]),
406 Region::Seattle => RegionDetails::new("sea", "Seattle, Washington (US)", "Seattle", "US", [47.6097, -122.3331]),
407 Region::Singapore => RegionDetails::new("sin", "Singapore, Singapore", "Singapore", "SG", [1.3, 103.8]),
408 Region::SanJose => RegionDetails::new("sjc", "San Jose, California (US)", "San Jose", "US", [37.351_6, -121.896_74]),
409 Region::Sydney => RegionDetails::new("syd", "Sydney, Australia", "Sydney", "AU", [-33.866_03, 151.20693]),
410 Region::Warsaw => RegionDetails::new("waw", "Warsaw, Poland", "Warsaw", "PL", [52.1657, 20.9671]),
411 Region::Montreal => RegionDetails::new("yul", "Montreal, Canada", "Montreal", "CA", [45.48647, -73.75549]),
412 Region::Toronto => RegionDetails::new("yyz", "Toronto, Canada", "Toronto", "CA", [43.644_63, -79.384_23]),
413 };
414}
415
416#[derive(PartialEq, Eq, Copy, Clone, Debug)]
437#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
438pub struct RegionCode([u8; 4]);
439
440impl RegionCode {
441 pub const LENGTH: usize = 3;
445
446 pub const NULL_ISLAND: (R32, R32) = (R32::unchecked_new(0.0), R32::unchecked_new(0.0));
449
450 pub fn valid(input: &str) -> bool {
460 input.len() == Self::LENGTH && input.chars().all(|c| c.is_ascii_lowercase())
461 }
462
463 #[inline(always)]
464 fn as_slice(&self) -> &[u8] {
465 &self.0[..3]
466 }
467
468 fn key(&self) -> RegionKey<'_> {
469 (Self::NULL_ISLAND.0, Self::NULL_ISLAND.1, self.as_ref())
470 }
471}
472
473impl AsRef<[u8]> for RegionCode {
474 fn as_ref(&self) -> &[u8] {
475 self.as_slice()
476 }
477}
478
479impl AsRef<str> for RegionCode {
480 fn as_ref(&self) -> &str {
481 std::str::from_utf8(self.as_slice()).expect("invalid region code")
482 }
483}
484
485impl FromStr for RegionCode {
486 type Err = RegionError;
487
488 fn from_str(s: &str) -> Result<Self, Self::Err> {
489 if Self::valid(s) {
490 let b = s.as_bytes();
491 Ok(Self([b[0], b[1], b[2], 0]))
492 } else {
493 Err(RegionError::Invalid)
494 }
495 }
496}
497
498impl fmt::Display for RegionCode {
499 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
500 match std::str::from_utf8(&self.0) {
501 Ok(code) => write!(f, "{code}"),
502 Err(_) => write!(f, "---"),
503 }
504 }
505}
506
507impl From<Region> for RegionCode {
508 fn from(value: Region) -> Self {
509 Self((value as u32).to_be_bytes())
510 }
511}
512
513impl Ord for RegionCode {
514 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
515 self.key().cmp(&other.key())
516 }
517}
518
519impl PartialOrd for RegionCode {
520 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
521 Some(self.cmp(other))
522 }
523}
524
525#[derive(thiserror::Error, PartialEq, Eq, PartialOrd, Ord, Debug)]
527pub enum RegionError {
528 #[error("invalid Fly.io region code")]
529 Invalid,
530 #[error("unknown Fly.io region code")]
531 Unrecognized,
532}
533
534#[inline(always)]
535const fn point(lat: f32, lon: f32) -> Point<R32> {
536 Point(geo_types::Coord {
537 x: R32::unchecked_new(lon),
538 y: R32::unchecked_new(lat),
539 })
540}
541
542#[cfg(test)]
543mod test {
544 use super::{City, Location, Region, RegionCode, RegionError};
545
546 #[test]
547 fn parse() {
548 assert_eq!(Ok(Location::Region(Region::Bogota)), "bog".parse());
549 assert_eq!(
550 Ok(Location::Unknown(RegionCode([0x6f, 0x61, 0x6b, 0]))),
551 "oak".parse()
552 );
553 assert!(matches!(
554 "hi".parse::<Location>(),
555 Err(RegionError::Invalid)
556 ));
557 }
558
559 #[test]
560 fn region_details() {
561 let ord = Region::Chicago;
562
563 assert_eq!("Chicago, Illinois (US)", ord.name);
564 }
565
566 #[test]
567 fn unpack() {
568 let cdg = Region::Paris;
569 let City { name, country, .. } = cdg.city;
570
571 assert_eq!("Paris", name);
572 assert_eq!("FR", country);
573 }
574
575 #[test]
576 fn ordering() {
577 use Region::*;
578
579 let mut regions = [
580 Bucharest,
581 Chicago,
582 HongKong,
583 Johannesburg,
584 LosAngeles,
585 Madrid,
586 Santiago,
587 Tokyo,
588 ];
589 regions.sort();
590
591 assert_eq!(
592 [
593 LosAngeles,
594 Chicago,
595 Santiago,
596 Madrid,
597 Bucharest,
598 Johannesburg,
599 HongKong,
600 Tokyo,
601 ],
602 regions
603 );
604 }
605
606 #[test]
607 fn all() {
608 assert!(Region::all().count() >= 30);
609 assert!(Region::all().all(|(r, d)| r.details() == d));
610 }
611}