wikibase_rest_api 0.1.16

A Rust client for the Wikibase REST API.
Documentation
use reqwest::header::InvalidHeaderValue;
use serde::Deserialize;
use serde_json::{json, Value};
use std::{
    collections::HashMap,
    fmt::{self, Display, Formatter},
};
use thiserror::Error;

#[derive(Debug, Clone, Deserialize, Default, PartialEq)]
pub struct RestApiErrorPayload {
    code: String,
    message: String,
    #[serde(default)]
    context: HashMap<String, Value>,
}

impl RestApiErrorPayload {
    pub fn code(&self) -> &str {
        &self.code
    }

    pub fn message(&self) -> &str {
        &self.message
    }

    pub const fn context(&self) -> &HashMap<String, Value> {
        &self.context
    }
}

impl Display for RestApiErrorPayload {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        write!(
            f,
            "{}: {} / {}",
            self.code,
            self.message,
            json!(self.context)
        )
    }
}

#[derive(Error, Debug)]
pub enum RestApiError {
    #[error("ApiError: {status} {status_text} / {payload:?}")]
    ApiError {
        status: reqwest::StatusCode,
        status_text: String,
        payload: RestApiErrorPayload,
    },
    #[error("Client ID required")]
    ClientIdRequired,
    #[error("Client secret required")]
    ClientSecretRequired,
    #[error("Refresh token required")]
    RefreshTokenRequired,
    #[error("Access token required")]
    AccessTokenRequired,
    #[error("Reqwest Error: {0}")]
    Reqwest(reqwest::Error),
    #[error("Invalid header value: {0}")]
    InvalidHeaderValue(InvalidHeaderValue),
    #[error("Method {method} not implemented for path {path} in REST API")]
    NotImplementedInRestApi {
        method: reqwest::Method,
        path: String,
    },
    #[error("Unexpected response: {0}")]
    UnexpectedResponse(Value),
    #[error("Missing ID")]
    MissingId,
    #[error("ID already set")]
    HasId,
    #[error("Missing field {field}: {j}")]
    MissingOrInvalidField { field: String, j: Value },
    #[error("Wrong type for {field}: {j}")]
    WrongType { field: String, j: Value },
    #[error("Entity ID is None")]
    IsNone,
    #[error("Unrecognized entity ID letter: {0}")]
    UnknownEntityLetter(String),
    #[error("Unknown value: {0}")]
    UnknownValue(String),
    #[error("Unknown data type: {0}")]
    UnknownDataType(String),
    #[error("Serde JSON error: {0}")]
    SerdeJson(serde_json::Error),
    #[error("Unknown statement rank: {0}")]
    UnknownStatementRank(String),
    #[error("API not set")]
    ApiNotSet,
    #[error("Empty value: {0}")]
    EmptyValue(String),
    #[error("Unsupported method: {0}")]
    UnsupportedMethod(reqwest::Method),
    #[error("REST API URL is invalid: {0}")]
    RestApiUrlInvalid(String),
    #[error("Invalid precision")]
    InvalidPrecision,
    #[error("Missing results field in response")]
    MissingResults,
}

impl From<reqwest::Error> for RestApiError {
    fn from(e: reqwest::Error) -> Self {
        Self::Reqwest(e)
    }
}

impl From<InvalidHeaderValue> for RestApiError {
    fn from(e: InvalidHeaderValue) -> Self {
        Self::InvalidHeaderValue(e)
    }
}

impl From<serde_json::Error> for RestApiError {
    fn from(e: serde_json::Error) -> Self {
        Self::SerdeJson(e)
    }
}

impl RestApiError {
    pub async fn from_response(response: reqwest::Response) -> Self {
        let status = response.status();
        let status_text = status.canonical_reason().unwrap_or_default().to_string();
        let payload = response.json().await.unwrap_or_default();
        RestApiError::ApiError {
            status,
            status_text,
            payload,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use http::HeaderValue;
    use serde_json::json;

    #[test]
    fn test_rest_api_error_payload() {
        let payload = RestApiErrorPayload {
            code: "code".to_string(),
            message: "message".to_string(),
            context: HashMap::from([("key".to_string(), json!("value"))]),
        };
        assert_eq!(payload.code(), "code");
        assert_eq!(payload.message(), "message");
        assert_eq!(payload.context().get("key").unwrap(), &json!("value"));
    }

    #[test]
    fn test_rest_api_error_display() {
        let payload = RestApiErrorPayload {
            code: "code".to_string(),
            message: "message".to_string(),
            context: HashMap::from([("key".to_string(), json!("value"))]),
        };
        let error = RestApiError::ApiError {
            status: reqwest::StatusCode::BAD_REQUEST,
            status_text: "Bad Request".to_string(),
            payload,
        };
        assert_eq!(
            error.to_string(),
            "ApiError: 400 Bad Request Bad Request / RestApiErrorPayload { code: \"code\", message: \"message\", context: {\"key\": String(\"value\")} }"
        );
    }

    #[tokio::test]
    #[cfg_attr(miri, ignore)]
    async fn test_error_reqwest() {
        let error = reqwest::get("not a url").await.unwrap_err();
        let rest_api_error: RestApiError = error.into();
        assert_eq!(rest_api_error.to_string(), "Reqwest Error: builder error");
    }

    #[test]
    fn test_invalid_header_value() {
        let error = HeaderValue::from_str("\u{0}").unwrap_err();
        let rest_api_error: RestApiError = error.into();
        assert_eq!(
            rest_api_error.to_string(),
            "Invalid header value: failed to parse header value"
        );
    }

    #[test]
    fn test_payload_code() {
        let payload = RestApiErrorPayload {
            code: "code".to_string(),
            message: "message".to_string(),
            context: HashMap::new(),
        };
        assert_eq!(payload.code(), "code");
    }

    #[test]
    fn test_payload_message() {
        let payload = RestApiErrorPayload {
            code: "code".to_string(),
            message: "message".to_string(),
            context: HashMap::new(),
        };
        assert_eq!(payload.message(), "message");
    }

    #[test]
    fn test_payload_context() {
        let payload = RestApiErrorPayload {
            code: "code".to_string(),
            message: "message".to_string(),
            context: HashMap::from([("key".to_string(), json!("value"))]),
        };
        assert_eq!(payload.context().get("key").unwrap(), &json!("value"));
    }

    #[test]
    fn test_payload_fmt() {
        let payload = RestApiErrorPayload {
            code: "code".to_string(),
            message: "message".to_string(),
            context: HashMap::from([("key".to_string(), json!("value"))]),
        };
        let s = format!("{payload}");
        assert_eq!(s, "code: message / {\"key\":\"value\"}");
    }

    #[test]
    fn test_from_serde_json_error() {
        let error = serde_json::from_str::<Value>("{").unwrap_err();
        let rest_api_error: RestApiError = error.into();
        assert_eq!(
            rest_api_error.to_string(),
            "Serde JSON error: EOF while parsing an object at line 1 column 1"
        );
    }
}