cloud_terrastodon_credentials 0.35.1

Helpers for getting Azure PAT and stuff for Cloud Terrastodon
use crate::RestResponseBody;
use crate::parse_response_body;
use eyre::Result;
use eyre::WrapErr;
use reqwest::Response;
use reqwest::header::HeaderMap;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;
use std::collections::BTreeMap;

#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct SerializableRestResponse {
    pub status: u16,
    pub ok: bool,
    pub reason_phrase: Option<String>,
    pub headers: BTreeMap<String, Vec<String>>,
    pub body: RestResponseBody,
}

impl SerializableRestResponse {
    pub fn new(status: http::StatusCode, headers: &HeaderMap, content: String) -> Self {
        Self {
            status: status.as_u16(),
            ok: status.is_success(),
            reason_phrase: status.canonical_reason().map(str::to_owned),
            headers: serialize_headers(headers),
            body: parse_response_body(content),
        }
    }

    pub async fn from_response(response: Response) -> Result<Self> {
        let status = response.status();
        let headers = response.headers().clone();
        let content = response.text().await?;
        Ok(Self::new(status, &headers, content))
    }

    pub fn header(&self, name: &str) -> Option<&str> {
        self.headers.iter().find_map(|(key, values)| {
            if key.eq_ignore_ascii_case(name) {
                values.first().map(String::as_str)
            } else {
                None
            }
        })
    }

    pub fn into_json_body(self) -> Result<Value> {
        match self.body {
            RestResponseBody::Json(body) => Ok(body),
            RestResponseBody::Text(content) => serde_json::from_str(&content)
                .wrap_err("Expected REST response body to contain JSON"),
        }
    }
}

pub fn serialize_headers(headers: &HeaderMap) -> BTreeMap<String, Vec<String>> {
    let mut serialized = BTreeMap::<String, Vec<String>>::new();
    for (name, value) in headers {
        let value = value
            .to_str()
            .map(str::to_owned)
            .unwrap_or_else(|_| String::from_utf8_lossy(value.as_bytes()).into_owned());
        serialized.entry(name.to_string()).or_default().push(value);
    }
    serialized
}

#[cfg(test)]
mod tests {
    use super::RestResponseBody;
    use super::SerializableRestResponse;
    use super::serialize_headers;
    use crate::parse_response_body;
    use http::StatusCode;
    use reqwest::header::HeaderMap;
    use reqwest::header::HeaderValue;

    #[test]
    fn parses_json_response_body() {
        let body = parse_response_body("{\"hello\":\"world\"}".to_string());
        assert_eq!(
            body,
            RestResponseBody::Json(serde_json::json!({"hello": "world"}))
        );
    }

    #[test]
    fn preserves_text_response_body() {
        let body = parse_response_body("not json".to_string());
        assert_eq!(body, RestResponseBody::Text("not json".to_string()));
    }

    #[test]
    fn serializes_repeated_headers() {
        let mut headers = HeaderMap::new();
        headers.append("x-test", HeaderValue::from_static("a"));
        headers.append("x-test", HeaderValue::from_static("b"));
        headers.append("content-type", HeaderValue::from_static("application/json"));

        let serialized = serialize_headers(&headers);
        assert_eq!(
            serialized.get("x-test").unwrap(),
            &vec!["a".to_string(), "b".to_string()]
        );
        assert_eq!(
            serialized.get("content-type").unwrap(),
            &vec!["application/json".to_string()]
        );
    }

    #[test]
    fn looks_up_headers_case_insensitively() {
        let response = SerializableRestResponse {
            status: 202,
            ok: true,
            reason_phrase: Some("Accepted".to_string()),
            headers: std::collections::BTreeMap::from([(
                String::from("Location"),
                vec![String::from("https://example.test/poll")],
            )]),
            body: RestResponseBody::Text(String::new()),
        };
        assert_eq!(
            response.header("location"),
            Some("https://example.test/poll")
        );
    }

    #[test]
    fn builds_response_from_status_headers_and_content() {
        let mut headers = HeaderMap::new();
        headers.insert("content-type", HeaderValue::from_static("application/json"));
        let response = SerializableRestResponse::new(
            StatusCode::OK,
            &headers,
            "{\"hello\":\"world\"}".to_string(),
        );
        assert!(response.ok);
        assert_eq!(response.status, 200);
        assert_eq!(response.reason_phrase.as_deref(), Some("OK"));
        assert_eq!(
            response.body,
            RestResponseBody::Json(serde_json::json!({"hello": "world"}))
        );
    }
}