astro_rs/coordinates/
lookup.rs

1use super::frames::Icrs;
2use super::lookup_config::SesameConfig;
3use super::EquatorialCoord;
4
5use hyper::client::HttpConnector;
6use hyper::Client;
7use once_cell::sync::OnceCell;
8use regex::Regex;
9use thiserror::Error;
10use uom::si::angle::{degree, Angle};
11use urlencoding::encode;
12
13static SESAME_CONFIG: OnceCell<SesameConfig> = OnceCell::new();
14static SESAME_PARSER: OnceCell<Regex> = OnceCell::new();
15
16fn init_sesame_parser() -> Regex {
17    Regex::new(r"%J\s*([0-9\.]+)\s*([\+\-\.0-9]+)").unwrap()
18}
19
20/// An enumeration of errors that can occur while performing a coordinate lookup.
21#[derive(Debug, Error)]
22pub enum AstroLookupError {
23    /// Indicates the environmental variables contributing to the SESAME configuration are invalid.
24    #[error("Invalid configuration: {reason}")]
25    InvalidConfiguration {
26        /// The reason the configuration is invalid.
27        reason: String,
28    },
29    /// Indicates an error occurred while obtaining the coordinate data.
30    #[error(transparent)]
31    NetworkError(#[from] hyper::Error),
32    /// Indicates an error occurred while parsing the coordinate data.
33    #[error("{reason}")]
34    ParseError {
35        /// The reason coordinate data parsing failed.
36        reason: String,
37    },
38    /// Indicates coordinate data for the given name could not be found.
39    #[error("Could not find coordinate data for {name}")]
40    InvalidName {
41        /// The name for which data could not be found.
42        name: String,
43    },
44}
45
46/// Fetches the coordinates of an object with the given identifier.
47///
48/// # Examples
49///
50/// ```
51/// use astro_rs::coordinates::{self, *};
52/// use uom::si::angle::radian;
53/// use uom::si::f64::Angle;
54///
55/// let m33_coords = tokio_test::block_on(async { coordinates::lookup_by_name("M33").await })?;
56/// assert_eq!(m33_coords.round(4), Icrs {
57///     coords: EquatorialCoord {
58///         ra: Angle::new::<radian>(0.4095),
59///         dec: Angle::new::<radian>(0.5351)
60///     },
61/// });
62///
63/// let no_coords = tokio_test::block_on(async {
64///     coordinates::lookup_by_name("something that should not resolve").await
65/// });
66/// assert!(no_coords.is_err());
67/// # Ok::<(), astro_rs::coordinates::AstroLookupError>(())
68/// ```
69pub async fn lookup_by_name(name: &str) -> Result<Icrs, AstroLookupError> {
70    let sesame_config = SESAME_CONFIG.get_or_init(SesameConfig::init);
71    let sesame_parser = SESAME_PARSER.get_or_init(init_sesame_parser);
72    let client = Client::new();
73
74    let mut err_result = Err(AstroLookupError::InvalidConfiguration {
75        reason: String::from("No configured SESAME URLs"),
76    });
77
78    for url in &sesame_config.urls {
79        let uri_string = [
80            url.as_str(),
81            if url.ends_with('/') { "" } else { "/" },
82            "~",
83            sesame_config.database.to_str(),
84            "?",
85            &encode(name),
86        ]
87        .concat();
88
89        let result = lookup_by_uri(name, sesame_parser, &client, uri_string).await;
90
91        if result.is_ok() {
92            return result;
93        } else {
94            err_result = result;
95        }
96    }
97
98    err_result
99}
100
101async fn lookup_by_uri(
102    name: &str,
103    sesame_parser: &Regex,
104    client: &Client<HttpConnector>,
105    uri_string: String,
106) -> Result<Icrs, AstroLookupError> {
107    let uri = uri_string
108        .parse()
109        .map_err(|_| AstroLookupError::InvalidName {
110            name: name.to_owned(),
111        })?;
112
113    let response = client.get(uri).await?;
114    let body_bytes = hyper::body::to_bytes(response).await?;
115    let body_string = String::from_utf8(body_bytes.as_ref().to_vec()).map_err(|er| {
116        AstroLookupError::ParseError {
117            reason: er.to_string(),
118        }
119    })?;
120
121    if let Some(cap) = sesame_parser.captures(&body_string) {
122        let ra_string = &cap[1];
123        let dec_string = &cap[2];
124
125        let ra: f64 = ra_string
126            .parse()
127            .map_err(|_| AstroLookupError::ParseError {
128                reason: ["Could not parse ra value: ", ra_string].concat(),
129            })?;
130        let dec: f64 = dec_string
131            .parse()
132            .map_err(|_| AstroLookupError::ParseError {
133                reason: ["Could not parse dec value: ", dec_string].concat(),
134            })?;
135
136        let coords = EquatorialCoord {
137            ra: Angle::new::<degree>(ra),
138            dec: Angle::new::<degree>(dec),
139        };
140        return Ok(Icrs { coords });
141    }
142
143    Err(AstroLookupError::InvalidName {
144        name: name.to_owned(),
145    })
146}