use core::fmt;
use serde::{Deserialize, Serialize};
use crate::types::{DateTime, Validate, Validator, Violations};
use super::status::{StatusClass, StatusCode};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(bound(deserialize = "T: Deserialize<'de>"))]
pub struct OcpiResponse<T> {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data: Option<T>,
pub status_code: StatusCode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status_message: Option<String>,
pub timestamp: DateTime,
}
impl<T> OcpiResponse<T> {
#[must_use]
pub fn success(data: T) -> Self {
Self {
data: Some(data),
status_code: StatusCode::SUCCESS,
status_message: None,
timestamp: DateTime::now(),
}
}
#[must_use]
pub fn success_empty() -> Self {
Self {
data: None,
status_code: StatusCode::SUCCESS,
status_message: None,
timestamp: DateTime::now(),
}
}
#[must_use]
pub fn error(status_code: StatusCode, status_message: impl Into<String>) -> Self {
Self {
data: None,
status_code,
status_message: Some(status_message.into()),
timestamp: DateTime::now(),
}
}
#[must_use]
pub const fn is_success(&self) -> bool {
self.status_code.is_success()
}
pub fn into_result(self) -> Result<T, OcpiError> {
if !self.is_success() {
return Err(OcpiError::Remote {
status_code: self.status_code,
status_message: self.status_message,
});
}
self.data.ok_or(OcpiError::MissingData { status_code: self.status_code })
}
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> OcpiResponse<U> {
OcpiResponse {
data: self.data.map(f),
status_code: self.status_code,
status_message: self.status_message,
timestamp: self.timestamp,
}
}
}
impl<T> OcpiResponse<Vec<T>> {
pub fn into_list(self) -> Result<Vec<T>, OcpiError> {
if !self.is_success() {
return Err(OcpiError::Remote {
status_code: self.status_code,
status_message: self.status_message,
});
}
Ok(self.data.unwrap_or_default())
}
}
impl<T: Validate> Validate for OcpiResponse<T> {
fn validate_in(&self, v: &mut Validator) {
v.field("data", &self.data);
v.field("timestamp", &self.timestamp);
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum OcpiError {
#[error("peer returned OCPI status {status_code}{}", format_message(.status_message.as_ref()))]
Remote {
status_code: StatusCode,
status_message: Option<String>,
},
#[error("peer returned OCPI status {status_code} but no data")]
MissingData {
status_code: StatusCode,
},
#[error("malformed JSON: {0}")]
MalformedJson(String),
#[error("cannot decode {path}: {message}")]
Decode {
path: String,
message: String,
},
#[error("object does not conform to the specification: {0}")]
Invalid(#[from] Violations),
#[error("unauthorized: {0}")]
Unauthorized(String),
#[error("CREDENTIALS_TOKEN_A may only be used on the credentials and versions modules")]
TokenAOutOfScope,
#[error("not found: {0}")]
NotFound(String),
#[error("method not allowed: {0}")]
MethodNotAllowed(String),
#[error("transport error: {0}")]
Transport(String),
#[error("cannot route this request: {0}")]
NotRoutable(String),
#[error("not supported by this build: {0}")]
Unsupported(String),
#[error("refused to call {url}: {reason}")]
UrlRefused {
url: String,
reason: String,
},
}
fn format_message(message: Option<&String>) -> String {
message.map_or_else(String::new, |m| format!(": {m}"))
}
impl OcpiError {
#[must_use]
pub fn status_code(&self) -> StatusCode {
match self {
Self::Remote { status_code, .. } | Self::MissingData { status_code } => *status_code,
Self::MalformedJson(_) | Self::Decode { .. } | Self::Invalid(_) | Self::NotRoutable(_) => {
StatusCode::INVALID_PARAMETERS
}
Self::Unauthorized(_)
| Self::TokenAOutOfScope
| Self::NotFound(_)
| Self::MethodNotAllowed(_) => StatusCode::CLIENT_ERROR,
Self::Transport(_) | Self::UrlRefused { .. } | Self::Unsupported(_) => StatusCode::SERVER_ERROR,
}
}
#[must_use]
pub const fn http_status(&self) -> u16 {
match self {
Self::MalformedJson(_) => 400,
Self::Unauthorized(_) | Self::TokenAOutOfScope => 401,
Self::NotFound(_) => 404,
Self::MethodNotAllowed(_) => 405,
_ => 200,
}
}
#[must_use]
pub fn is_transient(&self) -> bool {
match self {
Self::Transport(_) => true,
Self::Remote { status_code, .. } => {
matches!(status_code.class(), StatusClass::ServerError | StatusClass::HubError)
}
_ => false,
}
}
#[must_use]
pub fn to_response<T>(&self) -> OcpiResponse<T> {
OcpiResponse::error(self.status_code(), self.to_string())
}
}
impl fmt::Display for OcpiResponse<()> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.status_code)?;
if let Some(m) = &self.status_message {
write!(f, ": {m}")?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn null_and_absent_data_both_mean_none() {
let absent: OcpiResponse<String> =
serde_json::from_str(r#"{"status_code":1000,"timestamp":"2015-06-30T21:59:59Z"}"#).unwrap();
let null: OcpiResponse<String> =
serde_json::from_str(r#"{"data":null,"status_code":1000,"timestamp":"2015-06-30T21:59:59Z"}"#)
.unwrap();
assert_eq!(absent.data, None);
assert_eq!(null.data, None);
assert_eq!(
serde_json::to_string(&null).unwrap(),
r#"{"status_code":1000,"timestamp":"2015-06-30T21:59:59Z"}"#
);
}
#[test]
fn a_list_endpoint_treats_absent_data_as_empty() {
let r: OcpiResponse<Vec<String>> =
serde_json::from_str(r#"{"status_code":1000,"timestamp":"2015-06-30T21:59:59Z"}"#).unwrap();
assert_eq!(r.into_list().unwrap(), Vec::<String>::new());
}
#[test]
fn into_result_surfaces_the_peers_error() {
let r: OcpiResponse<String> = serde_json::from_str(
r#"{"status_code":2001,"status_message":"Missing required field: type","timestamp":"2015-06-30T21:59:59Z"}"#,
)
.unwrap();
let err = r.into_result().unwrap_err();
assert_eq!(err.status_code(), StatusCode::INVALID_PARAMETERS);
assert!(err.to_string().contains("Missing required field"), "{err}");
assert!(!err.is_transient());
}
#[test]
fn http_status_mapping_matches_the_spec_table() {
assert_eq!(OcpiError::MalformedJson("x".into()).http_status(), 400);
assert_eq!(OcpiError::Unauthorized("x".into()).http_status(), 401);
assert_eq!(OcpiError::TokenAOutOfScope.http_status(), 401);
assert_eq!(OcpiError::NotFound("x".into()).http_status(), 404);
assert_eq!(OcpiError::MethodNotAllowed("x".into()).http_status(), 405);
assert_eq!(OcpiError::Decode { path: "/evses/0".into(), message: "nope".into() }.http_status(), 200);
let unroutable = OcpiError::NotRoutable("GET is not a Broadcast Push".into());
assert_eq!(unroutable.http_status(), 200);
assert_eq!(unroutable.status_code(), StatusCode::INVALID_PARAMETERS);
assert!(!unroutable.is_transient());
assert_eq!(OcpiError::Transport("timeout".into()).http_status(), 200);
assert_eq!(
OcpiError::Remote { status_code: StatusCode::HUB_ERROR, status_message: None }.http_status(),
200
);
}
#[test]
fn server_and_hub_errors_are_transient_client_errors_are_not() {
let transient =
OcpiError::Remote { status_code: StatusCode::CONNECTION_PROBLEM, status_message: None };
assert!(transient.is_transient());
let permanent = OcpiError::Remote { status_code: StatusCode::UNKNOWN_TOKEN, status_message: None };
assert!(!permanent.is_transient());
assert!(OcpiError::Transport("reset".into()).is_transient());
}
}