From 436ae4e46649ec1b56f0708e1fb6394b5828c68b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Lecrenier?= Date: Thu, 12 Jan 2023 13:55:53 +0100 Subject: [PATCH 1/9] Improve error messages generated by deserr Split Json and Query Parameter error types --- Cargo.lock | 2 +- meilisearch-types/Cargo.toml | 1 + meilisearch-types/src/error.rs | 431 +++++++++++++++++++- meilisearch-types/src/keys.rs | 30 +- meilisearch-types/src/lib.rs | 2 + meilisearch-types/src/settings.rs | 34 +- meilisearch-types/src/tasks.rs | 4 +- meilisearch/Cargo.toml | 1 - meilisearch/src/routes/api_key.rs | 16 +- meilisearch/src/routes/indexes/documents.rs | 33 +- meilisearch/src/routes/indexes/mod.rs | 30 +- meilisearch/src/routes/indexes/search.rs | 50 +-- meilisearch/src/routes/indexes/settings.rs | 26 +- meilisearch/src/routes/mod.rs | 26 +- meilisearch/src/routes/swap_indexes.rs | 9 +- meilisearch/src/routes/tasks.rs | 146 +++---- meilisearch/src/search.rs | 40 +- meilisearch/tests/auth/api_keys.rs | 32 +- meilisearch/tests/common/index.rs | 9 +- meilisearch/tests/common/server.rs | 14 +- meilisearch/tests/search/errors.rs | 58 +-- meilisearch/tests/settings/errors.rs | 44 +- meilisearch/tests/settings/get_settings.rs | 2 +- meilisearch/tests/tasks/errors.rs | 168 ++++---- meilisearch/tests/tasks/mod.rs | 42 +- 25 files changed, 802 insertions(+), 448 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f4954ca86..da1ec3011 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2348,7 +2348,6 @@ dependencies = [ "rustls-pemfile", "segment", "serde", - "serde-cs", "serde_json", "serde_urlencoded", "sha-1", @@ -2413,6 +2412,7 @@ dependencies = [ "proptest-derive", "roaring", "serde", + "serde-cs", "serde_json", "tar", "tempfile", diff --git a/meilisearch-types/Cargo.toml b/meilisearch-types/Cargo.toml index bd596ba2d..4c0b1ca93 100644 --- a/meilisearch-types/Cargo.toml +++ b/meilisearch-types/Cargo.toml @@ -21,6 +21,7 @@ proptest = { version = "1.0.0", optional = true } proptest-derive = { version = "0.3.0", optional = true } roaring = { version = "0.10.0", features = ["serde"] } serde = { version = "1.0.145", features = ["derive"] } +serde-cs = "0.2.4" serde_json = "1.0.85" tar = "0.4.38" tempfile = "3.3.0" diff --git a/meilisearch-types/src/error.rs b/meilisearch-types/src/error.rs index bc29f9e82..2be6ffff4 100644 --- a/meilisearch-types/src/error.rs +++ b/meilisearch-types/src/error.rs @@ -1,14 +1,18 @@ use std::convert::Infallible; use std::marker::PhantomData; +use std::str::FromStr; use std::{fmt, io}; use actix_web::http::StatusCode; use actix_web::{self as aweb, HttpResponseBuilder}; use aweb::rt::task::JoinError; use convert_case::Casing; -use deserr::{DeserializeError, IntoValue, MergeWithError, ValuePointerRef}; +use deserr::{DeserializeError, ErrorKind, IntoValue, MergeWithError, ValueKind, ValuePointerRef}; use milli::heed::{Error as HeedError, MdbError}; use serde::{Deserialize, Serialize}; +use serde_cs::vec::CS; + +use crate::star_or::StarOr; use self::deserr_codes::MissingIndexUid; @@ -422,41 +426,49 @@ mod strategy { } } -pub struct DeserrError { +pub struct DeserrJson; +pub struct DeserrQueryParam; + +pub type DeserrJsonError = DeserrError; +pub type DeserrQueryParamError = DeserrError; + +pub struct DeserrError { pub msg: String, pub code: Code, - _phantom: PhantomData, + _phantom: PhantomData<(Format, C)>, } -impl std::fmt::Debug for DeserrError { +impl std::fmt::Debug for DeserrError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("DeserrError").field("msg", &self.msg).field("code", &self.code).finish() } } -impl std::fmt::Display for DeserrError { +impl std::fmt::Display for DeserrError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.msg) } } -impl std::error::Error for DeserrError {} -impl ErrorCode for DeserrError { +impl std::error::Error for DeserrError {} +impl ErrorCode for DeserrError { fn error_code(&self) -> Code { self.code } } -impl MergeWithError> for DeserrError { +impl + MergeWithError> for DeserrError +{ fn merge( _self_: Option, - other: DeserrError, + other: DeserrError, _merge_location: ValuePointerRef, ) -> Result { Err(DeserrError { msg: other.msg, code: other.code, _phantom: PhantomData }) } } -impl DeserrError { +impl DeserrJsonError { pub fn missing_index_uid(field: &str, location: ValuePointerRef) -> Self { let x = unwrap_any(Self::error::( None, @@ -467,21 +479,364 @@ impl DeserrError { } } -impl deserr::DeserializeError for DeserrError { +// if the error happened in the root, then an empty string is returned. +pub fn location_json_description(location: ValuePointerRef, article: &str) -> String { + fn rec(location: ValuePointerRef) -> String { + match location { + ValuePointerRef::Origin => String::new(), + ValuePointerRef::Key { key, prev } => rec(*prev) + "." + key, + ValuePointerRef::Index { index, prev } => format!("{}[{index}]", rec(*prev)), + } + } + match location { + ValuePointerRef::Origin => String::new(), + _ => { + format!("{article} `{}`", rec(location)) + } + } +} + +fn value_kinds_description_json(kinds: &[ValueKind]) -> String { + fn order(kind: &ValueKind) -> u8 { + match kind { + ValueKind::Null => 0, + ValueKind::Boolean => 1, + ValueKind::Integer => 2, + ValueKind::NegativeInteger => 3, + ValueKind::Float => 4, + ValueKind::String => 5, + ValueKind::Sequence => 6, + ValueKind::Map => 7, + } + } + + fn single_description(kind: &ValueKind) -> &'static str { + match kind { + ValueKind::Null => "null", + ValueKind::Boolean => "a boolean", + ValueKind::Integer => "a positive integer", + ValueKind::NegativeInteger => "an integer", + ValueKind::Float => "a number", + ValueKind::String => "a string", + ValueKind::Sequence => "an array", + ValueKind::Map => "an object", + } + } + + fn description_rec(kinds: &[ValueKind], count_items: &mut usize, message: &mut String) { + let (msg_part, rest): (_, &[ValueKind]) = match kinds { + [] => (String::new(), &[]), + [ValueKind::Integer | ValueKind::NegativeInteger, ValueKind::Float, rest @ ..] => { + ("a number".to_owned(), rest) + } + [ValueKind::Integer, ValueKind::NegativeInteger, ValueKind::Float, rest @ ..] => { + ("a number".to_owned(), rest) + } + [ValueKind::Integer, ValueKind::NegativeInteger, rest @ ..] => { + ("an integer".to_owned(), rest) + } + [a] => (single_description(a).to_owned(), &[]), + [a, rest @ ..] => (single_description(a).to_owned(), rest), + }; + + if rest.is_empty() { + if *count_items == 0 { + message.push_str(&msg_part); + } else if *count_items == 1 { + message.push_str(&format!(" or {msg_part}")); + } else { + message.push_str(&format!(", or {msg_part}")); + } + } else { + if *count_items == 0 { + message.push_str(&msg_part); + } else { + message.push_str(&format!(", {msg_part}")); + } + + *count_items += 1; + description_rec(rest, count_items, message); + } + } + + let mut kinds = kinds.to_owned(); + kinds.sort_by_key(order); + kinds.dedup(); + + if kinds.is_empty() { + "a different value".to_owned() + } else { + let mut message = String::new(); + description_rec(kinds.as_slice(), &mut 0, &mut message); + message + } +} + +fn value_description_with_kind_json(v: &serde_json::Value) -> String { + match v.kind() { + ValueKind::Null => "null".to_owned(), + kind => { + format!( + "{}: `{}`", + value_kinds_description_json(&[kind]), + serde_json::to_string(v).unwrap() + ) + } + } +} + +impl deserr::DeserializeError for DeserrJsonError { fn error( _self_: Option, error: deserr::ErrorKind, location: ValuePointerRef, ) -> Result { - let msg = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0; + let mut message = String::new(); - Err(DeserrError { msg, code: C::default().error_code(), _phantom: PhantomData }) + message.push_str(&match error { + ErrorKind::IncorrectValueKind { actual, accepted } => { + let expected = value_kinds_description_json(accepted); + // if we're not able to get the value as a string then we print nothing. + let received = value_description_with_kind_json(&serde_json::Value::from(actual)); + + let location = location_json_description(location, " at"); + + format!("Invalid value type{location}: expected {expected}, but found {received}") + } + ErrorKind::MissingField { field } => { + // serde_json original message: + // Json deserialize error: missing field `lol` at line 1 column 2 + let location = location_json_description(location, " inside"); + format!("Missing field `{field}`{location}") + } + ErrorKind::UnknownKey { key, accepted } => { + let location = location_json_description(location, " inside"); + format!( + "Unknown field `{}`{location}: expected one of {}", + key, + accepted + .iter() + .map(|accepted| format!("`{}`", accepted)) + .collect::>() + .join(", ") + ) + } + ErrorKind::UnknownValue { value, accepted } => { + let location = location_json_description(location, " at"); + format!( + "Unknown value `{}`{location}: expected one of {}", + value, + accepted + .iter() + .map(|accepted| format!("`{}`", accepted)) + .collect::>() + .join(", "), + ) + } + ErrorKind::Unexpected { msg } => { + let location = location_json_description(location, " at"); + // serde_json original message: + // The json payload provided is malformed. `trailing characters at line 1 column 19`. + format!("Invalid value{location}: {msg}") + } + }); + + Err(DeserrJsonError { + msg: message, + code: C::default().error_code(), + _phantom: PhantomData, + }) } } +// if the error happened in the root, then an empty string is returned. +pub fn location_query_param_description(location: ValuePointerRef, article: &str) -> String { + fn rec(location: ValuePointerRef) -> String { + match location { + ValuePointerRef::Origin => String::new(), + ValuePointerRef::Key { key, prev } => { + if matches!(prev, ValuePointerRef::Origin) { + key.to_owned() + } else { + rec(*prev) + "." + key + } + } + ValuePointerRef::Index { index, prev } => format!("{}[{index}]", rec(*prev)), + } + } + match location { + ValuePointerRef::Origin => String::new(), + _ => { + format!("{article} `{}`", rec(location)) + } + } +} + +impl deserr::DeserializeError for DeserrQueryParamError { + fn error( + _self_: Option, + error: deserr::ErrorKind, + location: ValuePointerRef, + ) -> Result { + let mut message = String::new(); + + message.push_str(&match error { + ErrorKind::IncorrectValueKind { actual, accepted } => { + let expected = value_kinds_description_query_param(accepted); + // if we're not able to get the value as a string then we print nothing. + let received = value_description_with_kind_query_param(actual); + + let location = location_query_param_description(location, " for parameter"); + + format!("Invalid value type{location}: expected {expected}, but found {received}") + } + ErrorKind::MissingField { field } => { + // serde_json original message: + // Json deserialize error: missing field `lol` at line 1 column 2 + let location = location_query_param_description(location, " inside"); + format!("Missing parameter `{field}`{location}") + } + ErrorKind::UnknownKey { key, accepted } => { + let location = location_query_param_description(location, " inside"); + format!( + "Unknown parameter `{}`{location}: expected one of {}", + key, + accepted + .iter() + .map(|accepted| format!("`{}`", accepted)) + .collect::>() + .join(", ") + ) + } + ErrorKind::UnknownValue { value, accepted } => { + let location = location_query_param_description(location, " for parameter"); + format!( + "Unknown value `{}`{location}: expected one of {}", + value, + accepted + .iter() + .map(|accepted| format!("`{}`", accepted)) + .collect::>() + .join(", "), + ) + } + ErrorKind::Unexpected { msg } => { + let location = location_query_param_description(location, " in parameter"); + // serde_json original message: + // The json payload provided is malformed. `trailing characters at line 1 column 19`. + format!("Invalid value{location}: {msg}") + } + }); + + Err(DeserrQueryParamError { + msg: message, + code: C::default().error_code(), + _phantom: PhantomData, + }) + } +} + +fn value_kinds_description_query_param(_accepted: &[ValueKind]) -> String { + "a string".to_owned() +} + +fn value_description_with_kind_query_param(actual: deserr::Value) -> String { + match actual { + deserr::Value::Null => "null".to_owned(), + deserr::Value::Boolean(x) => format!("a boolean: `{x}`"), + deserr::Value::Integer(x) => format!("an integer: `{x}`"), + deserr::Value::NegativeInteger(x) => { + format!("an integer: `{x}`") + } + deserr::Value::Float(x) => { + format!("a number: `{x}`") + } + deserr::Value::String(x) => { + format!("a string: `{x}`") + } + deserr::Value::Sequence(_) => "multiple values".to_owned(), + deserr::Value::Map(_) => "multiple parameters".to_owned(), + } +} + +#[derive(Debug)] +pub struct DetailedParseIntError(String); +impl fmt::Display for DetailedParseIntError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "could not parse `{}` as a positive integer", self.0) + } +} +impl std::error::Error for DetailedParseIntError {} + +pub fn parse_u32_query_param(x: String) -> Result> { + x.parse::().map_err(|_e| TakeErrorMessage(DetailedParseIntError(x.to_owned()))) +} +pub fn parse_usize_query_param( + x: String, +) -> Result> { + x.parse::().map_err(|_e| TakeErrorMessage(DetailedParseIntError(x.to_owned()))) +} +pub fn parse_option_usize_query_param( + s: Option, +) -> Result, TakeErrorMessage> { + if let Some(s) = s { + parse_usize_query_param(s).map(Some) + } else { + Ok(None) + } +} +pub fn parse_option_u32_query_param( + s: Option, +) -> Result, TakeErrorMessage> { + if let Some(s) = s { + parse_u32_query_param(s).map(Some) + } else { + Ok(None) + } +} +pub fn parse_option_vec_u32_query_param( + s: Option>, +) -> Result>, TakeErrorMessage> { + if let Some(s) = s { + s.into_iter() + .map(parse_u32_query_param) + .collect::, TakeErrorMessage>>() + .map(Some) + } else { + Ok(None) + } +} +pub fn parse_option_cs_star_or( + s: Option>>, +) -> Result>, TakeErrorMessage> { + if let Some(s) = s.and_then(fold_star_or) as Option> { + s.into_iter() + .map(|s| T::from_str(&s)) + .collect::, T::Err>>() + .map_err(TakeErrorMessage) + .map(Some) + } else { + Ok(None) + } +} + +/// Extracts the raw values from the `StarOr` types and +/// return None if a `StarOr::Star` is encountered. +pub fn fold_star_or(content: impl IntoIterator>) -> Option +where + O: FromIterator, +{ + content + .into_iter() + .map(|value| match value { + StarOr::Star => None, + StarOr::Other(val) => Some(val), + }) + .collect() +} pub struct TakeErrorMessage(pub T); -impl MergeWithError> for DeserrError +impl MergeWithError> for DeserrJsonError where T: std::error::Error, { @@ -490,7 +845,24 @@ where other: TakeErrorMessage, merge_location: ValuePointerRef, ) -> Result { - DeserrError::error::( + DeserrJsonError::error::( + None, + deserr::ErrorKind::Unexpected { msg: other.0.to_string() }, + merge_location, + ) + } +} + +impl MergeWithError> for DeserrQueryParamError +where + T: std::error::Error, +{ + fn merge( + _self_: Option, + other: TakeErrorMessage, + merge_location: ValuePointerRef, + ) -> Result { + DeserrQueryParamError::error::( None, deserr::ErrorKind::Unexpected { msg: other.0.to_string() }, merge_location, @@ -510,3 +882,32 @@ macro_rules! internal_error { )* } } + +#[cfg(test)] +mod tests { + use deserr::ValueKind; + + use crate::error::value_kinds_description_json; + + #[test] + fn test_value_kinds_description_json() { + insta::assert_display_snapshot!(value_kinds_description_json(&[]), @"a different value"); + + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Boolean]), @"a boolean"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer]), @"a positive integer"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::NegativeInteger]), @"an integer"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer]), @"a positive integer"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::String]), @"a string"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Sequence]), @"an array"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Map]), @"an object"); + + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer, ValueKind::Boolean]), @"a boolean or a positive integer"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Null, ValueKind::Integer]), @"null or a positive integer"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Sequence, ValueKind::NegativeInteger]), @"an integer or an array"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer, ValueKind::Float]), @"a number"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer, ValueKind::Float, ValueKind::NegativeInteger]), @"a number"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer, ValueKind::Float, ValueKind::NegativeInteger, ValueKind::Null]), @"null or a number"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Boolean, ValueKind::Integer, ValueKind::Float, ValueKind::NegativeInteger, ValueKind::Null]), @"null, a boolean, or a number"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Null, ValueKind::Boolean, ValueKind::Integer, ValueKind::Float, ValueKind::NegativeInteger, ValueKind::Null]), @"null, a boolean, or a number"); + } +} diff --git a/meilisearch-types/src/keys.rs b/meilisearch-types/src/keys.rs index b41bb06b6..53776e489 100644 --- a/meilisearch-types/src/keys.rs +++ b/meilisearch-types/src/keys.rs @@ -11,19 +11,19 @@ use time::{Date, OffsetDateTime, PrimitiveDateTime}; use uuid::Uuid; use crate::error::deserr_codes::*; -use crate::error::{unwrap_any, Code, DeserrError, ErrorCode, TakeErrorMessage}; +use crate::error::{unwrap_any, Code, DeserrJsonError, ErrorCode, TakeErrorMessage}; use crate::index_uid::{IndexUid, IndexUidFormatError}; use crate::star_or::StarOr; pub type KeyId = Uuid; -impl MergeWithError for DeserrError { +impl MergeWithError for DeserrJsonError { fn merge( _self_: Option, other: IndexUidFormatError, merge_location: deserr::ValuePointerRef, ) -> std::result::Result { - DeserrError::error::( + DeserrJsonError::error::( None, deserr::ErrorKind::Unexpected { msg: other.to_string() }, merge_location, @@ -36,19 +36,19 @@ fn parse_uuid_from_str(s: &str) -> Result> { } #[derive(Debug, DeserializeFromValue)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] pub struct CreateApiKey { - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub description: Option, - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub name: Option, - #[deserr(default = Uuid::new_v4(), error = DeserrError, from(&String) = parse_uuid_from_str -> TakeErrorMessage)] + #[deserr(default = Uuid::new_v4(), error = DeserrJsonError, from(&String) = parse_uuid_from_str -> TakeErrorMessage)] pub uid: KeyId, - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub actions: Vec, - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub indexes: Vec>, - #[deserr(error = DeserrError, default = None, from(&String) = parse_expiration_date -> TakeErrorMessage)] + #[deserr(error = DeserrJsonError, default = None, from(&String) = parse_expiration_date -> TakeErrorMessage)] pub expires_at: Option, } impl CreateApiKey { @@ -72,8 +72,8 @@ fn deny_immutable_fields_api_key( field: &str, accepted: &[&str], location: ValuePointerRef, -) -> DeserrError { - let mut error = unwrap_any(DeserrError::::error::( +) -> DeserrJsonError { + let mut error = unwrap_any(DeserrJsonError::::error::( None, deserr::ErrorKind::UnknownKey { key: field, accepted }, location, @@ -92,11 +92,11 @@ fn deny_immutable_fields_api_key( } #[derive(Debug, DeserializeFromValue)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_api_key)] +#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_api_key)] pub struct PatchApiKey { - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub description: Option, - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub name: Option, } diff --git a/meilisearch-types/src/lib.rs b/meilisearch-types/src/lib.rs index c7f7ca7f5..f8fc47abd 100644 --- a/meilisearch-types/src/lib.rs +++ b/meilisearch-types/src/lib.rs @@ -8,8 +8,10 @@ pub mod star_or; pub mod tasks; pub mod versioning; +pub use deserr; pub use milli; pub use milli::{heed, Index}; +pub use serde_cs; use uuid::Uuid; pub use versioning::VERSION_FILE_NAME; diff --git a/meilisearch-types/src/settings.rs b/meilisearch-types/src/settings.rs index 3169920a6..0a79f865e 100644 --- a/meilisearch-types/src/settings.rs +++ b/meilisearch-types/src/settings.rs @@ -12,7 +12,7 @@ use milli::{Criterion, CriterionError, Index, DEFAULT_VALUES_PER_FACET}; use serde::{Deserialize, Serialize, Serializer}; use crate::error::deserr_codes::*; -use crate::error::{unwrap_any, DeserrError}; +use crate::error::{unwrap_any, DeserrJsonError}; /// The maximimum number of results that the engine /// will be able to return in one search call. @@ -66,7 +66,7 @@ fn validate_min_word_size_for_typo_setting( #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)] #[serde(deny_unknown_fields, rename_all = "camelCase")] -#[deserr(deny_unknown_fields, rename_all = camelCase, validate = validate_min_word_size_for_typo_setting -> DeserrError)] +#[deserr(deny_unknown_fields, rename_all = camelCase, validate = validate_min_word_size_for_typo_setting -> DeserrJsonError)] pub struct MinWordSizeTyposSetting { #[serde(default, skip_serializing_if = "Setting::is_not_set")] pub one_typo: Setting, @@ -76,12 +76,12 @@ pub struct MinWordSizeTyposSetting { #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)] #[serde(deny_unknown_fields, rename_all = "camelCase")] -#[deserr(deny_unknown_fields, rename_all = camelCase, where_predicate = __Deserr_E: deserr::MergeWithError>)] +#[deserr(deny_unknown_fields, rename_all = camelCase, where_predicate = __Deserr_E: deserr::MergeWithError>)] pub struct TypoSettings { #[serde(default, skip_serializing_if = "Setting::is_not_set")] pub enabled: Setting, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub min_word_size_for_typos: Setting, #[serde(default, skip_serializing_if = "Setting::is_not_set")] pub disable_on_words: Setting>, @@ -105,7 +105,7 @@ pub struct PaginationSettings { pub max_total_hits: Setting, } -impl MergeWithError for DeserrError { +impl MergeWithError for DeserrJsonError { fn merge( _self_: Option, other: milli::CriterionError, @@ -128,14 +128,14 @@ impl MergeWithError for DeserrError { #[serde( default, serialize_with = "serialize_with_wildcard", skip_serializing_if = "Setting::is_not_set" )] - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub displayed_attributes: Setting>, #[serde( @@ -143,35 +143,35 @@ pub struct Settings { serialize_with = "serialize_with_wildcard", skip_serializing_if = "Setting::is_not_set" )] - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub searchable_attributes: Setting>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub filterable_attributes: Setting>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub sortable_attributes: Setting>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub ranking_rules: Setting>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub stop_words: Setting>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub synonyms: Setting>>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub distinct_attribute: Setting, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub typo_tolerance: Setting, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub faceting: Setting, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub pagination: Setting, #[serde(skip)] diff --git a/meilisearch-types/src/tasks.rs b/meilisearch-types/src/tasks.rs index ceddbd51c..fd2d31e06 100644 --- a/meilisearch-types/src/tasks.rs +++ b/meilisearch-types/src/tasks.rs @@ -348,7 +348,7 @@ impl FromStr for Status { } else { Err(ResponseError::from_msg( format!( - "`{}` is not a status. Available status are {}.", + "`{}` is not a valid task status. Available statuses are {}.", status, enum_iterator::all::() .map(|s| format!("`{s}`")) @@ -440,7 +440,7 @@ impl FromStr for Kind { } else { Err(ResponseError::from_msg( format!( - "`{}` is not a type. Available types are {}.", + "`{}` is not a valid task type. Available types are {}.", kind, enum_iterator::all::() .map(|k| format!( diff --git a/meilisearch/Cargo.toml b/meilisearch/Cargo.toml index a42e5cc7b..be852c02e 100644 --- a/meilisearch/Cargo.toml +++ b/meilisearch/Cargo.toml @@ -55,7 +55,6 @@ rustls = "0.20.6" rustls-pemfile = "1.0.1" segment = { version = "0.2.1", optional = true } serde = { version = "1.0.145", features = ["derive"] } -serde-cs = "0.2.4" serde_json = { version = "1.0.85", features = ["preserve_order"] } sha2 = "0.10.6" siphasher = "0.3.10" diff --git a/meilisearch/src/routes/api_key.rs b/meilisearch/src/routes/api_key.rs index 76912bbaa..ce4ab0696 100644 --- a/meilisearch/src/routes/api_key.rs +++ b/meilisearch/src/routes/api_key.rs @@ -4,8 +4,8 @@ use actix_web::{web, HttpRequest, HttpResponse}; use deserr::DeserializeFromValue; use meilisearch_auth::error::AuthControllerError; use meilisearch_auth::AuthController; -use meilisearch_types::error::deserr_codes::*; -use meilisearch_types::error::{Code, DeserrError, ResponseError, TakeErrorMessage}; +use meilisearch_types::error::{deserr_codes::*, DeserrQueryParamError}; +use meilisearch_types::error::{Code, DeserrJsonError, ResponseError, TakeErrorMessage}; use meilisearch_types::keys::{Action, CreateApiKey, Key, PatchApiKey}; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; @@ -36,7 +36,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) { pub async fn create_api_key( auth_controller: GuardedData, AuthController>, - body: ValidatedJson, + body: ValidatedJson, _req: HttpRequest, ) -> Result { let v = body.into_inner(); @@ -51,14 +51,14 @@ pub async fn create_api_key( } #[derive(DeserializeFromValue, Deserialize, Debug, Clone, Copy)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ListApiKeys { #[serde(default)] - #[deserr(error = DeserrError, default, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] pub offset: usize, #[serde(default = "PAGINATION_DEFAULT_LIMIT")] - #[deserr(error = DeserrError, default = PAGINATION_DEFAULT_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default = PAGINATION_DEFAULT_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] pub limit: usize, } impl ListApiKeys { @@ -69,7 +69,7 @@ impl ListApiKeys { pub async fn list_api_keys( auth_controller: GuardedData, AuthController>, - list_api_keys: QueryParameter, + list_api_keys: QueryParameter, ) -> Result { let paginate = list_api_keys.into_inner().as_pagination(); let page_view = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> { @@ -106,7 +106,7 @@ pub async fn get_api_key( pub async fn patch_api_key( auth_controller: GuardedData, AuthController>, - body: ValidatedJson, + body: ValidatedJson, path: web::Path, ) -> Result { let key = path.into_inner().key; diff --git a/meilisearch/src/routes/indexes/documents.rs b/meilisearch/src/routes/indexes/documents.rs index 3a54bba6a..c09b12244 100644 --- a/meilisearch/src/routes/indexes/documents.rs +++ b/meilisearch/src/routes/indexes/documents.rs @@ -10,18 +10,18 @@ use futures::StreamExt; use index_scheduler::IndexScheduler; use log::debug; use meilisearch_types::document_formats::{read_csv, read_json, read_ndjson, PayloadType}; -use meilisearch_types::error::deserr_codes::*; -use meilisearch_types::error::{DeserrError, ResponseError, TakeErrorMessage}; +use meilisearch_types::error::{deserr_codes::*, fold_star_or, DeserrQueryParamError}; +use meilisearch_types::error::{DeserrJsonError, ResponseError, TakeErrorMessage}; use meilisearch_types::heed::RoTxn; use meilisearch_types::index_uid::IndexUid; use meilisearch_types::milli::update::IndexDocumentsMethod; +use meilisearch_types::serde_cs::vec::CS; use meilisearch_types::star_or::StarOr; use meilisearch_types::tasks::KindWithContent; use meilisearch_types::{milli, Document, Index}; use mime::Mime; use once_cell::sync::Lazy; use serde::Deserialize; -use serde_cs::vec::CS; use serde_json::Value; use tempfile::tempfile; use tokio::fs::File; @@ -36,7 +36,7 @@ use crate::extractors::authentication::GuardedData; use crate::extractors::payload::Payload; use crate::extractors::query_parameters::QueryParameter; use crate::extractors::sequential_extractor::SeqHandler; -use crate::routes::{fold_star_or, PaginationView, SummarizedTaskView}; +use crate::routes::{PaginationView, SummarizedTaskView}; static ACCEPTED_CONTENT_TYPE: Lazy> = Lazy::new(|| { vec!["application/json".to_string(), "application/x-ndjson".to_string(), "text/csv".to_string()] @@ -82,16 +82,17 @@ pub fn configure(cfg: &mut web::ServiceConfig) { } #[derive(Deserialize, Debug, DeserializeFromValue)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] pub struct GetDocument { - #[deserr(error = DeserrError)] + // TODO: strongly typed argument here + #[deserr(error = DeserrQueryParamError)] fields: Option>>, } pub async fn get_document( index_scheduler: GuardedData, Data>, path: web::Path, - params: QueryParameter, + params: QueryParameter, ) -> Result { let GetDocument { fields } = params.into_inner(); let attributes_to_retrieve = fields.and_then(fold_star_or); @@ -119,20 +120,20 @@ pub async fn delete_document( } #[derive(Deserialize, Debug, DeserializeFromValue)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] pub struct BrowseQuery { - #[deserr(error = DeserrError, default, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] offset: usize, - #[deserr(error = DeserrError, default = crate::routes::PAGINATION_DEFAULT_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default = crate::routes::PAGINATION_DEFAULT_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] limit: usize, - #[deserr(error = DeserrError)] + #[deserr(error = DeserrQueryParamError)] fields: Option>>, } pub async fn get_all_documents( index_scheduler: GuardedData, Data>, index_uid: web::Path, - params: QueryParameter, + params: QueryParameter, ) -> Result { debug!("called with params: {:?}", params); let BrowseQuery { limit, offset, fields } = params.into_inner(); @@ -148,16 +149,16 @@ pub async fn get_all_documents( } #[derive(Deserialize, Debug, DeserializeFromValue)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] pub struct UpdateDocumentsQuery { - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub primary_key: Option, } pub async fn add_documents( index_scheduler: GuardedData, Data>, index_uid: web::Path, - params: QueryParameter, + params: QueryParameter, body: Payload, req: HttpRequest, analytics: web::Data, @@ -185,7 +186,7 @@ pub async fn add_documents( pub async fn update_documents( index_scheduler: GuardedData, Data>, path: web::Path, - params: QueryParameter, + params: QueryParameter, body: Payload, req: HttpRequest, analytics: web::Data, diff --git a/meilisearch/src/routes/indexes/mod.rs b/meilisearch/src/routes/indexes/mod.rs index 7a3a97c1f..061eefaf6 100644 --- a/meilisearch/src/routes/indexes/mod.rs +++ b/meilisearch/src/routes/indexes/mod.rs @@ -5,8 +5,8 @@ use actix_web::{web, HttpRequest, HttpResponse}; use deserr::{DeserializeError, DeserializeFromValue, ValuePointerRef}; use index_scheduler::IndexScheduler; use log::debug; -use meilisearch_types::error::deserr_codes::*; -use meilisearch_types::error::{unwrap_any, Code, DeserrError, ResponseError, TakeErrorMessage}; +use meilisearch_types::error::{deserr_codes::*, unwrap_any, Code, DeserrQueryParamError}; +use meilisearch_types::error::{DeserrJsonError, ResponseError, TakeErrorMessage}; use meilisearch_types::index_uid::IndexUid; use meilisearch_types::milli::{self, FieldDistribution, Index}; use meilisearch_types::tasks::KindWithContent; @@ -72,14 +72,14 @@ impl IndexView { } #[derive(DeserializeFromValue, Deserialize, Debug, Clone, Copy)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ListIndexes { #[serde(default)] - #[deserr(error = DeserrError, default, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] pub offset: usize, #[serde(default = "PAGINATION_DEFAULT_LIMIT")] - #[deserr(error = DeserrError, default = PAGINATION_DEFAULT_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default = PAGINATION_DEFAULT_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] pub limit: usize, } impl ListIndexes { @@ -90,7 +90,7 @@ impl ListIndexes { pub async fn list_indexes( index_scheduler: GuardedData, Data>, - paginate: QueryParameter, + paginate: QueryParameter, ) -> Result { let search_rules = &index_scheduler.filters().search_rules; let indexes: Vec<_> = index_scheduler.indexes()?; @@ -107,17 +107,17 @@ pub async fn list_indexes( } #[derive(DeserializeFromValue, Debug)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] pub struct IndexCreateRequest { - #[deserr(error = DeserrError, missing_field_error = DeserrError::missing_index_uid)] + #[deserr(error = DeserrJsonError, missing_field_error = DeserrJsonError::missing_index_uid)] uid: String, - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] primary_key: Option, } pub async fn create_index( index_scheduler: GuardedData, Data>, - body: ValidatedJson, + body: ValidatedJson, req: HttpRequest, analytics: web::Data, ) -> Result { @@ -146,8 +146,8 @@ fn deny_immutable_fields_index( field: &str, accepted: &[&str], location: ValuePointerRef, -) -> DeserrError { - let mut error = unwrap_any(DeserrError::::error::( +) -> DeserrJsonError { + let mut error = unwrap_any(DeserrJsonError::::error::( None, deserr::ErrorKind::UnknownKey { key: field, accepted }, location, @@ -162,9 +162,9 @@ fn deny_immutable_fields_index( error } #[derive(DeserializeFromValue, Debug)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_index)] +#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_index)] pub struct UpdateIndexRequest { - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] primary_key: Option, } @@ -183,7 +183,7 @@ pub async fn get_index( pub async fn update_index( index_scheduler: GuardedData, Data>, path: web::Path, - body: ValidatedJson, + body: ValidatedJson, req: HttpRequest, analytics: web::Data, ) -> Result { diff --git a/meilisearch/src/routes/indexes/search.rs b/meilisearch/src/routes/indexes/search.rs index 6296772e0..8819ac8cf 100644 --- a/meilisearch/src/routes/indexes/search.rs +++ b/meilisearch/src/routes/indexes/search.rs @@ -5,9 +5,12 @@ use actix_web::{web, HttpRequest, HttpResponse}; use index_scheduler::IndexScheduler; use log::debug; use meilisearch_auth::IndexSearchRules; -use meilisearch_types::error::deserr_codes::*; -use meilisearch_types::error::{DeserrError, ResponseError, TakeErrorMessage}; -use serde_cs::vec::CS; +use meilisearch_types::error::{ + deserr_codes::*, parse_option_usize_query_param, parse_usize_query_param, + DeserrQueryParamError, DetailedParseIntError, +}; +use meilisearch_types::error::{DeserrJsonError, ResponseError, TakeErrorMessage}; +use meilisearch_types::serde_cs::vec::CS; use serde_json::Value; use crate::analytics::{Analytics, SearchAggregator}; @@ -16,7 +19,6 @@ use crate::extractors::authentication::GuardedData; use crate::extractors::json::ValidatedJson; use crate::extractors::query_parameters::QueryParameter; use crate::extractors::sequential_extractor::SeqHandler; -use crate::routes::from_string_to_option_take_error_message; use crate::search::{ perform_search, MatchingStrategy, SearchQuery, DEFAULT_CROP_LENGTH, DEFAULT_CROP_MARKER, DEFAULT_HIGHLIGHT_POST_TAG, DEFAULT_HIGHLIGHT_PRE_TAG, DEFAULT_SEARCH_LIMIT, @@ -44,41 +46,41 @@ pub fn parse_bool_take_error_message( } #[derive(Debug, deserr::DeserializeFromValue)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] pub struct SearchQueryGet { - #[deserr(error = DeserrError)] + #[deserr(error = DeserrQueryParamError)] q: Option, - #[deserr(error = DeserrError, default = DEFAULT_SEARCH_OFFSET(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default = DEFAULT_SEARCH_OFFSET(), from(String) = parse_usize_query_param -> TakeErrorMessage)] offset: usize, - #[deserr(error = DeserrError, default = DEFAULT_SEARCH_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default = DEFAULT_SEARCH_LIMIT(), from(String) = parse_usize_query_param -> TakeErrorMessage)] limit: usize, - #[deserr(error = DeserrError, from(&String) = from_string_to_option_take_error_message -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, from(Option) = parse_option_usize_query_param -> TakeErrorMessage)] page: Option, - #[deserr(error = DeserrError, from(&String) = from_string_to_option_take_error_message -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, from(Option) = parse_option_usize_query_param -> TakeErrorMessage)] hits_per_page: Option, - #[deserr(error = DeserrError)] + #[deserr(error = DeserrQueryParamError)] attributes_to_retrieve: Option>, - #[deserr(error = DeserrError)] + #[deserr(error = DeserrQueryParamError)] attributes_to_crop: Option>, - #[deserr(error = DeserrError, default = DEFAULT_CROP_LENGTH(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default = DEFAULT_CROP_LENGTH(), from(String) = parse_usize_query_param -> TakeErrorMessage)] crop_length: usize, - #[deserr(error = DeserrError)] + #[deserr(error = DeserrQueryParamError)] attributes_to_highlight: Option>, - #[deserr(error = DeserrError)] + #[deserr(error = DeserrQueryParamError)] filter: Option, - #[deserr(error = DeserrError)] + #[deserr(error = DeserrQueryParamError)] sort: Option, - #[deserr(error = DeserrError, default, from(&String) = parse_bool_take_error_message -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default, from(&String) = parse_bool_take_error_message -> TakeErrorMessage)] show_matches_position: bool, - #[deserr(error = DeserrError)] + #[deserr(error = DeserrQueryParamError)] facets: Option>, - #[deserr(error = DeserrError, default = DEFAULT_HIGHLIGHT_PRE_TAG())] + #[deserr(error = DeserrQueryParamError, default = DEFAULT_HIGHLIGHT_PRE_TAG())] highlight_pre_tag: String, - #[deserr(error = DeserrError, default = DEFAULT_HIGHLIGHT_POST_TAG())] + #[deserr(error = DeserrQueryParamError, default = DEFAULT_HIGHLIGHT_POST_TAG())] highlight_post_tag: String, - #[deserr(error = DeserrError, default = DEFAULT_CROP_MARKER())] + #[deserr(error = DeserrQueryParamError, default = DEFAULT_CROP_MARKER())] crop_marker: String, - #[deserr(error = DeserrError, default)] + #[deserr(error = DeserrQueryParamError, default)] matching_strategy: MatchingStrategy, } @@ -162,7 +164,7 @@ fn fix_sort_query_parameters(sort_query: &str) -> Vec { pub async fn search_with_url_query( index_scheduler: GuardedData, Data>, index_uid: web::Path, - params: QueryParameter, + params: QueryParameter, req: HttpRequest, analytics: web::Data, ) -> Result { @@ -194,7 +196,7 @@ pub async fn search_with_url_query( pub async fn search_with_post( index_scheduler: GuardedData, Data>, index_uid: web::Path, - params: ValidatedJson, + params: ValidatedJson, req: HttpRequest, analytics: web::Data, ) -> Result { diff --git a/meilisearch/src/routes/indexes/settings.rs b/meilisearch/src/routes/indexes/settings.rs index 13c280d63..404835833 100644 --- a/meilisearch/src/routes/indexes/settings.rs +++ b/meilisearch/src/routes/indexes/settings.rs @@ -2,7 +2,7 @@ use actix_web::web::Data; use actix_web::{web, HttpRequest, HttpResponse}; use index_scheduler::IndexScheduler; use log::debug; -use meilisearch_types::error::{DeserrError, ResponseError}; +use meilisearch_types::error::{DeserrJsonError, ResponseError}; use meilisearch_types::index_uid::IndexUid; use meilisearch_types::settings::{settings, RankingRuleView, Settings, Unchecked}; use meilisearch_types::tasks::KindWithContent; @@ -130,7 +130,7 @@ make_setting_route!( "/filterable-attributes", put, std::collections::BTreeSet, - meilisearch_types::error::DeserrError< + meilisearch_types::error::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsFilterableAttributes, >, filterable_attributes, @@ -156,7 +156,7 @@ make_setting_route!( "/sortable-attributes", put, std::collections::BTreeSet, - meilisearch_types::error::DeserrError< + meilisearch_types::error::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsSortableAttributes, >, sortable_attributes, @@ -182,7 +182,7 @@ make_setting_route!( "/displayed-attributes", put, Vec, - meilisearch_types::error::DeserrError< + meilisearch_types::error::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsDisplayedAttributes, >, displayed_attributes, @@ -208,7 +208,7 @@ make_setting_route!( "/typo-tolerance", patch, meilisearch_types::settings::TypoSettings, - meilisearch_types::error::DeserrError< + meilisearch_types::error::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsTypoTolerance, >, typo_tolerance, @@ -253,7 +253,7 @@ make_setting_route!( "/searchable-attributes", put, Vec, - meilisearch_types::error::DeserrError< + meilisearch_types::error::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsSearchableAttributes, >, searchable_attributes, @@ -279,7 +279,7 @@ make_setting_route!( "/stop-words", put, std::collections::BTreeSet, - meilisearch_types::error::DeserrError< + meilisearch_types::error::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsStopWords, >, stop_words, @@ -304,7 +304,7 @@ make_setting_route!( "/synonyms", put, std::collections::BTreeMap>, - meilisearch_types::error::DeserrError< + meilisearch_types::error::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsSynonyms, >, synonyms, @@ -329,7 +329,7 @@ make_setting_route!( "/distinct-attribute", put, String, - meilisearch_types::error::DeserrError< + meilisearch_types::error::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsDistinctAttribute, >, distinct_attribute, @@ -353,7 +353,7 @@ make_setting_route!( "/ranking-rules", put, Vec, - meilisearch_types::error::DeserrError< + meilisearch_types::error::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsRankingRules, >, ranking_rules, @@ -384,7 +384,7 @@ make_setting_route!( "/faceting", patch, meilisearch_types::settings::FacetingSettings, - meilisearch_types::error::DeserrError< + meilisearch_types::error::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsFaceting, >, faceting, @@ -409,7 +409,7 @@ make_setting_route!( "/pagination", patch, meilisearch_types::settings::PaginationSettings, - meilisearch_types::error::DeserrError< + meilisearch_types::error::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsPagination, >, pagination, @@ -461,7 +461,7 @@ generate_configure!( pub async fn update_all( index_scheduler: GuardedData, Data>, index_uid: web::Path, - body: ValidatedJson, DeserrError>, + body: ValidatedJson, DeserrJsonError>, req: HttpRequest, analytics: web::Data, ) -> Result { diff --git a/meilisearch/src/routes/mod.rs b/meilisearch/src/routes/mod.rs index 2e619540a..e681910a2 100644 --- a/meilisearch/src/routes/mod.rs +++ b/meilisearch/src/routes/mod.rs @@ -5,9 +5,8 @@ use actix_web::web::Data; use actix_web::{web, HttpRequest, HttpResponse}; use index_scheduler::{IndexScheduler, Query}; use log::debug; -use meilisearch_types::error::{ResponseError, TakeErrorMessage}; +use meilisearch_types::error::ResponseError; use meilisearch_types::settings::{Settings, Unchecked}; -use meilisearch_types::star_or::StarOr; use meilisearch_types::tasks::{Kind, Status, Task, TaskId}; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -35,35 +34,12 @@ pub fn configure(cfg: &mut web::ServiceConfig) { .service(web::scope("/swap-indexes").configure(swap_indexes::configure)); } -/// Extracts the raw values from the `StarOr` types and -/// return None if a `StarOr::Star` is encountered. -pub fn fold_star_or(content: impl IntoIterator>) -> Option -where - O: FromIterator, -{ - content - .into_iter() - .map(|value| match value { - StarOr::Star => None, - StarOr::Other(val) => Some(val), - }) - .collect() -} - pub fn from_string_to_option(input: &str) -> Result, E> where T: FromStr, { Ok(Some(input.parse()?)) } -pub fn from_string_to_option_take_error_message( - input: &str, -) -> Result, TakeErrorMessage> -where - T: FromStr, -{ - Ok(Some(input.parse().map_err(TakeErrorMessage)?)) -} const PAGINATION_DEFAULT_LIMIT: fn() -> usize = || 20; diff --git a/meilisearch/src/routes/swap_indexes.rs b/meilisearch/src/routes/swap_indexes.rs index c5b371cd9..57015f1f1 100644 --- a/meilisearch/src/routes/swap_indexes.rs +++ b/meilisearch/src/routes/swap_indexes.rs @@ -3,7 +3,7 @@ use actix_web::{web, HttpRequest, HttpResponse}; use deserr::DeserializeFromValue; use index_scheduler::IndexScheduler; use meilisearch_types::error::deserr_codes::InvalidSwapIndexes; -use meilisearch_types::error::{DeserrError, ResponseError}; +use meilisearch_types::error::{DeserrJsonError, ResponseError}; use meilisearch_types::tasks::{IndexSwap, KindWithContent}; use serde_json::json; @@ -20,15 +20,15 @@ pub fn configure(cfg: &mut web::ServiceConfig) { } #[derive(DeserializeFromValue, Debug, Clone, PartialEq, Eq)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] pub struct SwapIndexesPayload { - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] indexes: Vec, } pub async fn swap_indexes( index_scheduler: GuardedData, Data>, - params: ValidatedJson, DeserrError>, + params: ValidatedJson, DeserrJsonError>, req: HttpRequest, analytics: web::Data, ) -> Result { @@ -44,6 +44,7 @@ pub async fn swap_indexes( let mut swaps = vec![]; for SwapIndexesPayload { indexes } in params.into_iter() { + // TODO: switch to deserr let (lhs, rhs) = match indexes.as_slice() { [lhs, rhs] => (lhs, rhs), _ => { diff --git a/meilisearch/src/routes/tasks.rs b/meilisearch/src/routes/tasks.rs index 09723623f..dbf1380e2 100644 --- a/meilisearch/src/routes/tasks.rs +++ b/meilisearch/src/routes/tasks.rs @@ -1,13 +1,15 @@ -use std::num::ParseIntError; -use std::str::FromStr; - use actix_web::web::Data; use actix_web::{web, HttpRequest, HttpResponse}; use deserr::DeserializeFromValue; use index_scheduler::{IndexScheduler, Query, TaskId}; -use meilisearch_types::error::deserr_codes::*; -use meilisearch_types::error::{DeserrError, ResponseError, TakeErrorMessage}; +use meilisearch_types::error::{ + deserr_codes::*, parse_option_cs_star_or, parse_option_u32_query_param, + parse_option_vec_u32_query_param, DeserrQueryParamError, DetailedParseIntError, + TakeErrorMessage, +}; +use meilisearch_types::error::{parse_u32_query_param, ResponseError}; use meilisearch_types::index_uid::IndexUid; +use meilisearch_types::serde_cs; use meilisearch_types::settings::{Settings, Unchecked}; use meilisearch_types::star_or::StarOr; use meilisearch_types::tasks::{ @@ -21,7 +23,7 @@ use time::macros::format_description; use time::{Date, Duration, OffsetDateTime, Time}; use tokio::task; -use super::{fold_star_or, SummarizedTaskView}; +use super::SummarizedTaskView; use crate::analytics::Analytics; use crate::extractors::authentication::policies::*; use crate::extractors::authentication::GuardedData; @@ -164,108 +166,70 @@ impl From
for DetailsView { } } -fn parse_option_cs( - s: Option>, -) -> Result>, TakeErrorMessage> { - if let Some(s) = s { - s.into_iter() - .map(|s| T::from_str(&s)) - .collect::, T::Err>>() - .map_err(TakeErrorMessage) - .map(Some) - } else { - Ok(None) - } -} -fn parse_option_cs_star_or( - s: Option>>, -) -> Result>, TakeErrorMessage> { - if let Some(s) = s.and_then(fold_star_or) as Option> { - s.into_iter() - .map(|s| T::from_str(&s)) - .collect::, T::Err>>() - .map_err(TakeErrorMessage) - .map(Some) - } else { - Ok(None) - } -} -fn parse_option_str(s: Option) -> Result, TakeErrorMessage> { - if let Some(s) = s { - T::from_str(&s).map_err(TakeErrorMessage).map(Some) - } else { - Ok(None) - } -} - -fn parse_str(s: String) -> Result> { - T::from_str(&s).map_err(TakeErrorMessage) -} - #[derive(Debug, DeserializeFromValue)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] pub struct TasksFilterQuery { - #[deserr(error = DeserrError, default = DEFAULT_LIMIT(), from(String) = parse_str:: -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default = DEFAULT_LIMIT(), from(String) = parse_u32_query_param -> TakeErrorMessage)] pub limit: u32, - #[deserr(error = DeserrError, from(Option) = parse_option_str:: -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, from(Option) = parse_option_u32_query_param -> TakeErrorMessage)] pub from: Option, - #[deserr(error = DeserrError, from(Option>) = parse_option_cs:: -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, from(Option>) = parse_option_vec_u32_query_param -> TakeErrorMessage)] pub uids: Option>, - #[deserr(error = DeserrError, from(Option>) = parse_option_cs:: -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, from(Option>) = parse_option_vec_u32_query_param -> TakeErrorMessage)] pub canceled_by: Option>, - #[deserr(error = DeserrError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] pub types: Option>, - #[deserr(error = DeserrError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] pub statuses: Option>, - #[deserr(error = DeserrError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] pub index_uids: Option>, - #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] pub after_enqueued_at: Option, - #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] pub before_enqueued_at: Option, - #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] pub after_started_at: Option, - #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] pub before_started_at: Option, - #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] pub after_finished_at: Option, - #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] pub before_finished_at: Option, } #[derive(Deserialize, Debug, DeserializeFromValue)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] pub struct TaskDeletionOrCancelationQuery { - #[deserr(error = DeserrError, from(Option>) = parse_option_cs:: -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, from(Option>) = parse_option_vec_u32_query_param -> TakeErrorMessage)] pub uids: Option>, - #[deserr(error = DeserrError, from(Option>) = parse_option_cs:: -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, from(Option>) = parse_option_vec_u32_query_param -> TakeErrorMessage)] pub canceled_by: Option>, - #[deserr(error = DeserrError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] pub types: Option>, - #[deserr(error = DeserrError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] pub statuses: Option>, - #[deserr(error = DeserrError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] pub index_uids: Option>, - #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] pub after_enqueued_at: Option, - #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] pub before_enqueued_at: Option, - #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] pub after_started_at: Option, - #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] pub before_started_at: Option, - #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] pub after_finished_at: Option, - #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] + #[deserr(error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] pub before_finished_at: Option, } async fn cancel_tasks( index_scheduler: GuardedData, Data>, - params: QueryParameter, + params: QueryParameter, req: HttpRequest, analytics: web::Data, ) -> Result { @@ -337,7 +301,7 @@ async fn cancel_tasks( async fn delete_tasks( index_scheduler: GuardedData, Data>, - params: QueryParameter, + params: QueryParameter, req: HttpRequest, analytics: web::Data, ) -> Result { @@ -418,7 +382,7 @@ pub struct AllTasks { async fn get_tasks( index_scheduler: GuardedData, Data>, - params: QueryParameter, + params: QueryParameter, req: HttpRequest, analytics: web::Data, ) -> Result { @@ -584,16 +548,16 @@ impl std::error::Error for InvalidTaskDateError {} mod tests { use deserr::DeserializeFromValue; use meili_snap::snapshot; - use meilisearch_types::error::DeserrError; + use meilisearch_types::error::DeserrQueryParamError; use crate::extractors::query_parameters::QueryParameter; use crate::routes::tasks::{TaskDeletionOrCancelationQuery, TasksFilterQuery}; fn deserr_query_params(j: &str) -> Result where - T: DeserializeFromValue, + T: DeserializeFromValue, { - QueryParameter::::from_query(j).map(|p| p.0) + QueryParameter::::from_query(j).map(|p| p.0) } #[test] @@ -634,33 +598,33 @@ mod tests { { let params = "afterFinishedAt=2021"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"`2021` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterFinishedAt`."); + snapshot!(format!("{err}"), @"Invalid value in parameter `afterFinishedAt`: `2021` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format."); } { let params = "beforeFinishedAt=2021"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"`2021` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeFinishedAt`."); + snapshot!(format!("{err}"), @"Invalid value in parameter `beforeFinishedAt`: `2021` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format."); } { let params = "afterEnqueuedAt=2021-12"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"`2021-12` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterEnqueuedAt`."); + snapshot!(format!("{err}"), @"Invalid value in parameter `afterEnqueuedAt`: `2021-12` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format."); } { let params = "beforeEnqueuedAt=2021-12-03T23"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"`2021-12-03T23` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeEnqueuedAt`."); + snapshot!(format!("{err}"), @"Invalid value in parameter `beforeEnqueuedAt`: `2021-12-03T23` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format."); } { let params = "afterStartedAt=2021-12-03T23:45"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"`2021-12-03T23:45` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterStartedAt`."); + snapshot!(format!("{err}"), @"Invalid value in parameter `afterStartedAt`: `2021-12-03T23:45` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format."); } { let params = "beforeStartedAt=2021-12-03T23:45"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"`2021-12-03T23:45` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeStartedAt`."); + snapshot!(format!("{err}"), @"Invalid value in parameter `beforeStartedAt`: `2021-12-03T23:45` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format."); } } @@ -679,12 +643,12 @@ mod tests { { let params = "uids=78,hello,world"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"invalid digit found in string at `.uids`."); + snapshot!(format!("{err}"), @"Invalid value in parameter `uids`: could not parse `hello` as a positive integer"); } { let params = "uids=cat"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"invalid digit found in string at `.uids`."); + snapshot!(format!("{err}"), @"Invalid value in parameter `uids`: could not parse `cat` as a positive integer"); } } @@ -703,7 +667,7 @@ mod tests { { let params = "statuses=finished"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"`finished` is not a status. Available status are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`. at `.statuses`."); + snapshot!(format!("{err}"), @"Invalid value in parameter `statuses`: `finished` is not a valid task status. Available statuses are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`."); } } #[test] @@ -721,7 +685,7 @@ mod tests { { let params = "types=createIndex"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"`createIndex` is not a type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`. at `.types`."); + snapshot!(format!("{err}"), @"Invalid value in parameter `types`: `createIndex` is not a valid task type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`."); } } #[test] @@ -739,12 +703,12 @@ mod tests { { let params = "indexUids=1,hé"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"`hé` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_). at `.indexUids`."); + snapshot!(format!("{err}"), @"Invalid value in parameter `indexUids`: `hé` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_)."); } { let params = "indexUids=hé"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"`hé` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_). at `.indexUids`."); + snapshot!(format!("{err}"), @"Invalid value in parameter `indexUids`: `hé` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_)."); } } @@ -772,19 +736,19 @@ mod tests { // Stars in uids not allowed let params = "uids=*"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"invalid digit found in string at `.uids`."); + snapshot!(format!("{err}"), @"Invalid value in parameter `uids`: could not parse `*` as a positive integer"); } { // From not allowed in task deletion/cancelation queries let params = "from=12"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"Json deserialize error: unknown field `from`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``."); + snapshot!(format!("{err}"), @"Unknown parameter `from`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`"); } { // Limit not allowed in task deletion/cancelation queries let params = "limit=12"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"Json deserialize error: unknown field `limit`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``."); + snapshot!(format!("{err}"), @"Unknown parameter `limit`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`"); } } } diff --git a/meilisearch/src/search.rs b/meilisearch/src/search.rs index 129137859..4e2c43f18 100644 --- a/meilisearch/src/search.rs +++ b/meilisearch/src/search.rs @@ -3,10 +3,10 @@ use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::str::FromStr; use std::time::Instant; -use deserr::DeserializeFromValue; use either::Either; +use meilisearch_types::deserr::DeserializeFromValue; use meilisearch_types::error::deserr_codes::*; -use meilisearch_types::error::DeserrError; +use meilisearch_types::error::DeserrJsonError; use meilisearch_types::settings::DEFAULT_PAGINATION_MAX_TOTAL_HITS; use meilisearch_types::{milli, Document}; use milli::tokenizer::TokenizerBuilder; @@ -30,41 +30,41 @@ pub const DEFAULT_HIGHLIGHT_PRE_TAG: fn() -> String = || "".to_string(); pub const DEFAULT_HIGHLIGHT_POST_TAG: fn() -> String = || "".to_string(); #[derive(Debug, Clone, Default, PartialEq, DeserializeFromValue)] -#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] pub struct SearchQuery { - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub q: Option, - #[deserr(error = DeserrError, default = DEFAULT_SEARCH_OFFSET())] + #[deserr(error = DeserrJsonError, default = DEFAULT_SEARCH_OFFSET())] pub offset: usize, - #[deserr(error = DeserrError, default = DEFAULT_SEARCH_LIMIT())] + #[deserr(error = DeserrJsonError, default = DEFAULT_SEARCH_LIMIT())] pub limit: usize, - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub page: Option, - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub hits_per_page: Option, - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub attributes_to_retrieve: Option>, - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub attributes_to_crop: Option>, - #[deserr(error = DeserrError, default = DEFAULT_CROP_LENGTH())] + #[deserr(error = DeserrJsonError, default = DEFAULT_CROP_LENGTH())] pub crop_length: usize, - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub attributes_to_highlight: Option>, - #[deserr(error = DeserrError, default)] + #[deserr(error = DeserrJsonError, default)] pub show_matches_position: bool, - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub filter: Option, - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub sort: Option>, - #[deserr(error = DeserrError)] + #[deserr(error = DeserrJsonError)] pub facets: Option>, - #[deserr(error = DeserrError, default = DEFAULT_HIGHLIGHT_PRE_TAG())] + #[deserr(error = DeserrJsonError, default = DEFAULT_HIGHLIGHT_PRE_TAG())] pub highlight_pre_tag: String, - #[deserr(error = DeserrError, default = DEFAULT_HIGHLIGHT_POST_TAG())] + #[deserr(error = DeserrJsonError, default = DEFAULT_HIGHLIGHT_POST_TAG())] pub highlight_post_tag: String, - #[deserr(error = DeserrError, default = DEFAULT_CROP_MARKER())] + #[deserr(error = DeserrJsonError, default = DEFAULT_CROP_MARKER())] pub crop_marker: String, - #[deserr(error = DeserrError, default)] + #[deserr(error = DeserrJsonError, default)] pub matching_strategy: MatchingStrategy, } diff --git a/meilisearch/tests/auth/api_keys.rs b/meilisearch/tests/auth/api_keys.rs index 0a14107a8..8d7cb9130 100644 --- a/meilisearch/tests/auth/api_keys.rs +++ b/meilisearch/tests/auth/api_keys.rs @@ -248,7 +248,7 @@ async fn error_add_api_key_missing_parameter() { meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "Json deserialize error: missing field `indexes` at ``", + "message": "Missing field `indexes`", "code": "bad_request", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#bad-request" @@ -265,7 +265,7 @@ async fn error_add_api_key_missing_parameter() { meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "Json deserialize error: missing field `actions` at ``", + "message": "Missing field `actions`", "code": "bad_request", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#bad-request" @@ -314,7 +314,7 @@ async fn error_add_api_key_invalid_parameters_description() { meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "invalid type: Map `{\"name\":\"products\"}`, expected a String at `.description`.", + "message": "Invalid value type at `.description`: expected a string, but found an object: `{\"name\":\"products\"}`", "code": "invalid_api_key_description", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-api-key-description" @@ -337,7 +337,7 @@ async fn error_add_api_key_invalid_parameters_name() { meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "invalid type: Map `{\"name\":\"products\"}`, expected a String at `.name`.", + "message": "Invalid value type at `.name`: expected a string, but found an object: `{\"name\":\"products\"}`", "code": "invalid_api_key_name", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-api-key-name" @@ -360,7 +360,7 @@ async fn error_add_api_key_invalid_parameters_indexes() { meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "invalid type: Map `{\"name\":\"products\"}`, expected a Sequence at `.indexes`.", + "message": "Invalid value type at `.indexes`: expected an array, but found an object: `{\"name\":\"products\"}`", "code": "invalid_api_key_indexes", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-api-key-indexes" @@ -386,7 +386,7 @@ async fn error_add_api_key_invalid_index_uids() { meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "`invalid index # / \\name with spaces` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_). at `.indexes[0]`.", + "message": "Invalid value at `.indexes[0]`: `invalid index # / \\name with spaces` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", "code": "invalid_api_key_indexes", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-api-key-indexes" @@ -411,7 +411,7 @@ async fn error_add_api_key_invalid_parameters_actions() { meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "invalid type: Map `{\"name\":\"products\"}`, expected a Sequence at `.actions`.", + "message": "Invalid value type at `.actions`: expected an array, but found an object: `{\"name\":\"products\"}`", "code": "invalid_api_key_actions", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-api-key-actions" @@ -431,7 +431,7 @@ async fn error_add_api_key_invalid_parameters_actions() { meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "Json deserialize error: unknown value `doc.add`, expected one of `*`, `search`, `documents.*`, `documents.add`, `documents.get`, `documents.delete`, `indexes.*`, `indexes.create`, `indexes.get`, `indexes.update`, `indexes.delete`, `indexes.swap`, `tasks.*`, `tasks.cancel`, `tasks.delete`, `tasks.get`, `settings.*`, `settings.get`, `settings.update`, `stats.*`, `stats.get`, `metrics.*`, `metrics.get`, `dumps.*`, `dumps.create`, `version`, `keys.create`, `keys.get`, `keys.update`, `keys.delete` at `.actions[0]`.", + "message": "Unknown value `doc.add` at `.actions[0]`: expected one of `*`, `search`, `documents.*`, `documents.add`, `documents.get`, `documents.delete`, `indexes.*`, `indexes.create`, `indexes.get`, `indexes.update`, `indexes.delete`, `indexes.swap`, `tasks.*`, `tasks.cancel`, `tasks.delete`, `tasks.get`, `settings.*`, `settings.get`, `settings.update`, `stats.*`, `stats.get`, `metrics.*`, `metrics.get`, `dumps.*`, `dumps.create`, `version`, `keys.create`, `keys.get`, `keys.update`, `keys.delete`", "code": "invalid_api_key_actions", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-api-key-actions" @@ -455,7 +455,7 @@ async fn error_add_api_key_invalid_parameters_expires_at() { meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "invalid type: Map `{\"name\":\"products\"}`, expected a String at `.expiresAt`.", + "message": "Invalid value type at `.expiresAt`: expected a string, but found an object: `{\"name\":\"products\"}`", "code": "invalid_api_key_expires_at", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-api-key-expires-at" @@ -478,7 +478,7 @@ async fn error_add_api_key_invalid_parameters_expires_at_in_the_past() { meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "`2010-11-13T00:00:00Z` is not a valid date. It should follow the RFC 3339 format to represents a date or datetime in the future or specified as a null value. e.g. 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'.\n at `.expiresAt`.", + "message": "Invalid value at `.expiresAt`: `2010-11-13T00:00:00Z` is not a valid date. It should follow the RFC 3339 format to represents a date or datetime in the future or specified as a null value. e.g. 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'.\n", "code": "invalid_api_key_expires_at", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-api-key-expires-at" @@ -503,7 +503,7 @@ async fn error_add_api_key_invalid_parameters_uid() { meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "invalid length: expected length 32 for simple format, found 13 at `.uid`.", + "message": "Invalid value at `.uid`: invalid length: expected length 32 for simple format, found 13", "code": "invalid_api_key_uid", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-api-key-uid" @@ -1403,7 +1403,7 @@ async fn error_patch_api_key_indexes() { let (response, code) = server.patch_api_key(&uid, content).await; meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "Json deserialize error: unknown field `indexes`, expected one of `description`, `name` at ``.", + "message": "Unknown field `indexes`: expected one of `description`, `name`", "code": "immutable_api_key_indexes", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#immutable-api-key-indexes" @@ -1480,7 +1480,7 @@ async fn error_patch_api_key_actions() { let (response, code) = server.patch_api_key(&uid, content).await; meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "Json deserialize error: unknown field `actions`, expected one of `description`, `name` at ``.", + "message": "Unknown field `actions`: expected one of `description`, `name`", "code": "immutable_api_key_actions", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#immutable-api-key-actions" @@ -1549,7 +1549,7 @@ async fn error_patch_api_key_expiration_date() { let (response, code) = server.patch_api_key(&uid, content).await; meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "Json deserialize error: unknown field `expiresAt`, expected one of `description`, `name` at ``.", + "message": "Unknown field `expiresAt`: expected one of `description`, `name`", "code": "immutable_api_key_expires_at", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#immutable-api-key-expires-at" @@ -1670,7 +1670,7 @@ async fn error_patch_api_key_indexes_invalid_parameters() { let (response, code) = server.patch_api_key(&uid, content).await; meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "invalid type: Integer `13`, expected a String at `.description`.", + "message": "Invalid value type at `.description`: expected a string, but found a positive integer: `13`", "code": "invalid_api_key_description", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-api-key-description" @@ -1686,7 +1686,7 @@ async fn error_patch_api_key_indexes_invalid_parameters() { let (response, code) = server.patch_api_key(&uid, content).await; meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { - "message": "invalid type: Integer `13`, expected a String at `.name`.", + "message": "Invalid value type at `.name`: expected a string, but found a positive integer: `13`", "code": "invalid_api_key_name", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-api-key-name" diff --git a/meilisearch/tests/common/index.rs b/meilisearch/tests/common/index.rs index d42f18d2c..b98ed9827 100644 --- a/meilisearch/tests/common/index.rs +++ b/meilisearch/tests/common/index.rs @@ -289,8 +289,8 @@ impl Index<'_> { eprintln!("Error with post search"); resume_unwind(e); } - - let (response, code) = self.search_get(query).await; + let query = yaup::to_string(&query).unwrap(); + let (response, code) = self.search_get(&query).await; if let Err(e) = catch_unwind(move || test(response, code)) { eprintln!("Error with get search"); resume_unwind(e); @@ -302,9 +302,8 @@ impl Index<'_> { self.service.post_encoded(url, query, self.encoder).await } - pub async fn search_get(&self, query: Value) -> (Value, StatusCode) { - let params = yaup::to_string(&query).unwrap(); - let url = format!("/indexes/{}/search?{}", urlencode(self.uid.as_ref()), params); + pub async fn search_get(&self, query: &str) -> (Value, StatusCode) { + let url = format!("/indexes/{}/search?{}", urlencode(self.uid.as_ref()), query); self.service.get(url).await } diff --git a/meilisearch/tests/common/server.rs b/meilisearch/tests/common/server.rs index c3c9b7c60..e325da0cb 100644 --- a/meilisearch/tests/common/server.rs +++ b/meilisearch/tests/common/server.rs @@ -132,8 +132,8 @@ impl Server { self.service.get("/tasks").await } - pub async fn tasks_filter(&self, filter: Value) -> (Value, StatusCode) { - self.service.get(format!("/tasks?{}", yaup::to_string(&filter).unwrap())).await + pub async fn tasks_filter(&self, filter: &str) -> (Value, StatusCode) { + self.service.get(format!("/tasks?{}", filter)).await } pub async fn get_dump_status(&self, uid: &str) -> (Value, StatusCode) { @@ -148,14 +148,12 @@ impl Server { self.service.post("/swap-indexes", value).await } - pub async fn cancel_tasks(&self, value: Value) -> (Value, StatusCode) { - self.service - .post(format!("/tasks/cancel?{}", yaup::to_string(&value).unwrap()), json!(null)) - .await + pub async fn cancel_tasks(&self, value: &str) -> (Value, StatusCode) { + self.service.post(format!("/tasks/cancel?{}", value), json!(null)).await } - pub async fn delete_tasks(&self, value: Value) -> (Value, StatusCode) { - self.service.delete(format!("/tasks?{}", yaup::to_string(&value).unwrap())).await + pub async fn delete_tasks(&self, value: &str) -> (Value, StatusCode) { + self.service.delete(format!("/tasks?{}", value)).await } pub async fn wait_task(&self, update_id: u64) -> Value { diff --git a/meilisearch/tests/search/errors.rs b/meilisearch/tests/search/errors.rs index d582a3672..99f711745 100644 --- a/meilisearch/tests/search/errors.rs +++ b/meilisearch/tests/search/errors.rs @@ -46,7 +46,7 @@ async fn search_bad_q() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: Sequence `[\"doggo\"]`, expected a String at `.q`.", + "message": "Invalid value type at `.q`: expected a string, but found an array: `[\"doggo\"]`", "code": "invalid_search_q", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-search-q" @@ -64,18 +64,18 @@ async fn search_bad_offset() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Integer at `.offset`.", + "message": "Invalid value type at `.offset`: expected a positive integer, but found a string: `\"doggo\"`", "code": "invalid_search_offset", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-search-offset" } "###); - let (response, code) = index.search_get(json!({"offset": "doggo"})).await; + let (response, code) = index.search_get("offset=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.offset`.", + "message": "Invalid value in parameter `offset`: could not parse `doggo` as a positive integer", "code": "invalid_search_offset", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-search-offset" @@ -92,18 +92,18 @@ async fn search_bad_limit() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Integer at `.limit`.", + "message": "Invalid value type at `.limit`: expected a positive integer, but found a string: `\"doggo\"`", "code": "invalid_search_limit", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-search-limit" } "###); - let (response, code) = index.search_get(json!({"limit": "doggo"})).await; + let (response, code) = index.search_get("limit=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.limit`.", + "message": "Invalid value in parameter `limit`: could not parse `doggo` as a positive integer", "code": "invalid_search_limit", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-search-limit" @@ -120,18 +120,18 @@ async fn search_bad_page() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Integer at `.page`.", + "message": "Invalid value type at `.page`: expected a positive integer, but found a string: `\"doggo\"`", "code": "invalid_search_page", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-search-page" } "###); - let (response, code) = index.search_get(json!({"page": "doggo"})).await; + let (response, code) = index.search_get("page=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.page`.", + "message": "Invalid value in parameter `page`: could not parse `doggo` as a positive integer", "code": "invalid_search_page", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-search-page" @@ -148,18 +148,18 @@ async fn search_bad_hits_per_page() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Integer at `.hitsPerPage`.", + "message": "Invalid value type at `.hitsPerPage`: expected a positive integer, but found a string: `\"doggo\"`", "code": "invalid_search_hits_per_page", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-search-hits-per-page" } "###); - let (response, code) = index.search_get(json!({"hitsPerPage": "doggo"})).await; + let (response, code) = index.search_get("hitsPerPage=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.hitsPerPage`.", + "message": "Invalid value in parameter `hitsPerPage`: could not parse `doggo` as a positive integer", "code": "invalid_search_hits_per_page", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-search-hits-per-page" @@ -176,7 +176,7 @@ async fn search_bad_attributes_to_crop() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.attributesToCrop`.", + "message": "Invalid value type at `.attributesToCrop`: expected an array, but found a string: `\"doggo\"`", "code": "invalid_search_attributes_to_crop", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-search-attributes-to-crop" @@ -194,18 +194,18 @@ async fn search_bad_crop_length() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Integer at `.cropLength`.", + "message": "Invalid value type at `.cropLength`: expected a positive integer, but found a string: `\"doggo\"`", "code": "invalid_search_crop_length", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-search-crop-length" } "###); - let (response, code) = index.search_get(json!({"cropLength": "doggo"})).await; + let (response, code) = index.search_get("cropLength=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.cropLength`.", + "message": "Invalid value in parameter `cropLength`: could not parse `doggo` as a positive integer", "code": "invalid_search_crop_length", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-search-crop-length" @@ -222,7 +222,7 @@ async fn search_bad_attributes_to_highlight() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.attributesToHighlight`.", + "message": "Invalid value type at `.attributesToHighlight`: expected an array, but found a string: `\"doggo\"`", "code": "invalid_search_attributes_to_highlight", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-search-attributes-to-highlight" @@ -266,7 +266,7 @@ async fn search_bad_sort() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.sort`.", + "message": "Invalid value type at `.sort`: expected an array, but found a string: `\"doggo\"`", "code": "invalid_search_sort", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-search-sort" @@ -284,18 +284,18 @@ async fn search_bad_show_matches_position() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Boolean at `.showMatchesPosition`.", + "message": "Invalid value type at `.showMatchesPosition`: expected a boolean, but found a string: `\"doggo\"`", "code": "invalid_search_show_matches_position", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-search-show-matches-position" } "###); - let (response, code) = index.search_get(json!({"showMatchesPosition": "doggo"})).await; + let (response, code) = index.search_get("showMatchesPosition=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "provided string was not `true` or `false` at `.showMatchesPosition`.", + "message": "Invalid value in parameter `showMatchesPosition`: provided string was not `true` or `false`", "code": "invalid_search_show_matches_position", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-search-show-matches-position" @@ -312,7 +312,7 @@ async fn search_bad_facets() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.facets`.", + "message": "Invalid value type at `.facets`: expected an array, but found a string: `\"doggo\"`", "code": "invalid_search_facets", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-search-facets" @@ -330,7 +330,7 @@ async fn search_bad_highlight_pre_tag() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: Sequence `[\"doggo\"]`, expected a String at `.highlightPreTag`.", + "message": "Invalid value type at `.highlightPreTag`: expected a string, but found an array: `[\"doggo\"]`", "code": "invalid_search_highlight_pre_tag", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-search-highlight-pre-tag" @@ -348,7 +348,7 @@ async fn search_bad_highlight_post_tag() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: Sequence `[\"doggo\"]`, expected a String at `.highlightPostTag`.", + "message": "Invalid value type at `.highlightPostTag`: expected a string, but found an array: `[\"doggo\"]`", "code": "invalid_search_highlight_post_tag", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-search-highlight-post-tag" @@ -366,7 +366,7 @@ async fn search_bad_crop_marker() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: Sequence `[\"doggo\"]`, expected a String at `.cropMarker`.", + "message": "Invalid value type at `.cropMarker`: expected a string, but found an array: `[\"doggo\"]`", "code": "invalid_search_crop_marker", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-search-crop-marker" @@ -384,18 +384,18 @@ async fn search_bad_matching_strategy() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "Json deserialize error: unknown value `doggo`, expected one of `last`, `all` at `.matchingStrategy`.", + "message": "Unknown value `doggo` at `.matchingStrategy`: expected one of `last`, `all`", "code": "invalid_search_matching_strategy", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-search-matching-strategy" } "###); - let (response, code) = index.search_get(json!({"matchingStrategy": "doggo"})).await; + let (response, code) = index.search_get("matchingStrategy=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "Json deserialize error: unknown value `doggo`, expected one of `last`, `all` at `.matchingStrategy`.", + "message": "Unknown value `doggo` for parameter `matchingStrategy`: expected one of `last`, `all`", "code": "invalid_search_matching_strategy", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-search-matching-strategy" diff --git a/meilisearch/tests/settings/errors.rs b/meilisearch/tests/settings/errors.rs index 77e62303a..a3deeccfb 100644 --- a/meilisearch/tests/settings/errors.rs +++ b/meilisearch/tests/settings/errors.rs @@ -12,7 +12,7 @@ async fn settings_bad_displayed_attributes() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.displayedAttributes`.", + "message": "Invalid value type at `.displayedAttributes`: expected an array, but found a string: `\"doggo\"`", "code": "invalid_settings_displayed_attributes", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-settings-displayed-attributes" @@ -23,7 +23,7 @@ async fn settings_bad_displayed_attributes() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at ``.", + "message": "Invalid value type: expected an array, but found a string: `\"doggo\"`", "code": "invalid_settings_displayed_attributes", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-settings-displayed-attributes" @@ -40,7 +40,7 @@ async fn settings_bad_searchable_attributes() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.searchableAttributes`.", + "message": "Invalid value type at `.searchableAttributes`: expected an array, but found a string: `\"doggo\"`", "code": "invalid_settings_searchable_attributes", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-settings-searchable-attributes" @@ -51,7 +51,7 @@ async fn settings_bad_searchable_attributes() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at ``.", + "message": "Invalid value type: expected an array, but found a string: `\"doggo\"`", "code": "invalid_settings_searchable_attributes", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-settings-searchable-attributes" @@ -68,7 +68,7 @@ async fn settings_bad_filterable_attributes() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.filterableAttributes`.", + "message": "Invalid value type at `.filterableAttributes`: expected an array, but found a string: `\"doggo\"`", "code": "invalid_settings_filterable_attributes", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-settings-filterable-attributes" @@ -79,7 +79,7 @@ async fn settings_bad_filterable_attributes() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at ``.", + "message": "Invalid value type: expected an array, but found a string: `\"doggo\"`", "code": "invalid_settings_filterable_attributes", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-settings-filterable-attributes" @@ -96,7 +96,7 @@ async fn settings_bad_sortable_attributes() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.sortableAttributes`.", + "message": "Invalid value type at `.sortableAttributes`: expected an array, but found a string: `\"doggo\"`", "code": "invalid_settings_sortable_attributes", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-settings-sortable-attributes" @@ -107,7 +107,7 @@ async fn settings_bad_sortable_attributes() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at ``.", + "message": "Invalid value type: expected an array, but found a string: `\"doggo\"`", "code": "invalid_settings_sortable_attributes", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-settings-sortable-attributes" @@ -124,7 +124,7 @@ async fn settings_bad_ranking_rules() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.rankingRules`.", + "message": "Invalid value type at `.rankingRules`: expected an array, but found a string: `\"doggo\"`", "code": "invalid_settings_ranking_rules", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-settings-ranking-rules" @@ -135,7 +135,7 @@ async fn settings_bad_ranking_rules() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at ``.", + "message": "Invalid value type: expected an array, but found a string: `\"doggo\"`", "code": "invalid_settings_ranking_rules", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-settings-ranking-rules" @@ -152,7 +152,7 @@ async fn settings_bad_stop_words() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.stopWords`.", + "message": "Invalid value type at `.stopWords`: expected an array, but found a string: `\"doggo\"`", "code": "invalid_settings_stop_words", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-settings-stop-words" @@ -163,7 +163,7 @@ async fn settings_bad_stop_words() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Sequence at ``.", + "message": "Invalid value type: expected an array, but found a string: `\"doggo\"`", "code": "invalid_settings_stop_words", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-settings-stop-words" @@ -180,7 +180,7 @@ async fn settings_bad_synonyms() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Map at `.synonyms`.", + "message": "Invalid value type at `.synonyms`: expected an object, but found a string: `\"doggo\"`", "code": "invalid_settings_synonyms", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-settings-synonyms" @@ -191,7 +191,7 @@ async fn settings_bad_synonyms() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Map at ``.", + "message": "Invalid value type: expected an object, but found a string: `\"doggo\"`", "code": "invalid_settings_synonyms", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-settings-synonyms" @@ -208,7 +208,7 @@ async fn settings_bad_distinct_attribute() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: Sequence `[\"doggo\"]`, expected a String at `.distinctAttribute`.", + "message": "Invalid value type at `.distinctAttribute`: expected a string, but found an array: `[\"doggo\"]`", "code": "invalid_settings_distinct_attribute", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-settings-distinct-attribute" @@ -219,7 +219,7 @@ async fn settings_bad_distinct_attribute() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: Sequence `[\"doggo\"]`, expected a String at ``.", + "message": "Invalid value type: expected a string, but found an array: `[\"doggo\"]`", "code": "invalid_settings_distinct_attribute", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-settings-distinct-attribute" @@ -236,7 +236,7 @@ async fn settings_bad_typo_tolerance() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Map at `.typoTolerance`.", + "message": "Invalid value type at `.typoTolerance`: expected an object, but found a string: `\"doggo\"`", "code": "invalid_settings_typo_tolerance", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-settings-typo-tolerance" @@ -247,7 +247,7 @@ async fn settings_bad_typo_tolerance() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Map at ``.", + "message": "Invalid value type: expected an object, but found a string: `\"doggo\"`", "code": "invalid_settings_typo_tolerance", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-settings-typo-tolerance" @@ -264,7 +264,7 @@ async fn settings_bad_faceting() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Map at `.faceting`.", + "message": "Invalid value type at `.faceting`: expected an object, but found a string: `\"doggo\"`", "code": "invalid_settings_faceting", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-settings-faceting" @@ -275,7 +275,7 @@ async fn settings_bad_faceting() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Map at ``.", + "message": "Invalid value type: expected an object, but found a string: `\"doggo\"`", "code": "invalid_settings_faceting", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-settings-faceting" @@ -292,7 +292,7 @@ async fn settings_bad_pagination() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Map at `.pagination`.", + "message": "Invalid value type at `.pagination`: expected an object, but found a string: `\"doggo\"`", "code": "invalid_settings_pagination", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-settings-pagination" @@ -303,7 +303,7 @@ async fn settings_bad_pagination() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid type: String `\"doggo\"`, expected a Map at ``.", + "message": "Invalid value type: expected an object, but found a string: `\"doggo\"`", "code": "invalid_settings_pagination", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-settings-pagination" diff --git a/meilisearch/tests/settings/get_settings.rs b/meilisearch/tests/settings/get_settings.rs index 3ac7d3801..f18787e19 100644 --- a/meilisearch/tests/settings/get_settings.rs +++ b/meilisearch/tests/settings/get_settings.rs @@ -282,7 +282,7 @@ async fn error_set_invalid_ranking_rules() { meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { - "message": "`manyTheFish` ranking rule is invalid. Valid ranking rules are words, typo, sort, proximity, attribute, exactness and custom ranking rules. at `.rankingRules[0]`.", + "message": "Invalid value at `.rankingRules[0]`: `manyTheFish` ranking rule is invalid. Valid ranking rules are words, typo, sort, proximity, attribute, exactness and custom ranking rules.", "code": "invalid_settings_ranking_rules", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-settings-ranking-rules" diff --git a/meilisearch/tests/tasks/errors.rs b/meilisearch/tests/tasks/errors.rs index 305ab8b9c..fd4c6d489 100644 --- a/meilisearch/tests/tasks/errors.rs +++ b/meilisearch/tests/tasks/errors.rs @@ -1,5 +1,4 @@ use meili_snap::*; -use serde_json::json; use crate::common::Server; @@ -7,33 +6,44 @@ use crate::common::Server; async fn task_bad_uids() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"uids": "doggo"})).await; + let (response, code) = server.tasks_filter("uids=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.uids`.", + "message": "Invalid value in parameter `uids`: could not parse `doggo` as a positive integer", "code": "invalid_task_uids", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-uids" } "###); - let (response, code) = server.cancel_tasks(json!({"uids": "doggo"})).await; + let (response, code) = server.cancel_tasks("uids=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.uids`.", + "message": "Invalid value in parameter `uids`: could not parse `doggo` as a positive integer", "code": "invalid_task_uids", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-uids" } "###); - let (response, code) = server.delete_tasks(json!({"uids": "doggo"})).await; + let (response, code) = server.delete_tasks("uids=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.uids`.", + "message": "Invalid value in parameter `uids`: could not parse `doggo` as a positive integer", + "code": "invalid_task_uids", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-uids" + } + "###); + + let (response, code) = server.delete_tasks("uids=1,dogo").await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value in parameter `uids`: could not parse `dogo` as a positive integer", "code": "invalid_task_uids", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-uids" @@ -45,33 +55,33 @@ async fn task_bad_uids() { async fn task_bad_canceled_by() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"canceledBy": "doggo"})).await; + let (response, code) = server.tasks_filter("canceledBy=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.canceledBy`.", + "message": "Invalid value in parameter `canceledBy`: could not parse `doggo` as a positive integer", "code": "invalid_task_canceled_by", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-canceled-by" } "###); - let (response, code) = server.cancel_tasks(json!({"canceledBy": "doggo"})).await; + let (response, code) = server.cancel_tasks("canceledBy=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.canceledBy`.", + "message": "Invalid value in parameter `canceledBy`: could not parse `doggo` as a positive integer", "code": "invalid_task_canceled_by", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-canceled-by" } "###); - let (response, code) = server.delete_tasks(json!({"canceledBy": "doggo"})).await; + let (response, code) = server.delete_tasks("canceledBy=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.canceledBy`.", + "message": "Invalid value in parameter `canceledBy`: could not parse `doggo` as a positive integer", "code": "invalid_task_canceled_by", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-canceled-by" @@ -83,33 +93,33 @@ async fn task_bad_canceled_by() { async fn task_bad_types() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"types": "doggo"})).await; + let (response, code) = server.tasks_filter("types=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is not a type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`. at `.types`.", + "message": "Invalid value in parameter `types`: `doggo` is not a valid task type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`.", "code": "invalid_task_types", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-types" } "###); - let (response, code) = server.cancel_tasks(json!({"types": "doggo"})).await; + let (response, code) = server.cancel_tasks("types=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is not a type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`. at `.types`.", + "message": "Invalid value in parameter `types`: `doggo` is not a valid task type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`.", "code": "invalid_task_types", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-types" } "###); - let (response, code) = server.delete_tasks(json!({"types": "doggo"})).await; + let (response, code) = server.delete_tasks("types=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is not a type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`. at `.types`.", + "message": "Invalid value in parameter `types`: `doggo` is not a valid task type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`.", "code": "invalid_task_types", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-types" @@ -121,33 +131,33 @@ async fn task_bad_types() { async fn task_bad_statuses() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"statuses": "doggo"})).await; + let (response, code) = server.tasks_filter("statuses=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is not a status. Available status are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`. at `.statuses`.", + "message": "Invalid value in parameter `statuses`: `doggo` is not a valid task status. Available statuses are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`.", "code": "invalid_task_statuses", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-statuses" } "###); - let (response, code) = server.cancel_tasks(json!({"statuses": "doggo"})).await; + let (response, code) = server.cancel_tasks("statuses=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is not a status. Available status are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`. at `.statuses`.", + "message": "Invalid value in parameter `statuses`: `doggo` is not a valid task status. Available statuses are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`.", "code": "invalid_task_statuses", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-statuses" } "###); - let (response, code) = server.delete_tasks(json!({"statuses": "doggo"})).await; + let (response, code) = server.delete_tasks("statuses=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is not a status. Available status are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`. at `.statuses`.", + "message": "Invalid value in parameter `statuses`: `doggo` is not a valid task status. Available statuses are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`.", "code": "invalid_task_statuses", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-statuses" @@ -159,33 +169,33 @@ async fn task_bad_statuses() { async fn task_bad_index_uids() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"indexUids": "the good doggo"})).await; + let (response, code) = server.tasks_filter("indexUids=the%20good%20doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`the good doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_). at `.indexUids`.", + "message": "Invalid value in parameter `indexUids`: `the good doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", "code": "invalid_index_uid", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-index-uid" } "###); - let (response, code) = server.cancel_tasks(json!({"indexUids": "the good doggo"})).await; + let (response, code) = server.cancel_tasks("indexUids=the%20good%20doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`the good doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_). at `.indexUids`.", + "message": "Invalid value in parameter `indexUids`: `the good doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", "code": "invalid_index_uid", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-index-uid" } "###); - let (response, code) = server.delete_tasks(json!({"indexUids": "the good doggo"})).await; + let (response, code) = server.delete_tasks("indexUids=the%20good%20doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`the good doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_). at `.indexUids`.", + "message": "Invalid value in parameter `indexUids`: `the good doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", "code": "invalid_index_uid", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-index-uid" @@ -197,33 +207,33 @@ async fn task_bad_index_uids() { async fn task_bad_limit() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"limit": "doggo"})).await; + let (response, code) = server.tasks_filter("limit=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.limit`.", + "message": "Invalid value in parameter `limit`: could not parse `doggo` as a positive integer", "code": "invalid_task_limit", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-limit" } "###); - let (response, code) = server.cancel_tasks(json!({"limit": "doggo"})).await; + let (response, code) = server.cancel_tasks("limit=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "Json deserialize error: unknown field `limit`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", + "message": "Unknown parameter `limit`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`", "code": "bad_request", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#bad-request" } "###); - let (response, code) = server.delete_tasks(json!({"limit": "doggo"})).await; + let (response, code) = server.delete_tasks("limit=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "Json deserialize error: unknown field `limit`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", + "message": "Unknown parameter `limit`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`", "code": "bad_request", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#bad-request" @@ -235,33 +245,33 @@ async fn task_bad_limit() { async fn task_bad_from() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"from": "doggo"})).await; + let (response, code) = server.tasks_filter("from=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "invalid digit found in string at `.from`.", + "message": "Invalid value in parameter `from`: could not parse `doggo` as a positive integer", "code": "invalid_task_from", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-from" } "###); - let (response, code) = server.cancel_tasks(json!({"from": "doggo"})).await; + let (response, code) = server.cancel_tasks("from=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "Json deserialize error: unknown field `from`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", + "message": "Unknown parameter `from`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`", "code": "bad_request", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#bad-request" } "###); - let (response, code) = server.delete_tasks(json!({"from": "doggo"})).await; + let (response, code) = server.delete_tasks("from=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "Json deserialize error: unknown field `from`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", + "message": "Unknown parameter `from`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`", "code": "bad_request", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#bad-request" @@ -273,33 +283,33 @@ async fn task_bad_from() { async fn task_bad_after_enqueued_at() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"afterEnqueuedAt": "doggo"})).await; + let (response, code) = server.tasks_filter("afterEnqueuedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterEnqueuedAt`.", + "message": "Invalid value in parameter `afterEnqueuedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_after_enqueued_at", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-after-enqueued-at" } "###); - let (response, code) = server.cancel_tasks(json!({"afterEnqueuedAt": "doggo"})).await; + let (response, code) = server.cancel_tasks("afterEnqueuedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterEnqueuedAt`.", + "message": "Invalid value in parameter `afterEnqueuedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_after_enqueued_at", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-after-enqueued-at" } "###); - let (response, code) = server.delete_tasks(json!({"afterEnqueuedAt": "doggo"})).await; + let (response, code) = server.delete_tasks("afterEnqueuedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterEnqueuedAt`.", + "message": "Invalid value in parameter `afterEnqueuedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_after_enqueued_at", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-after-enqueued-at" @@ -311,33 +321,33 @@ async fn task_bad_after_enqueued_at() { async fn task_bad_before_enqueued_at() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"beforeEnqueuedAt": "doggo"})).await; + let (response, code) = server.tasks_filter("beforeEnqueuedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeEnqueuedAt`.", + "message": "Invalid value in parameter `beforeEnqueuedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_before_enqueued_at", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-before-enqueued-at" } "###); - let (response, code) = server.cancel_tasks(json!({"beforeEnqueuedAt": "doggo"})).await; + let (response, code) = server.cancel_tasks("beforeEnqueuedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeEnqueuedAt`.", + "message": "Invalid value in parameter `beforeEnqueuedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_before_enqueued_at", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-before-enqueued-at" } "###); - let (response, code) = server.delete_tasks(json!({"beforeEnqueuedAt": "doggo"})).await; + let (response, code) = server.delete_tasks("beforeEnqueuedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeEnqueuedAt`.", + "message": "Invalid value in parameter `beforeEnqueuedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_before_enqueued_at", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-before-enqueued-at" @@ -349,33 +359,33 @@ async fn task_bad_before_enqueued_at() { async fn task_bad_after_started_at() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"afterStartedAt": "doggo"})).await; + let (response, code) = server.tasks_filter("afterStartedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterStartedAt`.", + "message": "Invalid value in parameter `afterStartedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_after_started_at", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-after-started-at" } "###); - let (response, code) = server.cancel_tasks(json!({"afterStartedAt": "doggo"})).await; + let (response, code) = server.cancel_tasks("afterStartedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterStartedAt`.", + "message": "Invalid value in parameter `afterStartedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_after_started_at", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-after-started-at" } "###); - let (response, code) = server.delete_tasks(json!({"afterStartedAt": "doggo"})).await; + let (response, code) = server.delete_tasks("afterStartedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterStartedAt`.", + "message": "Invalid value in parameter `afterStartedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_after_started_at", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-after-started-at" @@ -387,33 +397,33 @@ async fn task_bad_after_started_at() { async fn task_bad_before_started_at() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"beforeStartedAt": "doggo"})).await; + let (response, code) = server.tasks_filter("beforeStartedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeStartedAt`.", + "message": "Invalid value in parameter `beforeStartedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_before_started_at", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-before-started-at" } "###); - let (response, code) = server.cancel_tasks(json!({"beforeStartedAt": "doggo"})).await; + let (response, code) = server.cancel_tasks("beforeStartedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeStartedAt`.", + "message": "Invalid value in parameter `beforeStartedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_before_started_at", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-before-started-at" } "###); - let (response, code) = server.delete_tasks(json!({"beforeStartedAt": "doggo"})).await; + let (response, code) = server.delete_tasks("beforeStartedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeStartedAt`.", + "message": "Invalid value in parameter `beforeStartedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_before_started_at", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-before-started-at" @@ -425,33 +435,33 @@ async fn task_bad_before_started_at() { async fn task_bad_after_finished_at() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"afterFinishedAt": "doggo"})).await; + let (response, code) = server.tasks_filter("afterFinishedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterFinishedAt`.", + "message": "Invalid value in parameter `afterFinishedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_after_finished_at", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-after-finished-at" } "###); - let (response, code) = server.cancel_tasks(json!({"afterFinishedAt": "doggo"})).await; + let (response, code) = server.cancel_tasks("afterFinishedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterFinishedAt`.", + "message": "Invalid value in parameter `afterFinishedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_after_finished_at", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-after-finished-at" } "###); - let (response, code) = server.delete_tasks(json!({"afterFinishedAt": "doggo"})).await; + let (response, code) = server.delete_tasks("afterFinishedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterFinishedAt`.", + "message": "Invalid value in parameter `afterFinishedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_after_finished_at", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-after-finished-at" @@ -463,33 +473,33 @@ async fn task_bad_after_finished_at() { async fn task_bad_before_finished_at() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!({"beforeFinishedAt": "doggo"})).await; + let (response, code) = server.tasks_filter("beforeFinishedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeFinishedAt`.", + "message": "Invalid value in parameter `beforeFinishedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_before_finished_at", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-before-finished-at" } "###); - let (response, code) = server.cancel_tasks(json!({"beforeFinishedAt": "doggo"})).await; + let (response, code) = server.cancel_tasks("beforeFinishedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeFinishedAt`.", + "message": "Invalid value in parameter `beforeFinishedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_before_finished_at", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-before-finished-at" } "###); - let (response, code) = server.delete_tasks(json!({"beforeFinishedAt": "doggo"})).await; + let (response, code) = server.delete_tasks("beforeFinishedAt=doggo").await; snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeFinishedAt`.", + "message": "Invalid value in parameter `beforeFinishedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_before_finished_at", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-before-finished-at" diff --git a/meilisearch/tests/tasks/mod.rs b/meilisearch/tests/tasks/mod.rs index 46775e05f..7fadf0a10 100644 --- a/meilisearch/tests/tasks/mod.rs +++ b/meilisearch/tests/tasks/mod.rs @@ -179,44 +179,44 @@ async fn list_tasks_status_and_type_filtered() { async fn get_task_filter_error() { let server = Server::new().await; - let (response, code) = server.tasks_filter(json!( { "lol": "pied" })).await; + let (response, code) = server.tasks_filter("lol=pied").await; assert_eq!(code, 400, "{}", response); meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { - "message": "Json deserialize error: unknown field `lol`, expected one of `limit`, `from`, `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", + "message": "Unknown parameter `lol`: expected one of `limit`, `from`, `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`", "code": "bad_request", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#bad-request" } "###); - let (response, code) = server.tasks_filter(json!( { "uids": "pied" })).await; + let (response, code) = server.tasks_filter("uids=pied").await; assert_eq!(code, 400, "{}", response); meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { - "message": "invalid digit found in string at `.uids`.", + "message": "Invalid value in parameter `uids`: could not parse `pied` as a positive integer", "code": "invalid_task_uids", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-uids" } "###); - let (response, code) = server.tasks_filter(json!( { "from": "pied" })).await; + let (response, code) = server.tasks_filter("from=pied").await; assert_eq!(code, 400, "{}", response); meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { - "message": "invalid digit found in string at `.from`.", + "message": "Invalid value in parameter `from`: could not parse `pied` as a positive integer", "code": "invalid_task_from", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-from" } "###); - let (response, code) = server.tasks_filter(json!( { "beforeStartedAt": "pied" })).await; + let (response, code) = server.tasks_filter("beforeStartedAt=pied").await; assert_eq!(code, 400, "{}", response); meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { - "message": "`pied` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeStartedAt`.", + "message": "Invalid value in parameter `beforeStartedAt`: `pied` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_before_started_at", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-before-started-at" @@ -228,7 +228,7 @@ async fn get_task_filter_error() { async fn delete_task_filter_error() { let server = Server::new().await; - let (response, code) = server.delete_tasks(json!(null)).await; + let (response, code) = server.delete_tasks("").await; assert_eq!(code, 400, "{}", response); meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { @@ -239,22 +239,22 @@ async fn delete_task_filter_error() { } "###); - let (response, code) = server.delete_tasks(json!({ "lol": "pied" })).await; + let (response, code) = server.delete_tasks("lol=pied").await; assert_eq!(code, 400, "{}", response); meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { - "message": "Json deserialize error: unknown field `lol`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", + "message": "Unknown parameter `lol`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`", "code": "bad_request", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#bad-request" } "###); - let (response, code) = server.delete_tasks(json!({ "uids": "pied" })).await; + let (response, code) = server.delete_tasks("uids=pied").await; assert_eq!(code, 400, "{}", response); meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { - "message": "invalid digit found in string at `.uids`.", + "message": "Invalid value in parameter `uids`: could not parse `pied` as a positive integer", "code": "invalid_task_uids", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-uids" @@ -266,7 +266,7 @@ async fn delete_task_filter_error() { async fn cancel_task_filter_error() { let server = Server::new().await; - let (response, code) = server.cancel_tasks(json!(null)).await; + let (response, code) = server.cancel_tasks("").await; assert_eq!(code, 400, "{}", response); meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { @@ -277,22 +277,22 @@ async fn cancel_task_filter_error() { } "###); - let (response, code) = server.cancel_tasks(json!({ "lol": "pied" })).await; + let (response, code) = server.cancel_tasks("lol=pied").await; assert_eq!(code, 400, "{}", response); meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { - "message": "Json deserialize error: unknown field `lol`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", + "message": "Unknown parameter `lol`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`", "code": "bad_request", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#bad-request" } "###); - let (response, code) = server.cancel_tasks(json!({ "uids": "pied" })).await; + let (response, code) = server.cancel_tasks("uids=pied").await; assert_eq!(code, 400, "{}", response); meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { - "message": "invalid digit found in string at `.uids`.", + "message": "Invalid value in parameter `uids`: could not parse `pied` as a positive integer", "code": "invalid_task_uids", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-uids" @@ -523,7 +523,7 @@ async fn test_summarized_settings_update() { meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { - "message": "`custom` ranking rule is invalid. Valid ranking rules are words, typo, sort, proximity, attribute, exactness and custom ranking rules. at `.rankingRules[0]`.", + "message": "Invalid value at `.rankingRules[0]`: `custom` ranking rule is invalid. Valid ranking rules are words, typo, sort, proximity, attribute, exactness and custom ranking rules.", "code": "invalid_settings_ranking_rules", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-settings-ranking-rules" @@ -899,7 +899,7 @@ async fn test_summarized_task_cancelation() { // to avoid being flaky we're only going to cancel an already finished task :( index.create(None).await; index.wait_task(0).await; - server.cancel_tasks(json!({ "uids": [0] })).await; + server.cancel_tasks("uids=0").await; index.wait_task(1).await; let (task, _) = index.get_task(1).await; assert_json_snapshot!(task, @@ -932,7 +932,7 @@ async fn test_summarized_task_deletion() { // to avoid being flaky we're only going to delete an already finished task :( index.create(None).await; index.wait_task(0).await; - server.delete_tasks(json!({ "uids": [0] })).await; + server.delete_tasks("uids=0").await; index.wait_task(1).await; let (task, _) = index.get_task(1).await; assert_json_snapshot!(task, From 766dd830ae9434fa0da1bfd7e5fdec3973a3ef82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Lecrenier?= Date: Thu, 12 Jan 2023 15:35:03 +0100 Subject: [PATCH 2/9] Update deserr to latest version + add new error codes for missing fields - missing_api_key_indexes - missing_api_key_actions - missing_api_key_expires_at - missing_swap_indexes_indexes --- Cargo.lock | 16 ++----- meilisearch-types/Cargo.toml | 4 +- meilisearch-types/src/error.rs | 49 ++++++++++++++++++-- meilisearch-types/src/keys.rs | 14 +++--- meilisearch-types/src/lib.rs | 1 - meilisearch-types/src/settings.rs | 31 ++++++++----- meilisearch/Cargo.toml | 2 +- meilisearch/src/routes/api_key.rs | 4 +- meilisearch/src/routes/indexes/documents.rs | 10 ++-- meilisearch/src/routes/indexes/mod.rs | 4 +- meilisearch/src/routes/indexes/search.rs | 34 +++++++------- meilisearch/src/routes/swap_indexes.rs | 2 +- meilisearch/src/routes/tasks.rs | 48 +++++++++---------- meilisearch/src/search.rs | 36 +++++++------- meilisearch/tests/auth/api_keys.rs | 27 ++++------- meilisearch/tests/documents/add_documents.rs | 4 +- 16 files changed, 158 insertions(+), 128 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index da1ec3011..6e754abd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1025,8 +1025,6 @@ dependencies = [ [[package]] name = "deserr" version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86290491a2b5c21a1a5083da8dae831006761258fabd5617309c3eebc5f89468" dependencies = [ "deserr-internal", "serde-cs", @@ -1036,8 +1034,6 @@ dependencies = [ [[package]] name = "deserr-internal" version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7131de1c27581bc376a22166c9f570be91b76cb096be2f6aecf224c27bf7c49a" dependencies = [ "convert_case 0.5.0", "proc-macro2 1.0.49", @@ -1315,8 +1311,7 @@ dependencies = [ [[package]] name = "filter-parser" -version = "0.39.0" -source = "git+https://github.com/meilisearch/milli.git?tag=v0.39.0#e6bea999740b153871f665abce869ffbb5aa94c5" +version = "0.38.0" dependencies = [ "nom", "nom_locate", @@ -1334,8 +1329,7 @@ dependencies = [ [[package]] name = "flatten-serde-json" -version = "0.39.0" -source = "git+https://github.com/meilisearch/milli.git?tag=v0.39.0#e6bea999740b153871f665abce869ffbb5aa94c5" +version = "0.38.0" dependencies = [ "serde_json", ] @@ -1899,8 +1893,7 @@ dependencies = [ [[package]] name = "json-depth-checker" -version = "0.39.0" -source = "git+https://github.com/meilisearch/milli.git?tag=v0.39.0#e6bea999740b153871f665abce869ffbb5aa94c5" +version = "0.38.0" dependencies = [ "serde_json", ] @@ -2448,8 +2441,7 @@ dependencies = [ [[package]] name = "milli" -version = "0.39.0" -source = "git+https://github.com/meilisearch/milli.git?tag=v0.39.0#e6bea999740b153871f665abce869ffbb5aa94c5" +version = "0.38.0" dependencies = [ "bimap", "bincode", diff --git a/meilisearch-types/Cargo.toml b/meilisearch-types/Cargo.toml index 4c0b1ca93..257ac9c2d 100644 --- a/meilisearch-types/Cargo.toml +++ b/meilisearch-types/Cargo.toml @@ -9,14 +9,14 @@ actix-web = { version = "4.2.1", default-features = false } anyhow = "1.0.65" convert_case = "0.6.0" csv = "1.1.6" -deserr = "0.1.4" +deserr = { path = "/Users/meilisearch/Documents/deserr" } either = { version = "1.6.1", features = ["serde"] } enum-iterator = "1.1.3" file-store = { path = "../file-store" } flate2 = "1.0.24" fst = "0.4.7" memmap2 = "0.5.7" -milli = { git = "https://github.com/meilisearch/milli.git", tag = "v0.39.0", default-features = false } +milli = { path = "/Users/meilisearch/Documents/milli2/milli", default-features = false } proptest = { version = "1.0.0", optional = true } proptest-derive = { version = "0.3.0", optional = true } roaring = { version = "0.10.0", features = ["serde"] } diff --git a/meilisearch-types/src/error.rs b/meilisearch-types/src/error.rs index 2be6ffff4..0d7f126a5 100644 --- a/meilisearch-types/src/error.rs +++ b/meilisearch-types/src/error.rs @@ -14,7 +14,9 @@ use serde_cs::vec::CS; use crate::star_or::StarOr; -use self::deserr_codes::MissingIndexUid; +use self::deserr_codes::{ + MissingApiKeyActions, MissingApiKeyExpiresAt, MissingApiKeyIndexes, MissingIndexUid, InvalidSwapIndexes, MissingSwapIndexesIndexes, +}; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] @@ -185,7 +187,6 @@ DumpAlreadyProcessing , invalid , CONFLICT; DumpNotFound , invalid , NOT_FOUND; DumpProcessFailed , internal , INTERNAL_SERVER_ERROR; DuplicateIndexFound , invalid , BAD_REQUEST; - ImmutableApiKeyUid , invalid , BAD_REQUEST; ImmutableApiKeyKey , invalid , BAD_REQUEST; ImmutableApiKeyActions , invalid , BAD_REQUEST; @@ -193,11 +194,9 @@ ImmutableApiKeyIndexes , invalid , BAD_REQUEST; ImmutableApiKeyExpiresAt , invalid , BAD_REQUEST; ImmutableApiKeyCreatedAt , invalid , BAD_REQUEST; ImmutableApiKeyUpdatedAt , invalid , BAD_REQUEST; - ImmutableIndexUid , invalid , BAD_REQUEST; ImmutableIndexCreatedAt , invalid , BAD_REQUEST; ImmutableIndexUpdatedAt , invalid , BAD_REQUEST; - IndexAlreadyExists , invalid , CONFLICT ; IndexCreationFailed , internal , INTERNAL_SERVER_ERROR; IndexNotFound , invalid , NOT_FOUND; @@ -281,6 +280,7 @@ MissingDocumentId , invalid , BAD_REQUEST ; MissingIndexUid , invalid , BAD_REQUEST ; MissingMasterKey , authentication, UNAUTHORIZED ; MissingPayload , invalid , BAD_REQUEST ; +MissingSwapIndexesIndexes , invalid , BAD_REQUEST ; MissingTaskFilters , invalid , BAD_REQUEST ; NoSpaceLeftOnDevice , system , UNPROCESSABLE_ENTITY; PayloadTooLarge , invalid , PAYLOAD_TOO_LARGE ; @@ -478,6 +478,47 @@ impl DeserrJsonError { Self { msg: x.msg, code: MissingIndexUid.error_code(), _phantom: PhantomData } } } +impl DeserrJsonError { + pub fn missing_api_key_actions(field: &str, location: ValuePointerRef) -> Self { + let x = unwrap_any(Self::error::( + None, + deserr::ErrorKind::MissingField { field }, + location, + )); + Self { msg: x.msg, code: MissingApiKeyActions.error_code(), _phantom: PhantomData } + } +} +impl DeserrJsonError { + pub fn missing_api_key_expires_at(field: &str, location: ValuePointerRef) -> Self { + let x = unwrap_any(Self::error::( + None, + deserr::ErrorKind::MissingField { field }, + location, + )); + Self { msg: x.msg, code: MissingApiKeyExpiresAt.error_code(), _phantom: PhantomData } + } +} +impl DeserrJsonError { + pub fn missing_api_key_indexes(field: &str, location: ValuePointerRef) -> Self { + let x = unwrap_any(Self::error::( + None, + deserr::ErrorKind::MissingField { field }, + location, + )); + Self { msg: x.msg, code: MissingApiKeyIndexes.error_code(), _phantom: PhantomData } + } +} + +impl DeserrJsonError { + pub fn missing_swap_indexes_indexes(field: &str, location: ValuePointerRef) -> Self { + let x = unwrap_any(Self::error::( + None, + deserr::ErrorKind::MissingField { field }, + location, + )); + Self { msg: x.msg, code: MissingSwapIndexesIndexes.error_code(), _phantom: PhantomData } + } +} // if the error happened in the root, then an empty string is returned. pub fn location_json_description(location: ValuePointerRef, article: &str) -> String { diff --git a/meilisearch-types/src/keys.rs b/meilisearch-types/src/keys.rs index 53776e489..d736fd8c1 100644 --- a/meilisearch-types/src/keys.rs +++ b/meilisearch-types/src/keys.rs @@ -38,17 +38,17 @@ fn parse_uuid_from_str(s: &str) -> Result> { #[derive(Debug, DeserializeFromValue)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] pub struct CreateApiKey { - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub description: Option, - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub name: Option, #[deserr(default = Uuid::new_v4(), error = DeserrJsonError, from(&String) = parse_uuid_from_str -> TakeErrorMessage)] pub uid: KeyId, - #[deserr(error = DeserrJsonError)] + #[deserr(error = DeserrJsonError, missing_field_error = DeserrJsonError::missing_api_key_actions)] pub actions: Vec, - #[deserr(error = DeserrJsonError)] + #[deserr(error = DeserrJsonError, missing_field_error = DeserrJsonError::missing_api_key_indexes)] pub indexes: Vec>, - #[deserr(error = DeserrJsonError, default = None, from(&String) = parse_expiration_date -> TakeErrorMessage)] + #[deserr(error = DeserrJsonError, from(&String) = parse_expiration_date -> TakeErrorMessage, missing_field_error = DeserrJsonError::missing_api_key_expires_at)] pub expires_at: Option, } impl CreateApiKey { @@ -94,9 +94,9 @@ fn deny_immutable_fields_api_key( #[derive(Debug, DeserializeFromValue)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_api_key)] pub struct PatchApiKey { - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub description: Option, - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub name: Option, } diff --git a/meilisearch-types/src/lib.rs b/meilisearch-types/src/lib.rs index f8fc47abd..354a25fa1 100644 --- a/meilisearch-types/src/lib.rs +++ b/meilisearch-types/src/lib.rs @@ -8,7 +8,6 @@ pub mod star_or; pub mod tasks; pub mod versioning; -pub use deserr; pub use milli; pub use milli::{heed, Index}; pub use serde_cs; diff --git a/meilisearch-types/src/settings.rs b/meilisearch-types/src/settings.rs index 0a79f865e..99f4ae9e3 100644 --- a/meilisearch-types/src/settings.rs +++ b/meilisearch-types/src/settings.rs @@ -69,8 +69,10 @@ fn validate_min_word_size_for_typo_setting( #[deserr(deny_unknown_fields, rename_all = camelCase, validate = validate_min_word_size_for_typo_setting -> DeserrJsonError)] pub struct MinWordSizeTyposSetting { #[serde(default, skip_serializing_if = "Setting::is_not_set")] + #[deserr(default)] pub one_typo: Setting, #[serde(default, skip_serializing_if = "Setting::is_not_set")] + #[deserr(default)] pub two_typos: Setting, } @@ -79,13 +81,16 @@ pub struct MinWordSizeTyposSetting { #[deserr(deny_unknown_fields, rename_all = camelCase, where_predicate = __Deserr_E: deserr::MergeWithError>)] pub struct TypoSettings { #[serde(default, skip_serializing_if = "Setting::is_not_set")] + #[deserr(default)] pub enabled: Setting, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub min_word_size_for_typos: Setting, #[serde(default, skip_serializing_if = "Setting::is_not_set")] + #[deserr(default)] pub disable_on_words: Setting>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] + #[deserr(default)] pub disable_on_attributes: Setting>, } @@ -94,6 +99,7 @@ pub struct TypoSettings { #[deserr(rename_all = camelCase, deny_unknown_fields)] pub struct FacetingSettings { #[serde(default, skip_serializing_if = "Setting::is_not_set")] + #[deserr(default)] pub max_values_per_facet: Setting, } @@ -102,6 +108,7 @@ pub struct FacetingSettings { #[deserr(rename_all = camelCase, deny_unknown_fields)] pub struct PaginationSettings { #[serde(default, skip_serializing_if = "Setting::is_not_set")] + #[deserr(default)] pub max_total_hits: Setting, } @@ -135,7 +142,7 @@ pub struct Settings { serialize_with = "serialize_with_wildcard", skip_serializing_if = "Setting::is_not_set" )] - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub displayed_attributes: Setting>, #[serde( @@ -143,35 +150,35 @@ pub struct Settings { serialize_with = "serialize_with_wildcard", skip_serializing_if = "Setting::is_not_set" )] - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub searchable_attributes: Setting>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub filterable_attributes: Setting>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub sortable_attributes: Setting>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub ranking_rules: Setting>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub stop_words: Setting>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub synonyms: Setting>>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub distinct_attribute: Setting, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub typo_tolerance: Setting, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub faceting: Setting, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub pagination: Setting, #[serde(skip)] diff --git a/meilisearch/Cargo.toml b/meilisearch/Cargo.toml index be852c02e..2c2c2aca8 100644 --- a/meilisearch/Cargo.toml +++ b/meilisearch/Cargo.toml @@ -19,7 +19,7 @@ byte-unit = { version = "4.0.14", default-features = false, features = ["std", " bytes = "1.2.1" clap = { version = "4.0.9", features = ["derive", "env"] } crossbeam-channel = "0.5.6" -deserr = "0.1.4" +deserr = { path = "/Users/meilisearch/Documents/deserr" } dump = { path = "../dump" } either = "1.8.0" env_logger = "0.9.1" diff --git a/meilisearch/src/routes/api_key.rs b/meilisearch/src/routes/api_key.rs index ce4ab0696..917a5e285 100644 --- a/meilisearch/src/routes/api_key.rs +++ b/meilisearch/src/routes/api_key.rs @@ -55,10 +55,10 @@ pub async fn create_api_key( #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ListApiKeys { #[serde(default)] - #[deserr(error = DeserrQueryParamError, default, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] pub offset: usize, #[serde(default = "PAGINATION_DEFAULT_LIMIT")] - #[deserr(error = DeserrQueryParamError, default = PAGINATION_DEFAULT_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] + #[deserr(default = PAGINATION_DEFAULT_LIMIT(), error = DeserrQueryParamError, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] pub limit: usize, } impl ListApiKeys { diff --git a/meilisearch/src/routes/indexes/documents.rs b/meilisearch/src/routes/indexes/documents.rs index c09b12244..2c1b0f692 100644 --- a/meilisearch/src/routes/indexes/documents.rs +++ b/meilisearch/src/routes/indexes/documents.rs @@ -85,7 +85,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) { #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] pub struct GetDocument { // TODO: strongly typed argument here - #[deserr(error = DeserrQueryParamError)] + #[deserr(default, error = DeserrQueryParamError)] fields: Option>>, } @@ -122,11 +122,11 @@ pub async fn delete_document( #[derive(Deserialize, Debug, DeserializeFromValue)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] pub struct BrowseQuery { - #[deserr(error = DeserrQueryParamError, default, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] offset: usize, - #[deserr(error = DeserrQueryParamError, default = crate::routes::PAGINATION_DEFAULT_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] + #[deserr(default = crate::routes::PAGINATION_DEFAULT_LIMIT(), error = DeserrQueryParamError, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] limit: usize, - #[deserr(error = DeserrQueryParamError)] + #[deserr(default, error = DeserrQueryParamError)] fields: Option>>, } @@ -151,7 +151,7 @@ pub async fn get_all_documents( #[derive(Deserialize, Debug, DeserializeFromValue)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] pub struct UpdateDocumentsQuery { - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub primary_key: Option, } diff --git a/meilisearch/src/routes/indexes/mod.rs b/meilisearch/src/routes/indexes/mod.rs index 061eefaf6..216cc448e 100644 --- a/meilisearch/src/routes/indexes/mod.rs +++ b/meilisearch/src/routes/indexes/mod.rs @@ -76,10 +76,10 @@ impl IndexView { #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ListIndexes { #[serde(default)] - #[deserr(error = DeserrQueryParamError, default, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] pub offset: usize, #[serde(default = "PAGINATION_DEFAULT_LIMIT")] - #[deserr(error = DeserrQueryParamError, default = PAGINATION_DEFAULT_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] + #[deserr(default = PAGINATION_DEFAULT_LIMIT(), error = DeserrQueryParamError, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] pub limit: usize, } impl ListIndexes { diff --git a/meilisearch/src/routes/indexes/search.rs b/meilisearch/src/routes/indexes/search.rs index 8819ac8cf..ec9364711 100644 --- a/meilisearch/src/routes/indexes/search.rs +++ b/meilisearch/src/routes/indexes/search.rs @@ -48,39 +48,39 @@ pub fn parse_bool_take_error_message( #[derive(Debug, deserr::DeserializeFromValue)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] pub struct SearchQueryGet { - #[deserr(error = DeserrQueryParamError)] + #[deserr(default, error = DeserrQueryParamError)] q: Option, - #[deserr(error = DeserrQueryParamError, default = DEFAULT_SEARCH_OFFSET(), from(String) = parse_usize_query_param -> TakeErrorMessage)] + #[deserr(default = DEFAULT_SEARCH_OFFSET(), error = DeserrQueryParamError, from(String) = parse_usize_query_param -> TakeErrorMessage)] offset: usize, - #[deserr(error = DeserrQueryParamError, default = DEFAULT_SEARCH_LIMIT(), from(String) = parse_usize_query_param -> TakeErrorMessage)] + #[deserr(default = DEFAULT_SEARCH_LIMIT(), error = DeserrQueryParamError, from(String) = parse_usize_query_param -> TakeErrorMessage)] limit: usize, - #[deserr(error = DeserrQueryParamError, from(Option) = parse_option_usize_query_param -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, from(Option) = parse_option_usize_query_param -> TakeErrorMessage)] page: Option, - #[deserr(error = DeserrQueryParamError, from(Option) = parse_option_usize_query_param -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, from(Option) = parse_option_usize_query_param -> TakeErrorMessage)] hits_per_page: Option, - #[deserr(error = DeserrQueryParamError)] + #[deserr(default, error = DeserrQueryParamError)] attributes_to_retrieve: Option>, - #[deserr(error = DeserrQueryParamError)] + #[deserr(default, error = DeserrQueryParamError)] attributes_to_crop: Option>, - #[deserr(error = DeserrQueryParamError, default = DEFAULT_CROP_LENGTH(), from(String) = parse_usize_query_param -> TakeErrorMessage)] + #[deserr(default = DEFAULT_CROP_LENGTH(), error = DeserrQueryParamError, from(String) = parse_usize_query_param -> TakeErrorMessage)] crop_length: usize, - #[deserr(error = DeserrQueryParamError)] + #[deserr(default, error = DeserrQueryParamError)] attributes_to_highlight: Option>, - #[deserr(error = DeserrQueryParamError)] + #[deserr(default, error = DeserrQueryParamError)] filter: Option, - #[deserr(error = DeserrQueryParamError)] + #[deserr(default, error = DeserrQueryParamError)] sort: Option, - #[deserr(error = DeserrQueryParamError, default, from(&String) = parse_bool_take_error_message -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, from(&String) = parse_bool_take_error_message -> TakeErrorMessage)] show_matches_position: bool, - #[deserr(error = DeserrQueryParamError)] + #[deserr(default, error = DeserrQueryParamError)] facets: Option>, - #[deserr(error = DeserrQueryParamError, default = DEFAULT_HIGHLIGHT_PRE_TAG())] + #[deserr( default = DEFAULT_HIGHLIGHT_PRE_TAG(), error = DeserrQueryParamError)] highlight_pre_tag: String, - #[deserr(error = DeserrQueryParamError, default = DEFAULT_HIGHLIGHT_POST_TAG())] + #[deserr( default = DEFAULT_HIGHLIGHT_POST_TAG(), error = DeserrQueryParamError)] highlight_post_tag: String, - #[deserr(error = DeserrQueryParamError, default = DEFAULT_CROP_MARKER())] + #[deserr(default = DEFAULT_CROP_MARKER(), error = DeserrQueryParamError)] crop_marker: String, - #[deserr(error = DeserrQueryParamError, default)] + #[deserr(default, error = DeserrQueryParamError)] matching_strategy: MatchingStrategy, } diff --git a/meilisearch/src/routes/swap_indexes.rs b/meilisearch/src/routes/swap_indexes.rs index 57015f1f1..5d6d1e1e5 100644 --- a/meilisearch/src/routes/swap_indexes.rs +++ b/meilisearch/src/routes/swap_indexes.rs @@ -22,7 +22,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) { #[derive(DeserializeFromValue, Debug, Clone, PartialEq, Eq)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] pub struct SwapIndexesPayload { - #[deserr(error = DeserrJsonError)] + #[deserr(error = DeserrJsonError, missing_field_error = DeserrJsonError::missing_swap_indexes_indexes)] indexes: Vec, } diff --git a/meilisearch/src/routes/tasks.rs b/meilisearch/src/routes/tasks.rs index dbf1380e2..d9c498e4e 100644 --- a/meilisearch/src/routes/tasks.rs +++ b/meilisearch/src/routes/tasks.rs @@ -169,61 +169,61 @@ impl From
for DetailsView { #[derive(Debug, DeserializeFromValue)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] pub struct TasksFilterQuery { - #[deserr(error = DeserrQueryParamError, default = DEFAULT_LIMIT(), from(String) = parse_u32_query_param -> TakeErrorMessage)] + #[deserr(default = DEFAULT_LIMIT(), error = DeserrQueryParamError, from(String) = parse_u32_query_param -> TakeErrorMessage)] pub limit: u32, - #[deserr(error = DeserrQueryParamError, from(Option) = parse_option_u32_query_param -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, from(Option) = parse_option_u32_query_param -> TakeErrorMessage)] pub from: Option, - #[deserr(error = DeserrQueryParamError, from(Option>) = parse_option_vec_u32_query_param -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, from(Option>) = parse_option_vec_u32_query_param -> TakeErrorMessage)] pub uids: Option>, - #[deserr(error = DeserrQueryParamError, from(Option>) = parse_option_vec_u32_query_param -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, from(Option>) = parse_option_vec_u32_query_param -> TakeErrorMessage)] pub canceled_by: Option>, - #[deserr(error = DeserrQueryParamError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] pub types: Option>, - #[deserr(error = DeserrQueryParamError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] pub statuses: Option>, - #[deserr(error = DeserrQueryParamError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] pub index_uids: Option>, - #[deserr(error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] pub after_enqueued_at: Option, - #[deserr(error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] pub before_enqueued_at: Option, - #[deserr(error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] pub after_started_at: Option, - #[deserr(error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] pub before_started_at: Option, - #[deserr(error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] pub after_finished_at: Option, - #[deserr(error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] pub before_finished_at: Option, } #[derive(Deserialize, Debug, DeserializeFromValue)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] pub struct TaskDeletionOrCancelationQuery { - #[deserr(error = DeserrQueryParamError, from(Option>) = parse_option_vec_u32_query_param -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, from(Option>) = parse_option_vec_u32_query_param -> TakeErrorMessage)] pub uids: Option>, - #[deserr(error = DeserrQueryParamError, from(Option>) = parse_option_vec_u32_query_param -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, from(Option>) = parse_option_vec_u32_query_param -> TakeErrorMessage)] pub canceled_by: Option>, - #[deserr(error = DeserrQueryParamError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] pub types: Option>, - #[deserr(error = DeserrQueryParamError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] pub statuses: Option>, - #[deserr(error = DeserrQueryParamError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] pub index_uids: Option>, - #[deserr(error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] pub after_enqueued_at: Option, - #[deserr(error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] pub before_enqueued_at: Option, - #[deserr(error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] pub after_started_at: Option, - #[deserr(error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] pub before_started_at: Option, - #[deserr(error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] pub after_finished_at: Option, - #[deserr(error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] + #[deserr(default, error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] pub before_finished_at: Option, } diff --git a/meilisearch/src/search.rs b/meilisearch/src/search.rs index 4e2c43f18..bfb0bf160 100644 --- a/meilisearch/src/search.rs +++ b/meilisearch/src/search.rs @@ -3,8 +3,8 @@ use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::str::FromStr; use std::time::Instant; +use deserr::DeserializeFromValue; use either::Either; -use meilisearch_types::deserr::DeserializeFromValue; use meilisearch_types::error::deserr_codes::*; use meilisearch_types::error::DeserrJsonError; use meilisearch_types::settings::DEFAULT_PAGINATION_MAX_TOTAL_HITS; @@ -32,39 +32,39 @@ pub const DEFAULT_HIGHLIGHT_POST_TAG: fn() -> String = || "".to_string(); #[derive(Debug, Clone, Default, PartialEq, DeserializeFromValue)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] pub struct SearchQuery { - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub q: Option, - #[deserr(error = DeserrJsonError, default = DEFAULT_SEARCH_OFFSET())] + #[deserr(default = DEFAULT_SEARCH_OFFSET(), error = DeserrJsonError)] pub offset: usize, - #[deserr(error = DeserrJsonError, default = DEFAULT_SEARCH_LIMIT())] + #[deserr(default = DEFAULT_SEARCH_LIMIT(), error = DeserrJsonError)] pub limit: usize, - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub page: Option, - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub hits_per_page: Option, - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub attributes_to_retrieve: Option>, - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub attributes_to_crop: Option>, - #[deserr(error = DeserrJsonError, default = DEFAULT_CROP_LENGTH())] + #[deserr(default, error = DeserrJsonError, default = DEFAULT_CROP_LENGTH())] pub crop_length: usize, - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub attributes_to_highlight: Option>, - #[deserr(error = DeserrJsonError, default)] + #[deserr(default, error = DeserrJsonError, default)] pub show_matches_position: bool, - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub filter: Option, - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub sort: Option>, - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] pub facets: Option>, - #[deserr(error = DeserrJsonError, default = DEFAULT_HIGHLIGHT_PRE_TAG())] + #[deserr(default, error = DeserrJsonError, default = DEFAULT_HIGHLIGHT_PRE_TAG())] pub highlight_pre_tag: String, - #[deserr(error = DeserrJsonError, default = DEFAULT_HIGHLIGHT_POST_TAG())] + #[deserr(default, error = DeserrJsonError, default = DEFAULT_HIGHLIGHT_POST_TAG())] pub highlight_post_tag: String, - #[deserr(error = DeserrJsonError, default = DEFAULT_CROP_MARKER())] + #[deserr(default, error = DeserrJsonError, default = DEFAULT_CROP_MARKER())] pub crop_marker: String, - #[deserr(error = DeserrJsonError, default)] + #[deserr(default, error = DeserrJsonError, default)] pub matching_strategy: MatchingStrategy, } diff --git a/meilisearch/tests/auth/api_keys.rs b/meilisearch/tests/auth/api_keys.rs index 8d7cb9130..03910c0a9 100644 --- a/meilisearch/tests/auth/api_keys.rs +++ b/meilisearch/tests/auth/api_keys.rs @@ -249,9 +249,9 @@ async fn error_add_api_key_missing_parameter() { meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { "message": "Missing field `indexes`", - "code": "bad_request", + "code": "missing_api_key_indexes", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#bad-request" + "link": "https://docs.meilisearch.com/errors#missing-api-key-indexes" } "###); @@ -266,9 +266,9 @@ async fn error_add_api_key_missing_parameter() { meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" { "message": "Missing field `actions`", - "code": "bad_request", + "code": "missing_api_key_actions", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#bad-request" + "link": "https://docs.meilisearch.com/errors#missing-api-key-actions" } "###); @@ -279,22 +279,13 @@ async fn error_add_api_key_missing_parameter() { "actions": ["documents.add"], }); let (response, code) = server.add_api_key(content).await; - meili_snap::snapshot!(code, @"201 Created"); + meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }), @r###" { - "name": null, - "description": "Indexing API key", - "key": "[ignored]", - "uid": "[ignored]", - "actions": [ - "documents.add" - ], - "indexes": [ - "products" - ], - "expiresAt": null, - "createdAt": "[ignored]", - "updatedAt": "[ignored]" + "message": "Missing field `expiresAt`", + "code": "missing_api_key_expires_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#missing-api-key-expires-at" } "###); } diff --git a/meilisearch/tests/documents/add_documents.rs b/meilisearch/tests/documents/add_documents.rs index 4af365a7e..c27b899c6 100644 --- a/meilisearch/tests/documents/add_documents.rs +++ b/meilisearch/tests/documents/add_documents.rs @@ -926,7 +926,7 @@ async fn error_primary_key_inference() { "indexedDocuments": 1 }, "error": { - "message": "The primary key inference failed as the engine did not find any field ending with `id` in its name. Please specify the primary key manually using the `primaryKey` query parameter.", + "message": "The primary key inference process failed because the engine did not find any field ending with `id` in its name. Please specify the primary key manually using the `primaryKey` query parameter.", "code": "index_primary_key_no_candidate_found", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#index-primary-key-no-candidate-found" @@ -966,7 +966,7 @@ async fn error_primary_key_inference() { "indexedDocuments": 1 }, "error": { - "message": "The primary key inference failed as the engine found 3 fields ending with `id` in their names: 'id' and 'object_id'. Please specify the primary key manually using the `primaryKey` query parameter.", + "message": "The primary key inference process failed because the engine found 3 fields ending with `id` in their name, such as 'id' and 'object_id'. Please specify the primary key manually using the `primaryKey` query parameter.", "code": "index_primary_key_multiple_candidates_found", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#index-primary-key-multiple-candidates-found" From 49ddaaef49646f586ba6c5e583313e24a6fb3d83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Lecrenier?= Date: Thu, 12 Jan 2023 16:42:50 +0100 Subject: [PATCH 3/9] Fix missing_swap_indexes error code and handling of expires_at param... of create api key route --- meilisearch-types/src/error.rs | 7 ++++--- meilisearch-types/src/keys.rs | 19 +++++++++++-------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/meilisearch-types/src/error.rs b/meilisearch-types/src/error.rs index 0d7f126a5..614449ff8 100644 --- a/meilisearch-types/src/error.rs +++ b/meilisearch-types/src/error.rs @@ -15,7 +15,8 @@ use serde_cs::vec::CS; use crate::star_or::StarOr; use self::deserr_codes::{ - MissingApiKeyActions, MissingApiKeyExpiresAt, MissingApiKeyIndexes, MissingIndexUid, InvalidSwapIndexes, MissingSwapIndexesIndexes, + InvalidSwapIndexes, MissingApiKeyActions, MissingApiKeyExpiresAt, MissingApiKeyIndexes, + MissingIndexUid, MissingSwapIndexes, }; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] @@ -280,7 +281,7 @@ MissingDocumentId , invalid , BAD_REQUEST ; MissingIndexUid , invalid , BAD_REQUEST ; MissingMasterKey , authentication, UNAUTHORIZED ; MissingPayload , invalid , BAD_REQUEST ; -MissingSwapIndexesIndexes , invalid , BAD_REQUEST ; +MissingSwapIndexes , invalid , BAD_REQUEST ; MissingTaskFilters , invalid , BAD_REQUEST ; NoSpaceLeftOnDevice , system , UNPROCESSABLE_ENTITY; PayloadTooLarge , invalid , PAYLOAD_TOO_LARGE ; @@ -516,7 +517,7 @@ impl DeserrJsonError { deserr::ErrorKind::MissingField { field }, location, )); - Self { msg: x.msg, code: MissingSwapIndexesIndexes.error_code(), _phantom: PhantomData } + Self { msg: x.msg, code: MissingSwapIndexes.error_code(), _phantom: PhantomData } } } diff --git a/meilisearch-types/src/keys.rs b/meilisearch-types/src/keys.rs index d736fd8c1..8543651c5 100644 --- a/meilisearch-types/src/keys.rs +++ b/meilisearch-types/src/keys.rs @@ -48,7 +48,7 @@ pub struct CreateApiKey { pub actions: Vec, #[deserr(error = DeserrJsonError, missing_field_error = DeserrJsonError::missing_api_key_indexes)] pub indexes: Vec>, - #[deserr(error = DeserrJsonError, from(&String) = parse_expiration_date -> TakeErrorMessage, missing_field_error = DeserrJsonError::missing_api_key_expires_at)] + #[deserr(error = DeserrJsonError, from(Option) = parse_expiration_date -> TakeErrorMessage, missing_field_error = DeserrJsonError::missing_api_key_expires_at)] pub expires_at: Option, } impl CreateApiKey { @@ -159,36 +159,39 @@ impl Display for ParseOffsetDateTimeError { impl std::error::Error for ParseOffsetDateTimeError {} fn parse_expiration_date( - string: &str, + string: Option, ) -> std::result::Result, TakeErrorMessage> { - let datetime = if let Ok(datetime) = OffsetDateTime::parse(string, &Rfc3339) { + let Some(string) = string else { + return Ok(None) + }; + let datetime = if let Ok(datetime) = OffsetDateTime::parse(&string, &Rfc3339) { datetime } else if let Ok(primitive_datetime) = PrimitiveDateTime::parse( - string, + &string, format_description!( "[year repr:full base:calendar]-[month repr:numerical]-[day]T[hour]:[minute]:[second]" ), ) { primitive_datetime.assume_utc() } else if let Ok(primitive_datetime) = PrimitiveDateTime::parse( - string, + &string, format_description!( "[year repr:full base:calendar]-[month repr:numerical]-[day] [hour]:[minute]:[second]" ), ) { primitive_datetime.assume_utc() } else if let Ok(date) = Date::parse( - string, + &string, format_description!("[year repr:full base:calendar]-[month repr:numerical]-[day]"), ) { PrimitiveDateTime::new(date, time!(00:00)).assume_utc() } else { - return Err(TakeErrorMessage(ParseOffsetDateTimeError(string.to_owned()))); + return Err(TakeErrorMessage(ParseOffsetDateTimeError(string))); }; if datetime > OffsetDateTime::now_utc() { Ok(Some(datetime)) } else { - Err(TakeErrorMessage(ParseOffsetDateTimeError(string.to_owned()))) + Err(TakeErrorMessage(ParseOffsetDateTimeError(string))) } } From 9194508a0f4d8bdde1fc35882ebdb389c2a20b78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Lecrenier?= Date: Mon, 16 Jan 2023 16:59:26 +0100 Subject: [PATCH 4/9] Refactor query parameter deserialisation logic --- Cargo.lock | 31 +- meilisearch-auth/src/lib.rs | 9 +- meilisearch-auth/src/store.rs | 3 +- meilisearch-types/Cargo.toml | 2 +- .../src/deserr/error_messages.rs | 315 +++++++ meilisearch-types/src/deserr/mod.rs | 134 +++ meilisearch-types/src/deserr/query_params.rs | 115 +++ meilisearch-types/src/error.rs | 868 ++++-------------- meilisearch-types/src/index_uid.rs | 6 + meilisearch-types/src/keys.rs | 48 +- meilisearch-types/src/lib.rs | 2 +- meilisearch-types/src/settings.rs | 3 +- meilisearch-types/src/star_or.rs | 284 ++++-- meilisearch-types/src/tasks.rs | 73 +- meilisearch/Cargo.toml | 2 +- meilisearch/src/routes/api_key.rs | 24 +- meilisearch/src/routes/indexes/documents.rs | 38 +- meilisearch/src/routes/indexes/mod.rs | 22 +- meilisearch/src/routes/indexes/search.rs | 59 +- meilisearch/src/routes/indexes/settings.rs | 25 +- meilisearch/src/routes/mod.rs | 2 +- meilisearch/src/routes/swap_indexes.rs | 5 +- meilisearch/src/routes/tasks.rs | 476 +++++----- meilisearch/src/search.rs | 7 +- meilisearch/tests/search/errors.rs | 2 +- meilisearch/tests/tasks/errors.rs | 2 +- 26 files changed, 1377 insertions(+), 1180 deletions(-) create mode 100644 meilisearch-types/src/deserr/error_messages.rs create mode 100644 meilisearch-types/src/deserr/mod.rs create mode 100644 meilisearch-types/src/deserr/query_params.rs diff --git a/Cargo.lock b/Cargo.lock index 6e754abd3..4bbe05745 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1026,7 +1026,18 @@ dependencies = [ name = "deserr" version = "0.1.4" dependencies = [ - "deserr-internal", + "deserr-internal 0.1.4", + "serde-cs", + "serde_json", +] + +[[package]] +name = "deserr" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86290491a2b5c21a1a5083da8dae831006761258fabd5617309c3eebc5f89468" +dependencies = [ + "deserr-internal 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "serde-cs", "serde_json", ] @@ -1041,6 +1052,18 @@ dependencies = [ "syn 1.0.107", ] +[[package]] +name = "deserr-internal" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7131de1c27581bc376a22166c9f570be91b76cb096be2f6aecf224c27bf7c49a" +dependencies = [ + "convert_case 0.5.0", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "deunicode" version = "1.3.3" @@ -2300,7 +2323,7 @@ dependencies = [ "cargo_toml", "clap 4.0.32", "crossbeam-channel", - "deserr", + "deserr 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "dump", "either", "env_logger", @@ -2391,7 +2414,7 @@ dependencies = [ "anyhow", "convert_case 0.6.0", "csv", - "deserr", + "deserr 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "either", "enum-iterator", "file-store", @@ -2451,7 +2474,7 @@ dependencies = [ "concat-arrays", "crossbeam-channel", "csv", - "deserr", + "deserr 0.1.4", "either", "filter-parser", "flatten-serde-json", diff --git a/meilisearch-auth/src/lib.rs b/meilisearch-auth/src/lib.rs index 8d4a7f2b7..072b87dad 100644 --- a/meilisearch-auth/src/lib.rs +++ b/meilisearch-auth/src/lib.rs @@ -3,7 +3,6 @@ pub mod error; mod store; use std::collections::{HashMap, HashSet}; -use std::ops::Deref; use std::path::Path; use std::sync::Arc; @@ -86,15 +85,13 @@ impl AuthController { key.indexes .into_iter() .filter_map(|index| { - search_rules.get_index_search_rules(index.deref()).map( - |index_search_rules| { - (String::from(index), Some(index_search_rules)) - }, + search_rules.get_index_search_rules(&format!("{index}")).map( + |index_search_rules| (index.to_string(), Some(index_search_rules)), ) }) .collect(), ), - None => SearchRules::Set(key.indexes.into_iter().map(String::from).collect()), + None => SearchRules::Set(key.indexes.into_iter().map(|x| x.to_string()).collect()), }; } else if let Some(search_rules) = search_rules { filters.search_rules = search_rules; diff --git a/meilisearch-auth/src/store.rs b/meilisearch-auth/src/store.rs index b3f9ed672..2574572be 100644 --- a/meilisearch-auth/src/store.rs +++ b/meilisearch-auth/src/store.rs @@ -3,7 +3,6 @@ use std::cmp::Reverse; use std::collections::HashSet; use std::convert::{TryFrom, TryInto}; use std::fs::create_dir_all; -use std::ops::Deref; use std::path::Path; use std::str; use std::sync::Arc; @@ -135,7 +134,7 @@ impl HeedAuthStore { for index in key.indexes.iter() { db.put( &mut wtxn, - &(&uid, &action, Some(index.deref().as_bytes())), + &(&uid, &action, Some(index.to_string().as_bytes())), &key.expires_at, )?; } diff --git a/meilisearch-types/Cargo.toml b/meilisearch-types/Cargo.toml index 257ac9c2d..cba3fc5d9 100644 --- a/meilisearch-types/Cargo.toml +++ b/meilisearch-types/Cargo.toml @@ -9,7 +9,7 @@ actix-web = { version = "4.2.1", default-features = false } anyhow = "1.0.65" convert_case = "0.6.0" csv = "1.1.6" -deserr = { path = "/Users/meilisearch/Documents/deserr" } +deserr = "0.1.4" either = { version = "1.6.1", features = ["serde"] } enum-iterator = "1.1.3" file-store = { path = "../file-store" } diff --git a/meilisearch-types/src/deserr/error_messages.rs b/meilisearch-types/src/deserr/error_messages.rs new file mode 100644 index 000000000..b289e454d --- /dev/null +++ b/meilisearch-types/src/deserr/error_messages.rs @@ -0,0 +1,315 @@ +/*! +This module implements the error messages of deserialization errors. + +We try to: +1. Give a human-readable description of where the error originated. +2. Use the correct terms depending on the format of the request (json/query param) +3. Categorise the type of the error (e.g. missing field, wrong value type, unexpected error, etc.) + */ +use deserr::{ErrorKind, IntoValue, ValueKind, ValuePointerRef}; + +use super::{DeserrJsonError, DeserrQueryParamError}; +use crate::error::ErrorCode; + +/// Return a description of the given location in a Json, preceded by the given article. +/// e.g. `at .key1[8].key2`. If the location is the origin, the given article will not be +/// included in the description. +pub fn location_json_description(location: ValuePointerRef, article: &str) -> String { + fn rec(location: ValuePointerRef) -> String { + match location { + ValuePointerRef::Origin => String::new(), + ValuePointerRef::Key { key, prev } => rec(*prev) + "." + key, + ValuePointerRef::Index { index, prev } => format!("{}[{index}]", rec(*prev)), + } + } + match location { + ValuePointerRef::Origin => String::new(), + _ => { + format!("{article} `{}`", rec(location)) + } + } +} + +/// Return a description of the list of value kinds for a Json payload. +fn value_kinds_description_json(kinds: &[ValueKind]) -> String { + // Rank each value kind so that they can be sorted (and deduplicated) + // Having a predictable order helps with pattern matching + fn order(kind: &ValueKind) -> u8 { + match kind { + ValueKind::Null => 0, + ValueKind::Boolean => 1, + ValueKind::Integer => 2, + ValueKind::NegativeInteger => 3, + ValueKind::Float => 4, + ValueKind::String => 5, + ValueKind::Sequence => 6, + ValueKind::Map => 7, + } + } + // Return a description of a single value kind, preceded by an article + fn single_description(kind: &ValueKind) -> &'static str { + match kind { + ValueKind::Null => "null", + ValueKind::Boolean => "a boolean", + ValueKind::Integer => "a positive integer", + ValueKind::NegativeInteger => "an integer", + ValueKind::Float => "a number", + ValueKind::String => "a string", + ValueKind::Sequence => "an array", + ValueKind::Map => "an object", + } + } + + fn description_rec(kinds: &[ValueKind], count_items: &mut usize, message: &mut String) { + let (msg_part, rest): (_, &[ValueKind]) = match kinds { + [] => (String::new(), &[]), + [ValueKind::Integer | ValueKind::NegativeInteger, ValueKind::Float, rest @ ..] => { + ("a number".to_owned(), rest) + } + [ValueKind::Integer, ValueKind::NegativeInteger, ValueKind::Float, rest @ ..] => { + ("a number".to_owned(), rest) + } + [ValueKind::Integer, ValueKind::NegativeInteger, rest @ ..] => { + ("an integer".to_owned(), rest) + } + [a] => (single_description(a).to_owned(), &[]), + [a, rest @ ..] => (single_description(a).to_owned(), rest), + }; + + if rest.is_empty() { + if *count_items == 0 { + message.push_str(&msg_part); + } else if *count_items == 1 { + message.push_str(&format!(" or {msg_part}")); + } else { + message.push_str(&format!(", or {msg_part}")); + } + } else { + if *count_items == 0 { + message.push_str(&msg_part); + } else { + message.push_str(&format!(", {msg_part}")); + } + + *count_items += 1; + description_rec(rest, count_items, message); + } + } + + let mut kinds = kinds.to_owned(); + kinds.sort_by_key(order); + kinds.dedup(); + + if kinds.is_empty() { + // Should not happen ideally + "a different value".to_owned() + } else { + let mut message = String::new(); + description_rec(kinds.as_slice(), &mut 0, &mut message); + message + } +} + +/// Return the JSON string of the value preceded by a description of its kind +fn value_description_with_kind_json(v: &serde_json::Value) -> String { + match v.kind() { + ValueKind::Null => "null".to_owned(), + kind => { + format!( + "{}: `{}`", + value_kinds_description_json(&[kind]), + serde_json::to_string(v).unwrap() + ) + } + } +} + +impl deserr::DeserializeError for DeserrJsonError { + fn error( + _self_: Option, + error: deserr::ErrorKind, + location: ValuePointerRef, + ) -> Result { + let mut message = String::new(); + + message.push_str(&match error { + ErrorKind::IncorrectValueKind { actual, accepted } => { + let expected = value_kinds_description_json(accepted); + let received = value_description_with_kind_json(&serde_json::Value::from(actual)); + + let location = location_json_description(location, " at"); + + format!("Invalid value type{location}: expected {expected}, but found {received}") + } + ErrorKind::MissingField { field } => { + let location = location_json_description(location, " inside"); + format!("Missing field `{field}`{location}") + } + ErrorKind::UnknownKey { key, accepted } => { + let location = location_json_description(location, " inside"); + format!( + "Unknown field `{}`{location}: expected one of {}", + key, + accepted + .iter() + .map(|accepted| format!("`{}`", accepted)) + .collect::>() + .join(", ") + ) + } + ErrorKind::UnknownValue { value, accepted } => { + let location = location_json_description(location, " at"); + format!( + "Unknown value `{}`{location}: expected one of {}", + value, + accepted + .iter() + .map(|accepted| format!("`{}`", accepted)) + .collect::>() + .join(", "), + ) + } + ErrorKind::Unexpected { msg } => { + let location = location_json_description(location, " at"); + format!("Invalid value{location}: {msg}") + } + }); + + Err(DeserrJsonError::new(message, C::default().error_code())) + } +} + +/// Return a description of the given location in query parameters, preceded by the +/// given article. e.g. `at key5[2]`. If the location is the origin, the given article +/// will not be included in the description. +pub fn location_query_param_description(location: ValuePointerRef, article: &str) -> String { + fn rec(location: ValuePointerRef) -> String { + match location { + ValuePointerRef::Origin => String::new(), + ValuePointerRef::Key { key, prev } => { + if matches!(prev, ValuePointerRef::Origin) { + key.to_owned() + } else { + rec(*prev) + "." + key + } + } + ValuePointerRef::Index { index, prev } => format!("{}[{index}]", rec(*prev)), + } + } + match location { + ValuePointerRef::Origin => String::new(), + _ => { + format!("{article} `{}`", rec(location)) + } + } +} + +impl deserr::DeserializeError for DeserrQueryParamError { + fn error( + _self_: Option, + error: deserr::ErrorKind, + location: ValuePointerRef, + ) -> Result { + let mut message = String::new(); + + message.push_str(&match error { + ErrorKind::IncorrectValueKind { actual, accepted } => { + let expected = value_kinds_description_query_param(accepted); + let received = value_description_with_kind_query_param(actual); + + let location = location_query_param_description(location, " for parameter"); + + format!("Invalid value type{location}: expected {expected}, but found {received}") + } + ErrorKind::MissingField { field } => { + let location = location_query_param_description(location, " inside"); + format!("Missing parameter `{field}`{location}") + } + ErrorKind::UnknownKey { key, accepted } => { + let location = location_query_param_description(location, " inside"); + format!( + "Unknown parameter `{}`{location}: expected one of {}", + key, + accepted + .iter() + .map(|accepted| format!("`{}`", accepted)) + .collect::>() + .join(", ") + ) + } + ErrorKind::UnknownValue { value, accepted } => { + let location = location_query_param_description(location, " for parameter"); + format!( + "Unknown value `{}`{location}: expected one of {}", + value, + accepted + .iter() + .map(|accepted| format!("`{}`", accepted)) + .collect::>() + .join(", "), + ) + } + ErrorKind::Unexpected { msg } => { + let location = location_query_param_description(location, " in parameter"); + format!("Invalid value{location}: {msg}") + } + }); + + Err(DeserrQueryParamError::new(message, C::default().error_code())) + } +} + +/// Return a description of the list of value kinds for query parameters +/// Since query parameters are always treated as strings, we always return +/// "a string" for now. +fn value_kinds_description_query_param(_accepted: &[ValueKind]) -> String { + "a string".to_owned() +} + +fn value_description_with_kind_query_param(actual: deserr::Value) -> String { + match actual { + deserr::Value::Null => "null".to_owned(), + deserr::Value::Boolean(x) => format!("a boolean: `{x}`"), + deserr::Value::Integer(x) => format!("an integer: `{x}`"), + deserr::Value::NegativeInteger(x) => { + format!("an integer: `{x}`") + } + deserr::Value::Float(x) => { + format!("a number: `{x}`") + } + deserr::Value::String(x) => { + format!("a string: `{x}`") + } + deserr::Value::Sequence(_) => "multiple values".to_owned(), + deserr::Value::Map(_) => "multiple parameters".to_owned(), + } +} + +#[cfg(test)] +mod tests { + use deserr::ValueKind; + + use crate::deserr::error_messages::value_kinds_description_json; + + #[test] + fn test_value_kinds_description_json() { + insta::assert_display_snapshot!(value_kinds_description_json(&[]), @"a different value"); + + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Boolean]), @"a boolean"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer]), @"a positive integer"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::NegativeInteger]), @"an integer"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer]), @"a positive integer"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::String]), @"a string"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Sequence]), @"an array"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Map]), @"an object"); + + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer, ValueKind::Boolean]), @"a boolean or a positive integer"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Null, ValueKind::Integer]), @"null or a positive integer"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Sequence, ValueKind::NegativeInteger]), @"an integer or an array"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer, ValueKind::Float]), @"a number"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer, ValueKind::Float, ValueKind::NegativeInteger]), @"a number"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer, ValueKind::Float, ValueKind::NegativeInteger, ValueKind::Null]), @"null or a number"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Boolean, ValueKind::Integer, ValueKind::Float, ValueKind::NegativeInteger, ValueKind::Null]), @"null, a boolean, or a number"); + insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Null, ValueKind::Boolean, ValueKind::Integer, ValueKind::Float, ValueKind::NegativeInteger, ValueKind::Null]), @"null, a boolean, or a number"); + } +} diff --git a/meilisearch-types/src/deserr/mod.rs b/meilisearch-types/src/deserr/mod.rs new file mode 100644 index 000000000..c15b2c3a0 --- /dev/null +++ b/meilisearch-types/src/deserr/mod.rs @@ -0,0 +1,134 @@ +use std::convert::Infallible; +use std::fmt; +use std::marker::PhantomData; + +use deserr::{DeserializeError, MergeWithError, ValuePointerRef}; + +use crate::error::deserr_codes::{self, *}; +use crate::error::{ + unwrap_any, Code, DeserrParseBoolError, DeserrParseIntError, ErrorCode, InvalidTaskDateError, + ParseOffsetDateTimeError, +}; +use crate::index_uid::IndexUidFormatError; +use crate::tasks::{ParseTaskKindError, ParseTaskStatusError}; + +pub mod error_messages; +pub mod query_params; + +/// Marker type for the Json format +pub struct DeserrJson; +/// Marker type for the Query Parameter format +pub struct DeserrQueryParam; + +pub type DeserrJsonError = DeserrError; +pub type DeserrQueryParamError = DeserrError; + +/// A request deserialization error. +/// +/// The first generic paramater is a marker type describing the format of the request: either json (e.g. [`DeserrJson`] or [`DeserrQueryParam`]). +/// The second generic parameter is the default error code for the deserialization error, in case it is not given. +pub struct DeserrError { + pub msg: String, + pub code: Code, + _phantom: PhantomData<(Format, C)>, +} +impl DeserrError { + pub fn new(msg: String, code: Code) -> Self { + Self { msg, code, _phantom: PhantomData } + } +} +impl std::fmt::Debug for DeserrError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DeserrError").field("msg", &self.msg).field("code", &self.code).finish() + } +} + +impl std::fmt::Display for DeserrError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.msg) + } +} + +impl std::error::Error for DeserrError {} +impl ErrorCode for DeserrError { + fn error_code(&self) -> Code { + self.code + } +} + +// For now, we don't accumulate errors. Only one deserialisation error is ever returned at a time. +impl + MergeWithError> for DeserrError +{ + fn merge( + _self_: Option, + other: DeserrError, + _merge_location: ValuePointerRef, + ) -> Result { + Err(DeserrError { msg: other.msg, code: other.code, _phantom: PhantomData }) + } +} + +impl MergeWithError for DeserrError { + fn merge( + _self_: Option, + _other: Infallible, + _merge_location: ValuePointerRef, + ) -> Result { + unreachable!() + } +} + +// Implement a convenience function to build a `missing_field` error +macro_rules! make_missing_field_convenience_builder { + ($err_code:ident, $fn_name:ident) => { + impl DeserrJsonError<$err_code> { + pub fn $fn_name(field: &str, location: ValuePointerRef) -> Self { + let x = unwrap_any(Self::error::( + None, + deserr::ErrorKind::MissingField { field }, + location, + )); + Self { msg: x.msg, code: $err_code.error_code(), _phantom: PhantomData } + } + } + }; +} +make_missing_field_convenience_builder!(MissingIndexUid, missing_index_uid); +make_missing_field_convenience_builder!(MissingApiKeyActions, missing_api_key_actions); +make_missing_field_convenience_builder!(MissingApiKeyExpiresAt, missing_api_key_expires_at); +make_missing_field_convenience_builder!(MissingApiKeyIndexes, missing_api_key_indexes); +make_missing_field_convenience_builder!(MissingSwapIndexes, missing_swap_indexes); + +// Integrate a sub-error into a [`DeserrError`] by taking its error message but using +// the default error code (C) from `Self` +macro_rules! merge_with_error_impl_take_error_message { + ($err_type:ty) => { + impl MergeWithError<$err_type> for DeserrError + where + DeserrError: deserr::DeserializeError, + { + fn merge( + _self_: Option, + other: $err_type, + merge_location: ValuePointerRef, + ) -> Result { + DeserrError::::error::( + None, + deserr::ErrorKind::Unexpected { msg: other.to_string() }, + merge_location, + ) + } + } + }; +} + +// All these errors can be merged into a `DeserrError` +merge_with_error_impl_take_error_message!(DeserrParseIntError); +merge_with_error_impl_take_error_message!(DeserrParseBoolError); +merge_with_error_impl_take_error_message!(uuid::Error); +merge_with_error_impl_take_error_message!(InvalidTaskDateError); +merge_with_error_impl_take_error_message!(ParseOffsetDateTimeError); +merge_with_error_impl_take_error_message!(ParseTaskKindError); +merge_with_error_impl_take_error_message!(ParseTaskStatusError); +merge_with_error_impl_take_error_message!(IndexUidFormatError); diff --git a/meilisearch-types/src/deserr/query_params.rs b/meilisearch-types/src/deserr/query_params.rs new file mode 100644 index 000000000..28629aa1b --- /dev/null +++ b/meilisearch-types/src/deserr/query_params.rs @@ -0,0 +1,115 @@ +/*! +This module provides helper traits, types, and functions to deserialize query parameters. + +The source of the problem is that query parameters only give us a string to work with. +This means `deserr` is never given a sequence or numbers, and thus the default deserialization +code for common types such as `usize` or `Vec` does not work. To work around it, we create a +wrapper type called `Param`, which is deserialised using the `from_query_param` method of the trait +`FromQueryParameter`. + +We also use other helper types such as `CS` (i.e. comma-separated) from `serde_cs` as well as +`StarOr`, `OptionStarOr`, and `OptionStarOrList`. +*/ + +use std::convert::Infallible; +use std::ops::Deref; +use std::str::FromStr; + +use deserr::{DeserializeError, DeserializeFromValue, MergeWithError, ValueKind}; + +use super::{DeserrParseBoolError, DeserrParseIntError}; +use crate::error::unwrap_any; +use crate::index_uid::IndexUid; +use crate::tasks::{Kind, Status}; + +/// A wrapper type indicating that the inner value should be +/// deserialised from a query parameter string. +/// +/// Note that if the field is optional, it is better to use +/// `Option>` instead of `Param>`. +#[derive(Default, Debug, Clone, Copy)] +pub struct Param(pub T); + +impl Deref for Param { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DeserializeFromValue for Param +where + E: DeserializeError + MergeWithError, + T: FromQueryParameter, +{ + fn deserialize_from_value( + value: deserr::Value, + location: deserr::ValuePointerRef, + ) -> Result { + match value { + deserr::Value::String(s) => match T::from_query_param(&s) { + Ok(x) => Ok(Param(x)), + Err(e) => Err(unwrap_any(E::merge(None, e, location))), + }, + _ => Err(unwrap_any(E::error( + None, + deserr::ErrorKind::IncorrectValueKind { + actual: value, + accepted: &[ValueKind::String], + }, + location, + ))), + } + } +} + +/// Parse a value from a query parameter string. +/// +/// This trait is functionally equivalent to `FromStr`. +/// Having a separate trait trait allows us to return better +/// deserializatio error messages. +pub trait FromQueryParameter: Sized { + type Err; + fn from_query_param(p: &str) -> Result; +} + +/// Implement `FromQueryParameter` for the given type using its `FromStr` +/// trait implementation. +macro_rules! impl_from_query_param_from_str { + ($type:ty) => { + impl FromQueryParameter for $type { + type Err = <$type as FromStr>::Err; + fn from_query_param(p: &str) -> Result { + p.parse() + } + } + }; +} +impl_from_query_param_from_str!(Kind); +impl_from_query_param_from_str!(Status); +impl_from_query_param_from_str!(IndexUid); + +/// Implement `FromQueryParameter` for the given type using its `FromStr` +/// trait implementation, replacing the returned error with a struct +/// that wraps the original query parameter. +macro_rules! impl_from_query_param_wrap_original_value_in_error { + ($type:ty, $err_type:path) => { + impl FromQueryParameter for $type { + type Err = $err_type; + fn from_query_param(p: &str) -> Result { + p.parse().map_err(|_| $err_type(p.to_owned())) + } + } + }; +} +impl_from_query_param_wrap_original_value_in_error!(usize, DeserrParseIntError); +impl_from_query_param_wrap_original_value_in_error!(u32, DeserrParseIntError); +impl_from_query_param_wrap_original_value_in_error!(bool, DeserrParseBoolError); + +impl FromQueryParameter for String { + type Err = Infallible; + fn from_query_param(p: &str) -> Result { + Ok(p.to_owned()) + } +} diff --git a/meilisearch-types/src/error.rs b/meilisearch-types/src/error.rs index 614449ff8..2fb55ee31 100644 --- a/meilisearch-types/src/error.rs +++ b/meilisearch-types/src/error.rs @@ -1,30 +1,17 @@ -use std::convert::Infallible; -use std::marker::PhantomData; -use std::str::FromStr; use std::{fmt, io}; use actix_web::http::StatusCode; use actix_web::{self as aweb, HttpResponseBuilder}; use aweb::rt::task::JoinError; use convert_case::Casing; -use deserr::{DeserializeError, ErrorKind, IntoValue, MergeWithError, ValueKind, ValuePointerRef}; use milli::heed::{Error as HeedError, MdbError}; use serde::{Deserialize, Serialize}; -use serde_cs::vec::CS; - -use crate::star_or::StarOr; - -use self::deserr_codes::{ - InvalidSwapIndexes, MissingApiKeyActions, MissingApiKeyExpiresAt, MissingApiKeyIndexes, - MissingIndexUid, MissingSwapIndexes, -}; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "test-traits", derive(proptest_derive::Arbitrary))] pub struct ResponseError { #[serde(skip)] - #[cfg_attr(feature = "test-traits", proptest(strategy = "strategy::status_code_strategy()"))] code: StatusCode, message: String, #[serde(rename = "code")] @@ -43,7 +30,7 @@ impl ResponseError { Self { code: code.http(), message, - error_code: code.err_code().error_name, + error_code: code.name(), error_type: code.type_(), error_link: code.url(), } @@ -104,9 +91,9 @@ pub trait ErrorCode { #[allow(clippy::enum_variant_names)] enum ErrorType { - InternalError, - InvalidRequestError, - AuthenticationError, + Internal, + InvalidRequest, + Auth, System, } @@ -115,14 +102,24 @@ impl fmt::Display for ErrorType { use ErrorType::*; match self { - InternalError => write!(f, "internal"), - InvalidRequestError => write!(f, "invalid_request"), - AuthenticationError => write!(f, "auth"), + Internal => write!(f, "internal"), + InvalidRequest => write!(f, "invalid_request"), + Auth => write!(f, "auth"), System => write!(f, "system"), } } } +/// Implement all the error codes. +/// +/// 1. Make an enum `Code` where each error code is a variant +/// 2. Implement the `http`, `name`, and `type_` method on the enum +/// 3. Make a unit type for each error code in the module `deserr_codes`. +/// +/// The unit type's purpose is to be used as a marker type parameter, e.g. +/// `DeserrJsonError`. It implements `Default` and `ErrorCode`, +/// so we can get a value of the `Code` enum with the correct variant by calling +/// `MyErrorCode::default().error_code()`. macro_rules! make_error_codes { ($($code_ident:ident, $err_type:ident, $status:ident);*) => { #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -130,29 +127,31 @@ macro_rules! make_error_codes { $($code_ident),* } impl Code { - /// associate a `Code` variant to the actual ErrCode - fn err_code(&self) -> ErrCode { - match self { - $( - Code::$code_ident => { - ErrCode::$err_type( stringify!($code_ident).to_case(convert_case::Case::Snake), StatusCode::$status) - } - )* - } - } /// return the HTTP status code associated with the `Code` fn http(&self) -> StatusCode { - self.err_code().status_code + match self { + $( + Code::$code_ident => StatusCode::$status + ),* + } } /// return error name, used as error code fn name(&self) -> String { - self.err_code().error_name.to_string() + match self { + $( + Code::$code_ident => stringify!($code_ident).to_case(convert_case::Case::Snake) + ),* + } } /// return the error type fn type_(&self) -> String { - self.err_code().error_type.to_string() + match self { + $( + Code::$code_ident => ErrorType::$err_type.to_string() + ),* + } } /// return the doc url associated with the error @@ -177,144 +176,121 @@ macro_rules! make_error_codes { } } } + +// An exhaustive list of all the error codes used by meilisearch. make_error_codes! { -ApiKeyAlreadyExists , invalid , CONFLICT ; -ApiKeyNotFound , invalid , NOT_FOUND ; -BadParameter , invalid , BAD_REQUEST; -BadRequest , invalid , BAD_REQUEST; -DatabaseSizeLimitReached , internal , INTERNAL_SERVER_ERROR; -DocumentNotFound , invalid , NOT_FOUND; -DumpAlreadyProcessing , invalid , CONFLICT; -DumpNotFound , invalid , NOT_FOUND; -DumpProcessFailed , internal , INTERNAL_SERVER_ERROR; -DuplicateIndexFound , invalid , BAD_REQUEST; -ImmutableApiKeyUid , invalid , BAD_REQUEST; -ImmutableApiKeyKey , invalid , BAD_REQUEST; -ImmutableApiKeyActions , invalid , BAD_REQUEST; -ImmutableApiKeyIndexes , invalid , BAD_REQUEST; -ImmutableApiKeyExpiresAt , invalid , BAD_REQUEST; -ImmutableApiKeyCreatedAt , invalid , BAD_REQUEST; -ImmutableApiKeyUpdatedAt , invalid , BAD_REQUEST; -ImmutableIndexUid , invalid , BAD_REQUEST; -ImmutableIndexCreatedAt , invalid , BAD_REQUEST; -ImmutableIndexUpdatedAt , invalid , BAD_REQUEST; -IndexAlreadyExists , invalid , CONFLICT ; -IndexCreationFailed , internal , INTERNAL_SERVER_ERROR; -IndexNotFound , invalid , NOT_FOUND; -IndexPrimaryKeyAlreadyExists , invalid , BAD_REQUEST ; -IndexPrimaryKeyNoCandidateFound , invalid , BAD_REQUEST ; -IndexPrimaryKeyMultipleCandidatesFound, invalid , BAD_REQUEST; -Internal , internal , INTERNAL_SERVER_ERROR ; -InvalidApiKeyActions , invalid , BAD_REQUEST ; -InvalidApiKeyDescription , invalid , BAD_REQUEST ; -InvalidApiKeyExpiresAt , invalid , BAD_REQUEST ; -InvalidApiKeyIndexes , invalid , BAD_REQUEST ; -InvalidApiKeyLimit , invalid , BAD_REQUEST ; -InvalidApiKeyName , invalid , BAD_REQUEST ; -InvalidApiKeyOffset , invalid , BAD_REQUEST ; -InvalidApiKeyUid , invalid , BAD_REQUEST ; -InvalidApiKey , authentication, FORBIDDEN ; -InvalidContentType , invalid , UNSUPPORTED_MEDIA_TYPE ; -InvalidDocumentFields , invalid , BAD_REQUEST ; -InvalidDocumentGeoField , invalid , BAD_REQUEST ; -InvalidDocumentId , invalid , BAD_REQUEST ; -InvalidDocumentLimit , invalid , BAD_REQUEST ; -InvalidDocumentOffset , invalid , BAD_REQUEST ; -InvalidIndexLimit , invalid , BAD_REQUEST ; -InvalidIndexOffset , invalid , BAD_REQUEST ; -InvalidIndexPrimaryKey , invalid , BAD_REQUEST ; -InvalidIndexUid , invalid , BAD_REQUEST ; -InvalidMinWordLengthForTypo , invalid , BAD_REQUEST ; -InvalidSearchAttributesToCrop , invalid , BAD_REQUEST ; -InvalidSearchAttributesToHighlight , invalid , BAD_REQUEST ; -InvalidSearchAttributesToRetrieve , invalid , BAD_REQUEST ; -InvalidSearchCropLength , invalid , BAD_REQUEST ; -InvalidSearchCropMarker , invalid , BAD_REQUEST ; -InvalidSearchFacets , invalid , BAD_REQUEST ; -InvalidSearchFilter , invalid , BAD_REQUEST ; -InvalidSearchHighlightPostTag , invalid , BAD_REQUEST ; -InvalidSearchHighlightPreTag , invalid , BAD_REQUEST ; -InvalidSearchHitsPerPage , invalid , BAD_REQUEST ; -InvalidSearchLimit , invalid , BAD_REQUEST ; -InvalidSearchMatchingStrategy , invalid , BAD_REQUEST ; -InvalidSearchOffset , invalid , BAD_REQUEST ; -InvalidSearchPage , invalid , BAD_REQUEST ; -InvalidSearchQ , invalid , BAD_REQUEST ; -InvalidSearchShowMatchesPosition , invalid , BAD_REQUEST ; -InvalidSearchSort , invalid , BAD_REQUEST ; -InvalidSettingsDisplayedAttributes , invalid , BAD_REQUEST ; -InvalidSettingsDistinctAttribute , invalid , BAD_REQUEST ; -InvalidSettingsFaceting , invalid , BAD_REQUEST ; -InvalidSettingsFilterableAttributes , invalid , BAD_REQUEST ; -InvalidSettingsPagination , invalid , BAD_REQUEST ; -InvalidSettingsRankingRules , invalid , BAD_REQUEST ; -InvalidSettingsSearchableAttributes , invalid , BAD_REQUEST ; -InvalidSettingsSortableAttributes , invalid , BAD_REQUEST ; -InvalidSettingsStopWords , invalid , BAD_REQUEST ; -InvalidSettingsSynonyms , invalid , BAD_REQUEST ; -InvalidSettingsTypoTolerance , invalid , BAD_REQUEST ; -InvalidState , internal , INTERNAL_SERVER_ERROR ; -InvalidStoreFile , internal , INTERNAL_SERVER_ERROR ; -InvalidSwapDuplicateIndexFound , invalid , BAD_REQUEST ; -InvalidSwapIndexes , invalid , BAD_REQUEST ; -InvalidTaskAfterEnqueuedAt , invalid , BAD_REQUEST ; -InvalidTaskAfterFinishedAt , invalid , BAD_REQUEST ; -InvalidTaskAfterStartedAt , invalid , BAD_REQUEST ; -InvalidTaskBeforeEnqueuedAt , invalid , BAD_REQUEST ; -InvalidTaskBeforeFinishedAt , invalid , BAD_REQUEST ; -InvalidTaskBeforeStartedAt , invalid , BAD_REQUEST ; -InvalidTaskCanceledBy , invalid , BAD_REQUEST ; -InvalidTaskFrom , invalid , BAD_REQUEST ; -InvalidTaskLimit , invalid , BAD_REQUEST ; -InvalidTaskStatuses , invalid , BAD_REQUEST ; -InvalidTaskTypes , invalid , BAD_REQUEST ; -InvalidTaskUids , invalid , BAD_REQUEST ; -IoError , system , UNPROCESSABLE_ENTITY; -MalformedPayload , invalid , BAD_REQUEST ; -MaxFieldsLimitExceeded , invalid , BAD_REQUEST ; -MissingApiKeyActions , invalid , BAD_REQUEST ; -MissingApiKeyExpiresAt , invalid , BAD_REQUEST ; -MissingApiKeyIndexes , invalid , BAD_REQUEST ; -MissingAuthorizationHeader , authentication, UNAUTHORIZED ; -MissingContentType , invalid , UNSUPPORTED_MEDIA_TYPE ; -MissingDocumentId , invalid , BAD_REQUEST ; -MissingIndexUid , invalid , BAD_REQUEST ; -MissingMasterKey , authentication, UNAUTHORIZED ; -MissingPayload , invalid , BAD_REQUEST ; -MissingSwapIndexes , invalid , BAD_REQUEST ; -MissingTaskFilters , invalid , BAD_REQUEST ; -NoSpaceLeftOnDevice , system , UNPROCESSABLE_ENTITY; -PayloadTooLarge , invalid , PAYLOAD_TOO_LARGE ; -TaskNotFound , invalid , NOT_FOUND ; -TooManyOpenFiles , system , UNPROCESSABLE_ENTITY ; -UnretrievableDocument , internal , BAD_REQUEST ; -UnretrievableErrorCode , invalid , BAD_REQUEST ; -UnsupportedMediaType , invalid , UNSUPPORTED_MEDIA_TYPE -} - -/// Internal structure providing a convenient way to create error codes -struct ErrCode { - status_code: StatusCode, - error_type: ErrorType, - error_name: String, -} - -impl ErrCode { - fn authentication(error_name: String, status_code: StatusCode) -> ErrCode { - ErrCode { status_code, error_name, error_type: ErrorType::AuthenticationError } - } - - fn internal(error_name: String, status_code: StatusCode) -> ErrCode { - ErrCode { status_code, error_name, error_type: ErrorType::InternalError } - } - - fn invalid(error_name: String, status_code: StatusCode) -> ErrCode { - ErrCode { status_code, error_name, error_type: ErrorType::InvalidRequestError } - } - - fn system(error_name: String, status_code: StatusCode) -> ErrCode { - ErrCode { status_code, error_name, error_type: ErrorType::System } - } +ApiKeyAlreadyExists , InvalidRequest , CONFLICT ; +ApiKeyNotFound , InvalidRequest , NOT_FOUND ; +BadParameter , InvalidRequest , BAD_REQUEST; +BadRequest , InvalidRequest , BAD_REQUEST; +DatabaseSizeLimitReached , Internal , INTERNAL_SERVER_ERROR; +DocumentNotFound , InvalidRequest , NOT_FOUND; +DumpAlreadyProcessing , InvalidRequest , CONFLICT; +DumpNotFound , InvalidRequest , NOT_FOUND; +DumpProcessFailed , Internal , INTERNAL_SERVER_ERROR; +DuplicateIndexFound , InvalidRequest , BAD_REQUEST; +ImmutableApiKeyActions , InvalidRequest , BAD_REQUEST; +ImmutableApiKeyCreatedAt , InvalidRequest , BAD_REQUEST; +ImmutableApiKeyExpiresAt , InvalidRequest , BAD_REQUEST; +ImmutableApiKeyIndexes , InvalidRequest , BAD_REQUEST; +ImmutableApiKeyKey , InvalidRequest , BAD_REQUEST; +ImmutableApiKeyUid , InvalidRequest , BAD_REQUEST; +ImmutableApiKeyUpdatedAt , InvalidRequest , BAD_REQUEST; +ImmutableIndexCreatedAt , InvalidRequest , BAD_REQUEST; +ImmutableIndexUid , InvalidRequest , BAD_REQUEST; +ImmutableIndexUpdatedAt , InvalidRequest , BAD_REQUEST; +IndexAlreadyExists , InvalidRequest , CONFLICT ; +IndexCreationFailed , Internal , INTERNAL_SERVER_ERROR; +IndexNotFound , InvalidRequest , NOT_FOUND; +IndexPrimaryKeyAlreadyExists , InvalidRequest , BAD_REQUEST ; +IndexPrimaryKeyMultipleCandidatesFound, InvalidRequest , BAD_REQUEST; +IndexPrimaryKeyNoCandidateFound , InvalidRequest , BAD_REQUEST ; +Internal , Internal , INTERNAL_SERVER_ERROR ; +InvalidApiKey , Auth , FORBIDDEN ; +InvalidApiKeyActions , InvalidRequest , BAD_REQUEST ; +InvalidApiKeyDescription , InvalidRequest , BAD_REQUEST ; +InvalidApiKeyExpiresAt , InvalidRequest , BAD_REQUEST ; +InvalidApiKeyIndexes , InvalidRequest , BAD_REQUEST ; +InvalidApiKeyLimit , InvalidRequest , BAD_REQUEST ; +InvalidApiKeyName , InvalidRequest , BAD_REQUEST ; +InvalidApiKeyOffset , InvalidRequest , BAD_REQUEST ; +InvalidApiKeyUid , InvalidRequest , BAD_REQUEST ; +InvalidContentType , InvalidRequest , UNSUPPORTED_MEDIA_TYPE ; +InvalidDocumentFields , InvalidRequest , BAD_REQUEST ; +InvalidDocumentGeoField , InvalidRequest , BAD_REQUEST ; +InvalidDocumentId , InvalidRequest , BAD_REQUEST ; +InvalidDocumentLimit , InvalidRequest , BAD_REQUEST ; +InvalidDocumentOffset , InvalidRequest , BAD_REQUEST ; +InvalidIndexLimit , InvalidRequest , BAD_REQUEST ; +InvalidIndexOffset , InvalidRequest , BAD_REQUEST ; +InvalidIndexPrimaryKey , InvalidRequest , BAD_REQUEST ; +InvalidIndexUid , InvalidRequest , BAD_REQUEST ; +InvalidMinWordLengthForTypo , InvalidRequest , BAD_REQUEST ; +InvalidSearchAttributesToCrop , InvalidRequest , BAD_REQUEST ; +InvalidSearchAttributesToHighlight , InvalidRequest , BAD_REQUEST ; +InvalidSearchAttributesToRetrieve , InvalidRequest , BAD_REQUEST ; +InvalidSearchCropLength , InvalidRequest , BAD_REQUEST ; +InvalidSearchCropMarker , InvalidRequest , BAD_REQUEST ; +InvalidSearchFacets , InvalidRequest , BAD_REQUEST ; +InvalidSearchFilter , InvalidRequest , BAD_REQUEST ; +InvalidSearchHighlightPostTag , InvalidRequest , BAD_REQUEST ; +InvalidSearchHighlightPreTag , InvalidRequest , BAD_REQUEST ; +InvalidSearchHitsPerPage , InvalidRequest , BAD_REQUEST ; +InvalidSearchLimit , InvalidRequest , BAD_REQUEST ; +InvalidSearchMatchingStrategy , InvalidRequest , BAD_REQUEST ; +InvalidSearchOffset , InvalidRequest , BAD_REQUEST ; +InvalidSearchPage , InvalidRequest , BAD_REQUEST ; +InvalidSearchQ , InvalidRequest , BAD_REQUEST ; +InvalidSearchShowMatchesPosition , InvalidRequest , BAD_REQUEST ; +InvalidSearchSort , InvalidRequest , BAD_REQUEST ; +InvalidSettingsDisplayedAttributes , InvalidRequest , BAD_REQUEST ; +InvalidSettingsDistinctAttribute , InvalidRequest , BAD_REQUEST ; +InvalidSettingsFaceting , InvalidRequest , BAD_REQUEST ; +InvalidSettingsFilterableAttributes , InvalidRequest , BAD_REQUEST ; +InvalidSettingsPagination , InvalidRequest , BAD_REQUEST ; +InvalidSettingsRankingRules , InvalidRequest , BAD_REQUEST ; +InvalidSettingsSearchableAttributes , InvalidRequest , BAD_REQUEST ; +InvalidSettingsSortableAttributes , InvalidRequest , BAD_REQUEST ; +InvalidSettingsStopWords , InvalidRequest , BAD_REQUEST ; +InvalidSettingsSynonyms , InvalidRequest , BAD_REQUEST ; +InvalidSettingsTypoTolerance , InvalidRequest , BAD_REQUEST ; +InvalidState , Internal , INTERNAL_SERVER_ERROR ; +InvalidStoreFile , Internal , INTERNAL_SERVER_ERROR ; +InvalidSwapDuplicateIndexFound , InvalidRequest , BAD_REQUEST ; +InvalidSwapIndexes , InvalidRequest , BAD_REQUEST ; +InvalidTaskAfterEnqueuedAt , InvalidRequest , BAD_REQUEST ; +InvalidTaskAfterFinishedAt , InvalidRequest , BAD_REQUEST ; +InvalidTaskAfterStartedAt , InvalidRequest , BAD_REQUEST ; +InvalidTaskBeforeEnqueuedAt , InvalidRequest , BAD_REQUEST ; +InvalidTaskBeforeFinishedAt , InvalidRequest , BAD_REQUEST ; +InvalidTaskBeforeStartedAt , InvalidRequest , BAD_REQUEST ; +InvalidTaskCanceledBy , InvalidRequest , BAD_REQUEST ; +InvalidTaskFrom , InvalidRequest , BAD_REQUEST ; +InvalidTaskLimit , InvalidRequest , BAD_REQUEST ; +InvalidTaskStatuses , InvalidRequest , BAD_REQUEST ; +InvalidTaskTypes , InvalidRequest , BAD_REQUEST ; +InvalidTaskUids , InvalidRequest , BAD_REQUEST ; +IoError , System , UNPROCESSABLE_ENTITY; +MalformedPayload , InvalidRequest , BAD_REQUEST ; +MaxFieldsLimitExceeded , InvalidRequest , BAD_REQUEST ; +MissingApiKeyActions , InvalidRequest , BAD_REQUEST ; +MissingApiKeyExpiresAt , InvalidRequest , BAD_REQUEST ; +MissingApiKeyIndexes , InvalidRequest , BAD_REQUEST ; +MissingAuthorizationHeader , Auth , UNAUTHORIZED ; +MissingContentType , InvalidRequest , UNSUPPORTED_MEDIA_TYPE ; +MissingDocumentId , InvalidRequest , BAD_REQUEST ; +MissingIndexUid , InvalidRequest , BAD_REQUEST ; +MissingMasterKey , Auth , UNAUTHORIZED ; +MissingPayload , InvalidRequest , BAD_REQUEST ; +MissingSwapIndexes , InvalidRequest , BAD_REQUEST ; +MissingTaskFilters , InvalidRequest , BAD_REQUEST ; +NoSpaceLeftOnDevice , System , UNPROCESSABLE_ENTITY; +PayloadTooLarge , InvalidRequest , PAYLOAD_TOO_LARGE ; +TaskNotFound , InvalidRequest , NOT_FOUND ; +TooManyOpenFiles , System , UNPROCESSABLE_ENTITY ; +UnretrievableDocument , Internal , BAD_REQUEST ; +UnretrievableErrorCode , InvalidRequest , BAD_REQUEST ; +UnsupportedMediaType , InvalidRequest , UNSUPPORTED_MEDIA_TYPE } impl ErrorCode for JoinError { @@ -409,6 +385,7 @@ impl ErrorCode for io::Error { } } +/// Unwrap a result, either its Ok or Err value. pub fn unwrap_any(any: Result) -> T { match any { Ok(any) => any, @@ -416,501 +393,43 @@ pub fn unwrap_any(any: Result) -> T { } } -#[cfg(feature = "test-traits")] -mod strategy { - use proptest::strategy::Strategy; - - use super::*; - - pub(super) fn status_code_strategy() -> impl Strategy { - (100..999u16).prop_map(|i| StatusCode::from_u16(i).unwrap()) - } -} - -pub struct DeserrJson; -pub struct DeserrQueryParam; - -pub type DeserrJsonError = DeserrError; -pub type DeserrQueryParamError = DeserrError; - -pub struct DeserrError { - pub msg: String, - pub code: Code, - _phantom: PhantomData<(Format, C)>, -} -impl std::fmt::Debug for DeserrError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("DeserrError").field("msg", &self.msg).field("code", &self.code).finish() - } -} - -impl std::fmt::Display for DeserrError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.msg) - } -} - -impl std::error::Error for DeserrError {} -impl ErrorCode for DeserrError { - fn error_code(&self) -> Code { - self.code - } -} - -impl - MergeWithError> for DeserrError -{ - fn merge( - _self_: Option, - other: DeserrError, - _merge_location: ValuePointerRef, - ) -> Result { - Err(DeserrError { msg: other.msg, code: other.code, _phantom: PhantomData }) - } -} - -impl DeserrJsonError { - pub fn missing_index_uid(field: &str, location: ValuePointerRef) -> Self { - let x = unwrap_any(Self::error::( - None, - deserr::ErrorKind::MissingField { field }, - location, - )); - Self { msg: x.msg, code: MissingIndexUid.error_code(), _phantom: PhantomData } - } -} -impl DeserrJsonError { - pub fn missing_api_key_actions(field: &str, location: ValuePointerRef) -> Self { - let x = unwrap_any(Self::error::( - None, - deserr::ErrorKind::MissingField { field }, - location, - )); - Self { msg: x.msg, code: MissingApiKeyActions.error_code(), _phantom: PhantomData } - } -} -impl DeserrJsonError { - pub fn missing_api_key_expires_at(field: &str, location: ValuePointerRef) -> Self { - let x = unwrap_any(Self::error::( - None, - deserr::ErrorKind::MissingField { field }, - location, - )); - Self { msg: x.msg, code: MissingApiKeyExpiresAt.error_code(), _phantom: PhantomData } - } -} -impl DeserrJsonError { - pub fn missing_api_key_indexes(field: &str, location: ValuePointerRef) -> Self { - let x = unwrap_any(Self::error::( - None, - deserr::ErrorKind::MissingField { field }, - location, - )); - Self { msg: x.msg, code: MissingApiKeyIndexes.error_code(), _phantom: PhantomData } - } -} - -impl DeserrJsonError { - pub fn missing_swap_indexes_indexes(field: &str, location: ValuePointerRef) -> Self { - let x = unwrap_any(Self::error::( - None, - deserr::ErrorKind::MissingField { field }, - location, - )); - Self { msg: x.msg, code: MissingSwapIndexes.error_code(), _phantom: PhantomData } - } -} - -// if the error happened in the root, then an empty string is returned. -pub fn location_json_description(location: ValuePointerRef, article: &str) -> String { - fn rec(location: ValuePointerRef) -> String { - match location { - ValuePointerRef::Origin => String::new(), - ValuePointerRef::Key { key, prev } => rec(*prev) + "." + key, - ValuePointerRef::Index { index, prev } => format!("{}[{index}]", rec(*prev)), - } - } - match location { - ValuePointerRef::Origin => String::new(), - _ => { - format!("{article} `{}`", rec(location)) - } - } -} - -fn value_kinds_description_json(kinds: &[ValueKind]) -> String { - fn order(kind: &ValueKind) -> u8 { - match kind { - ValueKind::Null => 0, - ValueKind::Boolean => 1, - ValueKind::Integer => 2, - ValueKind::NegativeInteger => 3, - ValueKind::Float => 4, - ValueKind::String => 5, - ValueKind::Sequence => 6, - ValueKind::Map => 7, - } - } - - fn single_description(kind: &ValueKind) -> &'static str { - match kind { - ValueKind::Null => "null", - ValueKind::Boolean => "a boolean", - ValueKind::Integer => "a positive integer", - ValueKind::NegativeInteger => "an integer", - ValueKind::Float => "a number", - ValueKind::String => "a string", - ValueKind::Sequence => "an array", - ValueKind::Map => "an object", - } - } - - fn description_rec(kinds: &[ValueKind], count_items: &mut usize, message: &mut String) { - let (msg_part, rest): (_, &[ValueKind]) = match kinds { - [] => (String::new(), &[]), - [ValueKind::Integer | ValueKind::NegativeInteger, ValueKind::Float, rest @ ..] => { - ("a number".to_owned(), rest) - } - [ValueKind::Integer, ValueKind::NegativeInteger, ValueKind::Float, rest @ ..] => { - ("a number".to_owned(), rest) - } - [ValueKind::Integer, ValueKind::NegativeInteger, rest @ ..] => { - ("an integer".to_owned(), rest) - } - [a] => (single_description(a).to_owned(), &[]), - [a, rest @ ..] => (single_description(a).to_owned(), rest), - }; - - if rest.is_empty() { - if *count_items == 0 { - message.push_str(&msg_part); - } else if *count_items == 1 { - message.push_str(&format!(" or {msg_part}")); - } else { - message.push_str(&format!(", or {msg_part}")); - } - } else { - if *count_items == 0 { - message.push_str(&msg_part); - } else { - message.push_str(&format!(", {msg_part}")); - } - - *count_items += 1; - description_rec(rest, count_items, message); - } - } - - let mut kinds = kinds.to_owned(); - kinds.sort_by_key(order); - kinds.dedup(); - - if kinds.is_empty() { - "a different value".to_owned() - } else { - let mut message = String::new(); - description_rec(kinds.as_slice(), &mut 0, &mut message); - message - } -} - -fn value_description_with_kind_json(v: &serde_json::Value) -> String { - match v.kind() { - ValueKind::Null => "null".to_owned(), - kind => { - format!( - "{}: `{}`", - value_kinds_description_json(&[kind]), - serde_json::to_string(v).unwrap() - ) - } - } -} - -impl deserr::DeserializeError for DeserrJsonError { - fn error( - _self_: Option, - error: deserr::ErrorKind, - location: ValuePointerRef, - ) -> Result { - let mut message = String::new(); - - message.push_str(&match error { - ErrorKind::IncorrectValueKind { actual, accepted } => { - let expected = value_kinds_description_json(accepted); - // if we're not able to get the value as a string then we print nothing. - let received = value_description_with_kind_json(&serde_json::Value::from(actual)); - - let location = location_json_description(location, " at"); - - format!("Invalid value type{location}: expected {expected}, but found {received}") - } - ErrorKind::MissingField { field } => { - // serde_json original message: - // Json deserialize error: missing field `lol` at line 1 column 2 - let location = location_json_description(location, " inside"); - format!("Missing field `{field}`{location}") - } - ErrorKind::UnknownKey { key, accepted } => { - let location = location_json_description(location, " inside"); - format!( - "Unknown field `{}`{location}: expected one of {}", - key, - accepted - .iter() - .map(|accepted| format!("`{}`", accepted)) - .collect::>() - .join(", ") - ) - } - ErrorKind::UnknownValue { value, accepted } => { - let location = location_json_description(location, " at"); - format!( - "Unknown value `{}`{location}: expected one of {}", - value, - accepted - .iter() - .map(|accepted| format!("`{}`", accepted)) - .collect::>() - .join(", "), - ) - } - ErrorKind::Unexpected { msg } => { - let location = location_json_description(location, " at"); - // serde_json original message: - // The json payload provided is malformed. `trailing characters at line 1 column 19`. - format!("Invalid value{location}: {msg}") - } - }); - - Err(DeserrJsonError { - msg: message, - code: C::default().error_code(), - _phantom: PhantomData, - }) - } -} - -// if the error happened in the root, then an empty string is returned. -pub fn location_query_param_description(location: ValuePointerRef, article: &str) -> String { - fn rec(location: ValuePointerRef) -> String { - match location { - ValuePointerRef::Origin => String::new(), - ValuePointerRef::Key { key, prev } => { - if matches!(prev, ValuePointerRef::Origin) { - key.to_owned() - } else { - rec(*prev) + "." + key - } - } - ValuePointerRef::Index { index, prev } => format!("{}[{index}]", rec(*prev)), - } - } - match location { - ValuePointerRef::Origin => String::new(), - _ => { - format!("{article} `{}`", rec(location)) - } - } -} - -impl deserr::DeserializeError for DeserrQueryParamError { - fn error( - _self_: Option, - error: deserr::ErrorKind, - location: ValuePointerRef, - ) -> Result { - let mut message = String::new(); - - message.push_str(&match error { - ErrorKind::IncorrectValueKind { actual, accepted } => { - let expected = value_kinds_description_query_param(accepted); - // if we're not able to get the value as a string then we print nothing. - let received = value_description_with_kind_query_param(actual); - - let location = location_query_param_description(location, " for parameter"); - - format!("Invalid value type{location}: expected {expected}, but found {received}") - } - ErrorKind::MissingField { field } => { - // serde_json original message: - // Json deserialize error: missing field `lol` at line 1 column 2 - let location = location_query_param_description(location, " inside"); - format!("Missing parameter `{field}`{location}") - } - ErrorKind::UnknownKey { key, accepted } => { - let location = location_query_param_description(location, " inside"); - format!( - "Unknown parameter `{}`{location}: expected one of {}", - key, - accepted - .iter() - .map(|accepted| format!("`{}`", accepted)) - .collect::>() - .join(", ") - ) - } - ErrorKind::UnknownValue { value, accepted } => { - let location = location_query_param_description(location, " for parameter"); - format!( - "Unknown value `{}`{location}: expected one of {}", - value, - accepted - .iter() - .map(|accepted| format!("`{}`", accepted)) - .collect::>() - .join(", "), - ) - } - ErrorKind::Unexpected { msg } => { - let location = location_query_param_description(location, " in parameter"); - // serde_json original message: - // The json payload provided is malformed. `trailing characters at line 1 column 19`. - format!("Invalid value{location}: {msg}") - } - }); - - Err(DeserrQueryParamError { - msg: message, - code: C::default().error_code(), - _phantom: PhantomData, - }) - } -} - -fn value_kinds_description_query_param(_accepted: &[ValueKind]) -> String { - "a string".to_owned() -} - -fn value_description_with_kind_query_param(actual: deserr::Value) -> String { - match actual { - deserr::Value::Null => "null".to_owned(), - deserr::Value::Boolean(x) => format!("a boolean: `{x}`"), - deserr::Value::Integer(x) => format!("an integer: `{x}`"), - deserr::Value::NegativeInteger(x) => { - format!("an integer: `{x}`") - } - deserr::Value::Float(x) => { - format!("a number: `{x}`") - } - deserr::Value::String(x) => { - format!("a string: `{x}`") - } - deserr::Value::Sequence(_) => "multiple values".to_owned(), - deserr::Value::Map(_) => "multiple parameters".to_owned(), - } -} - +/// Deserialization when `deserr` cannot parse an API key date. #[derive(Debug)] -pub struct DetailedParseIntError(String); -impl fmt::Display for DetailedParseIntError { +pub struct ParseOffsetDateTimeError(pub String); +impl fmt::Display for ParseOffsetDateTimeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "`{original}` is not a valid date. It should follow the RFC 3339 format to represents a date or datetime in the future or specified as a null value. e.g. 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'.", original = self.0) + } +} + +/// Deserialization when `deserr` cannot parse a task date. +#[derive(Debug)] +pub struct InvalidTaskDateError(pub String); +impl std::fmt::Display for InvalidTaskDateError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "`{}` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", self.0) + } +} + +/// Deserialization error when `deserr` cannot parse a String +/// into a bool. +#[derive(Debug)] +pub struct DeserrParseBoolError(pub String); +impl fmt::Display for DeserrParseBoolError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "could not parse `{}` as a boolean, expected either `true` or `false`", self.0) + } +} + +/// Deserialization error when `deserr` cannot parse a String +/// into an integer. +#[derive(Debug)] +pub struct DeserrParseIntError(pub String); +impl fmt::Display for DeserrParseIntError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "could not parse `{}` as a positive integer", self.0) } } -impl std::error::Error for DetailedParseIntError {} - -pub fn parse_u32_query_param(x: String) -> Result> { - x.parse::().map_err(|_e| TakeErrorMessage(DetailedParseIntError(x.to_owned()))) -} -pub fn parse_usize_query_param( - x: String, -) -> Result> { - x.parse::().map_err(|_e| TakeErrorMessage(DetailedParseIntError(x.to_owned()))) -} -pub fn parse_option_usize_query_param( - s: Option, -) -> Result, TakeErrorMessage> { - if let Some(s) = s { - parse_usize_query_param(s).map(Some) - } else { - Ok(None) - } -} -pub fn parse_option_u32_query_param( - s: Option, -) -> Result, TakeErrorMessage> { - if let Some(s) = s { - parse_u32_query_param(s).map(Some) - } else { - Ok(None) - } -} -pub fn parse_option_vec_u32_query_param( - s: Option>, -) -> Result>, TakeErrorMessage> { - if let Some(s) = s { - s.into_iter() - .map(parse_u32_query_param) - .collect::, TakeErrorMessage>>() - .map(Some) - } else { - Ok(None) - } -} -pub fn parse_option_cs_star_or( - s: Option>>, -) -> Result>, TakeErrorMessage> { - if let Some(s) = s.and_then(fold_star_or) as Option> { - s.into_iter() - .map(|s| T::from_str(&s)) - .collect::, T::Err>>() - .map_err(TakeErrorMessage) - .map(Some) - } else { - Ok(None) - } -} - -/// Extracts the raw values from the `StarOr` types and -/// return None if a `StarOr::Star` is encountered. -pub fn fold_star_or(content: impl IntoIterator>) -> Option -where - O: FromIterator, -{ - content - .into_iter() - .map(|value| match value { - StarOr::Star => None, - StarOr::Other(val) => Some(val), - }) - .collect() -} -pub struct TakeErrorMessage(pub T); - -impl MergeWithError> for DeserrJsonError -where - T: std::error::Error, -{ - fn merge( - _self_: Option, - other: TakeErrorMessage, - merge_location: ValuePointerRef, - ) -> Result { - DeserrJsonError::error::( - None, - deserr::ErrorKind::Unexpected { msg: other.0.to_string() }, - merge_location, - ) - } -} - -impl MergeWithError> for DeserrQueryParamError -where - T: std::error::Error, -{ - fn merge( - _self_: Option, - other: TakeErrorMessage, - merge_location: ValuePointerRef, - ) -> Result { - DeserrQueryParamError::error::( - None, - deserr::ErrorKind::Unexpected { msg: other.0.to_string() }, - merge_location, - ) - } -} #[macro_export] macro_rules! internal_error { @@ -924,32 +443,3 @@ macro_rules! internal_error { )* } } - -#[cfg(test)] -mod tests { - use deserr::ValueKind; - - use crate::error::value_kinds_description_json; - - #[test] - fn test_value_kinds_description_json() { - insta::assert_display_snapshot!(value_kinds_description_json(&[]), @"a different value"); - - insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Boolean]), @"a boolean"); - insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer]), @"a positive integer"); - insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::NegativeInteger]), @"an integer"); - insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer]), @"a positive integer"); - insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::String]), @"a string"); - insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Sequence]), @"an array"); - insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Map]), @"an object"); - - insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer, ValueKind::Boolean]), @"a boolean or a positive integer"); - insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Null, ValueKind::Integer]), @"null or a positive integer"); - insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Sequence, ValueKind::NegativeInteger]), @"an integer or an array"); - insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer, ValueKind::Float]), @"a number"); - insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer, ValueKind::Float, ValueKind::NegativeInteger]), @"a number"); - insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer, ValueKind::Float, ValueKind::NegativeInteger, ValueKind::Null]), @"null or a number"); - insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Boolean, ValueKind::Integer, ValueKind::Float, ValueKind::NegativeInteger, ValueKind::Null]), @"null, a boolean, or a number"); - insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Null, ValueKind::Boolean, ValueKind::Integer, ValueKind::Float, ValueKind::NegativeInteger, ValueKind::Null]), @"null, a boolean, or a number"); - } -} diff --git a/meilisearch-types/src/index_uid.rs b/meilisearch-types/src/index_uid.rs index 945a57e9e..30b707665 100644 --- a/meilisearch-types/src/index_uid.rs +++ b/meilisearch-types/src/index_uid.rs @@ -29,6 +29,12 @@ impl IndexUid { } } +impl fmt::Display for IndexUid { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + impl std::ops::Deref for IndexUid { type Target = str; diff --git a/meilisearch-types/src/keys.rs b/meilisearch-types/src/keys.rs index 8543651c5..ea941b775 100644 --- a/meilisearch-types/src/keys.rs +++ b/meilisearch-types/src/keys.rs @@ -1,8 +1,8 @@ use std::convert::Infallible; -use std::fmt::Display; use std::hash::Hash; +use std::str::FromStr; -use deserr::{DeserializeError, DeserializeFromValue, MergeWithError, ValuePointerRef}; +use deserr::{DeserializeError, DeserializeFromValue, ValuePointerRef}; use enum_iterator::Sequence; use serde::{Deserialize, Serialize}; use time::format_description::well_known::Rfc3339; @@ -10,31 +10,14 @@ use time::macros::{format_description, time}; use time::{Date, OffsetDateTime, PrimitiveDateTime}; use uuid::Uuid; -use crate::error::deserr_codes::*; -use crate::error::{unwrap_any, Code, DeserrJsonError, ErrorCode, TakeErrorMessage}; -use crate::index_uid::{IndexUid, IndexUidFormatError}; +use crate::deserr::DeserrJsonError; +use crate::error::{deserr_codes::*, ParseOffsetDateTimeError}; +use crate::error::{unwrap_any, Code}; +use crate::index_uid::IndexUid; use crate::star_or::StarOr; pub type KeyId = Uuid; -impl MergeWithError for DeserrJsonError { - fn merge( - _self_: Option, - other: IndexUidFormatError, - merge_location: deserr::ValuePointerRef, - ) -> std::result::Result { - DeserrJsonError::error::( - None, - deserr::ErrorKind::Unexpected { msg: other.to_string() }, - merge_location, - ) - } -} - -fn parse_uuid_from_str(s: &str) -> Result> { - Uuid::parse_str(s).map_err(TakeErrorMessage) -} - #[derive(Debug, DeserializeFromValue)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] pub struct CreateApiKey { @@ -42,13 +25,13 @@ pub struct CreateApiKey { pub description: Option, #[deserr(default, error = DeserrJsonError)] pub name: Option, - #[deserr(default = Uuid::new_v4(), error = DeserrJsonError, from(&String) = parse_uuid_from_str -> TakeErrorMessage)] + #[deserr(default = Uuid::new_v4(), error = DeserrJsonError, from(&String) = Uuid::from_str -> uuid::Error)] pub uid: KeyId, #[deserr(error = DeserrJsonError, missing_field_error = DeserrJsonError::missing_api_key_actions)] pub actions: Vec, #[deserr(error = DeserrJsonError, missing_field_error = DeserrJsonError::missing_api_key_indexes)] pub indexes: Vec>, - #[deserr(error = DeserrJsonError, from(Option) = parse_expiration_date -> TakeErrorMessage, missing_field_error = DeserrJsonError::missing_api_key_expires_at)] + #[deserr(error = DeserrJsonError, from(Option) = parse_expiration_date -> ParseOffsetDateTimeError, missing_field_error = DeserrJsonError::missing_api_key_expires_at)] pub expires_at: Option, } impl CreateApiKey { @@ -149,18 +132,9 @@ impl Key { } } -#[derive(Debug)] -pub struct ParseOffsetDateTimeError(String); -impl Display for ParseOffsetDateTimeError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - writeln!(f, "`{original}` is not a valid date. It should follow the RFC 3339 format to represents a date or datetime in the future or specified as a null value. e.g. 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'.", original = self.0) - } -} -impl std::error::Error for ParseOffsetDateTimeError {} - fn parse_expiration_date( string: Option, -) -> std::result::Result, TakeErrorMessage> { +) -> std::result::Result, ParseOffsetDateTimeError> { let Some(string) = string else { return Ok(None) }; @@ -186,12 +160,12 @@ fn parse_expiration_date( ) { PrimitiveDateTime::new(date, time!(00:00)).assume_utc() } else { - return Err(TakeErrorMessage(ParseOffsetDateTimeError(string))); + return Err(ParseOffsetDateTimeError(string)); }; if datetime > OffsetDateTime::now_utc() { Ok(Some(datetime)) } else { - Err(TakeErrorMessage(ParseOffsetDateTimeError(string))) + Err(ParseOffsetDateTimeError(string)) } } diff --git a/meilisearch-types/src/lib.rs b/meilisearch-types/src/lib.rs index 354a25fa1..de4084388 100644 --- a/meilisearch-types/src/lib.rs +++ b/meilisearch-types/src/lib.rs @@ -7,7 +7,7 @@ pub mod settings; pub mod star_or; pub mod tasks; pub mod versioning; - +pub mod deserr; pub use milli; pub use milli::{heed, Index}; pub use serde_cs; diff --git a/meilisearch-types/src/settings.rs b/meilisearch-types/src/settings.rs index 99f4ae9e3..8d085d0ff 100644 --- a/meilisearch-types/src/settings.rs +++ b/meilisearch-types/src/settings.rs @@ -11,8 +11,9 @@ use milli::update::Setting; use milli::{Criterion, CriterionError, Index, DEFAULT_VALUES_PER_FACET}; use serde::{Deserialize, Serialize, Serializer}; +use crate::deserr::DeserrJsonError; use crate::error::deserr_codes::*; -use crate::error::{unwrap_any, DeserrJsonError}; +use crate::error::{unwrap_any}; /// The maximimum number of results that the engine /// will be able to return in one search call. diff --git a/meilisearch-types/src/star_or.rs b/meilisearch-types/src/star_or.rs index f56c30b4e..e40884925 100644 --- a/meilisearch-types/src/star_or.rs +++ b/meilisearch-types/src/star_or.rs @@ -1,13 +1,9 @@ -use std::fmt::{Display, Formatter}; -use std::marker::PhantomData; -use std::ops::Deref; -use std::str::FromStr; +use std::{fmt, marker::PhantomData, str::FromStr}; use deserr::{DeserializeError, DeserializeFromValue, MergeWithError, ValueKind}; -use serde::de::Visitor; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; -use crate::error::unwrap_any; +use crate::{deserr::query_params::FromQueryParameter, error::unwrap_any}; /// A type that tries to match either a star (*) or /// any other thing that implements `FromStr`. @@ -17,35 +13,6 @@ pub enum StarOr { Other(T), } -impl DeserializeFromValue for StarOr -where - T: FromStr, - E: MergeWithError, -{ - fn deserialize_from_value( - value: deserr::Value, - location: deserr::ValuePointerRef, - ) -> Result { - match value { - deserr::Value::String(v) => match v.as_str() { - "*" => Ok(StarOr::Star), - v => match FromStr::from_str(v) { - Ok(x) => Ok(StarOr::Other(x)), - Err(e) => Err(unwrap_any(E::merge(None, e, location))), - }, - }, - _ => Err(unwrap_any(E::error::( - None, - deserr::ErrorKind::IncorrectValueKind { - actual: value, - accepted: &[ValueKind::String], - }, - location, - ))), - } - } -} - impl FromStr for StarOr { type Err = T::Err; @@ -57,23 +24,11 @@ impl FromStr for StarOr { } } } - -impl> Deref for StarOr { - type Target = str; - - fn deref(&self) -> &Self::Target { +impl fmt::Display for StarOr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Star => "*", - Self::Other(t) => t.deref(), - } - } -} - -impl> From> for String { - fn from(s: StarOr) -> Self { - match s { - StarOr::Star => "*".to_string(), - StarOr::Other(t) => t.into(), + StarOr::Star => write!(f, "*"), + StarOr::Other(x) => fmt::Display::fmt(x, f), } } } @@ -93,7 +48,7 @@ impl Eq for StarOr {} impl<'de, T, E> Deserialize<'de> for StarOr where T: FromStr, - E: Display, + E: fmt::Display, { fn deserialize(deserializer: D) -> Result where @@ -109,11 +64,11 @@ where impl<'de, T, FE> Visitor<'de> for StarOrVisitor where T: FromStr, - FE: Display, + FE: fmt::Display, { type Value = StarOr; - fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + fn expecting(&self, formatter: &mut fmt::Formatter) -> std::fmt::Result { formatter.write_str("a string") } @@ -139,7 +94,7 @@ where impl Serialize for StarOr where - T: Deref, + T: ToString, { fn serialize(&self, serializer: S) -> Result where @@ -147,7 +102,222 @@ where { match self { StarOr::Star => serializer.serialize_str("*"), - StarOr::Other(other) => serializer.serialize_str(other.deref()), + StarOr::Other(other) => serializer.serialize_str(&other.to_string()), + } + } +} + +impl DeserializeFromValue for StarOr +where + T: FromStr, + E: DeserializeError + MergeWithError, +{ + fn deserialize_from_value( + value: deserr::Value, + location: deserr::ValuePointerRef, + ) -> Result { + match value { + deserr::Value::String(v) => { + if v == "*" { + Ok(StarOr::Star) + } else { + match T::from_str(&v) { + Ok(parsed) => Ok(StarOr::Other(parsed)), + Err(e) => Err(unwrap_any(E::merge(None, e, location))), + } + } + } + _ => Err(unwrap_any(E::error::( + None, + deserr::ErrorKind::IncorrectValueKind { + actual: value, + accepted: &[ValueKind::String], + }, + location, + ))), + } + } +} + +/// A type representing the content of a query parameter that can either not exist, +/// be equal to a star (*), or another value +/// +/// It is a convenient alternative to `Option>`. +#[derive(Debug, Default, Clone, Copy)] +pub enum OptionStarOr { + #[default] + None, + Star, + Other(T), +} + +impl OptionStarOr { + pub fn is_some(&self) -> bool { + match self { + Self::None => false, + Self::Star => false, + Self::Other(_) => true, + } + } + pub fn merge_star_and_none(self) -> Option { + match self { + Self::None | Self::Star => None, + Self::Other(x) => Some(x), + } + } + pub fn try_map Result>(self, map_f: F) -> Result, E> { + match self { + OptionStarOr::None => Ok(OptionStarOr::None), + OptionStarOr::Star => Ok(OptionStarOr::Star), + OptionStarOr::Other(x) => map_f(x).map(OptionStarOr::Other), + } + } +} + +impl FromQueryParameter for OptionStarOr +where + T: FromQueryParameter, +{ + type Err = T::Err; + fn from_query_param(p: &str) -> Result { + match p { + "*" => Ok(OptionStarOr::Star), + s => T::from_query_param(s).map(OptionStarOr::Other), + } + } +} + +impl DeserializeFromValue for OptionStarOr +where + E: DeserializeError + MergeWithError, + T: FromQueryParameter, +{ + fn deserialize_from_value( + value: deserr::Value, + location: deserr::ValuePointerRef, + ) -> Result { + match value { + deserr::Value::String(s) => match s.as_str() { + "*" => Ok(OptionStarOr::Star), + s => match T::from_query_param(s) { + Ok(x) => Ok(OptionStarOr::Other(x)), + Err(e) => Err(unwrap_any(E::merge(None, e, location))), + }, + }, + _ => Err(unwrap_any(E::error::( + None, + deserr::ErrorKind::IncorrectValueKind { + actual: value, + accepted: &[ValueKind::String], + }, + location, + ))), + } + } +} + +/// A type representing the content of a query parameter that can either not exist, be equal to a star (*), or represent a list of other values +#[derive(Debug, Default, Clone)] +pub enum OptionStarOrList { + #[default] + None, + Star, + List(Vec), +} + +impl OptionStarOrList { + pub fn is_some(&self) -> bool { + match self { + Self::None => false, + Self::Star => false, + Self::List(_) => true, + } + } + pub fn map U>(self, map_f: F) -> OptionStarOrList { + match self { + Self::None => OptionStarOrList::None, + Self::Star => OptionStarOrList::Star, + Self::List(xs) => OptionStarOrList::List(xs.into_iter().map(map_f).collect()), + } + } + pub fn try_map Result>( + self, + map_f: F, + ) -> Result, E> { + match self { + Self::None => Ok(OptionStarOrList::None), + Self::Star => Ok(OptionStarOrList::Star), + Self::List(xs) => { + xs.into_iter().map(map_f).collect::, _>>().map(OptionStarOrList::List) + } + } + } + pub fn merge_star_and_none(self) -> Option> { + match self { + Self::None | Self::Star => None, + Self::List(xs) => Some(xs), + } + } + pub fn push(&mut self, el: T) { + match self { + Self::None => *self = Self::List(vec![el]), + Self::Star => (), + Self::List(xs) => xs.push(el), + } + } +} + +impl DeserializeFromValue for OptionStarOrList +where + E: DeserializeError + MergeWithError, + T: FromQueryParameter, +{ + fn deserialize_from_value( + value: deserr::Value, + location: deserr::ValuePointerRef, + ) -> Result { + match value { + deserr::Value::String(s) => { + let mut error = None; + let mut is_star = false; + // CS::::from_str is infaillible + let cs = serde_cs::vec::CS::::from_str(&s).unwrap(); + let len_cs = cs.0.len(); + let mut els = vec![]; + for (i, el_str) in cs.into_iter().enumerate() { + if el_str == "*" { + is_star = true; + } else { + match T::from_query_param(&el_str) { + Ok(el) => { + els.push(el); + } + Err(e) => { + let location = + if len_cs > 1 { location.push_index(i) } else { location }; + error = Some(E::merge(error, e, location)?); + } + } + } + } + if let Some(error) = error { + return Err(error); + } + + if is_star { + Ok(OptionStarOrList::Star) + } else { + Ok(OptionStarOrList::List(els)) + } + } + _ => Err(unwrap_any(E::error::( + None, + deserr::ErrorKind::IncorrectValueKind { + actual: value, + accepted: &[ValueKind::String], + }, + location, + ))), } } } diff --git a/meilisearch-types/src/tasks.rs b/meilisearch-types/src/tasks.rs index fd2d31e06..3b7efb97b 100644 --- a/meilisearch-types/src/tasks.rs +++ b/meilisearch-types/src/tasks.rs @@ -1,3 +1,4 @@ +use core::fmt; use std::collections::HashSet; use std::fmt::{Display, Write}; use std::str::FromStr; @@ -9,7 +10,7 @@ use serde::{Deserialize, Serialize, Serializer}; use time::{Duration, OffsetDateTime}; use uuid::Uuid; -use crate::error::{Code, ResponseError}; +use crate::error::ResponseError; use crate::keys::Key; use crate::settings::{Settings, Unchecked}; use crate::InstanceUid; @@ -332,7 +333,7 @@ impl Display for Status { } impl FromStr for Status { - type Err = ResponseError; + type Err = ParseTaskStatusError; fn from_str(status: &str) -> Result { if status.eq_ignore_ascii_case("enqueued") { @@ -346,21 +347,28 @@ impl FromStr for Status { } else if status.eq_ignore_ascii_case("canceled") { Ok(Status::Canceled) } else { - Err(ResponseError::from_msg( - format!( - "`{}` is not a valid task status. Available statuses are {}.", - status, - enum_iterator::all::() - .map(|s| format!("`{s}`")) - .collect::>() - .join(", ") - ), - Code::BadRequest, - )) + Err(ParseTaskStatusError(status.to_owned())) } } } +#[derive(Debug)] +pub struct ParseTaskStatusError(pub String); +impl fmt::Display for ParseTaskStatusError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "`{}` is not a valid task status. Available statuses are {}.", + self.0, + enum_iterator::all::() + .map(|s| format!("`{s}`")) + .collect::>() + .join(", ") + ) + } +} +impl std::error::Error for ParseTaskStatusError {} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Sequence)] #[serde(rename_all = "camelCase")] pub enum Kind { @@ -412,7 +420,7 @@ impl Display for Kind { } } impl FromStr for Kind { - type Err = ResponseError; + type Err = ParseTaskKindError; fn from_str(kind: &str) -> Result { if kind.eq_ignore_ascii_case("indexCreation") { @@ -438,25 +446,32 @@ impl FromStr for Kind { } else if kind.eq_ignore_ascii_case("snapshotCreation") { Ok(Kind::SnapshotCreation) } else { - Err(ResponseError::from_msg( - format!( - "`{}` is not a valid task type. Available types are {}.", - kind, - enum_iterator::all::() - .map(|k| format!( - "`{}`", - // by default serde is going to insert `"` around the value. - serde_json::to_string(&k).unwrap().trim_matches('"') - )) - .collect::>() - .join(", ") - ), - Code::BadRequest, - )) + Err(ParseTaskKindError(kind.to_owned())) } } } +#[derive(Debug)] +pub struct ParseTaskKindError(pub String); +impl fmt::Display for ParseTaskKindError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "`{}` is not a valid task type. Available types are {}.", + self.0, + enum_iterator::all::() + .map(|k| format!( + "`{}`", + // by default serde is going to insert `"` around the value. + serde_json::to_string(&k).unwrap().trim_matches('"') + )) + .collect::>() + .join(", ") + ) + } +} +impl std::error::Error for ParseTaskKindError {} + #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub enum Details { DocumentAdditionOrUpdate { received_documents: u64, indexed_documents: Option }, diff --git a/meilisearch/Cargo.toml b/meilisearch/Cargo.toml index 2c2c2aca8..be852c02e 100644 --- a/meilisearch/Cargo.toml +++ b/meilisearch/Cargo.toml @@ -19,7 +19,7 @@ byte-unit = { version = "4.0.14", default-features = false, features = ["std", " bytes = "1.2.1" clap = { version = "4.0.9", features = ["derive", "env"] } crossbeam-channel = "0.5.6" -deserr = { path = "/Users/meilisearch/Documents/deserr" } +deserr = "0.1.4" dump = { path = "../dump" } either = "1.8.0" env_logger = "0.9.1" diff --git a/meilisearch/src/routes/api_key.rs b/meilisearch/src/routes/api_key.rs index 917a5e285..cd5bfe0c7 100644 --- a/meilisearch/src/routes/api_key.rs +++ b/meilisearch/src/routes/api_key.rs @@ -4,14 +4,15 @@ use actix_web::{web, HttpRequest, HttpResponse}; use deserr::DeserializeFromValue; use meilisearch_auth::error::AuthControllerError; use meilisearch_auth::AuthController; -use meilisearch_types::error::{deserr_codes::*, DeserrQueryParamError}; -use meilisearch_types::error::{Code, DeserrJsonError, ResponseError, TakeErrorMessage}; +use meilisearch_types::deserr::query_params::Param; +use meilisearch_types::deserr::{DeserrJsonError, DeserrQueryParamError}; +use meilisearch_types::error::deserr_codes::*; +use meilisearch_types::error::{Code, ResponseError}; use meilisearch_types::keys::{Action, CreateApiKey, Key, PatchApiKey}; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use uuid::Uuid; -use super::indexes::search::parse_usize_take_error_message; use super::PAGINATION_DEFAULT_LIMIT; use crate::extractors::authentication::policies::*; use crate::extractors::authentication::GuardedData; @@ -50,20 +51,17 @@ pub async fn create_api_key( Ok(HttpResponse::Created().json(res)) } -#[derive(DeserializeFromValue, Deserialize, Debug, Clone, Copy)] +#[derive(DeserializeFromValue, Debug, Clone, Copy)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ListApiKeys { - #[serde(default)] - #[deserr(default, error = DeserrQueryParamError, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] - pub offset: usize, - #[serde(default = "PAGINATION_DEFAULT_LIMIT")] - #[deserr(default = PAGINATION_DEFAULT_LIMIT(), error = DeserrQueryParamError, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] - pub limit: usize, + #[deserr(default, error = DeserrQueryParamError)] + pub offset: Param, + #[deserr(default = Param(PAGINATION_DEFAULT_LIMIT), error = DeserrQueryParamError)] + pub limit: Param, } impl ListApiKeys { fn as_pagination(self) -> Pagination { - Pagination { offset: self.offset, limit: self.limit } + Pagination { offset: self.offset.0, limit: self.limit.0 } } } @@ -172,7 +170,7 @@ impl KeyView { key: generated_key, uid: key.uid, actions: key.actions, - indexes: key.indexes.into_iter().map(String::from).collect(), + indexes: key.indexes.into_iter().map(|x| x.to_string()).collect(), expires_at: key.expires_at, created_at: key.created_at, updated_at: key.updated_at, diff --git a/meilisearch/src/routes/indexes/documents.rs b/meilisearch/src/routes/indexes/documents.rs index 2c1b0f692..3316ee10b 100644 --- a/meilisearch/src/routes/indexes/documents.rs +++ b/meilisearch/src/routes/indexes/documents.rs @@ -1,5 +1,4 @@ use std::io::ErrorKind; -use std::num::ParseIntError; use actix_web::http::header::CONTENT_TYPE; use actix_web::web::Data; @@ -9,14 +8,15 @@ use deserr::DeserializeFromValue; use futures::StreamExt; use index_scheduler::IndexScheduler; use log::debug; +use meilisearch_types::deserr::query_params::Param; +use meilisearch_types::deserr::{DeserrJsonError, DeserrQueryParamError}; use meilisearch_types::document_formats::{read_csv, read_json, read_ndjson, PayloadType}; -use meilisearch_types::error::{deserr_codes::*, fold_star_or, DeserrQueryParamError}; -use meilisearch_types::error::{DeserrJsonError, ResponseError, TakeErrorMessage}; +use meilisearch_types::error::deserr_codes::*; +use meilisearch_types::error::ResponseError; use meilisearch_types::heed::RoTxn; use meilisearch_types::index_uid::IndexUid; use meilisearch_types::milli::update::IndexDocumentsMethod; -use meilisearch_types::serde_cs::vec::CS; -use meilisearch_types::star_or::StarOr; +use meilisearch_types::star_or::OptionStarOrList; use meilisearch_types::tasks::KindWithContent; use meilisearch_types::{milli, Document, Index}; use mime::Mime; @@ -27,7 +27,6 @@ use tempfile::tempfile; use tokio::fs::File; use tokio::io::{AsyncSeekExt, AsyncWriteExt, BufWriter}; -use super::search::parse_usize_take_error_message; use crate::analytics::{Analytics, DocumentDeletionKind}; use crate::error::MeilisearchHttpError; use crate::error::PayloadError::ReceivePayload; @@ -36,7 +35,7 @@ use crate::extractors::authentication::GuardedData; use crate::extractors::payload::Payload; use crate::extractors::query_parameters::QueryParameter; use crate::extractors::sequential_extractor::SeqHandler; -use crate::routes::{PaginationView, SummarizedTaskView}; +use crate::routes::{PaginationView, SummarizedTaskView, PAGINATION_DEFAULT_LIMIT}; static ACCEPTED_CONTENT_TYPE: Lazy> = Lazy::new(|| { vec!["application/json".to_string(), "application/x-ndjson".to_string(), "text/csv".to_string()] @@ -81,12 +80,11 @@ pub fn configure(cfg: &mut web::ServiceConfig) { ); } -#[derive(Deserialize, Debug, DeserializeFromValue)] +#[derive(Debug, DeserializeFromValue)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] pub struct GetDocument { - // TODO: strongly typed argument here #[deserr(default, error = DeserrQueryParamError)] - fields: Option>>, + fields: OptionStarOrList, } pub async fn get_document( @@ -95,7 +93,7 @@ pub async fn get_document( params: QueryParameter, ) -> Result { let GetDocument { fields } = params.into_inner(); - let attributes_to_retrieve = fields.and_then(fold_star_or); + let attributes_to_retrieve = fields.merge_star_and_none(); let index = index_scheduler.index(&path.index_uid)?; let document = retrieve_document(&index, &path.document_id, attributes_to_retrieve)?; @@ -119,15 +117,15 @@ pub async fn delete_document( Ok(HttpResponse::Accepted().json(task)) } -#[derive(Deserialize, Debug, DeserializeFromValue)] +#[derive(Debug, DeserializeFromValue)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] pub struct BrowseQuery { - #[deserr(default, error = DeserrQueryParamError, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] - offset: usize, - #[deserr(default = crate::routes::PAGINATION_DEFAULT_LIMIT(), error = DeserrQueryParamError, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] - limit: usize, + #[deserr(default, error = DeserrQueryParamError)] + offset: Param, + #[deserr(default = Param(PAGINATION_DEFAULT_LIMIT), error = DeserrQueryParamError)] + limit: Param, #[deserr(default, error = DeserrQueryParamError)] - fields: Option>>, + fields: OptionStarOrList, } pub async fn get_all_documents( @@ -137,12 +135,12 @@ pub async fn get_all_documents( ) -> Result { debug!("called with params: {:?}", params); let BrowseQuery { limit, offset, fields } = params.into_inner(); - let attributes_to_retrieve = fields.and_then(fold_star_or); + let attributes_to_retrieve = fields.merge_star_and_none(); let index = index_scheduler.index(&index_uid)?; - let (total, documents) = retrieve_documents(&index, offset, limit, attributes_to_retrieve)?; + let (total, documents) = retrieve_documents(&index, offset.0, limit.0, attributes_to_retrieve)?; - let ret = PaginationView::new(offset, limit, total as usize, documents); + let ret = PaginationView::new(offset.0, limit.0, total as usize, documents); debug!("returns: {:?}", ret); Ok(HttpResponse::Ok().json(ret)) diff --git a/meilisearch/src/routes/indexes/mod.rs b/meilisearch/src/routes/indexes/mod.rs index 216cc448e..9e76f3be6 100644 --- a/meilisearch/src/routes/indexes/mod.rs +++ b/meilisearch/src/routes/indexes/mod.rs @@ -5,8 +5,10 @@ use actix_web::{web, HttpRequest, HttpResponse}; use deserr::{DeserializeError, DeserializeFromValue, ValuePointerRef}; use index_scheduler::IndexScheduler; use log::debug; -use meilisearch_types::error::{deserr_codes::*, unwrap_any, Code, DeserrQueryParamError}; -use meilisearch_types::error::{DeserrJsonError, ResponseError, TakeErrorMessage}; +use meilisearch_types::deserr::query_params::Param; +use meilisearch_types::deserr::{DeserrJsonError, DeserrQueryParamError}; +use meilisearch_types::error::ResponseError; +use meilisearch_types::error::{deserr_codes::*, unwrap_any, Code}; use meilisearch_types::index_uid::IndexUid; use meilisearch_types::milli::{self, FieldDistribution, Index}; use meilisearch_types::tasks::KindWithContent; @@ -14,7 +16,6 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use time::OffsetDateTime; -use self::search::parse_usize_take_error_message; use super::{Pagination, SummarizedTaskView, PAGINATION_DEFAULT_LIMIT}; use crate::analytics::Analytics; use crate::extractors::authentication::policies::*; @@ -71,20 +72,17 @@ impl IndexView { } } -#[derive(DeserializeFromValue, Deserialize, Debug, Clone, Copy)] +#[derive(DeserializeFromValue, Debug, Clone, Copy)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ListIndexes { - #[serde(default)] - #[deserr(default, error = DeserrQueryParamError, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] - pub offset: usize, - #[serde(default = "PAGINATION_DEFAULT_LIMIT")] - #[deserr(default = PAGINATION_DEFAULT_LIMIT(), error = DeserrQueryParamError, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] - pub limit: usize, + #[deserr(default, error = DeserrQueryParamError)] + pub offset: Param, + #[deserr(default = Param(PAGINATION_DEFAULT_LIMIT), error = DeserrQueryParamError)] + pub limit: Param, } impl ListIndexes { fn as_pagination(self) -> Pagination { - Pagination { offset: self.offset, limit: self.limit } + Pagination { offset: self.offset.0, limit: self.limit.0 } } } diff --git a/meilisearch/src/routes/indexes/search.rs b/meilisearch/src/routes/indexes/search.rs index ec9364711..7eabfc2ee 100644 --- a/meilisearch/src/routes/indexes/search.rs +++ b/meilisearch/src/routes/indexes/search.rs @@ -1,15 +1,12 @@ -use std::str::FromStr; - use actix_web::web::Data; use actix_web::{web, HttpRequest, HttpResponse}; use index_scheduler::IndexScheduler; use log::debug; use meilisearch_auth::IndexSearchRules; -use meilisearch_types::error::{ - deserr_codes::*, parse_option_usize_query_param, parse_usize_query_param, - DeserrQueryParamError, DetailedParseIntError, -}; -use meilisearch_types::error::{DeserrJsonError, ResponseError, TakeErrorMessage}; +use meilisearch_types::deserr::{DeserrQueryParamError, DeserrJsonError}; +use meilisearch_types::deserr::query_params::Param; +use meilisearch_types::error::deserr_codes::*; +use meilisearch_types::error::ResponseError; use meilisearch_types::serde_cs::vec::CS; use serde_json::Value; @@ -33,45 +30,33 @@ pub fn configure(cfg: &mut web::ServiceConfig) { ); } -pub fn parse_usize_take_error_message( - s: &str, -) -> Result> { - usize::from_str(s).map_err(TakeErrorMessage) -} - -pub fn parse_bool_take_error_message( - s: &str, -) -> Result> { - s.parse().map_err(TakeErrorMessage) -} - #[derive(Debug, deserr::DeserializeFromValue)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] pub struct SearchQueryGet { #[deserr(default, error = DeserrQueryParamError)] q: Option, - #[deserr(default = DEFAULT_SEARCH_OFFSET(), error = DeserrQueryParamError, from(String) = parse_usize_query_param -> TakeErrorMessage)] - offset: usize, - #[deserr(default = DEFAULT_SEARCH_LIMIT(), error = DeserrQueryParamError, from(String) = parse_usize_query_param -> TakeErrorMessage)] - limit: usize, - #[deserr(default, error = DeserrQueryParamError, from(Option) = parse_option_usize_query_param -> TakeErrorMessage)] - page: Option, - #[deserr(default, error = DeserrQueryParamError, from(Option) = parse_option_usize_query_param -> TakeErrorMessage)] - hits_per_page: Option, + #[deserr(default = Param(DEFAULT_SEARCH_OFFSET()), error = DeserrQueryParamError)] + offset: Param, + #[deserr(default = Param(DEFAULT_SEARCH_LIMIT()), error = DeserrQueryParamError)] + limit: Param, + #[deserr(default, error = DeserrQueryParamError)] + page: Option>, + #[deserr(default, error = DeserrQueryParamError)] + hits_per_page: Option>, #[deserr(default, error = DeserrQueryParamError)] attributes_to_retrieve: Option>, #[deserr(default, error = DeserrQueryParamError)] attributes_to_crop: Option>, - #[deserr(default = DEFAULT_CROP_LENGTH(), error = DeserrQueryParamError, from(String) = parse_usize_query_param -> TakeErrorMessage)] - crop_length: usize, + #[deserr(default = Param(DEFAULT_CROP_LENGTH()), error = DeserrQueryParamError)] + crop_length: Param, #[deserr(default, error = DeserrQueryParamError)] attributes_to_highlight: Option>, #[deserr(default, error = DeserrQueryParamError)] filter: Option, #[deserr(default, error = DeserrQueryParamError)] sort: Option, - #[deserr(default, error = DeserrQueryParamError, from(&String) = parse_bool_take_error_message -> TakeErrorMessage)] - show_matches_position: bool, + #[deserr(default, error = DeserrQueryParamError)] + show_matches_position: Param, #[deserr(default, error = DeserrQueryParamError)] facets: Option>, #[deserr( default = DEFAULT_HIGHLIGHT_PRE_TAG(), error = DeserrQueryParamError)] @@ -96,17 +81,17 @@ impl From for SearchQuery { Self { q: other.q, - offset: other.offset, - limit: other.limit, - page: other.page, - hits_per_page: other.hits_per_page, + offset: other.offset.0, + limit: other.limit.0, + page: other.page.as_deref().copied(), + hits_per_page: other.hits_per_page.as_deref().copied(), attributes_to_retrieve: other.attributes_to_retrieve.map(|o| o.into_iter().collect()), attributes_to_crop: other.attributes_to_crop.map(|o| o.into_iter().collect()), - crop_length: other.crop_length, + crop_length: other.crop_length.0, attributes_to_highlight: other.attributes_to_highlight.map(|o| o.into_iter().collect()), filter, sort: other.sort.map(|attr| fix_sort_query_parameters(&attr)), - show_matches_position: other.show_matches_position, + show_matches_position: other.show_matches_position.0, facets: other.facets.map(|o| o.into_iter().collect()), highlight_pre_tag: other.highlight_pre_tag, highlight_post_tag: other.highlight_post_tag, diff --git a/meilisearch/src/routes/indexes/settings.rs b/meilisearch/src/routes/indexes/settings.rs index 404835833..91c3473fa 100644 --- a/meilisearch/src/routes/indexes/settings.rs +++ b/meilisearch/src/routes/indexes/settings.rs @@ -2,7 +2,8 @@ use actix_web::web::Data; use actix_web::{web, HttpRequest, HttpResponse}; use index_scheduler::IndexScheduler; use log::debug; -use meilisearch_types::error::{DeserrJsonError, ResponseError}; +use meilisearch_types::deserr::DeserrJsonError; +use meilisearch_types::error::ResponseError; use meilisearch_types::index_uid::IndexUid; use meilisearch_types::settings::{settings, RankingRuleView, Settings, Unchecked}; use meilisearch_types::tasks::KindWithContent; @@ -130,7 +131,7 @@ make_setting_route!( "/filterable-attributes", put, std::collections::BTreeSet, - meilisearch_types::error::DeserrJsonError< + meilisearch_types::deserr::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsFilterableAttributes, >, filterable_attributes, @@ -156,7 +157,7 @@ make_setting_route!( "/sortable-attributes", put, std::collections::BTreeSet, - meilisearch_types::error::DeserrJsonError< + meilisearch_types::deserr::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsSortableAttributes, >, sortable_attributes, @@ -182,7 +183,7 @@ make_setting_route!( "/displayed-attributes", put, Vec, - meilisearch_types::error::DeserrJsonError< + meilisearch_types::deserr::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsDisplayedAttributes, >, displayed_attributes, @@ -208,7 +209,7 @@ make_setting_route!( "/typo-tolerance", patch, meilisearch_types::settings::TypoSettings, - meilisearch_types::error::DeserrJsonError< + meilisearch_types::deserr::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsTypoTolerance, >, typo_tolerance, @@ -253,7 +254,7 @@ make_setting_route!( "/searchable-attributes", put, Vec, - meilisearch_types::error::DeserrJsonError< + meilisearch_types::deserr::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsSearchableAttributes, >, searchable_attributes, @@ -279,7 +280,7 @@ make_setting_route!( "/stop-words", put, std::collections::BTreeSet, - meilisearch_types::error::DeserrJsonError< + meilisearch_types::deserr::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsStopWords, >, stop_words, @@ -304,7 +305,7 @@ make_setting_route!( "/synonyms", put, std::collections::BTreeMap>, - meilisearch_types::error::DeserrJsonError< + meilisearch_types::deserr::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsSynonyms, >, synonyms, @@ -329,7 +330,7 @@ make_setting_route!( "/distinct-attribute", put, String, - meilisearch_types::error::DeserrJsonError< + meilisearch_types::deserr::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsDistinctAttribute, >, distinct_attribute, @@ -353,7 +354,7 @@ make_setting_route!( "/ranking-rules", put, Vec, - meilisearch_types::error::DeserrJsonError< + meilisearch_types::deserr::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsRankingRules, >, ranking_rules, @@ -384,7 +385,7 @@ make_setting_route!( "/faceting", patch, meilisearch_types::settings::FacetingSettings, - meilisearch_types::error::DeserrJsonError< + meilisearch_types::deserr::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsFaceting, >, faceting, @@ -409,7 +410,7 @@ make_setting_route!( "/pagination", patch, meilisearch_types::settings::PaginationSettings, - meilisearch_types::error::DeserrJsonError< + meilisearch_types::deserr::DeserrJsonError< meilisearch_types::error::deserr_codes::InvalidSettingsPagination, >, pagination, diff --git a/meilisearch/src/routes/mod.rs b/meilisearch/src/routes/mod.rs index e681910a2..52ad92c23 100644 --- a/meilisearch/src/routes/mod.rs +++ b/meilisearch/src/routes/mod.rs @@ -41,7 +41,7 @@ where Ok(Some(input.parse()?)) } -const PAGINATION_DEFAULT_LIMIT: fn() -> usize = || 20; +const PAGINATION_DEFAULT_LIMIT: usize = 20; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] diff --git a/meilisearch/src/routes/swap_indexes.rs b/meilisearch/src/routes/swap_indexes.rs index 5d6d1e1e5..9adbfecdd 100644 --- a/meilisearch/src/routes/swap_indexes.rs +++ b/meilisearch/src/routes/swap_indexes.rs @@ -2,8 +2,9 @@ use actix_web::web::Data; use actix_web::{web, HttpRequest, HttpResponse}; use deserr::DeserializeFromValue; use index_scheduler::IndexScheduler; +use meilisearch_types::deserr::DeserrJsonError; use meilisearch_types::error::deserr_codes::InvalidSwapIndexes; -use meilisearch_types::error::{DeserrJsonError, ResponseError}; +use meilisearch_types::error::ResponseError; use meilisearch_types::tasks::{IndexSwap, KindWithContent}; use serde_json::json; @@ -22,7 +23,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) { #[derive(DeserializeFromValue, Debug, Clone, PartialEq, Eq)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] pub struct SwapIndexesPayload { - #[deserr(error = DeserrJsonError, missing_field_error = DeserrJsonError::missing_swap_indexes_indexes)] + #[deserr(error = DeserrJsonError, missing_field_error = DeserrJsonError::missing_swap_indexes)] indexes: Vec, } diff --git a/meilisearch/src/routes/tasks.rs b/meilisearch/src/routes/tasks.rs index d9c498e4e..060c86910 100644 --- a/meilisearch/src/routes/tasks.rs +++ b/meilisearch/src/routes/tasks.rs @@ -2,21 +2,17 @@ use actix_web::web::Data; use actix_web::{web, HttpRequest, HttpResponse}; use deserr::DeserializeFromValue; use index_scheduler::{IndexScheduler, Query, TaskId}; -use meilisearch_types::error::{ - deserr_codes::*, parse_option_cs_star_or, parse_option_u32_query_param, - parse_option_vec_u32_query_param, DeserrQueryParamError, DetailedParseIntError, - TakeErrorMessage, -}; -use meilisearch_types::error::{parse_u32_query_param, ResponseError}; +use meilisearch_types::deserr::query_params::Param; +use meilisearch_types::deserr::DeserrQueryParamError; +use meilisearch_types::error::ResponseError; +use meilisearch_types::error::{deserr_codes::*, InvalidTaskDateError}; use meilisearch_types::index_uid::IndexUid; -use meilisearch_types::serde_cs; use meilisearch_types::settings::{Settings, Unchecked}; -use meilisearch_types::star_or::StarOr; +use meilisearch_types::star_or::{OptionStarOr, OptionStarOrList}; use meilisearch_types::tasks::{ serialize_duration, Details, IndexSwap, Kind, KindWithContent, Status, Task, }; -use serde::{Deserialize, Serialize}; -use serde_cs::vec::CS; +use serde::Serialize; use serde_json::json; use time::format_description::well_known::Rfc3339; use time::macros::format_description; @@ -30,7 +26,7 @@ use crate::extractors::authentication::GuardedData; use crate::extractors::query_parameters::QueryParameter; use crate::extractors::sequential_extractor::SeqHandler; -const DEFAULT_LIMIT: fn() -> u32 = || 20; +const DEFAULT_LIMIT: u32 = 20; pub fn configure(cfg: &mut web::ServiceConfig) { cfg.service( @@ -169,62 +165,121 @@ impl From
for DetailsView { #[derive(Debug, DeserializeFromValue)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] pub struct TasksFilterQuery { - #[deserr(default = DEFAULT_LIMIT(), error = DeserrQueryParamError, from(String) = parse_u32_query_param -> TakeErrorMessage)] - pub limit: u32, - #[deserr(default, error = DeserrQueryParamError, from(Option) = parse_option_u32_query_param -> TakeErrorMessage)] - pub from: Option, + #[deserr(default = Param(DEFAULT_LIMIT), error = DeserrQueryParamError)] + pub limit: Param, + #[deserr(default, error = DeserrQueryParamError)] + pub from: Option>, - #[deserr(default, error = DeserrQueryParamError, from(Option>) = parse_option_vec_u32_query_param -> TakeErrorMessage)] - pub uids: Option>, - #[deserr(default, error = DeserrQueryParamError, from(Option>) = parse_option_vec_u32_query_param -> TakeErrorMessage)] - pub canceled_by: Option>, - #[deserr(default, error = DeserrQueryParamError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] - pub types: Option>, - #[deserr(default, error = DeserrQueryParamError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] - pub statuses: Option>, - #[deserr(default, error = DeserrQueryParamError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] - pub index_uids: Option>, + #[deserr(default, error = DeserrQueryParamError)] + pub uids: OptionStarOrList, + #[deserr(default, error = DeserrQueryParamError)] + pub canceled_by: OptionStarOrList, + #[deserr(default, error = DeserrQueryParamError)] + pub types: OptionStarOrList, + #[deserr(default, error = DeserrQueryParamError)] + pub statuses: OptionStarOrList, + #[deserr(default, error = DeserrQueryParamError)] + pub index_uids: OptionStarOrList, - #[deserr(default, error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] - pub after_enqueued_at: Option, - #[deserr(default, error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] - pub before_enqueued_at: Option, - #[deserr(default, error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] - pub after_started_at: Option, - #[deserr(default, error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] - pub before_started_at: Option, - #[deserr(default, error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] - pub after_finished_at: Option, - #[deserr(default, error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] - pub before_finished_at: Option, + #[deserr(default, error = DeserrQueryParamError, from(OptionStarOr) = deserialize_date_after -> InvalidTaskDateError)] + pub after_enqueued_at: OptionStarOr, + #[deserr(default, error = DeserrQueryParamError, from(OptionStarOr) = deserialize_date_before -> InvalidTaskDateError)] + pub before_enqueued_at: OptionStarOr, + #[deserr(default, error = DeserrQueryParamError, from(OptionStarOr) = deserialize_date_after -> InvalidTaskDateError)] + pub after_started_at: OptionStarOr, + #[deserr(default, error = DeserrQueryParamError, from(OptionStarOr) = deserialize_date_before -> InvalidTaskDateError)] + pub before_started_at: OptionStarOr, + #[deserr(default, error = DeserrQueryParamError, from(OptionStarOr) = deserialize_date_after -> InvalidTaskDateError)] + pub after_finished_at: OptionStarOr, + #[deserr(default, error = DeserrQueryParamError, from(OptionStarOr) = deserialize_date_before -> InvalidTaskDateError)] + pub before_finished_at: OptionStarOr, +} +impl TasksFilterQuery { + fn into_query(self) -> Query { + Query { + limit: Some(self.limit.0), + from: self.from.as_deref().copied(), + statuses: self.statuses.merge_star_and_none(), + types: self.types.merge_star_and_none(), + index_uids: self.index_uids.map(|x| x.to_string()).merge_star_and_none(), + uids: self.uids.merge_star_and_none(), + canceled_by: self.canceled_by.merge_star_and_none(), + before_enqueued_at: self.before_enqueued_at.merge_star_and_none(), + after_enqueued_at: self.after_enqueued_at.merge_star_and_none(), + before_started_at: self.before_started_at.merge_star_and_none(), + after_started_at: self.after_started_at.merge_star_and_none(), + before_finished_at: self.before_finished_at.merge_star_and_none(), + after_finished_at: self.after_finished_at.merge_star_and_none(), + } + } } -#[derive(Deserialize, Debug, DeserializeFromValue)] +impl TaskDeletionOrCancelationQuery { + fn is_empty(&self) -> bool { + matches!( + self, + TaskDeletionOrCancelationQuery { + uids: OptionStarOrList::None, + canceled_by: OptionStarOrList::None, + types: OptionStarOrList::None, + statuses: OptionStarOrList::None, + index_uids: OptionStarOrList::None, + after_enqueued_at: OptionStarOr::None, + before_enqueued_at: OptionStarOr::None, + after_started_at: OptionStarOr::None, + before_started_at: OptionStarOr::None, + after_finished_at: OptionStarOr::None, + before_finished_at: OptionStarOr::None + } + ) + } +} + +#[derive(Debug, DeserializeFromValue)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] pub struct TaskDeletionOrCancelationQuery { - #[deserr(default, error = DeserrQueryParamError, from(Option>) = parse_option_vec_u32_query_param -> TakeErrorMessage)] - pub uids: Option>, - #[deserr(default, error = DeserrQueryParamError, from(Option>) = parse_option_vec_u32_query_param -> TakeErrorMessage)] - pub canceled_by: Option>, - #[deserr(default, error = DeserrQueryParamError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] - pub types: Option>, - #[deserr(default, error = DeserrQueryParamError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] - pub statuses: Option>, - #[deserr(default, error = DeserrQueryParamError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] - pub index_uids: Option>, + #[deserr(default, error = DeserrQueryParamError)] + pub uids: OptionStarOrList, + #[deserr(default, error = DeserrQueryParamError)] + pub canceled_by: OptionStarOrList, + #[deserr(default, error = DeserrQueryParamError)] + pub types: OptionStarOrList, + #[deserr(default, error = DeserrQueryParamError)] + pub statuses: OptionStarOrList, + #[deserr(default, error = DeserrQueryParamError)] + pub index_uids: OptionStarOrList, - #[deserr(default, error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] - pub after_enqueued_at: Option, - #[deserr(default, error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] - pub before_enqueued_at: Option, - #[deserr(default, error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] - pub after_started_at: Option, - #[deserr(default, error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] - pub before_started_at: Option, - #[deserr(default, error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] - pub after_finished_at: Option, - #[deserr(default, error = DeserrQueryParamError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] - pub before_finished_at: Option, + #[deserr(default, error = DeserrQueryParamError, from(OptionStarOr) = deserialize_date_after -> InvalidTaskDateError)] + pub after_enqueued_at: OptionStarOr, + #[deserr(default, error = DeserrQueryParamError, from(OptionStarOr) = deserialize_date_before -> InvalidTaskDateError)] + pub before_enqueued_at: OptionStarOr, + #[deserr(default, error = DeserrQueryParamError, from(OptionStarOr) = deserialize_date_after -> InvalidTaskDateError)] + pub after_started_at: OptionStarOr, + #[deserr(default, error = DeserrQueryParamError, from(OptionStarOr) = deserialize_date_before -> InvalidTaskDateError)] + pub before_started_at: OptionStarOr, + #[deserr(default, error = DeserrQueryParamError, from(OptionStarOr) = deserialize_date_after -> InvalidTaskDateError)] + pub after_finished_at: OptionStarOr, + #[deserr(default, error = DeserrQueryParamError, from(OptionStarOr) = deserialize_date_before -> InvalidTaskDateError)] + pub before_finished_at: OptionStarOr, +} +impl TaskDeletionOrCancelationQuery { + fn into_query(self) -> Query { + Query { + limit: None, + from: None, + statuses: self.statuses.merge_star_and_none(), + types: self.types.merge_star_and_none(), + index_uids: self.index_uids.map(|x| x.to_string()).merge_star_and_none(), + uids: self.uids.merge_star_and_none(), + canceled_by: self.canceled_by.merge_star_and_none(), + before_enqueued_at: self.before_enqueued_at.merge_star_and_none(), + after_enqueued_at: self.after_enqueued_at.merge_star_and_none(), + before_started_at: self.before_started_at.merge_star_and_none(), + after_started_at: self.after_started_at.merge_star_and_none(), + before_finished_at: self.before_finished_at.merge_star_and_none(), + after_finished_at: self.after_finished_at.merge_star_and_none(), + } + } } async fn cancel_tasks( @@ -233,57 +288,31 @@ async fn cancel_tasks( req: HttpRequest, analytics: web::Data, ) -> Result { - let TaskDeletionOrCancelationQuery { - types, - uids, - canceled_by, - statuses, - index_uids, - after_enqueued_at, - before_enqueued_at, - after_started_at, - before_started_at, - after_finished_at, - before_finished_at, - } = params.into_inner(); + let params = params.into_inner(); + + if params.is_empty() { + return Err(index_scheduler::Error::TaskCancelationWithEmptyQuery.into()); + } analytics.publish( "Tasks Canceled".to_string(), json!({ - "filtered_by_uid": uids.is_some(), - "filtered_by_index_uid": index_uids.is_some(), - "filtered_by_type": types.is_some(), - "filtered_by_status": statuses.is_some(), - "filtered_by_canceled_by": canceled_by.is_some(), - "filtered_by_before_enqueued_at": before_enqueued_at.is_some(), - "filtered_by_after_enqueued_at": after_enqueued_at.is_some(), - "filtered_by_before_started_at": before_started_at.is_some(), - "filtered_by_after_started_at": after_started_at.is_some(), - "filtered_by_before_finished_at": before_finished_at.is_some(), - "filtered_by_after_finished_at": after_finished_at.is_some(), + "filtered_by_uid": params.uids.is_some(), + "filtered_by_index_uid": params.index_uids.is_some(), + "filtered_by_type": params.types.is_some(), + "filtered_by_status": params.statuses.is_some(), + "filtered_by_canceled_by": params.canceled_by.is_some(), + "filtered_by_before_enqueued_at": params.before_enqueued_at.is_some(), + "filtered_by_after_enqueued_at": params.after_enqueued_at.is_some(), + "filtered_by_before_started_at": params.before_started_at.is_some(), + "filtered_by_after_started_at": params.after_started_at.is_some(), + "filtered_by_before_finished_at": params.before_finished_at.is_some(), + "filtered_by_after_finished_at": params.after_finished_at.is_some(), }), Some(&req), ); - let query = Query { - limit: None, - from: None, - statuses, - types, - index_uids: index_uids.map(|xs| xs.into_iter().map(|s| s.to_string()).collect()), - uids, - canceled_by, - before_enqueued_at, - after_enqueued_at, - before_started_at, - after_started_at, - before_finished_at, - after_finished_at, - }; - - if query.is_empty() { - return Err(index_scheduler::Error::TaskCancelationWithEmptyQuery.into()); - } + let query = params.into_query(); let tasks = index_scheduler.get_task_ids_from_authorized_indexes( &index_scheduler.read_txn()?, @@ -305,58 +334,30 @@ async fn delete_tasks( req: HttpRequest, analytics: web::Data, ) -> Result { - let TaskDeletionOrCancelationQuery { - types, - uids, - canceled_by, - statuses, - index_uids, + let params = params.into_inner(); - after_enqueued_at, - before_enqueued_at, - after_started_at, - before_started_at, - after_finished_at, - before_finished_at, - } = params.into_inner(); + if params.is_empty() { + return Err(index_scheduler::Error::TaskDeletionWithEmptyQuery.into()); + } analytics.publish( "Tasks Deleted".to_string(), json!({ - "filtered_by_uid": uids.is_some(), - "filtered_by_index_uid": index_uids.is_some(), - "filtered_by_type": types.is_some(), - "filtered_by_status": statuses.is_some(), - "filtered_by_canceled_by": canceled_by.is_some(), - "filtered_by_before_enqueued_at": before_enqueued_at.is_some(), - "filtered_by_after_enqueued_at": after_enqueued_at.is_some(), - "filtered_by_before_started_at": before_started_at.is_some(), - "filtered_by_after_started_at": after_started_at.is_some(), - "filtered_by_before_finished_at": before_finished_at.is_some(), - "filtered_by_after_finished_at": after_finished_at.is_some(), + "filtered_by_uid": params.uids.is_some(), + "filtered_by_index_uid": params.index_uids.is_some(), + "filtered_by_type": params.types.is_some(), + "filtered_by_status": params.statuses.is_some(), + "filtered_by_canceled_by": params.canceled_by.is_some(), + "filtered_by_before_enqueued_at": params.before_enqueued_at.is_some(), + "filtered_by_after_enqueued_at": params.after_enqueued_at.is_some(), + "filtered_by_before_started_at": params.before_started_at.is_some(), + "filtered_by_after_started_at": params.after_started_at.is_some(), + "filtered_by_before_finished_at": params.before_finished_at.is_some(), + "filtered_by_after_finished_at": params.after_finished_at.is_some(), }), Some(&req), ); - - let query = Query { - limit: None, - from: None, - statuses, - types, - index_uids: index_uids.map(|xs| xs.into_iter().map(|s| s.to_string()).collect()), - uids, - canceled_by, - after_enqueued_at, - before_enqueued_at, - after_started_at, - before_started_at, - after_finished_at, - before_finished_at, - }; - - if query.is_empty() { - return Err(index_scheduler::Error::TaskDeletionWithEmptyQuery.into()); - } + let query = params.into_query(); let tasks = index_scheduler.get_task_ids_from_authorized_indexes( &index_scheduler.read_txn()?, @@ -386,43 +387,13 @@ async fn get_tasks( req: HttpRequest, analytics: web::Data, ) -> Result { - let params = params.into_inner(); + let mut params = params.into_inner(); analytics.get_tasks(¶ms, &req); - let TasksFilterQuery { - types, - uids, - canceled_by, - statuses, - index_uids, - limit, - from, - after_enqueued_at, - before_enqueued_at, - after_started_at, - before_started_at, - after_finished_at, - before_finished_at, - } = params; - // We +1 just to know if there is more after this "page" or not. - let limit = limit.saturating_add(1); - - let query = index_scheduler::Query { - limit: Some(limit), - from, - statuses, - types, - index_uids: index_uids.map(|xs| xs.into_iter().map(|s| s.to_string()).collect()), - uids, - canceled_by, - before_enqueued_at, - after_enqueued_at, - before_started_at, - after_started_at, - before_finished_at, - after_finished_at, - }; + params.limit.0 = params.limit.0.saturating_add(1); + let limit = params.limit.0; + let query = params.into_query(); let mut tasks_results: Vec = index_scheduler .get_tasks_from_authorized_indexes( @@ -488,7 +459,7 @@ pub enum DeserializeDateOption { pub fn deserialize_date( value: &str, option: DeserializeDateOption, -) -> std::result::Result> { +) -> std::result::Result { // We can't parse using time's rfc3339 format, since then we won't know what part of the // datetime was not explicitly specified, and thus we won't be able to increment it to the // next step. @@ -510,45 +481,26 @@ pub fn deserialize_date( } } } else { - Err(TakeErrorMessage(InvalidTaskDateError(value.to_owned()))) + Err(InvalidTaskDateError(value.to_owned())) } } -pub fn deserialize_date_before( - value: Option, -) -> std::result::Result, TakeErrorMessage> { - if let Some(value) = value { - let date = deserialize_date(&value, DeserializeDateOption::Before)?; - Ok(Some(date)) - } else { - Ok(None) - } -} pub fn deserialize_date_after( - value: Option, -) -> std::result::Result, TakeErrorMessage> { - if let Some(value) = value { - let date = deserialize_date(&value, DeserializeDateOption::After)?; - Ok(Some(date)) - } else { - Ok(None) - } + value: OptionStarOr, +) -> std::result::Result, InvalidTaskDateError> { + value.try_map(|x| deserialize_date(&x, DeserializeDateOption::After)) } - -#[derive(Debug)] -pub struct InvalidTaskDateError(String); -impl std::fmt::Display for InvalidTaskDateError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "`{}` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", self.0) - } +pub fn deserialize_date_before( + value: OptionStarOr, +) -> std::result::Result, InvalidTaskDateError> { + value.try_map(|x| deserialize_date(&x, DeserializeDateOption::Before)) } -impl std::error::Error for InvalidTaskDateError {} #[cfg(test)] mod tests { use deserr::DeserializeFromValue; use meili_snap::snapshot; - use meilisearch_types::error::DeserrQueryParamError; + use meilisearch_types::deserr::DeserrQueryParamError; use crate::extractors::query_parameters::QueryParameter; use crate::routes::tasks::{TaskDeletionOrCancelationQuery, TasksFilterQuery}; @@ -566,65 +518,71 @@ mod tests { let params = "afterEnqueuedAt=2021-12-03&beforeEnqueuedAt=2021-12-03&afterStartedAt=2021-12-03&beforeStartedAt=2021-12-03&afterFinishedAt=2021-12-03&beforeFinishedAt=2021-12-03"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.after_enqueued_at.unwrap()), @"2021-12-04 0:00:00.0 +00:00:00"); - snapshot!(format!("{:?}", query.before_enqueued_at.unwrap()), @"2021-12-03 0:00:00.0 +00:00:00"); - snapshot!(format!("{:?}", query.after_started_at.unwrap()), @"2021-12-04 0:00:00.0 +00:00:00"); - snapshot!(format!("{:?}", query.before_started_at.unwrap()), @"2021-12-03 0:00:00.0 +00:00:00"); - snapshot!(format!("{:?}", query.after_finished_at.unwrap()), @"2021-12-04 0:00:00.0 +00:00:00"); - snapshot!(format!("{:?}", query.before_finished_at.unwrap()), @"2021-12-03 0:00:00.0 +00:00:00"); + snapshot!(format!("{:?}", query.after_enqueued_at), @"Other(2021-12-04 0:00:00.0 +00:00:00)"); + snapshot!(format!("{:?}", query.before_enqueued_at), @"Other(2021-12-03 0:00:00.0 +00:00:00)"); + snapshot!(format!("{:?}", query.after_started_at), @"Other(2021-12-04 0:00:00.0 +00:00:00)"); + snapshot!(format!("{:?}", query.before_started_at), @"Other(2021-12-03 0:00:00.0 +00:00:00)"); + snapshot!(format!("{:?}", query.after_finished_at), @"Other(2021-12-04 0:00:00.0 +00:00:00)"); + snapshot!(format!("{:?}", query.before_finished_at), @"Other(2021-12-03 0:00:00.0 +00:00:00)"); } { let params = "afterEnqueuedAt=2021-12-03T23:45:23Z&beforeEnqueuedAt=2021-12-03T23:45:23Z"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.after_enqueued_at.unwrap()), @"2021-12-03 23:45:23.0 +00:00:00"); - snapshot!(format!("{:?}", query.before_enqueued_at.unwrap()), @"2021-12-03 23:45:23.0 +00:00:00"); + snapshot!(format!("{:?}", query.after_enqueued_at), @"Other(2021-12-03 23:45:23.0 +00:00:00)"); + snapshot!(format!("{:?}", query.before_enqueued_at), @"Other(2021-12-03 23:45:23.0 +00:00:00)"); } { let params = "afterEnqueuedAt=1997-11-12T09:55:06-06:20"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.after_enqueued_at.unwrap()), @"1997-11-12 9:55:06.0 -06:20:00"); + snapshot!(format!("{:?}", query.after_enqueued_at), @"Other(1997-11-12 9:55:06.0 -06:20:00)"); } { let params = "afterEnqueuedAt=1997-11-12T09:55:06%2B00:00"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.after_enqueued_at.unwrap()), @"1997-11-12 9:55:06.0 +00:00:00"); + snapshot!(format!("{:?}", query.after_enqueued_at), @"Other(1997-11-12 9:55:06.0 +00:00:00)"); } { let params = "afterEnqueuedAt=1997-11-12T09:55:06.200000300Z"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.after_enqueued_at.unwrap()), @"1997-11-12 9:55:06.2000003 +00:00:00"); + snapshot!(format!("{:?}", query.after_enqueued_at), @"Other(1997-11-12 9:55:06.2000003 +00:00:00)"); + } + { + // Stars are allowed in date fields as well + let params = "afterEnqueuedAt=*&beforeStartedAt=*&afterFinishedAt=*&beforeFinishedAt=*&afterStartedAt=*&beforeEnqueuedAt=*"; + let query = deserr_query_params::(params).unwrap(); + snapshot!(format!("{:?}", query), @"TaskDeletionOrCancelationQuery { uids: None, canceled_by: None, types: None, statuses: None, index_uids: None, after_enqueued_at: Star, before_enqueued_at: Star, after_started_at: Star, before_started_at: Star, after_finished_at: Star, before_finished_at: Star }"); } { let params = "afterFinishedAt=2021"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"Invalid value in parameter `afterFinishedAt`: `2021` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format."); + snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `afterFinishedAt`: `2021` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", error_code: "invalid_task_after_finished_at", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-task-after-finished-at" }"###); } { let params = "beforeFinishedAt=2021"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"Invalid value in parameter `beforeFinishedAt`: `2021` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format."); + snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `beforeFinishedAt`: `2021` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", error_code: "invalid_task_before_finished_at", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-task-before-finished-at" }"###); } { let params = "afterEnqueuedAt=2021-12"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"Invalid value in parameter `afterEnqueuedAt`: `2021-12` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format."); + snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `afterEnqueuedAt`: `2021-12` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", error_code: "invalid_task_after_enqueued_at", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-task-after-enqueued-at" }"###); } { let params = "beforeEnqueuedAt=2021-12-03T23"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"Invalid value in parameter `beforeEnqueuedAt`: `2021-12-03T23` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format."); + snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `beforeEnqueuedAt`: `2021-12-03T23` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", error_code: "invalid_task_before_enqueued_at", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-task-before-enqueued-at" }"###); } { let params = "afterStartedAt=2021-12-03T23:45"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"Invalid value in parameter `afterStartedAt`: `2021-12-03T23:45` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format."); + snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `afterStartedAt`: `2021-12-03T23:45` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", error_code: "invalid_task_after_started_at", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-task-after-started-at" }"###); } { let params = "beforeStartedAt=2021-12-03T23:45"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"Invalid value in parameter `beforeStartedAt`: `2021-12-03T23:45` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format."); + snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `beforeStartedAt`: `2021-12-03T23:45` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", error_code: "invalid_task_before_started_at", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-task-before-started-at" }"###); } } @@ -633,22 +591,27 @@ mod tests { { let params = "uids=78,1,12,73"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.uids.unwrap()), @"[78, 1, 12, 73]"); + snapshot!(format!("{:?}", query.uids), @"List([78, 1, 12, 73])"); } { let params = "uids=1"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.uids.unwrap()), @"[1]"); + snapshot!(format!("{:?}", query.uids), @"List([1])"); + } + { + let params = "uids=cat,*,dog"; + let err = deserr_query_params::(params).unwrap_err(); + snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `uids[0]`: could not parse `cat` as a positive integer", error_code: "invalid_task_uids", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-task-uids" }"###); } { let params = "uids=78,hello,world"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"Invalid value in parameter `uids`: could not parse `hello` as a positive integer"); + snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `uids[1]`: could not parse `hello` as a positive integer", error_code: "invalid_task_uids", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-task-uids" }"###); } { let params = "uids=cat"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"Invalid value in parameter `uids`: could not parse `cat` as a positive integer"); + snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `uids`: could not parse `cat` as a positive integer", error_code: "invalid_task_uids", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-task-uids" }"###); } } @@ -657,17 +620,17 @@ mod tests { { let params = "statuses=succeeded,failed,enqueued,processing,canceled"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.statuses.unwrap()), @"[Succeeded, Failed, Enqueued, Processing, Canceled]"); + snapshot!(format!("{:?}", query.statuses), @"List([Succeeded, Failed, Enqueued, Processing, Canceled])"); } { let params = "statuses=enqueued"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.statuses.unwrap()), @"[Enqueued]"); + snapshot!(format!("{:?}", query.statuses), @"List([Enqueued])"); } { let params = "statuses=finished"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"Invalid value in parameter `statuses`: `finished` is not a valid task status. Available statuses are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`."); + snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `statuses`: `finished` is not a valid task status. Available statuses are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`.", error_code: "invalid_task_statuses", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-task-statuses" }"###); } } #[test] @@ -675,17 +638,17 @@ mod tests { { let params = "types=documentAdditionOrUpdate,documentDeletion,settingsUpdate,indexCreation,indexDeletion,indexUpdate,indexSwap,taskCancelation,taskDeletion,dumpCreation,snapshotCreation"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.types.unwrap()), @"[DocumentAdditionOrUpdate, DocumentDeletion, SettingsUpdate, IndexCreation, IndexDeletion, IndexUpdate, IndexSwap, TaskCancelation, TaskDeletion, DumpCreation, SnapshotCreation]"); + snapshot!(format!("{:?}", query.types), @"List([DocumentAdditionOrUpdate, DocumentDeletion, SettingsUpdate, IndexCreation, IndexDeletion, IndexUpdate, IndexSwap, TaskCancelation, TaskDeletion, DumpCreation, SnapshotCreation])"); } { let params = "types=settingsUpdate"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.types.unwrap()), @"[SettingsUpdate]"); + snapshot!(format!("{:?}", query.types), @"List([SettingsUpdate])"); } { let params = "types=createIndex"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"Invalid value in parameter `types`: `createIndex` is not a valid task type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`."); + snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `types`: `createIndex` is not a valid task type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`.", error_code: "invalid_task_types", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-task-types" }"###); } } #[test] @@ -693,22 +656,22 @@ mod tests { { let params = "indexUids=toto,tata-78"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.index_uids.unwrap()), @r###"[IndexUid("toto"), IndexUid("tata-78")]"###); + snapshot!(format!("{:?}", query.index_uids), @r###"List([IndexUid("toto"), IndexUid("tata-78")])"###); } { let params = "indexUids=index_a"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query.index_uids.unwrap()), @r###"[IndexUid("index_a")]"###); + snapshot!(format!("{:?}", query.index_uids), @r###"List([IndexUid("index_a")])"###); } { let params = "indexUids=1,hé"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"Invalid value in parameter `indexUids`: `hé` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_)."); + snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `indexUids[1]`: `hé` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", error_code: "invalid_index_uid", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-index-uid" }"###); } { let params = "indexUids=hé"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"Invalid value in parameter `indexUids`: `hé` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_)."); + snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `indexUids`: `hé` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", error_code: "invalid_index_uid", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-index-uid" }"###); } } @@ -717,38 +680,53 @@ mod tests { { let params = "from=12&limit=15&indexUids=toto,tata-78&statuses=succeeded,enqueued&afterEnqueuedAt=2012-04-23&uids=1,2,3"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query), @r###"TasksFilterQuery { limit: 15, from: Some(12), uids: Some([1, 2, 3]), canceled_by: None, types: None, statuses: Some([Succeeded, Enqueued]), index_uids: Some([IndexUid("toto"), IndexUid("tata-78")]), after_enqueued_at: Some(2012-04-24 0:00:00.0 +00:00:00), before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None }"###); + snapshot!(format!("{:?}", query), @r###"TasksFilterQuery { limit: Param(15), from: Some(Param(12)), uids: List([1, 2, 3]), canceled_by: None, types: None, statuses: List([Succeeded, Enqueued]), index_uids: List([IndexUid("toto"), IndexUid("tata-78")]), after_enqueued_at: Other(2012-04-24 0:00:00.0 +00:00:00), before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None }"###); } { // Stars should translate to `None` in the query // Verify value of the default limit let params = "indexUids=*&statuses=succeeded,*&afterEnqueuedAt=2012-04-23&uids=1,2,3"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query), @"TasksFilterQuery { limit: 20, from: None, uids: Some([1, 2, 3]), canceled_by: None, types: None, statuses: None, index_uids: None, after_enqueued_at: Some(2012-04-24 0:00:00.0 +00:00:00), before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None }"); + snapshot!(format!("{:?}", query), @"TasksFilterQuery { limit: Param(20), from: None, uids: List([1, 2, 3]), canceled_by: None, types: None, statuses: Star, index_uids: Star, after_enqueued_at: Other(2012-04-24 0:00:00.0 +00:00:00), before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None }"); } { // Stars should also translate to `None` in task deletion/cancelation queries let params = "indexUids=*&statuses=succeeded,*&afterEnqueuedAt=2012-04-23&uids=1,2,3"; let query = deserr_query_params::(params).unwrap(); - snapshot!(format!("{:?}", query), @"TaskDeletionOrCancelationQuery { uids: Some([1, 2, 3]), canceled_by: None, types: None, statuses: None, index_uids: None, after_enqueued_at: Some(2012-04-24 0:00:00.0 +00:00:00), before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None }"); + snapshot!(format!("{:?}", query), @"TaskDeletionOrCancelationQuery { uids: List([1, 2, 3]), canceled_by: None, types: None, statuses: Star, index_uids: Star, after_enqueued_at: Other(2012-04-24 0:00:00.0 +00:00:00), before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None }"); } { - // Stars in uids not allowed - let params = "uids=*"; + // Star in from not allowed + let params = "uids=*&from=*"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"Invalid value in parameter `uids`: could not parse `*` as a positive integer"); + snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `from`: could not parse `*` as a positive integer", error_code: "invalid_task_from", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-task-from" }"###); } { // From not allowed in task deletion/cancelation queries let params = "from=12"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"Unknown parameter `from`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`"); + snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Unknown parameter `from`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`", error_code: "bad_request", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#bad-request" }"###); } { // Limit not allowed in task deletion/cancelation queries let params = "limit=12"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err}"), @"Unknown parameter `limit`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`"); + snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Unknown parameter `limit`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`", error_code: "bad_request", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#bad-request" }"###); + } + } + + #[test] + fn deserialize_task_delete_or_cancel_empty() { + { + let params = ""; + let query = deserr_query_params::(params).unwrap(); + assert!(query.is_empty()); + } + { + let params = "statuses=*"; + let query = deserr_query_params::(params).unwrap(); + assert!(!query.is_empty()); + snapshot!(format!("{query:?}"), @"TaskDeletionOrCancelationQuery { uids: None, canceled_by: None, types: None, statuses: Star, index_uids: None, after_enqueued_at: None, before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None }"); } } } diff --git a/meilisearch/src/search.rs b/meilisearch/src/search.rs index bfb0bf160..c199944f1 100644 --- a/meilisearch/src/search.rs +++ b/meilisearch/src/search.rs @@ -5,8 +5,8 @@ use std::time::Instant; use deserr::DeserializeFromValue; use either::Either; +use meilisearch_types::deserr::DeserrJsonError; use meilisearch_types::error::deserr_codes::*; -use meilisearch_types::error::DeserrJsonError; use meilisearch_types::settings::DEFAULT_PAGINATION_MAX_TOTAL_HITS; use meilisearch_types::{milli, Document}; use milli::tokenizer::TokenizerBuilder; @@ -15,7 +15,7 @@ use milli::{ SortError, TermsMatchingStrategy, DEFAULT_VALUES_PER_FACET, }; use regex::Regex; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use serde_json::{json, Value}; use crate::error::MeilisearchHttpError; @@ -74,9 +74,8 @@ impl SearchQuery { } } -#[derive(Deserialize, Debug, Clone, PartialEq, Eq, DeserializeFromValue)] +#[derive(Debug, Clone, PartialEq, Eq, DeserializeFromValue)] #[deserr(rename_all = camelCase)] -#[serde(rename_all = "camelCase")] pub enum MatchingStrategy { /// Remove query words from last to first Last, diff --git a/meilisearch/tests/search/errors.rs b/meilisearch/tests/search/errors.rs index 99f711745..d8a19fff1 100644 --- a/meilisearch/tests/search/errors.rs +++ b/meilisearch/tests/search/errors.rs @@ -295,7 +295,7 @@ async fn search_bad_show_matches_position() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "Invalid value in parameter `showMatchesPosition`: provided string was not `true` or `false`", + "message": "Invalid value in parameter `showMatchesPosition`: could not parse `doggo` as a boolean, expected either `true` or `false`", "code": "invalid_search_show_matches_position", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-search-show-matches-position" diff --git a/meilisearch/tests/tasks/errors.rs b/meilisearch/tests/tasks/errors.rs index fd4c6d489..a15c0eca0 100644 --- a/meilisearch/tests/tasks/errors.rs +++ b/meilisearch/tests/tasks/errors.rs @@ -43,7 +43,7 @@ async fn task_bad_uids() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "Invalid value in parameter `uids`: could not parse `dogo` as a positive integer", + "message": "Invalid value in parameter `uids[1]`: could not parse `dogo` as a positive integer", "code": "invalid_task_uids", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-uids" From 07b90dec08fac0f5c7938d1e6cd7336b2df2030e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Lecrenier?= Date: Tue, 17 Jan 2023 09:40:42 +0100 Subject: [PATCH 5/9] Remove unused proptest dependency --- Cargo.lock | 303 +++++++++-------------------- dump/src/reader/v4/errors.rs | 2 - dump/src/reader/v5/errors.rs | 1 - meilisearch-types/Cargo.toml | 5 - meilisearch-types/src/error.rs | 1 - meilisearch-types/src/index_uid.rs | 5 +- 6 files changed, 90 insertions(+), 227 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4bbe05745..9bbaa0525 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,8 +77,8 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" dependencies = [ - "quote 1.0.23", - "syn 1.0.107", + "quote", + "syn", ] [[package]] @@ -211,9 +211,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa9362663c8643d67b2d5eafba49e4cb2c8a053a29ed00a0bea121f17c76b13" dependencies = [ "actix-router", - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -324,9 +324,9 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -335,9 +335,9 @@ version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "705339e0e4a9690e2908d2b3d049d85682cf19fbd5782494498fbf7003a6a282" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -423,21 +423,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bit-set" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - [[package]] name = "bitflags" version = "1.3.2" @@ -541,9 +526,9 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fe233b960f12f8007e3db2d136e3cb1c291bfd7396e384ee76025fc1a3932b4" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -702,9 +687,9 @@ checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" dependencies = [ "heck", "proc-macro-error", - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -715,9 +700,9 @@ checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" dependencies = [ "heck", "proc-macro-error", - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -744,9 +729,9 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df715824eb382e34b7afb7463b0247bf41538aeba731fba05241ecdb5dc3747" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -961,10 +946,10 @@ checksum = "a784d2ccaf7c98501746bf0be29b2022ba41fd62a2e622af997a03e9f972859f" dependencies = [ "fnv", "ident_case", - "proc-macro2 1.0.49", - "quote 1.0.23", + "proc-macro2", + "quote", "strsim", - "syn 1.0.107", + "syn", ] [[package]] @@ -974,8 +959,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e" dependencies = [ "darling_core", - "quote 1.0.23", - "syn 1.0.107", + "quote", + "syn", ] [[package]] @@ -994,9 +979,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4" dependencies = [ "darling", - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1006,7 +991,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68" dependencies = [ "derive_builder_core", - "syn 1.0.107", + "syn", ] [[package]] @@ -1016,10 +1001,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ "convert_case 0.4.0", - "proc-macro2 1.0.49", - "quote 1.0.23", + "proc-macro2", + "quote", "rustc_version", - "syn 1.0.107", + "syn", ] [[package]] @@ -1047,9 +1032,9 @@ name = "deserr-internal" version = "0.1.4" dependencies = [ "convert_case 0.5.0", - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1239,9 +1224,9 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "828de45d0ca18782232dfb8f3ea9cc428e8ced380eb26a520baaacfc70de39ce" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1304,9 +1289,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35c9bb4a2c13ffb3a93a39902aaf4e7190a1706a4779b6db0449aee433d26c4a" dependencies = [ "darling", - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", "uuid 0.8.2", ] @@ -1432,9 +1417,9 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1510,9 +1495,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9" dependencies = [ "proc-macro-error", - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2254,9 +2239,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a9062912d7952c5588cc474795e0b9ee008e7e6781127945b85413d4b99d81" dependencies = [ "log", - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2276,9 +2261,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f08150cf2bab1fc47c2196f4f41173a27fcd0f684165e5458c0046b53a472e2f" dependencies = [ "once_cell", - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2424,8 +2409,6 @@ dependencies = [ "meili-snap", "memmap2", "milli", - "proptest", - "proptest-derive", "roaring", "serde", "serde-cs", @@ -2815,9 +2798,9 @@ checksum = "46b53634d8c8196302953c74d5352f33d0c512a9499bd2ce468fc9f4128fa27c" dependencies = [ "pest", "pest_meta", - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2915,9 +2898,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", "version_check", ] @@ -2927,20 +2910,11 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", + "proc-macro2", + "quote", "version_check", ] -[[package]] -name = "proc-macro2" -version = "0.4.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" -dependencies = [ - "unicode-xid 0.1.0", -] - [[package]] name = "proc-macro2" version = "1.0.49" @@ -2980,71 +2954,19 @@ dependencies = [ "thiserror", ] -[[package]] -name = "proptest" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0d9cc07f18492d879586c92b485def06bc850da3118075cd45d50e9c95b0e5" -dependencies = [ - "bit-set", - "bitflags", - "byteorder", - "lazy_static", - "num-traits", - "quick-error 2.0.1", - "rand", - "rand_chacha", - "rand_xorshift", - "regex-syntax", - "rusty-fork", - "tempfile", -] - -[[package]] -name = "proptest-derive" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90b46295382dc76166cb7cf2bb4a97952464e4b7ed5a43e6cd34e1fec3349ddc" -dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", - "syn 0.15.44", -] - [[package]] name = "protobuf" version = "2.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - -[[package]] -name = "quote" -version = "0.6.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" -dependencies = [ - "proc-macro2 0.4.30", -] - [[package]] name = "quote" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" dependencies = [ - "proc-macro2 1.0.49", + "proc-macro2", ] [[package]] @@ -3077,15 +2999,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "rand_xorshift" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" -dependencies = [ - "rand_core", -] - [[package]] name = "rayon" version = "1.6.1" @@ -3300,18 +3213,6 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" -[[package]] -name = "rusty-fork" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" -dependencies = [ - "fnv", - "quick-error 1.2.3", - "tempfile", - "wait-timeout", -] - [[package]] name = "ryu" version = "1.0.12" @@ -3387,9 +3288,9 @@ version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -3584,25 +3485,14 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" -[[package]] -name = "syn" -version = "0.15.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" -dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", - "unicode-xid 0.1.0", -] - [[package]] name = "syn" version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", + "proc-macro2", + "quote", "unicode-ident", ] @@ -3621,10 +3511,10 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", - "unicode-xid 0.2.4", + "proc-macro2", + "quote", + "syn", + "unicode-xid", ] [[package]] @@ -3706,9 +3596,9 @@ version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -3779,9 +3669,9 @@ version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -3910,12 +3800,6 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" -[[package]] -name = "unicode-xid" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" - [[package]] name = "unicode-xid" version = "0.2.4" @@ -3998,15 +3882,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "wait-timeout" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" -dependencies = [ - "libc", -] - [[package]] name = "walkdir" version = "2.3.2" @@ -4053,9 +3928,9 @@ dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", "wasm-bindgen-shared", ] @@ -4077,7 +3952,7 @@ version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" dependencies = [ - "quote 1.0.23", + "quote", "wasm-bindgen-macro-support", ] @@ -4087,9 +3962,9 @@ version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ - "proc-macro2 1.0.49", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2", + "quote", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4286,8 +4161,8 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d498dbd1fd7beb83c86709ae1c33ca50942889473473d287d56ce4770a18edfb" dependencies = [ - "proc-macro2 1.0.49", - "syn 1.0.107", + "proc-macro2", + "syn", "synstructure", ] diff --git a/dump/src/reader/v4/errors.rs b/dump/src/reader/v4/errors.rs index 5a9a8d5df..afa640de4 100644 --- a/dump/src/reader/v4/errors.rs +++ b/dump/src/reader/v4/errors.rs @@ -5,10 +5,8 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "test-traits", derive(proptest_derive::Arbitrary))] pub struct ResponseError { #[serde(skip)] - #[cfg_attr(feature = "test-traits", proptest(strategy = "strategy::status_code_strategy()"))] pub code: StatusCode, pub message: String, #[serde(rename = "code")] diff --git a/dump/src/reader/v5/errors.rs b/dump/src/reader/v5/errors.rs index c918c301c..f4067d4c6 100644 --- a/dump/src/reader/v5/errors.rs +++ b/dump/src/reader/v5/errors.rs @@ -5,7 +5,6 @@ use serde::Deserialize; #[derive(Debug, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "test-traits", derive(proptest_derive::Arbitrary))] #[cfg_attr(test, derive(serde::Serialize))] pub struct ResponseError { #[serde(skip)] diff --git a/meilisearch-types/Cargo.toml b/meilisearch-types/Cargo.toml index cba3fc5d9..467454b4e 100644 --- a/meilisearch-types/Cargo.toml +++ b/meilisearch-types/Cargo.toml @@ -17,8 +17,6 @@ flate2 = "1.0.24" fst = "0.4.7" memmap2 = "0.5.7" milli = { path = "/Users/meilisearch/Documents/milli2/milli", default-features = false } -proptest = { version = "1.0.0", optional = true } -proptest-derive = { version = "0.3.0", optional = true } roaring = { version = "0.10.0", features = ["serde"] } serde = { version = "1.0.145", features = ["derive"] } serde-cs = "0.2.4" @@ -33,8 +31,6 @@ uuid = { version = "1.1.2", features = ["serde", "v4"] } [dev-dependencies] insta = "1.19.1" meili-snap = { path = "../meili-snap" } -proptest = "1.0.0" -proptest-derive = "0.3.0" [features] # all specialized tokenizations @@ -48,4 +44,3 @@ hebrew = ["milli/hebrew"] japanese = ["milli/japanese"] # thai specialized tokenization thai = ["milli/thai"] -test-traits = ["proptest", "proptest-derive"] diff --git a/meilisearch-types/src/error.rs b/meilisearch-types/src/error.rs index 2fb55ee31..21c079678 100644 --- a/meilisearch-types/src/error.rs +++ b/meilisearch-types/src/error.rs @@ -9,7 +9,6 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "test-traits", derive(proptest_derive::Arbitrary))] pub struct ResponseError { #[serde(skip)] code: StatusCode, diff --git a/meilisearch-types/src/index_uid.rs b/meilisearch-types/src/index_uid.rs index 30b707665..0fa5ef530 100644 --- a/meilisearch-types/src/index_uid.rs +++ b/meilisearch-types/src/index_uid.rs @@ -9,10 +9,7 @@ use crate::error::{Code, ErrorCode}; /// An index uid is composed of only ascii alphanumeric characters, - and _, between 1 and 400 /// bytes long #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "test-traits", derive(proptest_derive::Arbitrary))] -pub struct IndexUid( - #[cfg_attr(feature = "test-traits", proptest(regex("[a-zA-Z0-9_-]{1,400}")))] String, -); +pub struct IndexUid(String); impl IndexUid { pub fn new_unchecked(s: impl AsRef) -> Self { From b781f9a0f911f471ca06728f401e2439529346c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Lecrenier?= Date: Tue, 17 Jan 2023 11:05:01 +0100 Subject: [PATCH 6/9] cargo fmt --- meilisearch-types/src/keys.rs | 4 ++-- meilisearch-types/src/lib.rs | 5 ++--- meilisearch-types/src/settings.rs | 2 +- meilisearch-types/src/star_or.rs | 10 +++++++--- meilisearch/src/routes/indexes/mod.rs | 4 ++-- meilisearch/src/routes/indexes/search.rs | 2 +- meilisearch/src/routes/tasks.rs | 4 ++-- 7 files changed, 17 insertions(+), 14 deletions(-) diff --git a/meilisearch-types/src/keys.rs b/meilisearch-types/src/keys.rs index ea941b775..655af9b31 100644 --- a/meilisearch-types/src/keys.rs +++ b/meilisearch-types/src/keys.rs @@ -11,8 +11,8 @@ use time::{Date, OffsetDateTime, PrimitiveDateTime}; use uuid::Uuid; use crate::deserr::DeserrJsonError; -use crate::error::{deserr_codes::*, ParseOffsetDateTimeError}; -use crate::error::{unwrap_any, Code}; +use crate::error::deserr_codes::*; +use crate::error::{unwrap_any, Code, ParseOffsetDateTimeError}; use crate::index_uid::IndexUid; use crate::star_or::StarOr; diff --git a/meilisearch-types/src/lib.rs b/meilisearch-types/src/lib.rs index de4084388..14602b5aa 100644 --- a/meilisearch-types/src/lib.rs +++ b/meilisearch-types/src/lib.rs @@ -1,4 +1,5 @@ pub mod compression; +pub mod deserr; pub mod document_formats; pub mod error; pub mod index_uid; @@ -7,12 +8,10 @@ pub mod settings; pub mod star_or; pub mod tasks; pub mod versioning; -pub mod deserr; -pub use milli; pub use milli::{heed, Index}; -pub use serde_cs; use uuid::Uuid; pub use versioning::VERSION_FILE_NAME; +pub use {milli, serde_cs}; pub type Document = serde_json::Map; pub type InstanceUid = Uuid; diff --git a/meilisearch-types/src/settings.rs b/meilisearch-types/src/settings.rs index 8d085d0ff..57899a9d1 100644 --- a/meilisearch-types/src/settings.rs +++ b/meilisearch-types/src/settings.rs @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize, Serializer}; use crate::deserr::DeserrJsonError; use crate::error::deserr_codes::*; -use crate::error::{unwrap_any}; +use crate::error::unwrap_any; /// The maximimum number of results that the engine /// will be able to return in one search call. diff --git a/meilisearch-types/src/star_or.rs b/meilisearch-types/src/star_or.rs index e40884925..135f610c4 100644 --- a/meilisearch-types/src/star_or.rs +++ b/meilisearch-types/src/star_or.rs @@ -1,9 +1,13 @@ -use std::{fmt, marker::PhantomData, str::FromStr}; +use std::fmt; +use std::marker::PhantomData; +use std::str::FromStr; use deserr::{DeserializeError, DeserializeFromValue, MergeWithError, ValueKind}; -use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; +use serde::de::Visitor; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use crate::{deserr::query_params::FromQueryParameter, error::unwrap_any}; +use crate::deserr::query_params::FromQueryParameter; +use crate::error::unwrap_any; /// A type that tries to match either a star (*) or /// any other thing that implements `FromStr`. diff --git a/meilisearch/src/routes/indexes/mod.rs b/meilisearch/src/routes/indexes/mod.rs index 9e76f3be6..d19dc4773 100644 --- a/meilisearch/src/routes/indexes/mod.rs +++ b/meilisearch/src/routes/indexes/mod.rs @@ -7,8 +7,8 @@ use index_scheduler::IndexScheduler; use log::debug; use meilisearch_types::deserr::query_params::Param; use meilisearch_types::deserr::{DeserrJsonError, DeserrQueryParamError}; -use meilisearch_types::error::ResponseError; -use meilisearch_types::error::{deserr_codes::*, unwrap_any, Code}; +use meilisearch_types::error::deserr_codes::*; +use meilisearch_types::error::{unwrap_any, Code, ResponseError}; use meilisearch_types::index_uid::IndexUid; use meilisearch_types::milli::{self, FieldDistribution, Index}; use meilisearch_types::tasks::KindWithContent; diff --git a/meilisearch/src/routes/indexes/search.rs b/meilisearch/src/routes/indexes/search.rs index 7eabfc2ee..6bf5e3dae 100644 --- a/meilisearch/src/routes/indexes/search.rs +++ b/meilisearch/src/routes/indexes/search.rs @@ -3,8 +3,8 @@ use actix_web::{web, HttpRequest, HttpResponse}; use index_scheduler::IndexScheduler; use log::debug; use meilisearch_auth::IndexSearchRules; -use meilisearch_types::deserr::{DeserrQueryParamError, DeserrJsonError}; use meilisearch_types::deserr::query_params::Param; +use meilisearch_types::deserr::{DeserrJsonError, DeserrQueryParamError}; use meilisearch_types::error::deserr_codes::*; use meilisearch_types::error::ResponseError; use meilisearch_types::serde_cs::vec::CS; diff --git a/meilisearch/src/routes/tasks.rs b/meilisearch/src/routes/tasks.rs index 060c86910..17260cad4 100644 --- a/meilisearch/src/routes/tasks.rs +++ b/meilisearch/src/routes/tasks.rs @@ -4,8 +4,8 @@ use deserr::DeserializeFromValue; use index_scheduler::{IndexScheduler, Query, TaskId}; use meilisearch_types::deserr::query_params::Param; use meilisearch_types::deserr::DeserrQueryParamError; -use meilisearch_types::error::ResponseError; -use meilisearch_types::error::{deserr_codes::*, InvalidTaskDateError}; +use meilisearch_types::error::deserr_codes::*; +use meilisearch_types::error::{InvalidTaskDateError, ResponseError}; use meilisearch_types::index_uid::IndexUid; use meilisearch_types::settings::{Settings, Unchecked}; use meilisearch_types::star_or::{OptionStarOr, OptionStarOrList}; From c71a8ea183afa53312c8a91724ae93ff87124667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Lecrenier?= Date: Tue, 17 Jan 2023 13:10:38 +0100 Subject: [PATCH 7/9] Update to latest milli and deserr --- Cargo.lock | 47 ++++++++++++------------------------ meilisearch-types/Cargo.toml | 4 +-- meilisearch-types/src/lib.rs | 3 ++- meilisearch/Cargo.toml | 2 +- 4 files changed, 21 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9bbaa0525..5c41a3406 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1009,39 +1009,20 @@ dependencies = [ [[package]] name = "deserr" -version = "0.1.4" -dependencies = [ - "deserr-internal 0.1.4", - "serde-cs", - "serde_json", -] - -[[package]] -name = "deserr" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86290491a2b5c21a1a5083da8dae831006761258fabd5617309c3eebc5f89468" +checksum = "5d3c6417f0bf7561774690e3d47f9659b0cbc3614c7af7bfda404fda7a2c11d3" dependencies = [ - "deserr-internal 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "deserr-internal", "serde-cs", "serde_json", ] [[package]] name = "deserr-internal" -version = "0.1.4" -dependencies = [ - "convert_case 0.5.0", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "deserr-internal" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7131de1c27581bc376a22166c9f570be91b76cb096be2f6aecf224c27bf7c49a" +checksum = "196415cbd3b782cddecbdd69da18cd9b19e1bb0bdbb649e87b5afd83fa8d322b" dependencies = [ "convert_case 0.5.0", "proc-macro2", @@ -1319,7 +1300,8 @@ dependencies = [ [[package]] name = "filter-parser" -version = "0.38.0" +version = "0.39.1" +source = "git+https://github.com/meilisearch/milli.git?tag=v0.39.1#0c7d1f761e5db6d086f27d3f0f47a97c7f4a5f08" dependencies = [ "nom", "nom_locate", @@ -1337,7 +1319,8 @@ dependencies = [ [[package]] name = "flatten-serde-json" -version = "0.38.0" +version = "0.39.1" +source = "git+https://github.com/meilisearch/milli.git?tag=v0.39.1#0c7d1f761e5db6d086f27d3f0f47a97c7f4a5f08" dependencies = [ "serde_json", ] @@ -1901,7 +1884,8 @@ dependencies = [ [[package]] name = "json-depth-checker" -version = "0.38.0" +version = "0.39.1" +source = "git+https://github.com/meilisearch/milli.git?tag=v0.39.1#0c7d1f761e5db6d086f27d3f0f47a97c7f4a5f08" dependencies = [ "serde_json", ] @@ -2308,7 +2292,7 @@ dependencies = [ "cargo_toml", "clap 4.0.32", "crossbeam-channel", - "deserr 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "deserr", "dump", "either", "env_logger", @@ -2399,7 +2383,7 @@ dependencies = [ "anyhow", "convert_case 0.6.0", "csv", - "deserr 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "deserr", "either", "enum-iterator", "file-store", @@ -2447,7 +2431,8 @@ dependencies = [ [[package]] name = "milli" -version = "0.38.0" +version = "0.39.1" +source = "git+https://github.com/meilisearch/milli.git?tag=v0.39.1#0c7d1f761e5db6d086f27d3f0f47a97c7f4a5f08" dependencies = [ "bimap", "bincode", @@ -2457,7 +2442,7 @@ dependencies = [ "concat-arrays", "crossbeam-channel", "csv", - "deserr 0.1.4", + "deserr", "either", "filter-parser", "flatten-serde-json", diff --git a/meilisearch-types/Cargo.toml b/meilisearch-types/Cargo.toml index 467454b4e..8d7f673d9 100644 --- a/meilisearch-types/Cargo.toml +++ b/meilisearch-types/Cargo.toml @@ -9,14 +9,14 @@ actix-web = { version = "4.2.1", default-features = false } anyhow = "1.0.65" convert_case = "0.6.0" csv = "1.1.6" -deserr = "0.1.4" +deserr = "0.1.5" either = { version = "1.6.1", features = ["serde"] } enum-iterator = "1.1.3" file-store = { path = "../file-store" } flate2 = "1.0.24" fst = "0.4.7" memmap2 = "0.5.7" -milli = { path = "/Users/meilisearch/Documents/milli2/milli", default-features = false } +milli = { git = "https://github.com/meilisearch/milli.git", tag = "v0.39.1", default-features = false } roaring = { version = "0.10.0", features = ["serde"] } serde = { version = "1.0.145", features = ["derive"] } serde-cs = "0.2.4" diff --git a/meilisearch-types/src/lib.rs b/meilisearch-types/src/lib.rs index 14602b5aa..1e2985379 100644 --- a/meilisearch-types/src/lib.rs +++ b/meilisearch-types/src/lib.rs @@ -8,10 +8,11 @@ pub mod settings; pub mod star_or; pub mod tasks; pub mod versioning; +pub use milli; pub use milli::{heed, Index}; +pub use serde_cs; use uuid::Uuid; pub use versioning::VERSION_FILE_NAME; -pub use {milli, serde_cs}; pub type Document = serde_json::Map; pub type InstanceUid = Uuid; diff --git a/meilisearch/Cargo.toml b/meilisearch/Cargo.toml index be852c02e..9a0c9bd0b 100644 --- a/meilisearch/Cargo.toml +++ b/meilisearch/Cargo.toml @@ -19,7 +19,7 @@ byte-unit = { version = "4.0.14", default-features = false, features = ["std", " bytes = "1.2.1" clap = { version = "4.0.9", features = ["derive", "env"] } crossbeam-channel = "0.5.6" -deserr = "0.1.4" +deserr = "0.1.5" dump = { path = "../dump" } either = "1.8.0" env_logger = "0.9.1" From 56e79fa850e8141c01856e91d2f99aadee7ed1fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Lecrenier?= Date: Tue, 17 Jan 2023 13:17:47 +0100 Subject: [PATCH 8/9] Update task snapshot test and clean up details --- meilisearch-types/src/index_uid.rs | 4 +- meilisearch-types/src/lib.rs | 3 +- meilisearch/src/routes/mod.rs | 8 - meilisearch/src/routes/tasks.rs | 156 ++++++++++++++++--- meilisearch/tests/documents/add_documents.rs | 4 +- 5 files changed, 141 insertions(+), 34 deletions(-) diff --git a/meilisearch-types/src/index_uid.rs b/meilisearch-types/src/index_uid.rs index 0fa5ef530..1a5102355 100644 --- a/meilisearch-types/src/index_uid.rs +++ b/meilisearch-types/src/index_uid.rs @@ -2,13 +2,11 @@ use std::error::Error; use std::fmt; use std::str::FromStr; -use serde::{Deserialize, Serialize}; - use crate::error::{Code, ErrorCode}; /// An index uid is composed of only ascii alphanumeric characters, - and _, between 1 and 400 /// bytes long -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct IndexUid(String); impl IndexUid { diff --git a/meilisearch-types/src/lib.rs b/meilisearch-types/src/lib.rs index 1e2985379..14602b5aa 100644 --- a/meilisearch-types/src/lib.rs +++ b/meilisearch-types/src/lib.rs @@ -8,11 +8,10 @@ pub mod settings; pub mod star_or; pub mod tasks; pub mod versioning; -pub use milli; pub use milli::{heed, Index}; -pub use serde_cs; use uuid::Uuid; pub use versioning::VERSION_FILE_NAME; +pub use {milli, serde_cs}; pub type Document = serde_json::Map; pub type InstanceUid = Uuid; diff --git a/meilisearch/src/routes/mod.rs b/meilisearch/src/routes/mod.rs index 52ad92c23..9ef036554 100644 --- a/meilisearch/src/routes/mod.rs +++ b/meilisearch/src/routes/mod.rs @@ -1,5 +1,4 @@ use std::collections::BTreeMap; -use std::str::FromStr; use actix_web::web::Data; use actix_web::{web, HttpRequest, HttpResponse}; @@ -34,13 +33,6 @@ pub fn configure(cfg: &mut web::ServiceConfig) { .service(web::scope("/swap-indexes").configure(swap_indexes::configure)); } -pub fn from_string_to_option(input: &str) -> Result, E> -where - T: FromStr, -{ - Ok(Some(input.parse()?)) -} - const PAGINATION_DEFAULT_LIMIT: usize = 20; #[derive(Debug, Serialize)] diff --git a/meilisearch/src/routes/tasks.rs b/meilisearch/src/routes/tasks.rs index 17260cad4..eb5cadf2d 100644 --- a/meilisearch/src/routes/tasks.rs +++ b/meilisearch/src/routes/tasks.rs @@ -501,15 +501,21 @@ mod tests { use deserr::DeserializeFromValue; use meili_snap::snapshot; use meilisearch_types::deserr::DeserrQueryParamError; + use meilisearch_types::error::{Code, ResponseError}; - use crate::extractors::query_parameters::QueryParameter; use crate::routes::tasks::{TaskDeletionOrCancelationQuery, TasksFilterQuery}; - fn deserr_query_params(j: &str) -> Result + fn deserr_query_params(j: &str) -> Result where T: DeserializeFromValue, { - QueryParameter::::from_query(j).map(|p| p.0) + let value = serde_urlencoded::from_str::(j) + .map_err(|e| ResponseError::from_msg(e.to_string(), Code::BadRequest))?; + + match deserr::deserialize::<_, _, DeserrQueryParamError>(value) { + Ok(data) => Ok(data), + Err(e) => Err(ResponseError::from(e)), + } } #[test] @@ -556,33 +562,75 @@ mod tests { { let params = "afterFinishedAt=2021"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `afterFinishedAt`: `2021` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", error_code: "invalid_task_after_finished_at", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-task-after-finished-at" }"###); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `afterFinishedAt`: `2021` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", + "code": "invalid_task_after_finished_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-after-finished-at" + } + "###); } { let params = "beforeFinishedAt=2021"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `beforeFinishedAt`: `2021` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", error_code: "invalid_task_before_finished_at", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-task-before-finished-at" }"###); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `beforeFinishedAt`: `2021` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", + "code": "invalid_task_before_finished_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-before-finished-at" + } + "###); } { let params = "afterEnqueuedAt=2021-12"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `afterEnqueuedAt`: `2021-12` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", error_code: "invalid_task_after_enqueued_at", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-task-after-enqueued-at" }"###); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `afterEnqueuedAt`: `2021-12` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", + "code": "invalid_task_after_enqueued_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-after-enqueued-at" + } + "###); } { let params = "beforeEnqueuedAt=2021-12-03T23"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `beforeEnqueuedAt`: `2021-12-03T23` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", error_code: "invalid_task_before_enqueued_at", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-task-before-enqueued-at" }"###); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `beforeEnqueuedAt`: `2021-12-03T23` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", + "code": "invalid_task_before_enqueued_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-before-enqueued-at" + } + "###); } { let params = "afterStartedAt=2021-12-03T23:45"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `afterStartedAt`: `2021-12-03T23:45` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", error_code: "invalid_task_after_started_at", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-task-after-started-at" }"###); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `afterStartedAt`: `2021-12-03T23:45` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", + "code": "invalid_task_after_started_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-after-started-at" + } + "###); } { let params = "beforeStartedAt=2021-12-03T23:45"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `beforeStartedAt`: `2021-12-03T23:45` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", error_code: "invalid_task_before_started_at", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-task-before-started-at" }"###); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `beforeStartedAt`: `2021-12-03T23:45` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", + "code": "invalid_task_before_started_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-before-started-at" + } + "###); } } @@ -601,17 +649,38 @@ mod tests { { let params = "uids=cat,*,dog"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `uids[0]`: could not parse `cat` as a positive integer", error_code: "invalid_task_uids", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-task-uids" }"###); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `uids[0]`: could not parse `cat` as a positive integer", + "code": "invalid_task_uids", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-uids" + } + "###); } { let params = "uids=78,hello,world"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `uids[1]`: could not parse `hello` as a positive integer", error_code: "invalid_task_uids", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-task-uids" }"###); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `uids[1]`: could not parse `hello` as a positive integer", + "code": "invalid_task_uids", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-uids" + } + "###); } { let params = "uids=cat"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `uids`: could not parse `cat` as a positive integer", error_code: "invalid_task_uids", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-task-uids" }"###); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `uids`: could not parse `cat` as a positive integer", + "code": "invalid_task_uids", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-uids" + } + "###); } } @@ -630,7 +699,14 @@ mod tests { { let params = "statuses=finished"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `statuses`: `finished` is not a valid task status. Available statuses are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`.", error_code: "invalid_task_statuses", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-task-statuses" }"###); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `statuses`: `finished` is not a valid task status. Available statuses are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`.", + "code": "invalid_task_statuses", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-statuses" + } + "###); } } #[test] @@ -648,7 +724,14 @@ mod tests { { let params = "types=createIndex"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `types`: `createIndex` is not a valid task type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`.", error_code: "invalid_task_types", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-task-types" }"###); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `types`: `createIndex` is not a valid task type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`.", + "code": "invalid_task_types", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-types" + } + "###); } } #[test] @@ -666,12 +749,26 @@ mod tests { { let params = "indexUids=1,hé"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `indexUids[1]`: `hé` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", error_code: "invalid_index_uid", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-index-uid" }"###); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `indexUids[1]`: `hé` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", + "code": "invalid_index_uid", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-index-uid" + } + "###); } { let params = "indexUids=hé"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `indexUids`: `hé` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", error_code: "invalid_index_uid", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-index-uid" }"###); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `indexUids`: `hé` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", + "code": "invalid_index_uid", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-index-uid" + } + "###); } } @@ -699,19 +796,40 @@ mod tests { // Star in from not allowed let params = "uids=*&from=*"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Invalid value in parameter `from`: could not parse `*` as a positive integer", error_code: "invalid_task_from", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-task-from" }"###); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Invalid value in parameter `from`: could not parse `*` as a positive integer", + "code": "invalid_task_from", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-from" + } + "###); } { // From not allowed in task deletion/cancelation queries let params = "from=12"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Unknown parameter `from`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`", error_code: "bad_request", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#bad-request" }"###); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Unknown parameter `from`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`", + "code": "bad_request", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#bad-request" + } + "###); } { // Limit not allowed in task deletion/cancelation queries let params = "limit=12"; let err = deserr_query_params::(params).unwrap_err(); - snapshot!(format!("{err:?}"), @r###"ResponseError { code: 400, message: "Unknown parameter `limit`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`", error_code: "bad_request", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#bad-request" }"###); + snapshot!(meili_snap::json_string!(err), @r###" + { + "message": "Unknown parameter `limit`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`", + "code": "bad_request", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#bad-request" + } + "###); } } diff --git a/meilisearch/tests/documents/add_documents.rs b/meilisearch/tests/documents/add_documents.rs index c27b899c6..4af365a7e 100644 --- a/meilisearch/tests/documents/add_documents.rs +++ b/meilisearch/tests/documents/add_documents.rs @@ -926,7 +926,7 @@ async fn error_primary_key_inference() { "indexedDocuments": 1 }, "error": { - "message": "The primary key inference process failed because the engine did not find any field ending with `id` in its name. Please specify the primary key manually using the `primaryKey` query parameter.", + "message": "The primary key inference failed as the engine did not find any field ending with `id` in its name. Please specify the primary key manually using the `primaryKey` query parameter.", "code": "index_primary_key_no_candidate_found", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#index-primary-key-no-candidate-found" @@ -966,7 +966,7 @@ async fn error_primary_key_inference() { "indexedDocuments": 1 }, "error": { - "message": "The primary key inference process failed because the engine found 3 fields ending with `id` in their name, such as 'id' and 'object_id'. Please specify the primary key manually using the `primaryKey` query parameter.", + "message": "The primary key inference failed as the engine found 3 fields ending with `id` in their names: 'id' and 'object_id'. Please specify the primary key manually using the `primaryKey` query parameter.", "code": "index_primary_key_multiple_candidates_found", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#index-primary-key-multiple-candidates-found" From e2256083376ccca383dc3a8f8403635e363185bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Lecrenier?= Date: Tue, 17 Jan 2023 13:51:07 +0100 Subject: [PATCH 9/9] Use invalid_index_uid error code in more places --- meilisearch-types/src/index_uid.rs | 5 ++- meilisearch/src/error.rs | 4 +- meilisearch/src/routes/indexes/documents.rs | 45 +++++++++++++-------- meilisearch/src/routes/indexes/mod.rs | 22 +++++----- meilisearch/src/routes/indexes/search.rs | 5 +++ meilisearch/src/routes/indexes/settings.rs | 20 +++++++-- meilisearch/src/routes/swap_indexes.rs | 5 ++- meilisearch/tests/index/create_index.rs | 19 ++++----- meilisearch/tests/index/get_index.rs | 21 +++++----- 9 files changed, 91 insertions(+), 55 deletions(-) diff --git a/meilisearch-types/src/index_uid.rs b/meilisearch-types/src/index_uid.rs index 1a5102355..2f3f6e5df 100644 --- a/meilisearch-types/src/index_uid.rs +++ b/meilisearch-types/src/index_uid.rs @@ -2,11 +2,14 @@ use std::error::Error; use std::fmt; use std::str::FromStr; +use deserr::DeserializeFromValue; + use crate::error::{Code, ErrorCode}; /// An index uid is composed of only ascii alphanumeric characters, - and _, between 1 and 400 /// bytes long -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, DeserializeFromValue)] +#[deserr(from(String) = IndexUid::try_from -> IndexUidFormatError)] pub struct IndexUid(String); impl IndexUid { diff --git a/meilisearch/src/error.rs b/meilisearch/src/error.rs index 23a101080..9c77f4d3e 100644 --- a/meilisearch/src/error.rs +++ b/meilisearch/src/error.rs @@ -2,7 +2,7 @@ use actix_web as aweb; use aweb::error::{JsonPayloadError, QueryPayloadError}; use meilisearch_types::document_formats::{DocumentFormatError, PayloadType}; use meilisearch_types::error::{Code, ErrorCode, ResponseError}; -use meilisearch_types::index_uid::IndexUidFormatError; +use meilisearch_types::index_uid::{IndexUid, IndexUidFormatError}; use serde_json::Value; use tokio::task::JoinError; @@ -27,7 +27,7 @@ pub enum MeilisearchHttpError { #[error("Two indexes must be given for each swap. The list `{:?}` contains {} indexes.", .0, .0.len() )] - SwapIndexPayloadWrongLength(Vec), + SwapIndexPayloadWrongLength(Vec), #[error(transparent)] IndexUid(#[from] IndexUidFormatError), #[error(transparent)] diff --git a/meilisearch/src/routes/indexes/documents.rs b/meilisearch/src/routes/indexes/documents.rs index 3316ee10b..2b36ba834 100644 --- a/meilisearch/src/routes/indexes/documents.rs +++ b/meilisearch/src/routes/indexes/documents.rs @@ -89,14 +89,17 @@ pub struct GetDocument { pub async fn get_document( index_scheduler: GuardedData, Data>, - path: web::Path, + document_param: web::Path, params: QueryParameter, ) -> Result { + let DocumentParam { index_uid, document_id } = document_param.into_inner(); + let index_uid = IndexUid::try_from(index_uid)?; + let GetDocument { fields } = params.into_inner(); let attributes_to_retrieve = fields.merge_star_and_none(); - let index = index_scheduler.index(&path.index_uid)?; - let document = retrieve_document(&index, &path.document_id, attributes_to_retrieve)?; + let index = index_scheduler.index(&index_uid)?; + let document = retrieve_document(&index, &document_id, attributes_to_retrieve)?; debug!("returns: {:?}", document); Ok(HttpResponse::Ok().json(document)) } @@ -107,10 +110,15 @@ pub async fn delete_document( req: HttpRequest, analytics: web::Data, ) -> Result { + let DocumentParam { index_uid, document_id } = path.into_inner(); + let index_uid = IndexUid::try_from(index_uid)?; + analytics.delete_documents(DocumentDeletionKind::PerDocumentId, &req); - let DocumentParam { document_id, index_uid } = path.into_inner(); - let task = KindWithContent::DocumentDeletion { index_uid, documents_ids: vec![document_id] }; + let task = KindWithContent::DocumentDeletion { + index_uid: index_uid.to_string(), + documents_ids: vec![document_id], + }; let task: SummarizedTaskView = tokio::task::spawn_blocking(move || index_scheduler.register(task)).await??.into(); debug!("returns: {:?}", task); @@ -133,6 +141,7 @@ pub async fn get_all_documents( index_uid: web::Path, params: QueryParameter, ) -> Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; debug!("called with params: {:?}", params); let BrowseQuery { limit, offset, fields } = params.into_inner(); let attributes_to_retrieve = fields.merge_star_and_none(); @@ -161,6 +170,8 @@ pub async fn add_documents( req: HttpRequest, analytics: web::Data, ) -> Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; + debug!("called with params: {:?}", params); let params = params.into_inner(); @@ -170,7 +181,7 @@ pub async fn add_documents( let task = document_addition( extract_mime_type(&req)?, index_scheduler, - index_uid.into_inner(), + index_uid, params.primary_key, body, IndexDocumentsMethod::ReplaceDocuments, @@ -183,14 +194,15 @@ pub async fn add_documents( pub async fn update_documents( index_scheduler: GuardedData, Data>, - path: web::Path, + index_uid: web::Path, params: QueryParameter, body: Payload, req: HttpRequest, analytics: web::Data, ) -> Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; + debug!("called with params: {:?}", params); - let index_uid = path.into_inner(); analytics.update_documents(¶ms, index_scheduler.index(&index_uid).is_err(), &req); @@ -212,7 +224,7 @@ pub async fn update_documents( async fn document_addition( mime_type: Option, index_scheduler: GuardedData, Data>, - index_uid: String, + index_uid: IndexUid, primary_key: Option, mut body: Payload, method: IndexDocumentsMethod, @@ -233,9 +245,6 @@ async fn document_addition( } }; - // is your indexUid valid? - let index_uid = IndexUid::try_from(index_uid)?.into_inner(); - let (uuid, mut update_file) = index_scheduler.create_update_file()?; let temp_file = match tempfile() { @@ -311,7 +320,7 @@ async fn document_addition( documents_count, primary_key, allow_index_creation, - index_uid, + index_uid: index_uid.to_string(), }; let scheduler = index_scheduler.clone(); @@ -329,12 +338,13 @@ async fn document_addition( pub async fn delete_documents( index_scheduler: GuardedData, Data>, - path: web::Path, + index_uid: web::Path, body: web::Json>, req: HttpRequest, analytics: web::Data, ) -> Result { debug!("called with params: {:?}", body); + let index_uid = IndexUid::try_from(index_uid.into_inner())?; analytics.delete_documents(DocumentDeletionKind::PerBatch, &req); @@ -344,7 +354,7 @@ pub async fn delete_documents( .collect(); let task = - KindWithContent::DocumentDeletion { index_uid: path.into_inner(), documents_ids: ids }; + KindWithContent::DocumentDeletion { index_uid: index_uid.to_string(), documents_ids: ids }; let task: SummarizedTaskView = tokio::task::spawn_blocking(move || index_scheduler.register(task)).await??.into(); @@ -354,13 +364,14 @@ pub async fn delete_documents( pub async fn clear_all_documents( index_scheduler: GuardedData, Data>, - path: web::Path, + index_uid: web::Path, req: HttpRequest, analytics: web::Data, ) -> Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; analytics.delete_documents(DocumentDeletionKind::ClearAll, &req); - let task = KindWithContent::DocumentClear { index_uid: path.into_inner() }; + let task = KindWithContent::DocumentClear { index_uid: index_uid.to_string() }; let task: SummarizedTaskView = tokio::task::spawn_blocking(move || index_scheduler.register(task)).await??.into(); diff --git a/meilisearch/src/routes/indexes/mod.rs b/meilisearch/src/routes/indexes/mod.rs index d19dc4773..d2a842fe3 100644 --- a/meilisearch/src/routes/indexes/mod.rs +++ b/meilisearch/src/routes/indexes/mod.rs @@ -12,7 +12,7 @@ use meilisearch_types::error::{unwrap_any, Code, ResponseError}; use meilisearch_types::index_uid::IndexUid; use meilisearch_types::milli::{self, FieldDistribution, Index}; use meilisearch_types::tasks::KindWithContent; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use serde_json::json; use time::OffsetDateTime; @@ -49,7 +49,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) { ); } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct IndexView { pub uid: String, @@ -108,8 +108,8 @@ pub async fn list_indexes( #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] pub struct IndexCreateRequest { #[deserr(error = DeserrJsonError, missing_field_error = DeserrJsonError::missing_index_uid)] - uid: String, - #[deserr(error = DeserrJsonError)] + uid: IndexUid, + #[deserr(default, error = DeserrJsonError)] primary_key: Option, } @@ -120,7 +120,6 @@ pub async fn create_index( analytics: web::Data, ) -> Result { let IndexCreateRequest { primary_key, uid } = body.into_inner(); - let uid = IndexUid::try_from(uid)?.into_inner(); let allow_index_creation = index_scheduler.filters().search_rules.is_index_authorized(&uid); if allow_index_creation { @@ -130,7 +129,7 @@ pub async fn create_index( Some(&req), ); - let task = KindWithContent::IndexCreation { index_uid: uid, primary_key }; + let task = KindWithContent::IndexCreation { index_uid: uid.to_string(), primary_key }; let task: SummarizedTaskView = tokio::task::spawn_blocking(move || index_scheduler.register(task)).await??.into(); @@ -162,7 +161,7 @@ fn deny_immutable_fields_index( #[derive(DeserializeFromValue, Debug)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_index)] pub struct UpdateIndexRequest { - #[deserr(error = DeserrJsonError)] + #[deserr(default, error = DeserrJsonError)] primary_key: Option, } @@ -170,6 +169,8 @@ pub async fn get_index( index_scheduler: GuardedData, Data>, index_uid: web::Path, ) -> Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; + let index = index_scheduler.index(&index_uid)?; let index_view = IndexView::new(index_uid.into_inner(), &index)?; @@ -180,12 +181,13 @@ pub async fn get_index( pub async fn update_index( index_scheduler: GuardedData, Data>, - path: web::Path, + index_uid: web::Path, body: ValidatedJson, req: HttpRequest, analytics: web::Data, ) -> Result { debug!("called with params: {:?}", body); + let index_uid = IndexUid::try_from(index_uid.into_inner())?; let body = body.into_inner(); analytics.publish( "Index Updated".to_string(), @@ -194,7 +196,7 @@ pub async fn update_index( ); let task = KindWithContent::IndexUpdate { - index_uid: path.into_inner(), + index_uid: index_uid.into_inner(), primary_key: body.primary_key, }; @@ -209,6 +211,7 @@ pub async fn delete_index( index_scheduler: GuardedData, Data>, index_uid: web::Path, ) -> Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; let task = KindWithContent::IndexDeletion { index_uid: index_uid.into_inner() }; let task: SummarizedTaskView = tokio::task::spawn_blocking(move || index_scheduler.register(task)).await??.into(); @@ -222,6 +225,7 @@ pub async fn get_index_stats( req: HttpRequest, analytics: web::Data, ) -> Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; analytics.publish("Stats Seen".to_string(), json!({ "per_index_uid": true }), Some(&req)); let stats = IndexStats::new((*index_scheduler).clone(), index_uid.into_inner())?; diff --git a/meilisearch/src/routes/indexes/search.rs b/meilisearch/src/routes/indexes/search.rs index 6bf5e3dae..545c69ec5 100644 --- a/meilisearch/src/routes/indexes/search.rs +++ b/meilisearch/src/routes/indexes/search.rs @@ -7,6 +7,7 @@ use meilisearch_types::deserr::query_params::Param; use meilisearch_types::deserr::{DeserrJsonError, DeserrQueryParamError}; use meilisearch_types::error::deserr_codes::*; use meilisearch_types::error::ResponseError; +use meilisearch_types::index_uid::IndexUid; use meilisearch_types::serde_cs::vec::CS; use serde_json::Value; @@ -154,6 +155,8 @@ pub async fn search_with_url_query( analytics: web::Data, ) -> Result { debug!("called with params: {:?}", params); + let index_uid = IndexUid::try_from(index_uid.into_inner())?; + let mut query: SearchQuery = params.into_inner().into(); // Tenant token search_rules. @@ -185,6 +188,8 @@ pub async fn search_with_post( req: HttpRequest, analytics: web::Data, ) -> Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; + let mut query = params.into_inner(); debug!("search called with params: {:?}", query); diff --git a/meilisearch/src/routes/indexes/settings.rs b/meilisearch/src/routes/indexes/settings.rs index 91c3473fa..0c864cc73 100644 --- a/meilisearch/src/routes/indexes/settings.rs +++ b/meilisearch/src/routes/indexes/settings.rs @@ -41,12 +41,14 @@ macro_rules! make_setting_route { >, index_uid: web::Path, ) -> Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; + let new_settings = Settings { $attr: Setting::Reset.into(), ..Default::default() }; let allow_index_creation = index_scheduler.filters().allow_index_creation; - let index_uid = IndexUid::try_from(index_uid.into_inner())?.into_inner(); + let task = KindWithContent::SettingsUpdate { - index_uid, + index_uid: index_uid.to_string(), new_settings: Box::new(new_settings), is_deletion: true, allow_index_creation, @@ -70,6 +72,8 @@ macro_rules! make_setting_route { req: HttpRequest, $analytics_var: web::Data, ) -> std::result::Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; + let body = body.into_inner(); $analytics(&body, &req); @@ -83,9 +87,9 @@ macro_rules! make_setting_route { }; let allow_index_creation = index_scheduler.filters().allow_index_creation; - let index_uid = IndexUid::try_from(index_uid.into_inner())?.into_inner(); + let task = KindWithContent::SettingsUpdate { - index_uid, + index_uid: index_uid.to_string(), new_settings: Box::new(new_settings), is_deletion: false, allow_index_creation, @@ -106,6 +110,8 @@ macro_rules! make_setting_route { >, index_uid: actix_web::web::Path, ) -> std::result::Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; + let index = index_scheduler.index(&index_uid)?; let rtxn = index.read_txn()?; let settings = settings(&index, &rtxn)?; @@ -466,6 +472,8 @@ pub async fn update_all( req: HttpRequest, analytics: web::Data, ) -> Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; + let new_settings = body.into_inner(); analytics.publish( @@ -571,6 +579,8 @@ pub async fn get_all( index_scheduler: GuardedData, Data>, index_uid: web::Path, ) -> Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; + let index = index_scheduler.index(&index_uid)?; let rtxn = index.read_txn()?; let new_settings = settings(&index, &rtxn)?; @@ -582,6 +592,8 @@ pub async fn delete_all( index_scheduler: GuardedData, Data>, index_uid: web::Path, ) -> Result { + let index_uid = IndexUid::try_from(index_uid.into_inner())?; + let new_settings = Settings::cleared().into_unchecked(); let allow_index_creation = index_scheduler.filters().allow_index_creation; diff --git a/meilisearch/src/routes/swap_indexes.rs b/meilisearch/src/routes/swap_indexes.rs index 9adbfecdd..4a7802f2e 100644 --- a/meilisearch/src/routes/swap_indexes.rs +++ b/meilisearch/src/routes/swap_indexes.rs @@ -5,6 +5,7 @@ use index_scheduler::IndexScheduler; use meilisearch_types::deserr::DeserrJsonError; use meilisearch_types::error::deserr_codes::InvalidSwapIndexes; use meilisearch_types::error::ResponseError; +use meilisearch_types::index_uid::IndexUid; use meilisearch_types::tasks::{IndexSwap, KindWithContent}; use serde_json::json; @@ -24,7 +25,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) { #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] pub struct SwapIndexesPayload { #[deserr(error = DeserrJsonError, missing_field_error = DeserrJsonError::missing_swap_indexes)] - indexes: Vec, + indexes: Vec, } pub async fn swap_indexes( @@ -55,7 +56,7 @@ pub async fn swap_indexes( if !search_rules.is_index_authorized(lhs) || !search_rules.is_index_authorized(rhs) { return Err(AuthenticationError::InvalidToken.into()); } - swaps.push(IndexSwap { indexes: (lhs.clone(), rhs.clone()) }); + swaps.push(IndexSwap { indexes: (lhs.to_string(), rhs.to_string()) }); } let task = KindWithContent::IndexSwap { swaps }; diff --git a/meilisearch/tests/index/create_index.rs b/meilisearch/tests/index/create_index.rs index 6c5adb5c6..884a0b069 100644 --- a/meilisearch/tests/index/create_index.rs +++ b/meilisearch/tests/index/create_index.rs @@ -1,6 +1,7 @@ use actix_web::http::header::ContentType; use actix_web::test; use http::header::ACCEPT_ENCODING; +use meili_snap::{json_string, snapshot}; use serde_json::{json, Value}; use crate::common::encoder::Encoder; @@ -188,13 +189,13 @@ async fn error_create_with_invalid_index_uid() { let index = server.index("test test#!"); let (response, code) = index.create(None).await; - let expected_response = json!({ - "message": "`test test#!` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", - "code": "invalid_index_uid", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-index-uid" - }); - - assert_eq!(response, expected_response); - assert_eq!(code, 400); + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid value at `.uid`: `test test#!` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", + "code": "invalid_index_uid", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-index-uid" + } + "###); } diff --git a/meilisearch/tests/index/get_index.rs b/meilisearch/tests/index/get_index.rs index 7bd8a0184..6e70484f6 100644 --- a/meilisearch/tests/index/get_index.rs +++ b/meilisearch/tests/index/get_index.rs @@ -1,3 +1,4 @@ +use meili_snap::{json_string, snapshot}; use serde_json::{json, Value}; use crate::common::Server; @@ -182,15 +183,13 @@ async fn get_invalid_index_uid() { let index = server.index("this is not a valid index name"); let (response, code) = index.get().await; - assert_eq!(code, 404); - assert_eq!( - response, - json!( - { - "message": "Index `this is not a valid index name` not found.", - "code": "index_not_found", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#index-not-found" - }) - ); + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`this is not a valid index name` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", + "code": "invalid_index_uid", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-index-uid" + } + "###); }