diff --git a/src/client.rs b/src/client.rs index 03d056f4..49b8145e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -663,11 +663,11 @@ mod tests { let error = client.delete_key("invalid_key").await.unwrap_err(); assert!(matches!( error, - Error::MeiliSearchError { + Error::Meilisearch(MeilisearchError { error_code: ErrorCode::ApiKeyNotFound, error_type: ErrorType::InvalidRequest, .. - } + }) )); // ==> executing the action without enough right @@ -681,21 +681,21 @@ mod tests { let error = client.delete_key("invalid_key").await.unwrap_err(); assert!(matches!( error, - Error::MeiliSearchError { + Error::Meilisearch(MeilisearchError { error_code: ErrorCode::InvalidApiKey, error_type: ErrorType::Auth, .. - } + }) )); // with a good key let error = client.delete_key(&key.key).await.unwrap_err(); assert!(matches!( error, - Error::MeiliSearchError { + Error::Meilisearch(MeilisearchError { error_code: ErrorCode::InvalidApiKey, error_type: ErrorType::Auth, .. - } + }) )); // cleanup @@ -741,7 +741,7 @@ mod tests { assert!(matches!( error, - Error::MeiliSearchError { + Error::MeilisearchError { error_code: ErrorCode::InvalidApiKeyIndexes, error_type: ErrorType::InvalidRequest, .. @@ -756,11 +756,11 @@ mod tests { assert!(matches!( error, - Error::MeiliSearchError { + Error::Meilisearch(MeilisearchError { error_code: ErrorCode::InvalidApiKeyExpiresAt, error_type: ErrorType::InvalidRequest, .. - } + }) )); // ==> executing the action without enough right @@ -776,11 +776,11 @@ mod tests { assert!(matches!( error, - Error::MeiliSearchError { + Error::Meilisearch(MeilisearchError { error_code: ErrorCode::InvalidApiKey, error_type: ErrorType::Auth, .. - } + }) )); // cleanup @@ -830,7 +830,7 @@ mod tests { assert!(matches!( error, - Error::MeiliSearchError { + Error::MeilisearchError { error_code: ErrorCode::InvalidApiKeyIndexes, error_type: ErrorType::InvalidRequest, .. @@ -844,11 +844,11 @@ mod tests { assert!(matches!( error, - Error::MeiliSearchError { + Error::Meilisearch(MeilisearchError { error_code: ErrorCode::InvalidApiKeyExpiresAt, error_type: ErrorType::InvalidRequest, .. - } + }) )); key.expires_at = None; @@ -864,11 +864,11 @@ mod tests { assert!(matches!( error, - Error::MeiliSearchError { + Error::Meilisearch(MeilisearchError { error_code: ErrorCode::InvalidApiKey, error_type: ErrorType::Auth, .. - } + }) )); // cleanup diff --git a/src/errors.rs b/src/errors.rs index 604c5054..99dffbbb 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,3 +1,5 @@ +use serde::{Deserialize, Serialize}; + /// An enum representing the errors that can occur. #[derive(Debug)] @@ -5,19 +7,7 @@ pub enum Error { /// The exhaustive list of Meilisearch errors: /// Also check out: - MeiliSearchError { - /// The human readable error message - error_message: String, - /// The error code of the error. Officially documented at - /// . - error_code: ErrorCode, - /// The type of error (invalid request, internal error, or authentication - /// error) - error_type: ErrorType, - /// A link to the Meilisearch documentation for an error. - error_link: String, - }, - + Meilisearch(MeilisearchError), /// There is no Meilisearch server listening on the [specified host] /// (../client/struct.Client.html#method.new). UnreachableServer, @@ -37,8 +27,34 @@ pub enum Error { HttpError(String), } +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MeilisearchError { + /// The human readable error message + #[serde(rename = "message")] + pub error_message: String, + /// The error code of the error. Officially documented at + /// . + #[serde(rename = "code")] + pub error_code: ErrorCode, + /// The type of error (invalid request, internal error, or authentication + /// error) + #[serde(rename = "type")] + pub error_type: ErrorType, + /// A link to the Meilisearch documentation for an error. + #[serde(rename = "link")] + pub error_link: String, +} + +impl From for Error { + fn from(error: MeilisearchError) -> Self { + Self::Meilisearch(error) + } +} + /// The type of error that was encountered. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum ErrorType { /// The submitted request was invalid. @@ -47,12 +63,29 @@ pub enum ErrorType { Internal, /// Authentication was either incorrect or missing. Auth, + + /// That's unexpected. Please open a GitHub issue after ensuring you are + /// using the supported version of the Meilisearch server. + #[serde(other)] + Unknown, +} + +impl std::fmt::Display for ErrorType { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + write!( + fmt, + "{}", + // this can't fail + serde_json::to_value(self).unwrap().as_str().unwrap() + ) + } } /// The error code. /// /// Officially documented at . -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum ErrorCode { IndexCreationFailed, @@ -99,167 +132,33 @@ pub enum ErrorCode { /// That's unexpected. Please open a GitHub issue after ensuring you are /// using the supported version of the Meilisearch server. - Unknown(UnknownErrorCode), -} - -#[derive(Clone)] -pub struct UnknownErrorCode(String); - -impl std::fmt::Display for UnknownErrorCode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Display::fmt(&self.0, f) - } -} -impl std::fmt::Debug for UnknownErrorCode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Debug::fmt(&self.0, f) - } -} - -impl ErrorType { - /// Converts the error type to the string representation returned by - /// Meilisearch. - pub fn as_str(&self) -> &'static str { - match self { - ErrorType::InvalidRequest => "invalid_request", - ErrorType::Internal => "internal", - ErrorType::Auth => "auth", - } - } - /// Converts the error type string returned by Meilisearch into an - /// `ErrorType` enum. If the error type input is not recognized, None is - /// returned. - pub fn parse(input: &str) -> Option { - match input { - "invalid_request" => Some(ErrorType::InvalidRequest), - "internal" => Some(ErrorType::Internal), - "auth" => Some(ErrorType::Auth), - _ => None, - } - } -} - -impl ErrorCode { - /// Converts the error code to the string representation returned by - /// Meilisearch. - pub fn as_str(&self) -> &str { - match self { - ErrorCode::IndexCreationFailed => "index_creation_failed", - ErrorCode::IndexAlreadyExists => "index_already_exists", - ErrorCode::IndexNotFound => "index_not_found", - ErrorCode::InvalidIndexUid => "invalid_index_uid", - ErrorCode::InvalidState => "invalid_state", - ErrorCode::PrimaryKeyInferenceFailed => "primary_key_inference_failed", - ErrorCode::IndexPrimaryKeyAlreadyPresent => "index_primary_key_already_exists", - ErrorCode::InvalidRankingRule => "invalid_ranking_rule", - ErrorCode::InvalidStoreFile => "invalid_store_file", - ErrorCode::MaxFieldsLimitExceeded => "max_field_limit_exceeded", - ErrorCode::MissingDocumentId => "missing_document_id", - ErrorCode::InvalidDocumentId => "invalid_document_id", - ErrorCode::InvalidFilter => "invalid_filter", - ErrorCode::InvalidSort => "invalid_sort", - ErrorCode::BadParameter => "bad_parameter", - ErrorCode::BadRequest => "bad_request", - ErrorCode::DatabaseSizeLimitReached => "database_size_limit_reached", - ErrorCode::DocumentNotFound => "document_not_found", - ErrorCode::InternalError => "internal", - ErrorCode::InvalidGeoField => "invalid_geo_field", - ErrorCode::InvalidApiKey => "invalid_api_key", - ErrorCode::MissingAuthorizationHeader => "missing_authorization_header", - ErrorCode::TaskNotFound => "task_not_found", - ErrorCode::DumpNotFound => "dump_not_found", - ErrorCode::NoSpaceLeftOnDevice => "no_space_left_on_device", - ErrorCode::PayloadTooLarge => "payload_too_large", - ErrorCode::UnretrievableDocument => "unretrievable_document", - ErrorCode::SearchError => "search_error", - ErrorCode::UnsupportedMediaType => "unsupported_media_type", - ErrorCode::DumpAlreadyProcessing => "dump_already_processing", - ErrorCode::DumpProcessFailed => "dump_process_failed", - ErrorCode::MissingContentType => "missing_content_type", - ErrorCode::MalformedPayload => "malformed_payload", - ErrorCode::InvalidContentType => "invalid_content_type", - ErrorCode::MissingPayload => "missing_payload", - ErrorCode::MissingParameter => "missing_parameter", - ErrorCode::InvalidApiKeyDescription => "invalid_api_key_description", - ErrorCode::InvalidApiKeyActions => "invalid_api_key_actions", - ErrorCode::InvalidApiKeyIndexes => "invalid_api_key_indexes", - ErrorCode::InvalidApiKeyExpiresAt => "invalid_api_key_expires_at", - ErrorCode::ApiKeyNotFound => "api_key_not_found", - // Other than this variant, all the other `&str`s are 'static - ErrorCode::Unknown(inner) => &inner.0, - } - } - /// Converts the error code string returned by Meilisearch into an `ErrorCode` - /// enum. If the error type input is not recognized, `ErrorCode::Unknown` - /// is returned. - pub fn parse(input: &str) -> Self { - match input { - "index_creation_failed" => ErrorCode::IndexCreationFailed, - "index_already_exists" => ErrorCode::IndexAlreadyExists, - "index_not_found" => ErrorCode::IndexNotFound, - "invalid_index_uid" => ErrorCode::InvalidIndexUid, - "invalid_state" => ErrorCode::InvalidState, - "primary_key_inference_failed" => ErrorCode::PrimaryKeyInferenceFailed, - "index_primary_key_already_exists" => ErrorCode::IndexPrimaryKeyAlreadyPresent, - "invalid_ranking_rule" => ErrorCode::InvalidRankingRule, - "invalid_store_file" => ErrorCode::InvalidStoreFile, - "max_field_limit_exceeded" => ErrorCode::MaxFieldsLimitExceeded, - "missing_document_id" => ErrorCode::MissingDocumentId, - "invalid_document_id" => ErrorCode::InvalidDocumentId, - "invalid_filter" => ErrorCode::InvalidFilter, - "invalid_sort" => ErrorCode::InvalidSort, - "bad_parameter" => ErrorCode::BadParameter, - "bad_request" => ErrorCode::BadRequest, - "database_size_limit_reached" => ErrorCode::DatabaseSizeLimitReached, - "document_not_found" => ErrorCode::DocumentNotFound, - "internal" => ErrorCode::InternalError, - "invalid_geo_field" => ErrorCode::InvalidGeoField, - "invalid_api_key" => ErrorCode::InvalidApiKey, - "missing_authorization_header" => ErrorCode::MissingAuthorizationHeader, - "task_not_found" => ErrorCode::TaskNotFound, - "dump_not_found" => ErrorCode::DumpNotFound, - "no_space_left_on_device" => ErrorCode::NoSpaceLeftOnDevice, - "payload_too_large" => ErrorCode::PayloadTooLarge, - "unretrievable_document" => ErrorCode::UnretrievableDocument, - "search_error" => ErrorCode::SearchError, - "unsupported_media_type" => ErrorCode::UnsupportedMediaType, - "dump_already_processing" => ErrorCode::DumpAlreadyProcessing, - "dump_process_failed" => ErrorCode::DumpProcessFailed, - "missing_content_type" => ErrorCode::MissingContentType, - "malformed_payload" => ErrorCode::MalformedPayload, - "invalid_content_type" => ErrorCode::InvalidContentType, - "missing_payload" => ErrorCode::MissingPayload, - "invalid_api_key_description" => ErrorCode::InvalidApiKeyDescription, - "invalid_api_key_actions" => ErrorCode::InvalidApiKeyActions, - "invalid_api_key_indexes" => ErrorCode::InvalidApiKeyIndexes, - "invalid_api_key_expires_at" => ErrorCode::InvalidApiKeyExpiresAt, - "api_key_not_found" => ErrorCode::ApiKeyNotFound, - inner => ErrorCode::Unknown(UnknownErrorCode(inner.to_string())), - } - } + #[serde(other)] + Unknown, } impl std::fmt::Display for ErrorCode { fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - match self { - ErrorCode::Unknown(inner) => write!(fmt, "unknown ({})", inner), - _ => write!(fmt, "{}", self.as_str()), - } + write!( + fmt, + "{}", + // this can't fail + serde_json::to_value(self).unwrap().as_str().unwrap() + ) } } impl std::fmt::Display for Error { fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { match self { - Error::MeiliSearchError { + Error::Meilisearch(MeilisearchError { error_message, error_code, error_type, error_link, - } => write!( + }) => write!( fmt, "Meilisearch {}: {}: {}. {}", - error_type.as_str(), + error_type, error_code, error_message, error_link, @@ -275,46 +174,6 @@ impl std::fmt::Display for Error { impl std::error::Error for Error {} -impl From<&serde_json::Value> for Error { - fn from(json: &serde_json::Value) -> Error { - let error_message = json - .get("message") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - .unwrap_or_else(|| json.to_string()); - - let error_link = json - .get("link") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - .unwrap_or_else(String::new); - - let error_type = json - .get("type") - .and_then(|v| v.as_str()) - .and_then(ErrorType::parse) - .unwrap_or(ErrorType::Internal); - - // If the response doesn't contain a type field, the error type - // is assumed to be an internal error. - - let error_code = json - .get("code") - .and_then(|v| v.as_str()) - .map(ErrorCode::parse) - .unwrap_or_else(|| { - ErrorCode::Unknown(UnknownErrorCode(String::from("missing error code"))) - }); - - Error::MeiliSearchError { - error_message, - error_code, - error_type, - error_link, - } - } -} - #[cfg(not(target_arch = "wasm32"))] impl From for Error { fn from(error: isahc::Error) -> Error { @@ -325,3 +184,41 @@ impl From for Error { } } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_meilisearch_error() { + let error: MeilisearchError = serde_json::from_str( + r#" +{ + "message": "The cool error message.", + "code": "index_creation_failed", + "type": "internal", + "link": "https://the best link eveer" +}"#, + ) + .unwrap(); + + assert_eq!(error.error_message, "The cool error message."); + assert_eq!(error.error_code, ErrorCode::IndexCreationFailed); + assert_eq!(error.error_type, ErrorType::Internal); + assert_eq!(error.error_link, "https://the best link eveer"); + + let error: MeilisearchError = serde_json::from_str( + r#" +{ + "message": "", + "code": "An unknown error", + "type": "An unknown type", + "link": "" +}"#, + ) + .unwrap(); + + assert_eq!(error.error_code, ErrorCode::Unknown); + assert_eq!(error.error_type, ErrorType::Unknown); + } +} diff --git a/src/request.rs b/src/request.rs index 7556ed98..a967bf99 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,4 +1,4 @@ -use crate::errors::Error; +use crate::errors::{Error, MeilisearchError}; use log::{error, trace, warn}; use serde::{de::DeserializeOwned, Serialize}; use serde_json::{from_str, to_string}; @@ -194,8 +194,8 @@ fn parse_response( "Expected response code {}, got {}", expected_status_code, status_code ); - match from_str(&body) { - Ok(e) => Err(Error::from(&e)), + match from_str::(&body) { + Ok(e) => Err(Error::from(e)), Err(e) => Err(Error::ParseError(e)), } } diff --git a/src/tasks.rs b/src/tasks.rs index 66342425..f399ea3f 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -1,7 +1,9 @@ use serde::Deserialize; use std::time::Duration; -use crate::{client::Client, errors::Error, indexes::Index, settings::Settings}; +use crate::{ + client::Client, errors::Error, errors::MeilisearchError, indexes::Index, settings::Settings, +}; #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase", tag = "type")] @@ -41,23 +43,10 @@ pub struct IndexDeletion { pub deleted_documents: Option, } -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct TaskError { - #[serde(rename = "message")] - pub error_message: String, - #[serde(rename = "code")] - pub error_code: String, - #[serde(rename = "type")] - pub error_type: String, - #[serde(rename = "link")] - pub error_link: String, -} - #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct FailedTask { - pub error: TaskError, + pub error: MeilisearchError, #[serde(flatten)] pub task: ProcessedTask, } @@ -203,7 +192,6 @@ impl Task { /// // create the client /// let client = Client::new("http://localhost:7700", "masterKey"); /// - /// // create a new index called movies /// let task = client.create_index("try_make_index", None).await.unwrap(); /// let index = client.wait_for_task(task, None, None).await.unwrap().try_make_index(&client).unwrap(); /// @@ -225,6 +213,117 @@ impl Task { _ => Err(self), } } + + /// Unwrap the [MeilisearchError] from a [Self::Failed] [Task]. + /// + /// Will panic if the task was not [Self::Failed]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, errors::ErrorCode}; + /// # + /// # futures::executor::block_on(async move { + /// # let client = Client::new("http://localhost:7700", "masterKey"); + /// # let task = client.create_index("unwrap_failure", None).await.unwrap(); + /// # let index = client.wait_for_task(task, None, None).await.unwrap().try_make_index(&client).unwrap(); + /// + /// + /// let task = index.set_ranking_rules(["wrong_ranking_rule"]) + /// .await + /// .unwrap() + /// .wait_for_completion(&client, None, None) + /// .await + /// .unwrap(); + /// + /// assert!(task.is_failure()); + /// + /// let failure = task.unwrap_failure(); + /// + /// assert_eq!(failure.error_code, ErrorCode::InvalidRankingRule); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub fn unwrap_failure(self) -> MeilisearchError { + match self { + Self::Failed { + content: FailedTask { error, .. }, + } => error, + _ => panic!("Called `unwrap_failure` on a non `Failed` task."), + } + } + + /// Returns `true` if the [Task] is [Self::Failed]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, errors::ErrorCode}; + /// # + /// # futures::executor::block_on(async move { + /// # let client = Client::new("http://localhost:7700", "masterKey"); + /// # let task = client.create_index("is_failure", None).await.unwrap(); + /// # let index = client.wait_for_task(task, None, None).await.unwrap().try_make_index(&client).unwrap(); + /// + /// + /// let task = index.set_ranking_rules(["wrong_ranking_rule"]) + /// .await + /// .unwrap() + /// .wait_for_completion(&client, None, None) + /// .await + /// .unwrap(); + /// + /// assert!(task.is_failure()); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + pub fn is_failure(&self) -> bool { + matches!(self, Self::Failed { .. }) + } + + /// Returns `true` if the [Task] is [Self::Succeeded]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, errors::ErrorCode}; + /// # + /// # futures::executor::block_on(async move { + /// # let client = Client::new("http://localhost:7700", "masterKey"); + /// let task = client + /// .create_index("is_success", None) + /// .await + /// .unwrap() + /// .wait_for_completion(&client, None, None) + /// .await + /// .unwrap(); + /// + /// assert!(task.is_success()); + /// # task.try_make_index(&client).unwrap().delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + pub fn is_success(&self) -> bool { + matches!(self, Self::Succeeded { .. }) + } + + /// Returns `true` if the [Task] is pending ([Self::Enqueued] or [Self::Processing]). + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, errors::ErrorCode}; + /// # + /// # futures::executor::block_on(async move { + /// # let client = Client::new("http://localhost:7700", "masterKey"); + /// let task = client + /// .create_index("is_success", None) + /// .await + /// .unwrap(); + /// + /// assert!(task.is_pending()); + /// # task.wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap().delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + pub fn is_pending(&self) -> bool { + matches!(self, Self::Enqueued { .. } | Self::Processing { .. }) + } } impl AsRef for Task { @@ -268,7 +367,11 @@ pub(crate) async fn async_sleep(interval: Duration) { #[cfg(test)] mod test { use super::*; - use crate::{client::*, document}; + use crate::{ + client::*, + document, + errors::{ErrorCode, ErrorType}, + }; use meilisearch_test_macro::meilisearch_test; use serde::{Deserialize, Serialize}; use std::time::{self, Duration}; @@ -469,11 +572,9 @@ mod test { let task = movies.set_ranking_rules(["wrong_ranking_rule"]).await?; let status = client.wait_for_task(task, None, None).await?; - assert!(matches!(status, Task::Failed { .. })); - if let Task::Failed { content: status } = status { - assert_eq!(status.error.error_code, "invalid_ranking_rule"); - assert_eq!(status.error.error_type, "invalid_request"); - } + let error = status.unwrap_failure(); + assert_eq!(error.error_code, ErrorCode::InvalidRankingRule); + assert_eq!(error.error_type, ErrorType::InvalidRequest); Ok(()) } }