use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(i32)]
#[non_exhaustive]
pub enum ErrorCode {
OtherCause = -1,
InternalServerError = 1,
ConnectionFailed = 100,
ObjectNotFound = 101,
InvalidQuery = 102,
InvalidClassName = 103,
MissingObjectId = 104,
InvalidKeyName = 105,
InvalidPointer = 106,
InvalidJson = 107,
CommandUnavailable = 108,
NotInitialized = 109,
IncorrectType = 111,
InvalidChannelName = 112,
PushMisconfigured = 115,
ObjectTooLarge = 116,
OperationForbidden = 119,
CacheMiss = 120,
InvalidNestedKey = 121,
InvalidFileName = 122,
InvalidAcl = 123,
Timeout = 124,
InvalidEmailAddress = 125,
MissingContentType = 126,
MissingContentLength = 127,
InvalidContentLength = 128,
FileTooLarge = 129,
FileSaveError = 130,
MissingClassName = 135,
UnchangeableField = 136,
DuplicateValue = 137,
InvalidRoleName = 139,
ExceededQuota = 140,
ScriptFailed = 141,
ValidationError = 142,
InvalidImageData = 143,
UnsavedFileError = 151,
InvalidPushTimeError = 152,
FileDeleteError = 153,
RequestLimitExceeded = 155,
DuplicateRequest = 159,
InvalidEventName = 160,
FileDeleteUnnamedError = 161,
InvalidValue = 162,
UsernameMissing = 200,
PasswordMissing = 201,
UsernameTaken = 202,
EmailTaken = 203,
EmailMissing = 204,
EmailNotFound = 205,
SessionMissing = 206,
MustCreateUserThroughSignup = 207,
AccountAlreadyLinked = 208,
InvalidSessionToken = 209,
MfaError = 210,
MfaTokenRequired = 211,
LinkedIdMissing = 250,
InvalidLinkedSession = 251,
UnsupportedService = 252,
InvalidSchemaOperation = 255,
AggregateError = 600,
FileReadError = 601,
XDomainRequest = 602,
}
impl ErrorCode {
pub fn as_i32(self) -> i32 {
self as i32
}
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_i32())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorOrigin {
Parse,
Internal,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ParseErrorInfo {
pub duplicated_field: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("{code}: {message}")]
pub struct ParseError {
pub code: ErrorCode,
pub message: String,
pub origin: ErrorOrigin,
pub info: ParseErrorInfo,
}
impl ParseError {
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
origin: ErrorOrigin::Parse,
info: ParseErrorInfo::default(),
}
}
#[must_use]
pub fn internal(detail: impl Into<String>) -> Self {
let detail = detail.into();
log_detail(ErrorCode::InternalServerError, &detail);
Self {
code: ErrorCode::InternalServerError,
message: detail,
origin: ErrorOrigin::Internal,
info: ParseErrorInfo::default(),
}
}
#[must_use]
pub fn with_duplicated_field(mut self, field: impl Into<String>) -> Self {
self.info.duplicated_field = Some(field.into());
self
}
pub fn duplicated_field(&self) -> Option<&str> {
self.info.duplicated_field.as_deref()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorDetail {
Withheld,
Disclosed,
}
impl ErrorDetail {
pub fn from_sanitized(enable_sanitized_error_response: bool) -> Self {
if enable_sanitized_error_response {
ErrorDetail::Withheld
} else {
ErrorDetail::Disclosed
}
}
}
fn log_detail(code: ErrorCode, detailed: &str) {
static TRACE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
if *TRACE.get_or_init(|| std::env::var("PARSE_RUST_TRACE").is_ok()) {
eprintln!("[trace] sanitized error: {code}: {detailed}");
}
}
impl ParseError {
#[must_use]
pub fn sanitized(
code: ErrorCode,
detailed: impl Into<String>,
generic: &str,
detail: ErrorDetail,
) -> Self {
let detailed = detailed.into();
log_detail(code, &detailed);
match detail {
ErrorDetail::Withheld => Self::new(code, generic),
ErrorDetail::Disclosed => Self::new(code, detailed),
}
}
#[must_use]
pub fn permission_denied(
code: ErrorCode,
detailed: impl Into<String>,
detail: ErrorDetail,
) -> Self {
Self::sanitized(code, detailed, PERMISSION_DENIED, detail)
}
}
pub const PERMISSION_DENIED: &str = "Permission denied";
pub const DUPLICATE_VALUE_MESSAGE: &str =
"A duplicate value for a field with unique values was provided";
impl ParseError {
pub fn invalid_json(message: impl Into<String>) -> Self {
Self::new(ErrorCode::InvalidJson, message)
}
pub fn incorrect_type(message: impl Into<String>) -> Self {
Self::new(ErrorCode::IncorrectType, message)
}
pub fn invalid_key_name(message: impl Into<String>) -> Self {
Self::new(ErrorCode::InvalidKeyName, message)
}
pub fn invalid_acl(message: impl Into<String>) -> Self {
Self::new(ErrorCode::InvalidAcl, message)
}
pub fn invalid_query(message: impl Into<String>) -> Self {
Self::new(ErrorCode::InvalidQuery, message)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn discriminants_match_upstream() {
assert_eq!(ErrorCode::OtherCause.as_i32(), -1);
assert_eq!(ErrorCode::InternalServerError.as_i32(), 1);
assert_eq!(ErrorCode::ObjectNotFound.as_i32(), 101);
assert_eq!(ErrorCode::InvalidQuery.as_i32(), 102);
assert_eq!(ErrorCode::IncorrectType.as_i32(), 111);
assert_eq!(ErrorCode::OperationForbidden.as_i32(), 119);
assert_eq!(ErrorCode::DuplicateValue.as_i32(), 137);
assert_eq!(ErrorCode::ScriptFailed.as_i32(), 141);
assert_eq!(ErrorCode::DuplicateRequest.as_i32(), 159);
assert_eq!(ErrorCode::InvalidSessionToken.as_i32(), 209);
assert_eq!(ErrorCode::InvalidSchemaOperation.as_i32(), 255);
}
#[test]
fn display_is_the_number() {
assert_eq!(ErrorCode::ObjectNotFound.to_string(), "101");
}
#[test]
fn the_two_regimes_produce_the_two_upstream_messages() {
let detailed = "Permission denied for action find on class Post.";
assert_eq!(
ParseError::permission_denied(
ErrorCode::OperationForbidden,
detailed,
ErrorDetail::from_sanitized(true)
)
.message,
"Permission denied"
);
assert_eq!(
ParseError::permission_denied(
ErrorCode::OperationForbidden,
detailed,
ErrorDetail::from_sanitized(false)
)
.message,
detailed
);
}
#[test]
fn sanitizing_does_not_change_the_code() {
for detail in [ErrorDetail::Withheld, ErrorDetail::Disclosed] {
let e = ParseError::permission_denied(ErrorCode::ObjectNotFound, "why", detail);
assert_eq!(e.code, ErrorCode::ObjectNotFound);
}
}
#[test]
fn an_internal_error_is_distinguishable_from_a_parse_error_carrying_code_one() {
let internal = ParseError::internal("pointer permissions: Post owner");
assert_eq!(internal.code, ErrorCode::InternalServerError);
assert_eq!(internal.origin, ErrorOrigin::Internal);
let parse = ParseError::new(ErrorCode::InternalServerError, "Invalid object ID.");
assert_eq!(parse.origin, ErrorOrigin::Parse);
}
#[test]
fn the_ordinary_constructors_produce_parse_origin() {
for e in [
ParseError::new(ErrorCode::ObjectNotFound, "Object not found."),
ParseError::invalid_json("bad"),
ParseError::permission_denied(
ErrorCode::OperationForbidden,
"why",
ErrorDetail::Withheld,
),
] {
assert_eq!(e.origin, ErrorOrigin::Parse);
}
}
#[test]
fn the_duplicated_field_is_out_of_band() {
let e = ParseError::new(ErrorCode::DuplicateValue, DUPLICATE_VALUE_MESSAGE)
.with_duplicated_field("username");
assert_eq!(e.duplicated_field(), Some("username"));
assert_eq!(e.message, DUPLICATE_VALUE_MESSAGE);
assert!(!e.message.contains("username"));
assert_eq!(ParseError::invalid_json("x").duplicated_field(), None);
}
#[test]
fn the_generic_message_is_per_call_site() {
let e = ParseError::sanitized(
ErrorCode::InternalServerError,
"a driver said something specific",
"An internal server error occurred",
ErrorDetail::Withheld,
);
assert_eq!(e.message, "An internal server error occurred");
}
}