flytrap/
region.rs

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/// A Fly.io point of presence.
9///
10/// For region codes recognized by this package (e.g., `ams`, `nrt`, `ord`), the
11/// value will be a [`Region`] with known [details][RegionDetails]. For
12/// unrecognized codes, the value will be a bare [`RegionCode`].
13///
14/// ```
15/// use flytrap::{Location, Region};
16///
17/// let loc: Location = Region::Santiago.into();
18/// ```
19#[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    /// The known [`Region`] for this location, or `None` if the region
29    /// [code][RegionCode] was unrecognized.
30    #[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/// A [Fly.io region][regions].
101///
102/// Information about the region is available through the associated
103/// [`RegionDetails`], including the [`City`] where the region is located.
104///
105/// [regions]: https://fly.io/docs/reference/regions/
106///
107/// ```
108/// use flytrap::Region;
109/// # use std::mem;
110///
111/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
112/// let chicago: Region = "ord".parse()?;
113///
114/// assert_eq!(chicago.name, "Chicago, Illinois (US)");
115/// assert_eq!(chicago.city.name, "Chicago");
116/// assert_eq!(chicago.city.country, "US");
117/// assert!(chicago.city.geo.x() < Region::Amsterdam.city.geo.x());
118/// assert_eq!(chicago.to_string(), "ord");
119/// assert_eq!(mem::size_of::<Region>(), 4);
120/// # Ok(())
121/// # }
122/// ```
123#[derive(Enum, PartialEq, Eq, Copy, Clone, Debug)]
124#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
125#[repr(u32)]
126pub enum Region {
127    /// The _Amsterdam, Netherlands_ Fly.io region (`ams`).
128    #[cfg_attr(feature = "serde", serde(rename = "ams"))]
129    Amsterdam = 0x616d7300,
130    /// The _Ashburn, Virginia (US)_ Fly.io region (`iad`).
131    #[cfg_attr(feature = "serde", serde(rename = "iad"))]
132    Ashburn = 0x69616400,
133    /// The _Atlanta, Georgia (US)_ Fly.io region (`atl`).
134    #[cfg_attr(feature = "serde", serde(rename = "atl"))]
135    Atlanta = 0x61746c00,
136    /// The _Bogotá, Colombia_ Fly.io region (`bog`).
137    #[cfg_attr(feature = "serde", serde(rename = "bog"))]
138    Bogota = 0x626f6700,
139    /// The _Boston, Massachusetts (US)_ Fly.io region (`bos`).
140    #[cfg_attr(feature = "serde", serde(rename = "bos"))]
141    Boston = 0x626f7300,
142    /// The _Bucharest, Romania_ Fly.io region (`otp`).
143    #[cfg_attr(feature = "serde", serde(rename = "otp"))]
144    Bucharest = 0x6f747000,
145    /// The _Chennai (Madras), India_ Fly.io region (`maa`).
146    #[cfg_attr(feature = "serde", serde(rename = "maa"))]
147    Chennai = 0x6d616100,
148    /// The _Chicago, Illinois (US)_ Fly.io region (`ord`).
149    #[cfg_attr(feature = "serde", serde(rename = "ord"))]
150    Chicago = 0x6f726400,
151    /// The _Dallas, Texas (US)_ Fly.io region (`dfw`).
152    #[cfg_attr(feature = "serde", serde(rename = "dfw"))]
153    Dallas = 0x64667700,
154    /// The _Denver, Colorado (US)_ Fly.io region (`den`).
155    #[cfg_attr(feature = "serde", serde(rename = "den"))]
156    Denver = 0x64656e00,
157    /// The _Ezeiza, Argentina_ Fly.io region (`eze`).
158    #[cfg_attr(feature = "serde", serde(rename = "eze"))]
159    Ezeiza = 0x657a6500,
160    /// The _Frankfurt, Germany_ Fly.io region (`fra`).
161    #[cfg_attr(feature = "serde", serde(rename = "fra"))]
162    Frankfurt = 0x66726100,
163    /// The _Guadalajara, Mexico_ Fly.io region (`gdl`).
164    #[cfg_attr(feature = "serde", serde(rename = "gdl"))]
165    Guadalajara = 0x67646c00,
166    /// The _Hong Kong, Hong Kong_ Fly.io region (`hkg`).
167    #[cfg_attr(feature = "serde", serde(rename = "hkg"))]
168    HongKong = 0x686b6700,
169    /// The _Johannesburg, South Africa_ Fly.io region (`jnb`).
170    #[cfg_attr(feature = "serde", serde(rename = "jnb"))]
171    Johannesburg = 0x6a6e6200,
172    /// The _London, United Kingdom_ Fly.io region (`lhr`).
173    #[cfg_attr(feature = "serde", serde(rename = "lhr"))]
174    London = 0x6c687200,
175    /// The _Los Angeles, California (US)_ Fly.io region (`lax`).
176    #[cfg_attr(feature = "serde", serde(rename = "lax"))]
177    LosAngeles = 0x6c617800,
178    /// The _Madrid, Spain_ Fly.io region (`mad`).
179    #[cfg_attr(feature = "serde", serde(rename = "mad"))]
180    Madrid = 0x6d616400,
181    /// The _Miami, Florida (US)_ Fly.io region (`mia`).
182    #[cfg_attr(feature = "serde", serde(rename = "mia"))]
183    Miami = 0x6d696100,
184    /// The _Montreal, Canada_ Fly.io region (`yul`).
185    #[cfg_attr(feature = "serde", serde(rename = "yul"))]
186    Montreal = 0x79756c00,
187    /// The _Mumbai, India_ Fly.io region (`bom`).
188    #[cfg_attr(feature = "serde", serde(rename = "bom"))]
189    Mumbai = 0x626f6d00,
190    /// The _Paris, France_ Fly.io region (`cdg`).
191    #[cfg_attr(feature = "serde", serde(rename = "cdg"))]
192    Paris = 0x63646700,
193    /// The _Phoenix, Arizona (US)_ Fly.io region (`phx`).
194    #[cfg_attr(feature = "serde", serde(rename = "phx"))]
195    Phoenix = 0x70687800,
196    /// The _Querétaro, Mexico_ Fly.io region (`qro`).
197    #[cfg_attr(feature = "serde", serde(rename = "qro"))]
198    Queretaro = 0x71726f00,
199    /// The _Rio de Janeiro, Brazil_ Fly.io region (`gig`).
200    #[cfg_attr(feature = "serde", serde(rename = "gig"))]
201    RioDeJaneiro = 0x67696700,
202    /// The _San Jose, California (US)_ Fly.io region (`sjc`).
203    #[cfg_attr(feature = "serde", serde(rename = "sjc"))]
204    SanJose = 0x736a6300,
205    /// The _Santiago, Chile_ Fly.io region (`scl`).
206    #[cfg_attr(feature = "serde", serde(rename = "scl"))]
207    Santiago = 0x73636c00,
208    /// The _Sao Paulo, Brazil_ Fly.io region (`gru`).
209    #[cfg_attr(feature = "serde", serde(rename = "gru"))]
210    SaoPaulo = 0x67727500,
211    /// The _Seattle, Washington (US)_ Fly.io region (`sea`).
212    #[cfg_attr(feature = "serde", serde(rename = "sea"))]
213    Seattle = 0x73656100,
214    /// The _Secaucus, NJ (US)_ Fly.io region (`ewr`).
215    #[cfg_attr(feature = "serde", serde(rename = "ewr"))]
216    Secaucus = 0x65777200,
217    /// The _Singapore, Singapore_ Fly.io region (`sin`).
218    #[cfg_attr(feature = "serde", serde(rename = "sin"))]
219    Singapore = 0x73696e00,
220    /// The _Stockholm, Sweden_ Fly.io region (`arn`).
221    #[cfg_attr(feature = "serde", serde(rename = "arn"))]
222    Stockholm = 0x61726e00,
223    /// The _Sydney, Australia_ Fly.io region (`syd`).
224    #[cfg_attr(feature = "serde", serde(rename = "syd"))]
225    Sydney = 0x73796400,
226    /// The _Tokyo, Japan_ Fly.io region (`nrt`).
227    #[cfg_attr(feature = "serde", serde(rename = "nrt"))]
228    Tokyo = 0x6e727400,
229    /// The _Toronto, Canada_ Fly.io region (`yyz`).
230    #[cfg_attr(feature = "serde", serde(rename = "yyz"))]
231    Toronto = 0x79797a00,
232    /// The _Warsaw, Poland_ Fly.io region (`waw`).
233    #[cfg_attr(feature = "serde", serde(rename = "waw"))]
234    Warsaw = 0x77617700,
235}
236
237impl Region {
238    /// The known [details][RegionDetails] of the region.
239    pub fn details(&self) -> RegionDetails<'static> {
240        DETAILS[*self]
241    }
242
243    /// Iterate over all known [regions][Region].
244    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
253/// The [sort][Ord] comparison key for a [`Region`] or [`RegionCode`].
254type 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/// Attributes of a known [`Region`].
329///
330/// ```
331/// use flytrap::{Region, RegionDetails};
332///
333/// let RegionDetails { code, name, city } = Region::Atlanta.details();
334/// assert_eq!(code, "atl");
335/// assert_eq!(city.name, "Atlanta");
336/// assert_eq!(name, "Atlanta, Georgia (US)");
337/// ```
338#[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/// Describes a city where a Fly.io [region][Region] is hosted.
367#[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/// An unrecognized Fly.io [region][] code.
417///
418/// If Flytrap parses a region code which doesn't match any of the [`Region`]
419/// variants compiled into the crate, the bare value is preserved as a
420/// `RegionCode`.
421///
422/// [region]: https://fly.io/docs/reference/regions/
423///
424/// ```
425/// use flytrap::{Location, Region};
426///
427/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
428/// let cairo: Location = "cai".parse()?;
429/// let tokyo: Location = "nrt".parse()?;
430///
431/// assert_eq!(cairo, Location::Unknown("cai".parse()?));
432/// assert_eq!(tokyo, Location::Region(Region::Tokyo));
433/// # Ok(())
434/// # }
435/// ```
436#[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    /// The length in characters of a Fly.io [region][] code.
442    ///
443    /// [region]: https://fly.io/docs/reference/regions/
444    pub const LENGTH: usize = 3;
445
446    /// The geographic coordinates for "null island", a fictitious landform at
447    /// 0º latitude and longitude. Used as the coordinates for a [`Location::Unknown`].
448    pub const NULL_ISLAND: (R32, R32) = (R32::unchecked_new(0.0), R32::unchecked_new(0.0));
449
450    /// Checks if the `input` passes for a Fly.io region code – `/^[a-z]{3}$/`.
451    ///
452    /// ```
453    /// use flytrap::RegionCode;
454    ///
455    /// assert!(RegionCode::valid("oak"));
456    /// assert!(!RegionCode::valid("MDW"));
457    /// assert!(!RegionCode::valid("hi"));
458    /// ```
459    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/// An error parsing a [`Region`] or [`RegionCode`].
526#[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}