diff --git a/Cargo.lock b/Cargo.lock index 64012c10e..32dee719b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1310,7 +1310,6 @@ dependencies = [ [[package]] name = "filter-parser" version = "0.38.0" -source = "git+https://github.com/meilisearch/milli.git?tag=v0.38.0#c3f4835e8e102586bd6d5eb1e55c4bba5e92f994" dependencies = [ "nom", "nom_locate", @@ -1329,7 +1328,6 @@ dependencies = [ [[package]] name = "flatten-serde-json" version = "0.38.0" -source = "git+https://github.com/meilisearch/milli.git?tag=v0.38.0#c3f4835e8e102586bd6d5eb1e55c4bba5e92f994" dependencies = [ "serde_json", ] @@ -1894,7 +1892,6 @@ dependencies = [ [[package]] name = "json-depth-checker" version = "0.38.0" -source = "git+https://github.com/meilisearch/milli.git?tag=v0.38.0#c3f4835e8e102586bd6d5eb1e55c4bba5e92f994" dependencies = [ "serde_json", ] @@ -2443,7 +2440,6 @@ dependencies = [ [[package]] name = "milli" version = "0.38.0" -source = "git+https://github.com/meilisearch/milli.git?tag=v0.38.0#c3f4835e8e102586bd6d5eb1e55c4bba5e92f994" dependencies = [ "bimap", "bincode", @@ -2453,6 +2449,7 @@ dependencies = [ "concat-arrays", "crossbeam-channel", "csv", + "deserr", "either", "filter-parser", "flatten-serde-json", diff --git a/dump/src/lib.rs b/dump/src/lib.rs index c4a7cbae0..7a7b9a5b7 100644 --- a/dump/src/lib.rs +++ b/dump/src/lib.rs @@ -249,17 +249,17 @@ pub(crate) mod test { pub fn create_test_settings() -> Settings { let settings = Settings { - displayed_attributes: Setting::Set(vec![S("race"), S("name")]).into(), - searchable_attributes: Setting::Set(vec![S("name"), S("race")]).into(), - filterable_attributes: Setting::Set(btreeset! { S("race"), S("age") }).into(), - sortable_attributes: Setting::Set(btreeset! { S("age") }).into(), - ranking_rules: Setting::NotSet.into(), - stop_words: Setting::NotSet.into(), - synonyms: Setting::NotSet.into(), - distinct_attribute: Setting::NotSet.into(), - typo_tolerance: Setting::NotSet.into(), - faceting: Setting::NotSet.into(), - pagination: Setting::NotSet.into(), + displayed_attributes: Setting::Set(vec![S("race"), S("name")]), + searchable_attributes: Setting::Set(vec![S("name"), S("race")]), + filterable_attributes: Setting::Set(btreeset! { S("race"), S("age") }), + sortable_attributes: Setting::Set(btreeset! { S("age") }), + ranking_rules: Setting::NotSet, + stop_words: Setting::NotSet, + synonyms: Setting::NotSet, + distinct_attribute: Setting::NotSet, + typo_tolerance: Setting::NotSet, + faceting: Setting::NotSet, + pagination: Setting::NotSet, _kind: std::marker::PhantomData, }; settings.check() diff --git a/dump/src/reader/compat/v5_to_v6.rs b/dump/src/reader/compat/v5_to_v6.rs index 5039625ef..e9e1b659b 100644 --- a/dump/src/reader/compat/v5_to_v6.rs +++ b/dump/src/reader/compat/v5_to_v6.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use super::v4_to_v5::{CompatIndexV4ToV5, CompatV4ToV5}; use crate::reader::{v5, v6, Document, UpdateFile}; use crate::Result; @@ -315,7 +317,26 @@ impl From> for v6::Settings { searchable_attributes: settings.searchable_attributes.into(), filterable_attributes: settings.filterable_attributes.into(), sortable_attributes: settings.sortable_attributes.into(), - ranking_rules: settings.ranking_rules.into(), + ranking_rules: { + match settings.ranking_rules { + v5::settings::Setting::Set(ranking_rules) => { + let mut new_ranking_rules = vec![]; + for rule in ranking_rules { + match v6::RankingRuleView::from_str(&rule) { + Ok(new_rule) => { + new_ranking_rules.push(new_rule); + } + Err(_) => { + log::warn!("Error while importing settings. The ranking rule `{rule}` does not exist anymore.") + } + } + } + v6::Setting::Set(new_ranking_rules) + } + v5::settings::Setting::Reset => v6::Setting::Reset, + v5::settings::Setting::NotSet => v6::Setting::NotSet, + } + }, stop_words: settings.stop_words.into(), synonyms: settings.synonyms.into(), distinct_attribute: settings.distinct_attribute.into(), diff --git a/dump/src/reader/v6/mod.rs b/dump/src/reader/v6/mod.rs index b3cc6c7d3..edf552452 100644 --- a/dump/src/reader/v6/mod.rs +++ b/dump/src/reader/v6/mod.rs @@ -26,7 +26,7 @@ pub type Kind = crate::KindDump; pub type Details = meilisearch_types::tasks::Details; // everything related to the settings -pub type Setting = meilisearch_types::settings::Setting; +pub type Setting = meilisearch_types::milli::update::Setting; pub type TypoTolerance = meilisearch_types::settings::TypoSettings; pub type MinWordSizeForTypos = meilisearch_types::settings::MinWordSizeTyposSetting; pub type FacetingSettings = meilisearch_types::settings::FacetingSettings; @@ -40,6 +40,7 @@ pub type IndexUid = meilisearch_types::index_uid::IndexUid; // everything related to the errors pub type ResponseError = meilisearch_types::error::ResponseError; pub type Code = meilisearch_types::error::Code; +pub type RankingRuleView = meilisearch_types::settings::RankingRuleView; pub struct V6Reader { dump: TempDir, diff --git a/meilisearch-auth/src/error.rs b/meilisearch-auth/src/error.rs index 37d3dce60..72cd96470 100644 --- a/meilisearch-auth/src/error.rs +++ b/meilisearch-auth/src/error.rs @@ -1,7 +1,7 @@ use std::error::Error; use meilisearch_types::error::{Code, ErrorCode}; -use meilisearch_types::{internal_error, keys}; +use meilisearch_types::internal_error; pub type Result = std::result::Result; @@ -11,8 +11,6 @@ pub enum AuthControllerError { ApiKeyNotFound(String), #[error("`uid` field value `{0}` is already an existing API key.")] ApiKeyAlreadyExists(String), - #[error(transparent)] - ApiKey(#[from] keys::Error), #[error("Internal error: {0}")] Internal(Box), } @@ -27,7 +25,6 @@ internal_error!( impl ErrorCode for AuthControllerError { fn error_code(&self) -> Code { match self { - Self::ApiKey(e) => e.error_code(), Self::ApiKeyNotFound(_) => Code::ApiKeyNotFound, Self::ApiKeyAlreadyExists(_) => Code::ApiKeyAlreadyExists, Self::Internal(_) => Code::Internal, diff --git a/meilisearch-auth/src/lib.rs b/meilisearch-auth/src/lib.rs index 659447d44..aae71f55f 100644 --- a/meilisearch-auth/src/lib.rs +++ b/meilisearch-auth/src/lib.rs @@ -8,10 +8,9 @@ use std::path::Path; use std::sync::Arc; use error::{AuthControllerError, Result}; -use meilisearch_types::keys::{Action, Key}; +use meilisearch_types::keys::{Action, CreateApiKey, Key, PatchApiKey}; use meilisearch_types::star_or::StarOr; use serde::{Deserialize, Serialize}; -use serde_json::Value; pub use store::open_auth_store_env; use store::{generate_key_as_hexa, HeedAuthStore}; use time::OffsetDateTime; @@ -34,17 +33,19 @@ impl AuthController { Ok(Self { store: Arc::new(store), master_key: master_key.clone() }) } - pub fn create_key(&self, value: Value) -> Result { - let key = Key::create_from_value(value)?; - match self.store.get_api_key(key.uid)? { - Some(_) => Err(AuthControllerError::ApiKeyAlreadyExists(key.uid.to_string())), - None => self.store.put_api_key(key), + pub fn create_key(&self, value: CreateApiKey) -> Result { + let create_key = value; + match self.store.get_api_key(create_key.uid)? { + Some(_) => Err(AuthControllerError::ApiKeyAlreadyExists(create_key.uid.to_string())), + None => self.store.put_api_key(create_key.to_key()), } } - pub fn update_key(&self, uid: Uuid, value: Value) -> Result { + pub fn update_key(&self, uid: Uuid, patch: PatchApiKey) -> Result { let mut key = self.get_key(uid)?; - key.update_from_value(value)?; + key.description = patch.description; + key.name = patch.name; + key.updated_at = OffsetDateTime::now_utc(); self.store.put_api_key(key) } diff --git a/meilisearch-types/Cargo.toml b/meilisearch-types/Cargo.toml index dcf0e78f2..862998df3 100644 --- a/meilisearch-types/Cargo.toml +++ b/meilisearch-types/Cargo.toml @@ -16,7 +16,7 @@ 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.38.0", default-features = false } +milli = { path = "/Users/meilisearch/Documents/milli2/milli" } # git = "https://github.com/meilisearch/milli.git", tag = "v0.38.0", 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 32552df47..436857b90 100644 --- a/meilisearch-types/src/error.rs +++ b/meilisearch-types/src/error.rs @@ -10,6 +10,8 @@ use deserr::{DeserializeError, IntoValue, MergeWithError, ValuePointerRef}; use milli::heed::{Error as HeedError, MdbError}; use serde::{Deserialize, Serialize}; +use self::deserr_codes::MissingIndexUid; + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "test-traits", derive(proptest_derive::Arbitrary))] @@ -410,6 +412,82 @@ mod strategy { } } +pub struct DeserrError { + pub msg: String, + pub code: 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 + } +} + +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 DeserrError { + 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 deserr::DeserializeError for DeserrError { + fn error( + _self_: Option, + error: deserr::ErrorKind, + location: ValuePointerRef, + ) -> Result { + let msg = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0; + + Err(DeserrError { msg, code: C::default().error_code(), _phantom: PhantomData }) + } +} + +pub struct TakeErrorMessage(pub T); + +impl MergeWithError> for DeserrError +where + T: std::error::Error, +{ + fn merge( + _self_: Option, + other: TakeErrorMessage, + merge_location: ValuePointerRef, + ) -> Result { + DeserrError::error::( + None, + deserr::ErrorKind::Unexpected { msg: other.0.to_string() }, + merge_location, + ) + } +} + #[macro_export] macro_rules! internal_error { ($target:ty : $($other:path), *) => { diff --git a/meilisearch-types/src/keys.rs b/meilisearch-types/src/keys.rs index 481678a83..9b11c3317 100644 --- a/meilisearch-types/src/keys.rs +++ b/meilisearch-types/src/keys.rs @@ -1,22 +1,129 @@ +use std::convert::Infallible; +use std::fmt::Display; use std::hash::Hash; -use std::str::FromStr; +use deserr::{DeserializeError, DeserializeFromValue, MergeWithError, ValueKind, ValuePointerRef}; use enum_iterator::Sequence; use serde::{Deserialize, Serialize}; -use serde_json::{from_value, Value}; use time::format_description::well_known::Rfc3339; use time::macros::{format_description, time}; use time::{Date, OffsetDateTime, PrimitiveDateTime}; use uuid::Uuid; -use crate::error::{Code, ErrorCode}; +use crate::error::deserr_codes::*; +use crate::error::{unwrap_any, Code, DeserrError, ErrorCode, TakeErrorMessage}; use crate::index_uid::{IndexUid, IndexUidFormatError}; use crate::star_or::StarOr; -type Result = std::result::Result; - pub type KeyId = Uuid; +impl DeserializeFromValue> for Uuid { + fn deserialize_from_value( + value: deserr::Value, + location: deserr::ValuePointerRef, + ) -> std::result::Result> { + match value { + deserr::Value::String(s) => match Uuid::parse_str(&s) { + Ok(x) => Ok(x), + Err(e) => Err(unwrap_any(DeserrError::::error::( + None, + deserr::ErrorKind::Unexpected { msg: e.to_string() }, + location, + ))), + }, + _ => Err(unwrap_any(DeserrError::::error( + None, + deserr::ErrorKind::IncorrectValueKind { + actual: value, + accepted: &[ValueKind::String], + }, + location, + ))), + } + } +} +impl MergeWithError for DeserrError { + fn merge( + _self_: Option, + other: IndexUidFormatError, + merge_location: deserr::ValuePointerRef, + ) -> std::result::Result { + DeserrError::error::( + None, + deserr::ErrorKind::Unexpected { msg: other.to_string() }, + merge_location, + ) + } +} + +#[derive(Debug, DeserializeFromValue)] +#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] +pub struct CreateApiKey { + #[deserr(error = DeserrError)] + pub description: Option, + #[deserr(error = DeserrError)] + pub name: Option, + #[deserr(default = Uuid::new_v4(), error = DeserrError)] + pub uid: KeyId, + // Value at `.name` is invalid. It is a dictionary, but is expected to be an array of strings containing action names. + // Value `indeex.create` at `.name[1]` is invalid. It is expected to be one of: .... + #[deserr(error = DeserrError)] + //, expected = "an array of string containing action names.")] + pub actions: Vec, + #[deserr(error = DeserrError)] + pub indexes: Vec>, + #[deserr(error = DeserrError, default = None, from(&String) = parse_expiration_date -> TakeErrorMessage)] + pub expires_at: Option, +} +impl CreateApiKey { + pub fn to_key(self) -> Key { + let CreateApiKey { description, name, uid, actions, indexes, expires_at } = self; + let now = OffsetDateTime::now_utc(); + Key { + description, + name, + uid, + actions, + indexes, + expires_at, + created_at: now, + updated_at: now, + } + } +} + +fn deny_immutable_fields_api_key( + field: &str, + accepted: &[&str], + location: ValuePointerRef, +) -> DeserrError { + let mut error = unwrap_any(DeserrError::::error::( + None, + deserr::ErrorKind::UnknownKey { key: field, accepted }, + location, + )); + + error.code = match field { + "uid" => Code::ImmutableField, + "actions" => Code::ImmutableField, + "indexes" => Code::ImmutableField, + "expiresAt" => Code::ImmutableField, + "createdAt" => Code::ImmutableField, + "updatedAt" => Code::ImmutableField, + _ => Code::BadRequest, + }; + error +} + +#[derive(Debug, DeserializeFromValue)] +#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_api_key)] +pub struct PatchApiKey { + #[deserr(error = DeserrError)] + pub description: Option, + #[deserr(error = DeserrError)] + pub name: Option, +} + #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] pub struct Key { #[serde(skip_serializing_if = "Option::is_none")] @@ -35,100 +142,6 @@ pub struct Key { } impl Key { - pub fn create_from_value(value: Value) -> Result { - let name = match value.get("name") { - None | Some(Value::Null) => None, - Some(des) => from_value(des.clone()) - .map(Some) - .map_err(|_| Error::InvalidApiKeyName(des.clone()))?, - }; - - let description = match value.get("description") { - None | Some(Value::Null) => None, - Some(des) => from_value(des.clone()) - .map(Some) - .map_err(|_| Error::InvalidApiKeyDescription(des.clone()))?, - }; - - let uid = value.get("uid").map_or_else( - || Ok(Uuid::new_v4()), - |uid| from_value(uid.clone()).map_err(|_| Error::InvalidApiKeyUid(uid.clone())), - )?; - - let actions = value - .get("actions") - .map(|act| { - from_value(act.clone()).map_err(|_| Error::InvalidApiKeyActions(act.clone())) - }) - .ok_or(Error::MissingApiKeyActions)??; - - let indexes = value - .get("indexes") - .map(|ind| { - from_value::>(ind.clone()) - // If it's not a vec of string, return an API key parsing error. - .map_err(|_| Error::InvalidApiKeyIndexes(ind.clone())) - .and_then(|ind| { - ind.into_iter() - // If it's not a valid Index uid, return an Index Uid parsing error. - .map(|i| StarOr::::from_str(&i).map_err(Error::from)) - .collect() - }) - }) - .ok_or(Error::MissingApiKeyIndexes)??; - - let expires_at = value - .get("expiresAt") - .map(parse_expiration_date) - .ok_or(Error::MissingApiKeyExpiresAt)??; - - let created_at = OffsetDateTime::now_utc(); - let updated_at = created_at; - - Ok(Self { name, description, uid, actions, indexes, expires_at, created_at, updated_at }) - } - - pub fn update_from_value(&mut self, value: Value) -> Result<()> { - if let Some(des) = value.get("description") { - let des = - from_value(des.clone()).map_err(|_| Error::InvalidApiKeyDescription(des.clone())); - self.description = des?; - } - - if let Some(des) = value.get("name") { - let des = from_value(des.clone()).map_err(|_| Error::InvalidApiKeyName(des.clone())); - self.name = des?; - } - - if value.get("uid").is_some() { - return Err(Error::ImmutableField("uid".to_string())); - } - - if value.get("actions").is_some() { - return Err(Error::ImmutableField("actions".to_string())); - } - - if value.get("indexes").is_some() { - return Err(Error::ImmutableField("indexes".to_string())); - } - - if value.get("expiresAt").is_some() { - return Err(Error::ImmutableField("expiresAt".to_string())); - } - - if value.get("createdAt").is_some() { - return Err(Error::ImmutableField("createdAt".to_string())); - } - - if value.get("updatedAt").is_some() { - return Err(Error::ImmutableField("updatedAt".to_string())); - } - - self.updated_at = OffsetDateTime::now_utc(); - - Ok(()) - } - pub fn default_admin() -> Self { let now = OffsetDateTime::now_utc(); let uid = Uuid::new_v4(); @@ -160,107 +173,143 @@ impl Key { } } -fn parse_expiration_date(value: &Value) -> Result> { - match value { - Value::String(string) => OffsetDateTime::parse(string, &Rfc3339) - .or_else(|_| { - PrimitiveDateTime::parse( - string, - format_description!( - "[year repr:full base:calendar]-[month repr:numerical]-[day]T[hour]:[minute]:[second]" - ), - ).map(|datetime| datetime.assume_utc()) - }) - .or_else(|_| { - PrimitiveDateTime::parse( - string, - format_description!( - "[year repr:full base:calendar]-[month repr:numerical]-[day] [hour]:[minute]:[second]" - ), - ).map(|datetime| datetime.assume_utc()) - }) - .or_else(|_| { - Date::parse(string, format_description!( - "[year repr:full base:calendar]-[month repr:numerical]-[day]" - )).map(|date| PrimitiveDateTime::new(date, time!(00:00)).assume_utc()) - }) - .map_err(|_| Error::InvalidApiKeyExpiresAt(value.clone())) - // check if the key is already expired. - .and_then(|d| { - if d > OffsetDateTime::now_utc() { - Ok(d) - } else { - Err(Error::InvalidApiKeyExpiresAt(value.clone())) - } - }) - .map(Option::Some), - Value::Null => Ok(None), - _otherwise => Err(Error::InvalidApiKeyExpiresAt(value.clone())), +#[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: &str, +) -> std::result::Result, TakeErrorMessage> { + let datetime = if let Ok(datetime) = OffsetDateTime::parse(string, &Rfc3339) { + datetime + } else if let Ok(primitive_datetime) = PrimitiveDateTime::parse( + 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, + 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, + 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()))); + }; + if datetime > OffsetDateTime::now_utc() { + Ok(Some(datetime)) + } else { + Err(TakeErrorMessage(ParseOffsetDateTimeError(string.to_owned()))) } } -#[derive(Copy, Clone, Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Sequence)] +#[derive( + Copy, Clone, Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Sequence, DeserializeFromValue, +)] #[repr(u8)] pub enum Action { #[serde(rename = "*")] + #[deserr(rename = "*")] All = 0, #[serde(rename = "search")] + #[deserr(rename = "search")] Search, #[serde(rename = "documents.*")] + #[deserr(rename = "documents.*")] DocumentsAll, #[serde(rename = "documents.add")] + #[deserr(rename = "documents.add")] DocumentsAdd, #[serde(rename = "documents.get")] + #[deserr(rename = "documents.get")] DocumentsGet, #[serde(rename = "documents.delete")] + #[deserr(rename = "documents.delete")] DocumentsDelete, #[serde(rename = "indexes.*")] + #[deserr(rename = "indexes.*")] IndexesAll, #[serde(rename = "indexes.create")] + #[deserr(rename = "indexes.create")] IndexesAdd, #[serde(rename = "indexes.get")] + #[deserr(rename = "indexes.get")] IndexesGet, #[serde(rename = "indexes.update")] + #[deserr(rename = "indexes.update")] IndexesUpdate, #[serde(rename = "indexes.delete")] + #[deserr(rename = "indexes.delete")] IndexesDelete, #[serde(rename = "indexes.swap")] + #[deserr(rename = "indexes.swap")] IndexesSwap, #[serde(rename = "tasks.*")] + #[deserr(rename = "tasks.*")] TasksAll, #[serde(rename = "tasks.cancel")] + #[deserr(rename = "tasks.cancel")] TasksCancel, #[serde(rename = "tasks.delete")] + #[deserr(rename = "tasks.delete")] TasksDelete, #[serde(rename = "tasks.get")] + #[deserr(rename = "tasks.get")] TasksGet, #[serde(rename = "settings.*")] + #[deserr(rename = "settings.*")] SettingsAll, #[serde(rename = "settings.get")] + #[deserr(rename = "settings.get")] SettingsGet, #[serde(rename = "settings.update")] + #[deserr(rename = "settings.update")] SettingsUpdate, #[serde(rename = "stats.*")] + #[deserr(rename = "stats.*")] StatsAll, #[serde(rename = "stats.get")] + #[deserr(rename = "stats.get")] StatsGet, #[serde(rename = "metrics.*")] + #[deserr(rename = "metrics.*")] MetricsAll, #[serde(rename = "metrics.get")] + #[deserr(rename = "metrics.get")] MetricsGet, #[serde(rename = "dumps.*")] + #[deserr(rename = "dumps.*")] DumpsAll, #[serde(rename = "dumps.create")] + #[deserr(rename = "dumps.create")] DumpsCreate, #[serde(rename = "version")] + #[deserr(rename = "version")] Version, #[serde(rename = "keys.create")] + #[deserr(rename = "keys.create")] KeysAdd, #[serde(rename = "keys.get")] + #[deserr(rename = "keys.get")] KeysGet, #[serde(rename = "keys.update")] + #[deserr(rename = "keys.update")] KeysUpdate, #[serde(rename = "keys.delete")] + #[deserr(rename = "keys.delete")] KeysDelete, } @@ -341,56 +390,3 @@ pub mod actions { pub const KEYS_UPDATE: u8 = KeysUpdate.repr(); pub const KEYS_DELETE: u8 = KeysDelete.repr(); } - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("`expiresAt` field is mandatory.")] - MissingApiKeyExpiresAt, - #[error("`indexes` field is mandatory.")] - MissingApiKeyIndexes, - #[error("`actions` field is mandatory.")] - MissingApiKeyActions, - #[error("`actions` field value `{0}` is invalid. It should be an array of string representing action names.")] - InvalidApiKeyActions(Value), - #[error("`indexes` field value `{0}` is invalid. It should be an array of string representing index names.")] - InvalidApiKeyIndexes(Value), - #[error("{0}")] - InvalidApiKeyIndexUid(IndexUidFormatError), - #[error("`expiresAt` field value `{0}` is invalid. 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'.")] - InvalidApiKeyExpiresAt(Value), - #[error("`description` field value `{0}` is invalid. It should be a string or specified as a null value.")] - InvalidApiKeyDescription(Value), - #[error( - "`name` field value `{0}` is invalid. It should be a string or specified as a null value." - )] - InvalidApiKeyName(Value), - #[error("`uid` field value `{0}` is invalid. It should be a valid UUID v4 string or omitted.")] - InvalidApiKeyUid(Value), - #[error("The `{0}` field cannot be modified for the given resource.")] - ImmutableField(String), -} - -impl From for Error { - fn from(e: IndexUidFormatError) -> Self { - Self::InvalidApiKeyIndexUid(e) - } -} - -impl ErrorCode for Error { - fn error_code(&self) -> Code { - match self { - Self::MissingApiKeyExpiresAt => Code::MissingApiKeyExpiresAt, - Self::MissingApiKeyIndexes => Code::MissingApiKeyIndexes, - Self::MissingApiKeyActions => Code::MissingApiKeyActions, - Self::InvalidApiKeyActions(_) => Code::InvalidApiKeyActions, - Self::InvalidApiKeyIndexes(_) | Self::InvalidApiKeyIndexUid(_) => { - Code::InvalidApiKeyIndexes - } - Self::InvalidApiKeyExpiresAt(_) => Code::InvalidApiKeyExpiresAt, - Self::InvalidApiKeyDescription(_) => Code::InvalidApiKeyDescription, - Self::InvalidApiKeyName(_) => Code::InvalidApiKeyName, - Self::InvalidApiKeyUid(_) => Code::InvalidApiKeyUid, - Self::ImmutableField(_) => Code::ImmutableField, - } - } -} diff --git a/meilisearch-types/src/settings.rs b/meilisearch-types/src/settings.rs index f863e1905..20705539a 100644 --- a/meilisearch-types/src/settings.rs +++ b/meilisearch-types/src/settings.rs @@ -1,11 +1,18 @@ use std::collections::{BTreeMap, BTreeSet}; +use std::convert::Infallible; +use std::fmt; use std::marker::PhantomData; use std::num::NonZeroUsize; +use std::str::FromStr; -use deserr::{DeserializeError, DeserializeFromValue}; +use deserr::{DeserializeError, DeserializeFromValue, ErrorKind, MergeWithError, ValuePointerRef}; use fst::IntoStreamer; -use milli::{Index, DEFAULT_VALUES_PER_FACET}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use milli::update::Setting; +use milli::{Criterion, CriterionError, Index, DEFAULT_VALUES_PER_FACET}; +use serde::{Deserialize, Serialize, Serializer}; + +use crate::error::DeserrError; +use crate::error::{deserr_codes::*, unwrap_any}; /// The maximimum number of results that the engine /// will be able to return in one search call. @@ -27,112 +34,6 @@ where .serialize(s) } -#[derive(Debug, Clone, PartialEq, Eq, Copy)] -pub enum Setting { - Set(T), - Reset, - NotSet, -} - -impl Default for Setting { - fn default() -> Self { - Self::NotSet - } -} - -impl From> for milli::update::Setting { - fn from(value: Setting) -> Self { - match value { - Setting::Set(x) => milli::update::Setting::Set(x), - Setting::Reset => milli::update::Setting::Reset, - Setting::NotSet => milli::update::Setting::NotSet, - } - } -} -impl From> for Setting { - fn from(value: milli::update::Setting) -> Self { - match value { - milli::update::Setting::Set(x) => Setting::Set(x), - milli::update::Setting::Reset => Setting::Reset, - milli::update::Setting::NotSet => Setting::NotSet, - } - } -} - -impl Setting { - pub fn set(self) -> Option { - match self { - Self::Set(value) => Some(value), - _ => None, - } - } - - pub const fn as_ref(&self) -> Setting<&T> { - match *self { - Self::Set(ref value) => Setting::Set(value), - Self::Reset => Setting::Reset, - Self::NotSet => Setting::NotSet, - } - } - - pub const fn is_not_set(&self) -> bool { - matches!(self, Self::NotSet) - } - - /// If `Self` is `Reset`, then map self to `Set` with the provided `val`. - pub fn or_reset(self, val: T) -> Self { - match self { - Self::Reset => Self::Set(val), - otherwise => otherwise, - } - } -} - -impl Serialize for Setting { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer, - { - match self { - Self::Set(value) => Some(value), - // Usually not_set isn't serialized by setting skip_serializing_if field attribute - Self::NotSet | Self::Reset => None, - } - .serialize(serializer) - } -} - -impl<'de, T: Deserialize<'de>> Deserialize<'de> for Setting { - fn deserialize(deserializer: D) -> std::result::Result - where - D: Deserializer<'de>, - { - Deserialize::deserialize(deserializer).map(|x| match x { - Some(x) => Self::Set(x), - None => Self::Reset, // Reset is forced by sending null value - }) - } -} - -impl DeserializeFromValue for Setting -where - T: DeserializeFromValue, - E: DeserializeError, -{ - fn deserialize_from_value( - value: deserr::Value, - location: deserr::ValuePointerRef, - ) -> Result { - match value { - deserr::Value::Null => Ok(Setting::Reset), - _ => T::deserialize_from_value(value, location).map(Setting::Set), - } - } - fn default() -> Option { - Some(Self::NotSet) - } -} - #[derive(Clone, Default, Debug, Serialize, PartialEq, Eq)] pub struct Checked; @@ -151,78 +52,90 @@ where } } -#[cfg_attr(test, derive(proptest_derive::Arbitrary))] +fn validate_min_word_size_for_typo_setting( + s: MinWordSizeTyposSetting, + location: ValuePointerRef, +) -> Result { + if let (Setting::Set(one), Setting::Set(two)) = (s.one_typo, s.two_typos) { + if one > two { + return Err(unwrap_any(E::error::(None, ErrorKind::Unexpected { msg: format!("`minWordSizeForTypos` setting is invalid. `oneTypo` and `twoTypos` fields should be between `0` and `255`, and `twoTypos` should be greater or equals to `oneTypo` but found `oneTypo: {one}` and twoTypos: {two}`.") }, location))); + } + } + Ok(s) +} + #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)] -#[serde(deny_unknown_fields)] -#[serde(rename_all = "camelCase")] -#[deserr(rename_all = camelCase, deny_unknown_fields)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +#[deserr(deny_unknown_fields, rename_all = camelCase, validate = validate_min_word_size_for_typo_setting -> DeserrError)] pub struct MinWordSizeTyposSetting { - #[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))] #[serde(default, skip_serializing_if = "Setting::is_not_set")] pub one_typo: Setting, - #[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))] #[serde(default, skip_serializing_if = "Setting::is_not_set")] pub two_typos: Setting, } -#[cfg_attr(test, derive(proptest_derive::Arbitrary))] #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)] -#[serde(deny_unknown_fields)] -#[serde(rename_all = "camelCase")] -#[deserr(rename_all = camelCase, deny_unknown_fields)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +#[deserr(deny_unknown_fields, rename_all = camelCase, where_predicate = __Deserr_E: deserr::MergeWithError>)] pub struct TypoSettings { - #[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))] #[serde(default, skip_serializing_if = "Setting::is_not_set")] pub enabled: Setting, - #[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))] #[serde(default, skip_serializing_if = "Setting::is_not_set")] + #[deserr(error = DeserrError)] pub min_word_size_for_typos: Setting, - #[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))] #[serde(default, skip_serializing_if = "Setting::is_not_set")] pub disable_on_words: Setting>, - #[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))] #[serde(default, skip_serializing_if = "Setting::is_not_set")] pub disable_on_attributes: Setting>, } -#[cfg_attr(test, derive(proptest_derive::Arbitrary))] #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)] -#[serde(deny_unknown_fields)] -#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields, rename_all = "camelCase")] #[deserr(rename_all = camelCase, deny_unknown_fields)] pub struct FacetingSettings { - #[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))] #[serde(default, skip_serializing_if = "Setting::is_not_set")] pub max_values_per_facet: Setting, } -#[cfg_attr(test, derive(proptest_derive::Arbitrary))] #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)] -#[serde(deny_unknown_fields)] -#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields, rename_all = "camelCase")] #[deserr(rename_all = camelCase, deny_unknown_fields)] pub struct PaginationSettings { - #[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))] #[serde(default, skip_serializing_if = "Setting::is_not_set")] pub max_total_hits: Setting, } +impl MergeWithError for DeserrError { + fn merge( + _self_: Option, + other: milli::CriterionError, + merge_location: ValuePointerRef, + ) -> Result { + Self::error::( + None, + ErrorKind::Unexpected { msg: other.to_string() }, + merge_location, + ) + } +} + /// Holds all the settings for an index. `T` can either be `Checked` if they represents settings /// whose validity is guaranteed, or `Unchecked` if they need to be validated. In the later case, a /// call to `check` will return a `Settings` from a `Settings`. #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)] -#[serde(deny_unknown_fields)] -#[serde(rename_all = "camelCase")] -#[serde(bound(serialize = "T: Serialize", deserialize = "T: Deserialize<'static>"))] -#[deserr(rename_all = camelCase, deny_unknown_fields)] -#[cfg_attr(test, derive(proptest_derive::Arbitrary))] +#[serde( + deny_unknown_fields, + rename_all = "camelCase", + bound(serialize = "T: Serialize", deserialize = "T: Deserialize<'static>") +)] +#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] pub struct Settings { #[serde( default, serialize_with = "serialize_with_wildcard", skip_serializing_if = "Setting::is_not_set" )] - #[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))] + #[deserr(error = DeserrError)] pub displayed_attributes: Setting>, #[serde( @@ -230,38 +143,39 @@ pub struct Settings { serialize_with = "serialize_with_wildcard", skip_serializing_if = "Setting::is_not_set" )] - #[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))] + #[deserr(error = DeserrError)] pub searchable_attributes: Setting>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))] + #[deserr(error = DeserrError)] pub filterable_attributes: Setting>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))] + #[deserr(error = DeserrError)] pub sortable_attributes: Setting>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))] - pub ranking_rules: Setting>, + #[deserr(error = DeserrError)] + pub ranking_rules: Setting>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))] + #[deserr(error = DeserrError)] pub stop_words: Setting>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))] + #[deserr(error = DeserrError)] pub synonyms: Setting>>, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))] + #[deserr(error = DeserrError)] pub distinct_attribute: Setting, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))] + #[deserr(error = DeserrError)] pub typo_tolerance: Setting, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))] + #[deserr(error = DeserrError)] pub faceting: Setting, #[serde(default, skip_serializing_if = "Setting::is_not_set")] - #[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))] + #[deserr(error = DeserrError)] pub pagination: Setting, #[serde(skip)] + #[deserr(skip)] pub _kind: PhantomData, } @@ -396,7 +310,9 @@ pub fn apply_settings_to_builder( } match settings.ranking_rules { - Setting::Set(ref criteria) => builder.set_criteria(criteria.clone()), + Setting::Set(ref criteria) => { + builder.set_criteria(criteria.iter().map(|c| c.clone().into()).collect()) + } Setting::Reset => builder.reset_criteria(), Setting::NotSet => (), } @@ -510,7 +426,7 @@ pub fn settings( let sortable_attributes = index.sortable_fields(rtxn)?.into_iter().collect(); - let criteria = index.criteria(rtxn)?.into_iter().map(|c| c.to_string()).collect(); + let criteria = index.criteria(rtxn)?; let stop_words = index .stop_words(rtxn)? @@ -571,7 +487,7 @@ pub fn settings( }, filterable_attributes: Setting::Set(filterable_attributes), sortable_attributes: Setting::Set(sortable_attributes), - ranking_rules: Setting::Set(criteria), + ranking_rules: Setting::Set(criteria.iter().map(|c| c.clone().into()).collect()), stop_words: Setting::Set(stop_words), distinct_attribute: match distinct_field { Some(field) => Setting::Set(field), @@ -585,16 +501,106 @@ pub fn settings( }) } +#[derive(Debug, Clone, PartialEq, Eq, DeserializeFromValue)] +#[deserr(from(&String) = FromStr::from_str -> CriterionError)] +pub enum RankingRuleView { + /// Sorted by decreasing number of matched query terms. + /// Query words at the front of an attribute is considered better than if it was at the back. + Words, + /// Sorted by increasing number of typos. + Typo, + /// Sorted by increasing distance between matched query terms. + Proximity, + /// Documents with quey words contained in more important + /// attributes are considered better. + Attribute, + /// Dynamically sort at query time the documents. None, one or multiple Asc/Desc sortable + /// attributes can be used in place of this criterion at query time. + Sort, + /// Sorted by the similarity of the matched words with the query words. + Exactness, + /// Sorted by the increasing value of the field specified. + Asc(String), + /// Sorted by the decreasing value of the field specified. + Desc(String), +} +impl Serialize for RankingRuleView { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&format!("{}", Criterion::from(self.clone()))) + } +} +impl<'de> Deserialize<'de> for RankingRuleView { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct Visitor; + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = RankingRuleView; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "the name of a valid ranking rule (string)") + } + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + let criterion = Criterion::from_str(v).map_err(|_| { + E::invalid_value(serde::de::Unexpected::Str(v), &"a valid ranking rule") + })?; + Ok(RankingRuleView::from(criterion)) + } + } + deserializer.deserialize_str(Visitor) + } +} +impl FromStr for RankingRuleView { + type Err = ::Err; + + fn from_str(s: &str) -> Result { + Ok(RankingRuleView::from(Criterion::from_str(s)?)) + } +} +impl fmt::Display for RankingRuleView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fmt::Display::fmt(&Criterion::from(self.clone()), f) + } +} +impl From for RankingRuleView { + fn from(value: Criterion) -> Self { + match value { + Criterion::Words => RankingRuleView::Words, + Criterion::Typo => RankingRuleView::Typo, + Criterion::Proximity => RankingRuleView::Proximity, + Criterion::Attribute => RankingRuleView::Attribute, + Criterion::Sort => RankingRuleView::Sort, + Criterion::Exactness => RankingRuleView::Exactness, + Criterion::Asc(x) => RankingRuleView::Asc(x), + Criterion::Desc(x) => RankingRuleView::Desc(x), + } + } +} +impl From for Criterion { + fn from(value: RankingRuleView) -> Self { + match value { + RankingRuleView::Words => Criterion::Words, + RankingRuleView::Typo => Criterion::Typo, + RankingRuleView::Proximity => Criterion::Proximity, + RankingRuleView::Attribute => Criterion::Attribute, + RankingRuleView::Sort => Criterion::Sort, + RankingRuleView::Exactness => Criterion::Exactness, + RankingRuleView::Asc(x) => Criterion::Asc(x), + RankingRuleView::Desc(x) => Criterion::Desc(x), + } + } +} + #[cfg(test)] pub(crate) mod test { - use proptest::prelude::*; - use super::*; - pub(super) fn setting_strategy() -> impl Strategy> { - prop_oneof![Just(Setting::NotSet), Just(Setting::Reset), any::().prop_map(Setting::Set)] - } - #[test] fn test_setting_check() { // test no changes diff --git a/meilisearch-types/src/star_or.rs b/meilisearch-types/src/star_or.rs index e89ba6b0e..f56c30b4e 100644 --- a/meilisearch-types/src/star_or.rs +++ b/meilisearch-types/src/star_or.rs @@ -3,9 +3,12 @@ use std::marker::PhantomData; use std::ops::Deref; use std::str::FromStr; +use deserr::{DeserializeError, DeserializeFromValue, MergeWithError, ValueKind}; use serde::de::Visitor; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use crate::error::unwrap_any; + /// A type that tries to match either a star (*) or /// any other thing that implements `FromStr`. #[derive(Debug, Clone)] @@ -14,6 +17,35 @@ 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; diff --git a/meilisearch/src/extractors/authentication/error.rs b/meilisearch/src/extractors/authentication/error.rs index 7fa0319b8..39cb8d0ab 100644 --- a/meilisearch/src/extractors/authentication/error.rs +++ b/meilisearch/src/extractors/authentication/error.rs @@ -17,7 +17,7 @@ impl ErrorCode for AuthenticationError { fn error_code(&self) -> Code { match self { AuthenticationError::MissingAuthorizationHeader => Code::MissingAuthorizationHeader, - AuthenticationError::InvalidToken => Code::InvalidToken, + AuthenticationError::InvalidToken => Code::InvalidApiKey, AuthenticationError::IrretrievableState => Code::Internal, AuthenticationError::MissingMasterKey => Code::MissingMasterKey, } diff --git a/meilisearch/src/extractors/json.rs b/meilisearch/src/extractors/json.rs index f800a228e..e2418c9fb 100644 --- a/meilisearch/src/extractors/json.rs +++ b/meilisearch/src/extractors/json.rs @@ -32,7 +32,7 @@ impl ValidatedJson { impl FromRequest for ValidatedJson where - E: DeserializeError + ErrorCode + 'static, + E: DeserializeError + ErrorCode + std::error::Error + 'static, T: DeserializeFromValue, { type Error = actix_web::Error; @@ -55,7 +55,7 @@ pub struct ValidatedJsonExtractFut { impl Future for ValidatedJsonExtractFut where T: DeserializeFromValue, - E: DeserializeError + ErrorCode + 'static, + E: DeserializeError + ErrorCode + std::error::Error + 'static, { type Output = Result, actix_web::Error>; diff --git a/meilisearch/src/extractors/query_parameters.rs b/meilisearch/src/extractors/query_parameters.rs index ebbceacf8..99c76f3aa 100644 --- a/meilisearch/src/extractors/query_parameters.rs +++ b/meilisearch/src/extractors/query_parameters.rs @@ -22,7 +22,7 @@ impl QueryParameter { impl QueryParameter where T: DeserializeFromValue, - E: DeserializeError + ErrorCode + 'static, + E: DeserializeError + ErrorCode + std::error::Error + 'static, { pub fn from_query(query_str: &str) -> Result { let value = serde_urlencoded::from_str::(query_str) @@ -58,7 +58,7 @@ impl fmt::Display for QueryParameter { impl FromRequest for QueryParameter where T: DeserializeFromValue, - E: DeserializeError + ErrorCode + 'static, + E: DeserializeError + ErrorCode + std::error::Error + 'static, { type Error = actix_web::Error; type Future = Ready>; diff --git a/meilisearch/src/routes/api_key.rs b/meilisearch/src/routes/api_key.rs index cd8a51662..c232d4802 100644 --- a/meilisearch/src/routes/api_key.rs +++ b/meilisearch/src/routes/api_key.rs @@ -1,24 +1,26 @@ -use std::convert::Infallible; -use std::num::ParseIntError; -use std::{fmt, str}; +use std::str; use actix_web::{web, HttpRequest, HttpResponse}; -use deserr::{DeserializeError, IntoValue, MergeWithError, ValuePointerRef}; +use deserr::DeserializeFromValue; use meilisearch_auth::error::AuthControllerError; use meilisearch_auth::AuthController; -use meilisearch_types::error::{unwrap_any, Code, ErrorCode, ResponseError}; -use meilisearch_types::keys::{Action, Key}; +use meilisearch_types::error::{deserr_codes::*, TakeErrorMessage}; +use meilisearch_types::error::{Code, DeserrError, ResponseError}; +use meilisearch_types::keys::{Action, CreateApiKey, Key, PatchApiKey}; use serde::{Deserialize, Serialize}; -use serde_json::Value; use time::OffsetDateTime; use uuid::Uuid; use crate::extractors::authentication::policies::*; 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::Pagination; +use super::indexes::search::parse_usize_take_error_message; +use super::PAGINATION_DEFAULT_LIMIT; + pub fn configure(cfg: &mut web::ServiceConfig) { cfg.service( web::resource("") @@ -35,7 +37,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) { pub async fn create_api_key( auth_controller: GuardedData, AuthController>, - body: web::Json, + body: ValidatedJson, _req: HttpRequest, ) -> Result { let v = body.into_inner(); @@ -49,72 +51,28 @@ pub async fn create_api_key( Ok(HttpResponse::Created().json(res)) } -#[derive(Debug)] -pub struct PaginationDeserrError { - error: String, - code: Code, +#[derive(DeserializeFromValue, Deserialize, Debug, Clone, Copy)] +#[deserr(error = DeserrError, 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)] + pub offset: usize, + #[serde(default = "PAGINATION_DEFAULT_LIMIT")] + #[deserr(error = DeserrError, default = PAGINATION_DEFAULT_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] + pub limit: usize, } - -impl std::fmt::Display for PaginationDeserrError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.error) - } -} - -impl std::error::Error for PaginationDeserrError {} -impl ErrorCode for PaginationDeserrError { - fn error_code(&self) -> Code { - self.code - } -} - -impl MergeWithError for PaginationDeserrError { - fn merge( - _self_: Option, - other: PaginationDeserrError, - _merge_location: ValuePointerRef, - ) -> Result { - Err(other) - } -} - -impl DeserializeError for PaginationDeserrError { - fn error( - _self_: Option, - error: deserr::ErrorKind, - location: ValuePointerRef, - ) -> Result { - let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0; - - let code = match location.last_field() { - Some("offset") => Code::InvalidApiKeyLimit, - Some("limit") => Code::InvalidApiKeyOffset, - _ => Code::BadRequest, - }; - - Err(PaginationDeserrError { error, code }) - } -} - -impl MergeWithError for PaginationDeserrError { - fn merge( - _self_: Option, - other: ParseIntError, - merge_location: ValuePointerRef, - ) -> Result { - PaginationDeserrError::error::( - None, - deserr::ErrorKind::Unexpected { msg: other.to_string() }, - merge_location, - ) +impl ListApiKeys { + fn as_pagination(self) -> Pagination { + Pagination { offset: self.offset, limit: self.limit } } } pub async fn list_api_keys( auth_controller: GuardedData, AuthController>, - paginate: QueryParameter, + list_api_keys: QueryParameter, ) -> Result { - let paginate = paginate.into_inner(); + let paginate = list_api_keys.into_inner().as_pagination(); let page_view = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> { let keys = auth_controller.list_keys()?; let page_view = paginate @@ -149,15 +107,15 @@ pub async fn get_api_key( pub async fn patch_api_key( auth_controller: GuardedData, AuthController>, - body: web::Json, + body: ValidatedJson, path: web::Path, ) -> Result { let key = path.into_inner().key; - let body = body.into_inner(); + let patch_api_key = body.into_inner(); let res = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> { let uid = Uuid::parse_str(&key).or_else(|_| auth_controller.get_uid_from_encoded_key(&key))?; - let key = auth_controller.update_key(uid, body)?; + let key = auth_controller.update_key(uid, patch_api_key)?; Ok(KeyView::from_key(key, &auth_controller)) }) diff --git a/meilisearch/src/routes/indexes/documents.rs b/meilisearch/src/routes/indexes/documents.rs index 9de91c7fa..161882ca2 100644 --- a/meilisearch/src/routes/indexes/documents.rs +++ b/meilisearch/src/routes/indexes/documents.rs @@ -1,19 +1,23 @@ -use std::convert::Infallible; -use std::fmt; -use std::io::ErrorKind; -use std::num::ParseIntError; -use std::str::FromStr; - +use crate::analytics::{Analytics, DocumentDeletionKind}; +use crate::error::MeilisearchHttpError; +use crate::error::PayloadError::ReceivePayload; +use crate::extractors::authentication::policies::*; +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 actix_web::http::header::CONTENT_TYPE; use actix_web::web::Data; use actix_web::{web, HttpMessage, HttpRequest, HttpResponse}; use bstr::ByteSlice; -use deserr::{DeserializeError, DeserializeFromValue, IntoValue, MergeWithError, ValuePointerRef}; +use deserr::DeserializeFromValue; 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::{unwrap_any, Code, ErrorCode, ResponseError}; +use meilisearch_types::error::{deserr_codes::*, TakeErrorMessage}; +use meilisearch_types::error::{DeserrError, ResponseError}; use meilisearch_types::heed::RoTxn; use meilisearch_types::index_uid::IndexUid; use meilisearch_types::milli::update::IndexDocumentsMethod; @@ -25,19 +29,13 @@ use once_cell::sync::Lazy; use serde::Deserialize; use serde_cs::vec::CS; use serde_json::Value; +use std::io::ErrorKind; +use std::num::ParseIntError; use tempfile::tempfile; use tokio::fs::File; use tokio::io::{AsyncSeekExt, AsyncWriteExt, BufWriter}; -use crate::analytics::{Analytics, DocumentDeletionKind}; -use crate::error::MeilisearchHttpError; -use crate::error::PayloadError::ReceivePayload; -use crate::extractors::authentication::policies::*; -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 super::search::parse_usize_take_error_message; static ACCEPTED_CONTENT_TYPE: Lazy> = Lazy::new(|| { vec!["application/json".to_string(), "application/x-ndjson".to_string(), "text/csv".to_string()] @@ -83,61 +81,16 @@ pub fn configure(cfg: &mut web::ServiceConfig) { } #[derive(Deserialize, Debug, DeserializeFromValue)] -#[deserr(rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] pub struct GetDocument { + #[deserr(error = DeserrError)] fields: Option>>, } -#[derive(Debug)] -pub struct GetDocumentDeserrError { - error: String, - code: Code, -} - -impl std::fmt::Display for GetDocumentDeserrError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.error) - } -} - -impl std::error::Error for GetDocumentDeserrError {} -impl ErrorCode for GetDocumentDeserrError { - fn error_code(&self) -> Code { - self.code - } -} - -impl MergeWithError for GetDocumentDeserrError { - fn merge( - _self_: Option, - other: GetDocumentDeserrError, - _merge_location: ValuePointerRef, - ) -> Result { - Err(other) - } -} - -impl DeserializeError for GetDocumentDeserrError { - fn error( - _self_: Option, - error: deserr::ErrorKind, - location: ValuePointerRef, - ) -> Result { - let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0; - - let code = match location.last_field() { - Some("fields") => Code::InvalidDocumentFields, - _ => Code::BadRequest, - }; - - Err(GetDocumentDeserrError { error, code }) - } -} - 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); @@ -165,81 +118,20 @@ pub async fn delete_document( } #[derive(Deserialize, Debug, DeserializeFromValue)] -#[deserr(rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] pub struct BrowseQuery { - #[deserr(default, from(&String) = FromStr::from_str -> ParseIntError)] + #[deserr(error = DeserrError, default, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] offset: usize, - #[deserr(default = crate::routes::PAGINATION_DEFAULT_LIMIT(), from(&String) = FromStr::from_str -> ParseIntError)] + #[deserr(error = DeserrError, default = crate::routes::PAGINATION_DEFAULT_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] limit: usize, + #[deserr(error = DeserrError)] fields: Option>>, } -#[derive(Debug)] -pub struct BrowseQueryDeserrError { - error: String, - code: Code, -} - -impl std::fmt::Display for BrowseQueryDeserrError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.error) - } -} - -impl std::error::Error for BrowseQueryDeserrError {} -impl ErrorCode for BrowseQueryDeserrError { - fn error_code(&self) -> Code { - self.code - } -} - -impl MergeWithError for BrowseQueryDeserrError { - fn merge( - _self_: Option, - other: BrowseQueryDeserrError, - _merge_location: ValuePointerRef, - ) -> Result { - Err(other) - } -} - -impl DeserializeError for BrowseQueryDeserrError { - fn error( - _self_: Option, - error: deserr::ErrorKind, - location: ValuePointerRef, - ) -> Result { - let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0; - - let code = match location.last_field() { - Some("fields") => Code::InvalidDocumentFields, - Some("offset") => Code::InvalidDocumentOffset, - Some("limit") => Code::InvalidDocumentLimit, - _ => Code::BadRequest, - }; - - Err(BrowseQueryDeserrError { error, code }) - } -} - -impl MergeWithError for BrowseQueryDeserrError { - fn merge( - _self_: Option, - other: ParseIntError, - merge_location: ValuePointerRef, - ) -> Result { - BrowseQueryDeserrError::error::( - None, - deserr::ErrorKind::Unexpected { msg: other.to_string() }, - merge_location, - ) - } -} - 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(); @@ -255,61 +147,16 @@ pub async fn get_all_documents( } #[derive(Deserialize, Debug, DeserializeFromValue)] -#[deserr(rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] pub struct UpdateDocumentsQuery { + #[deserr(error = DeserrError)] pub primary_key: Option, } -#[derive(Debug)] -pub struct UpdateDocumentsQueryDeserrError { - error: String, - code: Code, -} - -impl std::fmt::Display for UpdateDocumentsQueryDeserrError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.error) - } -} - -impl std::error::Error for UpdateDocumentsQueryDeserrError {} -impl ErrorCode for UpdateDocumentsQueryDeserrError { - fn error_code(&self) -> Code { - self.code - } -} - -impl MergeWithError for UpdateDocumentsQueryDeserrError { - fn merge( - _self_: Option, - other: UpdateDocumentsQueryDeserrError, - _merge_location: ValuePointerRef, - ) -> Result { - Err(other) - } -} - -impl DeserializeError for UpdateDocumentsQueryDeserrError { - fn error( - _self_: Option, - error: deserr::ErrorKind, - location: ValuePointerRef, - ) -> Result { - let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0; - - let code = match location.last_field() { - Some("primaryKey") => Code::InvalidIndexPrimaryKey, - _ => Code::BadRequest, - }; - - Err(UpdateDocumentsQueryDeserrError { error, code }) - } -} - pub async fn add_documents( index_scheduler: GuardedData, Data>, index_uid: web::Path, - params: QueryParameter, + params: QueryParameter, body: Payload, req: HttpRequest, analytics: web::Data, @@ -337,7 +184,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 4eaba12f1..c6f39bce6 100644 --- a/meilisearch/src/routes/indexes/mod.rs +++ b/meilisearch/src/routes/indexes/mod.rs @@ -1,14 +1,10 @@ -use std::convert::Infallible; -use std::num::ParseIntError; - use actix_web::web::Data; use actix_web::{web, HttpRequest, HttpResponse}; -use deserr::{ - DeserializeError, DeserializeFromValue, ErrorKind, IntoValue, MergeWithError, ValuePointerRef, -}; +use deserr::DeserializeFromValue; use index_scheduler::IndexScheduler; use log::debug; -use meilisearch_types::error::{unwrap_any, Code, ErrorCode, ResponseError}; +use meilisearch_types::error::deserr_codes::*; +use meilisearch_types::error::{DeserrError, ResponseError}; use meilisearch_types::index_uid::IndexUid; use meilisearch_types::milli::{self, FieldDistribution, Index}; use meilisearch_types::tasks::KindWithContent; @@ -16,7 +12,7 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use time::OffsetDateTime; -use super::{Pagination, SummarizedTaskView}; +use super::{ListIndexes, SummarizedTaskView}; use crate::analytics::Analytics; use crate::extractors::authentication::policies::*; use crate::extractors::authentication::{AuthenticationError, GuardedData}; @@ -74,7 +70,7 @@ impl IndexView { 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()?; @@ -84,82 +80,24 @@ pub async fn list_indexes( .map(|(name, index)| IndexView::new(name, &index)) .collect::, _>>()?; - let ret = paginate.auto_paginate_sized(indexes.into_iter()); + let ret = paginate.as_pagination().auto_paginate_sized(indexes.into_iter()); debug!("returns: {:?}", ret); Ok(HttpResponse::Ok().json(ret)) } -#[derive(Debug)] -pub struct ListIndexesDeserrError { - error: String, - code: Code, -} - -impl std::fmt::Display for ListIndexesDeserrError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.error) - } -} - -impl std::error::Error for ListIndexesDeserrError {} -impl ErrorCode for ListIndexesDeserrError { - fn error_code(&self) -> Code { - self.code - } -} - -impl MergeWithError for ListIndexesDeserrError { - fn merge( - _self_: Option, - other: ListIndexesDeserrError, - _merge_location: ValuePointerRef, - ) -> Result { - Err(other) - } -} - -impl deserr::DeserializeError for ListIndexesDeserrError { - fn error( - _self_: Option, - error: ErrorKind, - location: ValuePointerRef, - ) -> Result { - let code = match location.last_field() { - Some("offset") => Code::InvalidIndexLimit, - Some("limit") => Code::InvalidIndexOffset, - _ => Code::BadRequest, - }; - let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0; - - Err(ListIndexesDeserrError { error, code }) - } -} - -impl MergeWithError for ListIndexesDeserrError { - fn merge( - _self_: Option, - other: ParseIntError, - merge_location: ValuePointerRef, - ) -> Result { - ListIndexesDeserrError::error::( - None, - ErrorKind::Unexpected { msg: other.to_string() }, - merge_location, - ) - } -} - #[derive(DeserializeFromValue, Debug)] -#[deserr(rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] pub struct IndexCreateRequest { + #[deserr(error = DeserrError, missing_field_error = DeserrError::missing_index_uid)] uid: String, + #[deserr(error = DeserrError)] primary_key: Option, } pub async fn create_index( index_scheduler: GuardedData, Data>, - body: ValidatedJson, + body: ValidatedJson, req: HttpRequest, analytics: web::Data, ) -> Result { @@ -184,58 +122,10 @@ pub async fn create_index( } } -#[derive(Debug)] -pub struct CreateIndexesDeserrError { - error: String, - code: Code, -} - -impl std::fmt::Display for CreateIndexesDeserrError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.error) - } -} - -impl std::error::Error for CreateIndexesDeserrError {} -impl ErrorCode for CreateIndexesDeserrError { - fn error_code(&self) -> Code { - self.code - } -} - -impl MergeWithError for CreateIndexesDeserrError { - fn merge( - _self_: Option, - other: CreateIndexesDeserrError, - _merge_location: ValuePointerRef, - ) -> Result { - Err(other) - } -} - -impl deserr::DeserializeError for CreateIndexesDeserrError { - fn error( - _self_: Option, - error: ErrorKind, - location: ValuePointerRef, - ) -> Result { - let code = match location.last_field() { - Some("uid") => Code::InvalidIndexUid, - Some("primaryKey") => Code::InvalidIndexPrimaryKey, - None if matches!(error, ErrorKind::MissingField { field } if field == "uid") => { - Code::MissingIndexUid - } - _ => Code::BadRequest, - }; - let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0; - - Err(CreateIndexesDeserrError { error, code }) - } -} - #[derive(DeserializeFromValue, Debug)] -#[deserr(rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] pub struct UpdateIndexRequest { + #[deserr(error = DeserrError)] primary_key: Option, } @@ -254,7 +144,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 { @@ -278,51 +168,6 @@ pub async fn update_index( Ok(HttpResponse::Accepted().json(task)) } -#[derive(Debug)] -pub struct UpdateIndexesDeserrError { - error: String, - code: Code, -} - -impl std::fmt::Display for UpdateIndexesDeserrError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.error) - } -} - -impl std::error::Error for UpdateIndexesDeserrError {} -impl ErrorCode for UpdateIndexesDeserrError { - fn error_code(&self) -> Code { - self.code - } -} - -impl MergeWithError for UpdateIndexesDeserrError { - fn merge( - _self_: Option, - other: UpdateIndexesDeserrError, - _merge_location: ValuePointerRef, - ) -> Result { - Err(other) - } -} - -impl deserr::DeserializeError for UpdateIndexesDeserrError { - fn error( - _self_: Option, - error: ErrorKind, - location: ValuePointerRef, - ) -> Result { - let code = match location.last_field() { - Some("primaryKey") => Code::InvalidIndexPrimaryKey, - _ => Code::BadRequest, - }; - let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0; - - Err(UpdateIndexesDeserrError { error, code }) - } -} - pub async fn delete_index( index_scheduler: GuardedData, Data>, index_uid: web::Path, diff --git a/meilisearch/src/routes/indexes/search.rs b/meilisearch/src/routes/indexes/search.rs index bf0115997..6296772e0 100644 --- a/meilisearch/src/routes/indexes/search.rs +++ b/meilisearch/src/routes/indexes/search.rs @@ -5,7 +5,8 @@ use actix_web::{web, HttpRequest, HttpResponse}; use index_scheduler::IndexScheduler; use log::debug; use meilisearch_auth::IndexSearchRules; -use meilisearch_types::error::ResponseError; +use meilisearch_types::error::deserr_codes::*; +use meilisearch_types::error::{DeserrError, ResponseError, TakeErrorMessage}; use serde_cs::vec::CS; use serde_json::Value; @@ -15,11 +16,11 @@ 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; +use crate::routes::from_string_to_option_take_error_message; use crate::search::{ - perform_search, MatchingStrategy, SearchDeserError, SearchQuery, DEFAULT_CROP_LENGTH, - DEFAULT_CROP_MARKER, DEFAULT_HIGHLIGHT_POST_TAG, DEFAULT_HIGHLIGHT_PRE_TAG, - DEFAULT_SEARCH_LIMIT, DEFAULT_SEARCH_OFFSET, + perform_search, MatchingStrategy, SearchQuery, DEFAULT_CROP_LENGTH, DEFAULT_CROP_MARKER, + DEFAULT_HIGHLIGHT_POST_TAG, DEFAULT_HIGHLIGHT_PRE_TAG, DEFAULT_SEARCH_LIMIT, + DEFAULT_SEARCH_OFFSET, }; pub fn configure(cfg: &mut web::ServiceConfig) { @@ -30,35 +31,54 @@ 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(rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] pub struct SearchQueryGet { + #[deserr(error = DeserrError)] q: Option, - #[deserr(default = DEFAULT_SEARCH_OFFSET(), from(&String) = FromStr::from_str -> std::num::ParseIntError)] + #[deserr(error = DeserrError, default = DEFAULT_SEARCH_OFFSET(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] offset: usize, - #[deserr(default = DEFAULT_SEARCH_LIMIT(), from(&String) = FromStr::from_str -> std::num::ParseIntError)] + #[deserr(error = DeserrError, default = DEFAULT_SEARCH_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] limit: usize, - #[deserr(from(&String) = from_string_to_option -> std::num::ParseIntError)] + #[deserr(error = DeserrError, from(&String) = from_string_to_option_take_error_message -> TakeErrorMessage)] page: Option, - #[deserr(from(&String) = from_string_to_option -> std::num::ParseIntError)] + #[deserr(error = DeserrError, from(&String) = from_string_to_option_take_error_message -> TakeErrorMessage)] hits_per_page: Option, + #[deserr(error = DeserrError)] attributes_to_retrieve: Option>, + #[deserr(error = DeserrError)] attributes_to_crop: Option>, - #[deserr(default = DEFAULT_CROP_LENGTH(), from(&String) = FromStr::from_str -> std::num::ParseIntError)] + #[deserr(error = DeserrError, default = DEFAULT_CROP_LENGTH(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] crop_length: usize, + #[deserr(error = DeserrError)] attributes_to_highlight: Option>, + #[deserr(error = DeserrError)] filter: Option, + #[deserr(error = DeserrError)] sort: Option, - #[deserr(default, from(&String) = FromStr::from_str -> std::str::ParseBoolError)] + #[deserr(error = DeserrError, default, from(&String) = parse_bool_take_error_message -> TakeErrorMessage)] show_matches_position: bool, + #[deserr(error = DeserrError)] facets: Option>, - #[deserr(default = DEFAULT_HIGHLIGHT_PRE_TAG())] + #[deserr(error = DeserrError, default = DEFAULT_HIGHLIGHT_PRE_TAG())] highlight_pre_tag: String, - #[deserr(default = DEFAULT_HIGHLIGHT_POST_TAG())] + #[deserr(error = DeserrError, default = DEFAULT_HIGHLIGHT_POST_TAG())] highlight_post_tag: String, - #[deserr(default = DEFAULT_CROP_MARKER())] + #[deserr(error = DeserrError, default = DEFAULT_CROP_MARKER())] crop_marker: String, - #[deserr(default)] + #[deserr(error = DeserrError, default)] matching_strategy: MatchingStrategy, } @@ -142,7 +162,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 { @@ -174,7 +194,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 47e5a4d50..c64eefd9c 100644 --- a/meilisearch/src/routes/indexes/settings.rs +++ b/meilisearch/src/routes/indexes/settings.rs @@ -1,13 +1,10 @@ -use std::fmt; - use actix_web::web::Data; use actix_web::{web, HttpRequest, HttpResponse}; -use deserr::{IntoValue, ValuePointerRef}; use index_scheduler::IndexScheduler; use log::debug; -use meilisearch_types::error::{unwrap_any, Code, ErrorCode, ResponseError}; +use meilisearch_types::error::{DeserrError, ResponseError}; use meilisearch_types::index_uid::IndexUid; -use meilisearch_types::settings::{settings, Settings, Unchecked}; +use meilisearch_types::settings::{settings, RankingRuleView, Settings, Unchecked}; use meilisearch_types::tasks::KindWithContent; use serde_json::json; @@ -212,7 +209,7 @@ make_setting_route!( "TypoTolerance Updated".to_string(), json!({ "typo_tolerance": { - "enabled": setting.as_ref().map(|s| !matches!(s.enabled.into(), Setting::Set(false))), + "enabled": setting.as_ref().map(|s| !matches!(s.enabled, Setting::Set(false))), "disable_on_attributes": setting .as_ref() .and_then(|s| s.disable_on_attributes.as_ref().set().map(|m| !m.is_empty())), @@ -331,24 +328,24 @@ make_setting_route!( make_setting_route!( "/ranking-rules", put, - Vec, + Vec, ranking_rules, "rankingRules", analytics, - |setting: &Option>, req: &HttpRequest| { + |setting: &Option>, req: &HttpRequest| { use serde_json::json; analytics.publish( "RankingRules Updated".to_string(), json!({ "ranking_rules": { - "words_position": setting.as_ref().map(|rr| rr.iter().position(|s| s == "words")), - "typo_position": setting.as_ref().map(|rr| rr.iter().position(|s| s == "typo")), - "proximity_position": setting.as_ref().map(|rr| rr.iter().position(|s| s == "proximity")), - "attribute_position": setting.as_ref().map(|rr| rr.iter().position(|s| s == "attribute")), - "sort_position": setting.as_ref().map(|rr| rr.iter().position(|s| s == "sort")), - "exactness_position": setting.as_ref().map(|rr| rr.iter().position(|s| s == "exactness")), - "values": setting.as_ref().map(|rr| rr.iter().filter(|s| !s.contains(':')).cloned().collect::>().join(", ")), + "words_position": setting.as_ref().map(|rr| rr.iter().position(|s| matches!(s, meilisearch_types::settings::RankingRuleView::Words))), + "typo_position": setting.as_ref().map(|rr| rr.iter().position(|s| matches!(s, meilisearch_types::settings::RankingRuleView::Typo))), + "proximity_position": setting.as_ref().map(|rr| rr.iter().position(|s| matches!(s, meilisearch_types::settings::RankingRuleView::Proximity))), + "attribute_position": setting.as_ref().map(|rr| rr.iter().position(|s| matches!(s, meilisearch_types::settings::RankingRuleView::Attribute))), + "sort_position": setting.as_ref().map(|rr| rr.iter().position(|s| matches!(s, meilisearch_types::settings::RankingRuleView::Sort))), + "exactness_position": setting.as_ref().map(|rr| rr.iter().position(|s| matches!(s, meilisearch_types::settings::RankingRuleView::Exactness))), + "values": setting.as_ref().map(|rr| rr.iter().filter(|s| matches!(s, meilisearch_types::settings::RankingRuleView::Asc(_) | meilisearch_types::settings::RankingRuleView::Desc(_)) ).map(|x| x.to_string()).collect::>().join(", ")), } }), Some(req), @@ -428,66 +425,10 @@ generate_configure!( faceting ); -#[derive(Debug)] -pub struct SettingsDeserrError { - error: String, - code: Code, -} - -impl std::fmt::Display for SettingsDeserrError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.error) - } -} - -impl std::error::Error for SettingsDeserrError {} -impl ErrorCode for SettingsDeserrError { - fn error_code(&self) -> Code { - self.code - } -} - -impl deserr::MergeWithError for SettingsDeserrError { - fn merge( - _self_: Option, - other: SettingsDeserrError, - _merge_location: ValuePointerRef, - ) -> Result { - Err(other) - } -} - -impl deserr::DeserializeError for SettingsDeserrError { - fn error( - _self_: Option, - error: deserr::ErrorKind, - location: ValuePointerRef, - ) -> Result { - let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0; - - let code = match location.first_field() { - Some("displayedAttributes") => Code::InvalidSettingsDisplayedAttributes, - Some("searchableAttributes") => Code::InvalidSettingsSearchableAttributes, - Some("filterableAttributes") => Code::InvalidSettingsFilterableAttributes, - Some("sortableAttributes") => Code::InvalidSettingsSortableAttributes, - Some("rankingRules") => Code::InvalidSettingsRankingRules, - Some("stopWords") => Code::InvalidSettingsStopWords, - Some("synonyms") => Code::InvalidSettingsSynonyms, - Some("distinctAttribute") => Code::InvalidSettingsDistinctAttribute, - Some("typoTolerance") => Code::InvalidSettingsTypoTolerance, - Some("faceting") => Code::InvalidSettingsFaceting, - Some("pagination") => Code::InvalidSettingsPagination, - _ => Code::BadRequest, - }; - - Err(SettingsDeserrError { error, code }) - } -} - pub async fn update_all( index_scheduler: GuardedData, Data>, index_uid: web::Path, - body: ValidatedJson, SettingsDeserrError>, + body: ValidatedJson, DeserrError>, req: HttpRequest, analytics: web::Data, ) -> Result { @@ -497,13 +438,13 @@ pub async fn update_all( "Settings Updated".to_string(), json!({ "ranking_rules": { - "words_position": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().position(|s| s == "words")), - "typo_position": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().position(|s| s == "typo")), - "proximity_position": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().position(|s| s == "proximity")), - "attribute_position": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().position(|s| s == "attribute")), - "sort_position": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().position(|s| s == "sort")), - "exactness_position": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().position(|s| s == "exactness")), - "values": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().filter(|s| !s.contains(':')).cloned().collect::>().join(", ")), + "words_position": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().position(|s| matches!(s, RankingRuleView::Words))), + "typo_position": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().position(|s| matches!(s, RankingRuleView::Typo))), + "proximity_position": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().position(|s| matches!(s, RankingRuleView::Proximity))), + "attribute_position": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().position(|s| matches!(s, RankingRuleView::Attribute))), + "sort_position": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().position(|s| matches!(s, RankingRuleView::Sort))), + "exactness_position": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().position(|s| matches!(s, RankingRuleView::Exactness))), + "values": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().filter(|s| !matches!(s, RankingRuleView::Asc(_) | RankingRuleView::Desc(_)) ).map(|x| x.to_string()).collect::>().join(", ")), }, "searchable_attributes": { "total": new_settings.searchable_attributes.as_ref().set().map(|searchable| searchable.len()), diff --git a/meilisearch/src/routes/mod.rs b/meilisearch/src/routes/mod.rs index 27d72f1d7..f271467ba 100644 --- a/meilisearch/src/routes/mod.rs +++ b/meilisearch/src/routes/mod.rs @@ -6,7 +6,8 @@ use actix_web::{web, HttpRequest, HttpResponse}; use deserr::DeserializeFromValue; use index_scheduler::{IndexScheduler, Query}; use log::debug; -use meilisearch_types::error::ResponseError; +use meilisearch_types::error::deserr_codes::*; +use meilisearch_types::error::{DeserrError, ResponseError, TakeErrorMessage}; use meilisearch_types::settings::{Settings, Unchecked}; use meilisearch_types::star_or::StarOr; use meilisearch_types::tasks::{Kind, Status, Task, TaskId}; @@ -14,6 +15,7 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use time::OffsetDateTime; +use self::indexes::search::parse_usize_take_error_message; use self::indexes::IndexStats; use crate::analytics::Analytics; use crate::extractors::authentication::policies::*; @@ -57,6 +59,14 @@ where { 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; @@ -83,18 +93,27 @@ impl From for SummarizedTaskView { } } } +pub struct Pagination { + pub offset: usize, + pub limit: usize, +} #[derive(DeserializeFromValue, Deserialize, Debug, Clone, Copy)] -#[deserr(rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct Pagination { +pub struct ListIndexes { #[serde(default)] - #[deserr(default, from(&String) = FromStr::from_str -> std::num::ParseIntError)] + #[deserr(error = DeserrError, default, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] pub offset: usize, #[serde(default = "PAGINATION_DEFAULT_LIMIT")] - #[deserr(default = PAGINATION_DEFAULT_LIMIT(), from(&String) = FromStr::from_str -> std::num::ParseIntError)] + #[deserr(error = DeserrError, default = PAGINATION_DEFAULT_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] pub limit: usize, } +impl ListIndexes { + fn as_pagination(self) -> Pagination { + Pagination { offset: self.offset, limit: self.limit } + } +} #[derive(Debug, Clone, Serialize)] pub struct PaginationView { diff --git a/meilisearch/src/routes/swap_indexes.rs b/meilisearch/src/routes/swap_indexes.rs index c2b80b5c4..c5b371cd9 100644 --- a/meilisearch/src/routes/swap_indexes.rs +++ b/meilisearch/src/routes/swap_indexes.rs @@ -1,10 +1,9 @@ -use std::fmt; - use actix_web::web::Data; use actix_web::{web, HttpRequest, HttpResponse}; -use deserr::{DeserializeFromValue, IntoValue, ValuePointerRef}; +use deserr::DeserializeFromValue; use index_scheduler::IndexScheduler; -use meilisearch_types::error::{unwrap_any, Code, ErrorCode, ResponseError}; +use meilisearch_types::error::deserr_codes::InvalidSwapIndexes; +use meilisearch_types::error::{DeserrError, ResponseError}; use meilisearch_types::tasks::{IndexSwap, KindWithContent}; use serde_json::json; @@ -21,14 +20,15 @@ pub fn configure(cfg: &mut web::ServiceConfig) { } #[derive(DeserializeFromValue, Debug, Clone, PartialEq, Eq)] -#[deserr(rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] pub struct SwapIndexesPayload { + #[deserr(error = DeserrError)] indexes: Vec, } pub async fn swap_indexes( index_scheduler: GuardedData, Data>, - params: ValidatedJson, SwapIndexesDeserrError>, + params: ValidatedJson, DeserrError>, req: HttpRequest, analytics: web::Data, ) -> Result { @@ -62,49 +62,3 @@ pub async fn swap_indexes( let task: SummarizedTaskView = task.into(); Ok(HttpResponse::Accepted().json(task)) } - -#[derive(Debug)] -pub struct SwapIndexesDeserrError { - error: String, - code: Code, -} - -impl std::fmt::Display for SwapIndexesDeserrError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.error) - } -} - -impl std::error::Error for SwapIndexesDeserrError {} -impl ErrorCode for SwapIndexesDeserrError { - fn error_code(&self) -> Code { - self.code - } -} - -impl deserr::MergeWithError for SwapIndexesDeserrError { - fn merge( - _self_: Option, - other: SwapIndexesDeserrError, - _merge_location: ValuePointerRef, - ) -> Result { - Err(other) - } -} - -impl deserr::DeserializeError for SwapIndexesDeserrError { - fn error( - _self_: Option, - error: deserr::ErrorKind, - location: ValuePointerRef, - ) -> Result { - let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0; - - let code = match location.last_field() { - Some("indexes") => Code::InvalidSwapIndexes, - _ => Code::BadRequest, - }; - - Err(SwapIndexesDeserrError { error, code }) - } -} diff --git a/meilisearch/src/search.rs b/meilisearch/src/search.rs index 39af9bedb..12e18a09c 100644 --- a/meilisearch/src/search.rs +++ b/meilisearch/src/search.rs @@ -1,16 +1,11 @@ use std::cmp::min; use std::collections::{BTreeMap, BTreeSet, HashSet}; -use std::convert::Infallible; -use std::fmt; -use std::num::ParseIntError; -use std::str::{FromStr, ParseBoolError}; +use std::str::FromStr; use std::time::Instant; -use deserr::{ - DeserializeError, DeserializeFromValue, ErrorKind, IntoValue, MergeWithError, ValuePointerRef, -}; +use deserr::DeserializeFromValue; use either::Either; -use meilisearch_types::error::{unwrap_any, Code, ErrorCode}; +use meilisearch_types::error::{deserr_codes::*, DeserrError}; use meilisearch_types::settings::DEFAULT_PAGINATION_MAX_TOTAL_HITS; use meilisearch_types::{milli, Document}; use milli::tokenizer::TokenizerBuilder; @@ -34,32 +29,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(rename_all = camelCase, deny_unknown_fields)] +#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] pub struct SearchQuery { + #[deserr(error = DeserrError)] pub q: Option, - #[deserr(default = DEFAULT_SEARCH_OFFSET())] + #[deserr(error = DeserrError, default = DEFAULT_SEARCH_OFFSET())] pub offset: usize, - #[deserr(default = DEFAULT_SEARCH_LIMIT())] + #[deserr(error = DeserrError, default = DEFAULT_SEARCH_LIMIT())] pub limit: usize, + #[deserr(error = DeserrError)] pub page: Option, + #[deserr(error = DeserrError)] pub hits_per_page: Option, + #[deserr(error = DeserrError)] pub attributes_to_retrieve: Option>, + #[deserr(error = DeserrError)] pub attributes_to_crop: Option>, - #[deserr(default = DEFAULT_CROP_LENGTH())] + #[deserr(error = DeserrError, default = DEFAULT_CROP_LENGTH())] pub crop_length: usize, + #[deserr(error = DeserrError)] pub attributes_to_highlight: Option>, - #[deserr(default)] + #[deserr(error = DeserrError, default)] pub show_matches_position: bool, + #[deserr(error = DeserrError)] pub filter: Option, + #[deserr(error = DeserrError)] pub sort: Option>, + #[deserr(error = DeserrError)] pub facets: Option>, - #[deserr(default = DEFAULT_HIGHLIGHT_PRE_TAG())] + #[deserr(error = DeserrError, default = DEFAULT_HIGHLIGHT_PRE_TAG())] pub highlight_pre_tag: String, - #[deserr(default = DEFAULT_HIGHLIGHT_POST_TAG())] + #[deserr(error = DeserrError, default = DEFAULT_HIGHLIGHT_POST_TAG())] pub highlight_post_tag: String, - #[deserr(default = DEFAULT_CROP_MARKER())] + #[deserr(error = DeserrError, default = DEFAULT_CROP_MARKER())] pub crop_marker: String, - #[deserr(default)] + #[deserr(error = DeserrError, default)] pub matching_strategy: MatchingStrategy, } @@ -94,96 +98,6 @@ impl From for TermsMatchingStrategy { } } -#[derive(Debug)] -pub struct SearchDeserError { - error: String, - code: Code, -} - -impl std::fmt::Display for SearchDeserError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.error) - } -} - -impl std::error::Error for SearchDeserError {} -impl ErrorCode for SearchDeserError { - fn error_code(&self) -> Code { - self.code - } -} - -impl MergeWithError for SearchDeserError { - fn merge( - _self_: Option, - other: SearchDeserError, - _merge_location: ValuePointerRef, - ) -> Result { - Err(other) - } -} - -impl DeserializeError for SearchDeserError { - fn error( - _self_: Option, - error: ErrorKind, - location: ValuePointerRef, - ) -> Result { - let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0; - - let code = match location.last_field() { - Some("q") => Code::InvalidSearchQ, - Some("offset") => Code::InvalidSearchOffset, - Some("limit") => Code::InvalidSearchLimit, - Some("page") => Code::InvalidSearchPage, - Some("hitsPerPage") => Code::InvalidSearchHitsPerPage, - Some("attributesToRetrieve") => Code::InvalidSearchAttributesToRetrieve, - Some("attributesToCrop") => Code::InvalidSearchAttributesToCrop, - Some("cropLength") => Code::InvalidSearchCropLength, - Some("attributesToHighlight") => Code::InvalidSearchAttributesToHighlight, - Some("showMatchesPosition") => Code::InvalidSearchShowMatchesPosition, - Some("filter") => Code::InvalidSearchFilter, - Some("sort") => Code::InvalidSearchSort, - Some("facets") => Code::InvalidSearchFacets, - Some("highlightPreTag") => Code::InvalidSearchHighlightPreTag, - Some("highlightPostTag") => Code::InvalidSearchHighlightPostTag, - Some("cropMarker") => Code::InvalidSearchCropMarker, - Some("matchingStrategy") => Code::InvalidSearchMatchingStrategy, - _ => Code::BadRequest, - }; - - Err(SearchDeserError { error, code }) - } -} - -impl MergeWithError for SearchDeserError { - fn merge( - _self_: Option, - other: ParseBoolError, - merge_location: ValuePointerRef, - ) -> Result { - SearchDeserError::error::( - None, - ErrorKind::Unexpected { msg: other.to_string() }, - merge_location, - ) - } -} - -impl MergeWithError for SearchDeserError { - fn merge( - _self_: Option, - other: ParseIntError, - merge_location: ValuePointerRef, - ) -> Result { - SearchDeserError::error::( - None, - ErrorKind::Unexpected { msg: other.to_string() }, - merge_location, - ) - } -} - #[derive(Debug, Clone, Serialize, PartialEq, Eq)] pub struct SearchHit { #[serde(flatten)] diff --git a/meilisearch/tests/auth/api_keys.rs b/meilisearch/tests/auth/api_keys.rs index 72f7cdff1..7b49c65b9 100644 --- a/meilisearch/tests/auth/api_keys.rs +++ b/meilisearch/tests/auth/api_keys.rs @@ -1,6 +1,5 @@ use std::{thread, time}; -use assert_json_diff::assert_json_include; use serde_json::{json, Value}; use crate::common::Server; @@ -34,37 +33,36 @@ async fn add_valid_api_key() { }); let (response, code) = server.add_api_key(content).await; - assert_eq!(201, code, "{:?}", &response); - assert!(response["key"].is_string()); - assert!(response["expiresAt"].is_string()); - assert!(response["createdAt"].is_string()); - assert!(response["updatedAt"].is_string()); - - let expected_response = json!({ - "name": "indexing-key", - "description": "Indexing API key", - "uid": "4bc0887a-0e41-4f3b-935d-0c451dcee9c8", - "key": "d9e776b8412f1db6974c9a5556b961c3559440b6588216f4ea5d9ed49f7c8f3c", - "indexes": ["products"], - "actions": [ - "search", - "documents.add", - "documents.get", - "documents.delete", - "indexes.create", - "indexes.get", - "indexes.update", - "indexes.delete", - "tasks.get", - "settings.get", - "settings.update", - "stats.get", - "dumps.create", - ], - "expiresAt": "2050-11-13T00:00:00Z" - }); - - assert_json_include!(actual: response, expected: expected_response); + meili_snap::insta::assert_debug_snapshot!(code, @"201"); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "name": "indexing-key", + "description": "Indexing API key", + "key": "d9e776b8412f1db6974c9a5556b961c3559440b6588216f4ea5d9ed49f7c8f3c", + "uid": "4bc0887a-0e41-4f3b-935d-0c451dcee9c8", + "actions": [ + "search", + "documents.add", + "documents.get", + "documents.delete", + "indexes.create", + "indexes.get", + "indexes.update", + "indexes.delete", + "tasks.get", + "settings.get", + "settings.update", + "stats.get", + "dumps.create" + ], + "indexes": [ + "products" + ], + "expiresAt": "2050-11-13T00:00:00Z", + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + } + "###); } #[actix_rt::test] @@ -94,34 +92,36 @@ async fn add_valid_api_key_expired_at() { }); let (response, code) = server.add_api_key(content).await; - assert_eq!(201, code, "{:?}", &response); - assert!(response["key"].is_string()); - assert!(response["expiresAt"].is_string()); - assert!(response["createdAt"].is_string()); - assert!(response["updatedAt"].is_string()); - - let expected_response = json!({ - "description": "Indexing API key", - "indexes": ["products"], - "actions": [ - "search", - "documents.add", - "documents.get", - "documents.delete", - "indexes.create", - "indexes.get", - "indexes.update", - "indexes.delete", - "tasks.get", - "settings.get", - "settings.update", - "stats.get", - "dumps.create", - ], - "expiresAt": "2050-11-13T00:00:00Z" - }); - - assert_json_include!(actual: response, expected: expected_response); + meili_snap::insta::assert_debug_snapshot!(code, @"201"); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }, @r###" + { + "name": null, + "description": "Indexing API key", + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "search", + "documents.add", + "documents.get", + "documents.delete", + "indexes.create", + "indexes.get", + "indexes.update", + "indexes.delete", + "tasks.get", + "settings.get", + "settings.update", + "stats.get", + "dumps.create" + ], + "indexes": [ + "products" + ], + "expiresAt": "2050-11-13T00:00:00Z", + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + } + "###); } #[actix_rt::test] @@ -136,21 +136,24 @@ async fn add_valid_api_key_no_description() { }); let (response, code) = server.add_api_key(content).await; - assert_eq!(201, code, "{:?}", &response); - assert!(response["key"].is_string()); - assert!(response["expiresAt"].is_string()); - assert!(response["createdAt"].is_string()); - assert!(response["updatedAt"].is_string()); - - let expected_response = json!({ - "actions": ["documents.add"], - "indexes": [ - "products" - ], - "expiresAt": "2050-11-13T00:00:00Z" - }); - - assert_json_include!(actual: response, expected: expected_response); + meili_snap::insta::assert_debug_snapshot!(code, @"201"); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }, @r###" + { + "name": null, + "description": null, + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "documents.add" + ], + "indexes": [ + "products" + ], + "expiresAt": "2050-11-13T00:00:00Z", + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + } + "###); } #[actix_rt::test] @@ -166,21 +169,24 @@ async fn add_valid_api_key_null_description() { }); let (response, code) = server.add_api_key(content).await; - assert_eq!(201, code, "{:?}", &response); - assert!(response["key"].is_string()); - assert!(response["expiresAt"].is_string()); - assert!(response["createdAt"].is_string()); - assert!(response["updatedAt"].is_string()); - - let expected_response = json!({ - "actions": ["documents.add"], - "indexes": [ - "products" - ], - "expiresAt": "2050-11-13T00:00:00Z" - }); - - assert_json_include!(actual: response, expected: expected_response); + meili_snap::insta::assert_debug_snapshot!(code, @"201"); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }, @r###" + { + "name": null, + "description": null, + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "documents.add" + ], + "indexes": [ + "products" + ], + "expiresAt": "2050-11-13T00:00:00Z", + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + } + "###); } #[actix_rt::test] @@ -193,16 +199,15 @@ async fn error_add_api_key_no_header() { "expiresAt": "2050-11-13T00:00:00Z" }); let (response, code) = server.add_api_key(content).await; - assert_eq!(401, code, "{:?}", &response); - - let expected_response = json!({ - "message": "The Authorization header is missing. It must use the bearer authorization method.", - "code": "missing_authorization_header", - "type": "auth", - "link": "https://docs.meilisearch.com/errors#missing-authorization-header" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_debug_snapshot!(code, @"401"); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "The Authorization header is missing. It must use the bearer authorization method.", + "code": "missing_authorization_header", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#missing-authorization-header" + } + "###); } #[actix_rt::test] @@ -217,16 +222,15 @@ async fn error_add_api_key_bad_key() { "expiresAt": "2050-11-13T00:00:00Z" }); let (response, code) = server.add_api_key(content).await; - assert_eq!(403, code, "{:?}", &response); - - let expected_response = json!({ - "message": "The provided API key is invalid.", - "code": "invalid_api_key", - "type": "auth", - "link": "https://docs.meilisearch.com/errors#invalid-api-key" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_debug_snapshot!(code, @"403"); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "The provided API key is invalid.", + "code": "invalid_api_key", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#invalid-api-key" + } + "###); } #[actix_rt::test] @@ -241,16 +245,15 @@ async fn error_add_api_key_missing_parameter() { "expiresAt": "2050-11-13T00:00:00Z" }); let (response, code) = server.add_api_key(content).await; - assert_eq!(400, code, "{:?}", &response); - - let expected_response = json!({ - "message": "`indexes` field is mandatory.", - "code": "missing_api_key_indexes", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#missing-api-key-indexes" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_debug_snapshot!(code, @"400"); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "Json deserialize error: missing field `indexes` at ``", + "code": "bad_request", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#bad-request" + } + "###); // missing actions let content = json!({ @@ -259,16 +262,15 @@ async fn error_add_api_key_missing_parameter() { "expiresAt": "2050-11-13T00:00:00Z" }); let (response, code) = server.add_api_key(content).await; - assert_eq!(400, code, "{:?}", &response); - - let expected_response = json!({ - "message": "`actions` field is mandatory.", - "code": "missing_api_key_actions", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#missing-api-key-actions" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_debug_snapshot!(code, @"400"); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "Json deserialize error: missing field `actions` at ``", + "code": "bad_request", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#bad-request" + } + "###); // missing expiration date let content = json!({ @@ -277,16 +279,24 @@ async fn error_add_api_key_missing_parameter() { "actions": ["documents.add"], }); let (response, code) = server.add_api_key(content).await; - assert_eq!(400, code, "{:?}", &response); - - let expected_response = json!({ - "message": "`expiresAt` field is mandatory.", - "code": "missing_api_key_expires_at", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#missing-api-key-expires-at" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_debug_snapshot!(code, @"201"); + meili_snap::insta::assert_json_snapshot!(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]" + } + "###); } #[actix_rt::test] @@ -301,16 +311,15 @@ async fn error_add_api_key_invalid_parameters_description() { "expiresAt": "2050-11-13T00:00:00Z" }); let (response, code) = server.add_api_key(content).await; - assert_eq!(400, code, "{:?}", &response); - - let expected_response = json!({ - "message": r#"`description` field value `{"name":"products"}` is invalid. It should be a string or specified as a null value."#, - "code": "invalid_api_key_description", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-api-key-description" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_debug_snapshot!(code, @"400"); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "invalid type: Map `{\"name\":\"products\"}`, expected a String at `.description`.", + "code": "invalid_api_key_description", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-api-key-description" + } + "###); } #[actix_rt::test] @@ -325,16 +334,15 @@ async fn error_add_api_key_invalid_parameters_name() { "expiresAt": "2050-11-13T00:00:00Z" }); let (response, code) = server.add_api_key(content).await; - assert_eq!(400, code, "{:?}", &response); - - let expected_response = json!({ - "message": r#"`name` field value `{"name":"products"}` is invalid. It should be a string or specified as a null value."#, - "code": "invalid_api_key_name", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-api-key-name" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_debug_snapshot!(code, @"400"); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "invalid type: Map `{\"name\":\"products\"}`, expected a String at `.name`.", + "code": "invalid_api_key_name", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-api-key-name" + } + "###); } #[actix_rt::test] @@ -349,16 +357,15 @@ async fn error_add_api_key_invalid_parameters_indexes() { "expiresAt": "2050-11-13T00:00:00Z" }); let (response, code) = server.add_api_key(content).await; - assert_eq!(400, code, "{:?}", &response); - - let expected_response = json!({ - "message": r#"`indexes` field value `{"name":"products"}` is invalid. It should be an array of string representing index names."#, - "code": "invalid_api_key_indexes", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-api-key-indexes" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_debug_snapshot!(code, @"400"); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "invalid type: Map `{\"name\":\"products\"}`, expected a Sequence at `.indexes`.", + "code": "invalid_api_key_indexes", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-api-key-indexes" + } + "###); } #[actix_rt::test] @@ -376,15 +383,15 @@ async fn error_add_api_key_invalid_index_uids() { }); let (response, code) = server.add_api_key(content).await; - let expected_response = json!({ - "message": r#"`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" - }); - - assert_eq!(response, expected_response); - assert_eq!(code, 400); + meili_snap::insta::assert_debug_snapshot!(code, @"400"); + meili_snap::insta::assert_json_snapshot!(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]`.", + "code": "invalid_api_key_indexes", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-api-key-indexes" + } + "###); } #[actix_rt::test] @@ -401,14 +408,15 @@ async fn error_add_api_key_invalid_parameters_actions() { let (response, code) = server.add_api_key(content).await; assert_eq!(400, code, "{:?}", &response); - let expected_response = json!({ - "message": r#"`actions` field value `{"name":"products"}` is invalid. It should be an array of string representing action names."#, - "code": "invalid_api_key_actions", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-api-key-actions" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_debug_snapshot!(code, @"400"); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "invalid type: Map `{\"name\":\"products\"}`, expected a Sequence at `.actions`.", + "code": "invalid_api_key_actions", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-api-key-actions" + } + "###); let content = json!({ "description": "Indexing API key", @@ -419,16 +427,16 @@ async fn error_add_api_key_invalid_parameters_actions() { "expiresAt": "2050-11-13T00:00:00Z" }); let (response, code) = server.add_api_key(content).await; - assert_eq!(400, code, "{:?}", &response); - let expected_response = json!({ - "message": r#"`actions` field value `["doc.add"]` is invalid. It should be an array of string representing action names."#, - "code": "invalid_api_key_actions", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-api-key-actions" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_debug_snapshot!(code, @"400"); + meili_snap::insta::assert_json_snapshot!(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]`.", + "code": "invalid_api_key_actions", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-api-key-actions" + } + "###); } #[actix_rt::test] @@ -443,16 +451,16 @@ async fn error_add_api_key_invalid_parameters_expires_at() { "expiresAt": {"name":"products"} }); let (response, code) = server.add_api_key(content).await; - assert_eq!(400, code, "{:?}", &response); - let expected_response = json!({ - "message": r#"`expiresAt` field value `{"name":"products"}` is invalid. 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'."#, - "code": "invalid_api_key_expires_at", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-api-key-expires-at" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_debug_snapshot!(code, @"400"); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "invalid type: Map `{\"name\":\"products\"}`, expected a String at `.expiresAt`.", + "code": "invalid_api_key_expires_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-api-key-expires-at" + } + "###); } #[actix_rt::test] @@ -467,16 +475,16 @@ async fn error_add_api_key_invalid_parameters_expires_at_in_the_past() { "expiresAt": "2010-11-13T00:00:00Z" }); let (response, code) = server.add_api_key(content).await; - assert_eq!(400, code, "{:?}", &response); - let expected_response = json!({ - "message": r#"`expiresAt` field value `"2010-11-13T00:00:00Z"` is invalid. 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'."#, - "code": "invalid_api_key_expires_at", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-api-key-expires-at" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_json_snapshot!(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`.", + "code": "invalid_api_key_expires_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-api-key-expires-at" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"400"); } #[actix_rt::test] @@ -492,16 +500,16 @@ async fn error_add_api_key_invalid_parameters_uid() { "expiresAt": "2050-11-13T00:00:00Z" }); let (response, code) = server.add_api_key(content).await; - assert_eq!(400, code, "{:?}", &response); - let expected_response = json!({ - "message": r#"`uid` field value `"aaaaabbbbbccc"` is invalid. It should be a valid UUID v4 string or omitted."#, - "code": "invalid_api_key_uid", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-api-key-uid" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "invalid length: expected length 32 for simple format, found 13 at `.uid`.", + "code": "invalid_api_key_uid", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-api-key-uid" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"400"); } #[actix_rt::test] @@ -517,20 +525,36 @@ async fn error_add_api_key_parameters_uid_already_exist() { // first creation is valid. let (response, code) = server.add_api_key(content.clone()).await; - assert_eq!(201, code, "{:?}", &response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "name": null, + "description": null, + "key": "d9e776b8412f1db6974c9a5556b961c3559440b6588216f4ea5d9ed49f7c8f3c", + "uid": "4bc0887a-0e41-4f3b-935d-0c451dcee9c8", + "actions": [ + "search" + ], + "indexes": [ + "products" + ], + "expiresAt": "2050-11-13T00:00:00Z", + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"201"); // uid already exist. let (response, code) = server.add_api_key(content).await; - assert_eq!(409, code, "{:?}", &response); - - let expected_response = json!({ - "message": "`uid` field value `4bc0887a-0e41-4f3b-935d-0c451dcee9c8` is already an existing API key.", - "code": "api_key_already_exists", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#api-key-already-exists" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "`uid` field value `4bc0887a-0e41-4f3b-935d-0c451dcee9c8` is already an existing API key.", + "code": "api_key_already_exists", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#api-key-already-exists" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"409"); } #[actix_rt::test] @@ -562,51 +586,103 @@ async fn get_api_key() { }); let (response, code) = server.add_api_key(content).await; - // must pass if add_valid_api_key test passes. - assert_eq!(201, code, "{:?}", &response); - assert!(response["key"].is_string()); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "name": null, + "description": "Indexing API key", + "key": "d9e776b8412f1db6974c9a5556b961c3559440b6588216f4ea5d9ed49f7c8f3c", + "uid": "4bc0887a-0e41-4f3b-935d-0c451dcee9c8", + "actions": [ + "search", + "documents.add", + "documents.get", + "documents.delete", + "indexes.create", + "indexes.get", + "indexes.update", + "indexes.delete", + "tasks.get", + "settings.get", + "settings.update", + "stats.get", + "dumps.create" + ], + "indexes": [ + "products" + ], + "expiresAt": "2050-11-13T00:00:00Z", + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"201"); let key = response["key"].as_str().unwrap(); - let expected_response = json!({ - "description": "Indexing API key", - "indexes": ["products"], - "uid": uid.to_string(), - "actions": [ - "search", - "documents.add", - "documents.get", - "documents.delete", - "indexes.create", - "indexes.get", - "indexes.update", - "indexes.delete", - "tasks.get", - "settings.get", - "settings.update", - "stats.get", - "dumps.create", - ], - "expiresAt": "2050-11-13T00:00:00Z" - }); - // get with uid let (response, code) = server.get_api_key(&uid).await; - assert_eq!(200, code, "{:?}", &response); - assert!(response["key"].is_string()); - assert!(response["expiresAt"].is_string()); - assert!(response["createdAt"].is_string()); - assert!(response["updatedAt"].is_string()); - assert_json_include!(actual: response, expected: &expected_response); - + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "name": null, + "description": "Indexing API key", + "key": "d9e776b8412f1db6974c9a5556b961c3559440b6588216f4ea5d9ed49f7c8f3c", + "uid": "4bc0887a-0e41-4f3b-935d-0c451dcee9c8", + "actions": [ + "search", + "documents.add", + "documents.get", + "documents.delete", + "indexes.create", + "indexes.get", + "indexes.update", + "indexes.delete", + "tasks.get", + "settings.get", + "settings.update", + "stats.get", + "dumps.create" + ], + "indexes": [ + "products" + ], + "expiresAt": "2050-11-13T00:00:00Z", + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"200"); // get with key let (response, code) = server.get_api_key(&key).await; - assert_eq!(200, code, "{:?}", &response); - assert!(response["key"].is_string()); - assert!(response["expiresAt"].is_string()); - assert!(response["createdAt"].is_string()); - assert!(response["updatedAt"].is_string()); - assert_json_include!(actual: response, expected: &expected_response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "name": null, + "description": "Indexing API key", + "key": "d9e776b8412f1db6974c9a5556b961c3559440b6588216f4ea5d9ed49f7c8f3c", + "uid": "4bc0887a-0e41-4f3b-935d-0c451dcee9c8", + "actions": [ + "search", + "documents.add", + "documents.get", + "documents.delete", + "indexes.create", + "indexes.get", + "indexes.update", + "indexes.delete", + "tasks.get", + "settings.get", + "settings.update", + "stats.get", + "dumps.create" + ], + "indexes": [ + "products" + ], + "expiresAt": "2050-11-13T00:00:00Z", + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"200"); } #[actix_rt::test] @@ -616,16 +692,15 @@ async fn error_get_api_key_no_header() { let (response, code) = server .get_api_key("d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4") .await; - assert_eq!(401, code, "{:?}", &response); - - let expected_response = json!({ - "message": "The Authorization header is missing. It must use the bearer authorization method.", - "code": "missing_authorization_header", - "type": "auth", - "link": "https://docs.meilisearch.com/errors#missing-authorization-header" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "The Authorization header is missing. It must use the bearer authorization method.", + "code": "missing_authorization_header", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#missing-authorization-header" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"401"); } #[actix_rt::test] @@ -636,16 +711,15 @@ async fn error_get_api_key_bad_key() { let (response, code) = server .get_api_key("d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4") .await; - assert_eq!(403, code, "{:?}", &response); - - let expected_response = json!({ - "message": "The provided API key is invalid.", - "code": "invalid_api_key", - "type": "auth", - "link": "https://docs.meilisearch.com/errors#invalid-api-key" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "The provided API key is invalid.", + "code": "invalid_api_key", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#invalid-api-key" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"403"); } #[actix_rt::test] @@ -656,16 +730,15 @@ async fn error_get_api_key_not_found() { let (response, code) = server .get_api_key("d0552b41d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4") .await; - assert_eq!(404, code, "{:?}", &response); - - let expected_response = json!({ - "message": "API key `d0552b41d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4` not found.", - "code": "api_key_not_found", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#api-key-not-found" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "API key `d0552b41d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4` not found.", + "code": "api_key_not_found", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#api-key-not-found" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"404"); } #[actix_rt::test] @@ -695,55 +768,105 @@ async fn list_api_keys() { }); let (response, code) = server.add_api_key(content).await; - // must pass if add_valid_api_key test passes. - assert_eq!(201, code, "{:?}", &response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }, @r###" + { + "name": null, + "description": "Indexing API key", + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "search", + "documents.add", + "documents.get", + "documents.delete", + "indexes.create", + "indexes.get", + "indexes.update", + "indexes.delete", + "tasks.get", + "settings.get", + "settings.update", + "stats.get", + "dumps.create" + ], + "indexes": [ + "products" + ], + "expiresAt": "2050-11-13T00:00:00Z", + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"201"); let (response, code) = server.list_api_keys().await; - assert_eq!(200, code, "{:?}", &response); - - let expected_response = json!({ "results": - [ - { - "description": "Indexing API key", - "indexes": ["products"], - "actions": [ - "search", - "documents.add", - "documents.get", - "documents.delete", - "indexes.create", - "indexes.get", - "indexes.update", - "indexes.delete", - "tasks.get", - "settings.get", - "settings.update", - "stats.get", - "dumps.create", - ], - "expiresAt": "2050-11-13T00:00:00Z" - }, - { - "name": "Default Search API Key", - "description": "Use it to search from the frontend", - "indexes": ["*"], - "actions": ["search"], - "expiresAt": serde_json::Value::Null, - }, - { - "name": "Default Admin API Key", - "description": "Use it for anything that is not a search operation. Caution! Do not expose it on a public frontend", - "indexes": ["*"], - "actions": ["*"], - "expiresAt": serde_json::Value::Null, - } - ], - "limit": 20, - "offset": 0, - "total": 3, - }); - - assert_json_include!(actual: response, expected: expected_response); + meili_snap::insta::assert_json_snapshot!(response, { ".results[].createdAt" => "[ignored]", ".results[].updatedAt" => "[ignored]", ".results[].uid" => "[ignored]", ".results[].key" => "[ignored]" }, @r###" + { + "results": [ + { + "name": null, + "description": "Indexing API key", + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "search", + "documents.add", + "documents.get", + "documents.delete", + "indexes.create", + "indexes.get", + "indexes.update", + "indexes.delete", + "tasks.get", + "settings.get", + "settings.update", + "stats.get", + "dumps.create" + ], + "indexes": [ + "products" + ], + "expiresAt": "2050-11-13T00:00:00Z", + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + }, + { + "name": "Default Search API Key", + "description": "Use it to search from the frontend", + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "search" + ], + "indexes": [ + "*" + ], + "expiresAt": null, + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + }, + { + "name": "Default Admin API Key", + "description": "Use it for anything that is not a search operation. Caution! Do not expose it on a public frontend", + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "*" + ], + "indexes": [ + "*" + ], + "expiresAt": null, + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + } + ], + "offset": 0, + "limit": 20, + "total": 3 + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"200"); } #[actix_rt::test] @@ -751,16 +874,15 @@ async fn error_list_api_keys_no_header() { let server = Server::new_auth().await; let (response, code) = server.list_api_keys().await; - assert_eq!(401, code, "{:?}", &response); - - let expected_response = json!({ - "message": "The Authorization header is missing. It must use the bearer authorization method.", - "code": "missing_authorization_header", - "type": "auth", - "link": "https://docs.meilisearch.com/errors#missing-authorization-header" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "The Authorization header is missing. It must use the bearer authorization method.", + "code": "missing_authorization_header", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#missing-authorization-header" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"401"); } #[actix_rt::test] @@ -769,16 +891,15 @@ async fn error_list_api_keys_bad_key() { server.use_api_key("d4000bd7225f77d1eb22cc706ed36772bbc36767c016a27f76def7537b68600d"); let (response, code) = server.list_api_keys().await; - assert_eq!(403, code, "{:?}", &response); - - let expected_response = json!({ - "message": "The provided API key is invalid.", - "code": "invalid_api_key", - "type": "auth", - "link": "https://docs.meilisearch.com/errors#invalid-api-key" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "The provided API key is invalid.", + "code": "invalid_api_key", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#invalid-api-key" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"403"); } #[actix_rt::test] @@ -808,18 +929,54 @@ async fn delete_api_key() { }); let (response, code) = server.add_api_key(content).await; - // must pass if add_valid_api_key test passes. - assert_eq!(201, code, "{:?}", &response); - assert!(response["key"].is_string()); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }, @r###" + { + "name": null, + "description": "Indexing API key", + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "search", + "documents.add", + "documents.get", + "documents.delete", + "indexes.create", + "indexes.get", + "indexes.update", + "indexes.delete", + "tasks.get", + "settings.get", + "settings.update", + "stats.get", + "dumps.create" + ], + "indexes": [ + "products" + ], + "expiresAt": "2050-11-13T00:00:00Z", + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"201"); let uid = response["uid"].as_str().unwrap(); let (response, code) = server.delete_api_key(&uid).await; - assert_eq!(204, code, "{:?}", &response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @"null"); + meili_snap::insta::assert_debug_snapshot!(code, @"204"); // check if API key no longer exist. let (response, code) = server.get_api_key(&uid).await; - assert_eq!(404, code, "{:?}", &response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".message" => "[ignored]" }, @r###" + { + "message": "[ignored]", + "code": "api_key_not_found", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#api-key-not-found" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"404"); } #[actix_rt::test] @@ -829,16 +986,16 @@ async fn error_delete_api_key_no_header() { let (response, code) = server .delete_api_key("d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4") .await; - assert_eq!(401, code, "{:?}", &response); - let expected_response = json!({ - "message": "The Authorization header is missing. It must use the bearer authorization method.", - "code": "missing_authorization_header", - "type": "auth", - "link": "https://docs.meilisearch.com/errors#missing-authorization-header" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "The Authorization header is missing. It must use the bearer authorization method.", + "code": "missing_authorization_header", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#missing-authorization-header" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"401"); } #[actix_rt::test] @@ -849,16 +1006,15 @@ async fn error_delete_api_key_bad_key() { let (response, code) = server .delete_api_key("d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4") .await; - assert_eq!(403, code, "{:?}", &response); - - let expected_response = json!({ - "message": "The provided API key is invalid.", - "code": "invalid_api_key", - "type": "auth", - "link": "https://docs.meilisearch.com/errors#invalid-api-key" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "The provided API key is invalid.", + "code": "invalid_api_key", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#invalid-api-key" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"403"); } #[actix_rt::test] @@ -869,16 +1025,15 @@ async fn error_delete_api_key_not_found() { let (response, code) = server .delete_api_key("d0552b41d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4") .await; - assert_eq!(404, code, "{:?}", &response); - - let expected_response = json!({ - "message": "API key `d0552b41d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4` not found.", - "code": "api_key_not_found", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#api-key-not-found" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "API key `d0552b41d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4` not found.", + "code": "api_key_not_found", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#api-key-not-found" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"404"); } #[actix_rt::test] @@ -904,104 +1059,132 @@ async fn patch_api_key_description() { }); let (response, code) = server.add_api_key(content).await; - // must pass if add_valid_api_key test passes. - assert_eq!(201, code, "{:?}", &response); - assert!(response["key"].is_string()); - assert!(response["createdAt"].is_string()); - assert!(response["updatedAt"].is_string()); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }, @r###" + { + "name": null, + "description": null, + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "search", + "documents.add", + "documents.get", + "documents.delete", + "indexes.create", + "indexes.get", + "indexes.update", + "indexes.delete", + "stats.get", + "dumps.create" + ], + "indexes": [ + "products" + ], + "expiresAt": "2050-11-13T00:00:00Z", + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"201"); let uid = response["uid"].as_str().unwrap(); - let created_at = response["createdAt"].as_str().unwrap(); - let updated_at = response["updatedAt"].as_str().unwrap(); // Add a description let content = json!({ "description": "Indexing API key" }); thread::sleep(time::Duration::new(1, 0)); let (response, code) = server.patch_api_key(&uid, content).await; - assert_eq!(200, code, "{:?}", &response); - assert!(response["key"].is_string()); - assert!(response["expiresAt"].is_string()); - assert!(response["createdAt"].is_string()); - assert_ne!(response["updatedAt"].as_str().unwrap(), updated_at); - assert_eq!(response["createdAt"].as_str().unwrap(), created_at); - - let expected = json!({ - "description": "Indexing API key", - "indexes": ["products"], - "actions": [ - "search", - "documents.add", - "documents.get", - "documents.delete", - "indexes.create", - "indexes.get", - "indexes.update", - "indexes.delete", - "stats.get", - "dumps.create", - ], - "expiresAt": "2050-11-13T00:00:00Z" - }); - - assert_json_include!(actual: response, expected: expected); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }, @r###" + { + "name": null, + "description": "Indexing API key", + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "search", + "documents.add", + "documents.get", + "documents.delete", + "indexes.create", + "indexes.get", + "indexes.update", + "indexes.delete", + "stats.get", + "dumps.create" + ], + "indexes": [ + "products" + ], + "expiresAt": "2050-11-13T00:00:00Z", + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"200"); // Change the description let content = json!({ "description": "Product API key" }); let (response, code) = server.patch_api_key(&uid, content).await; - assert_eq!(200, code, "{:?}", &response); - assert!(response["key"].is_string()); - assert!(response["expiresAt"].is_string()); - assert!(response["createdAt"].is_string()); - - let expected = json!({ - "description": "Product API key", - "indexes": ["products"], - "actions": [ - "search", - "documents.add", - "documents.get", - "documents.delete", - "indexes.create", - "indexes.get", - "indexes.update", - "indexes.delete", - "stats.get", - "dumps.create", - ], - "expiresAt": "2050-11-13T00:00:00Z" - }); - - assert_json_include!(actual: response, expected: expected); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }, @r###" + { + "name": null, + "description": "Product API key", + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "search", + "documents.add", + "documents.get", + "documents.delete", + "indexes.create", + "indexes.get", + "indexes.update", + "indexes.delete", + "stats.get", + "dumps.create" + ], + "indexes": [ + "products" + ], + "expiresAt": "2050-11-13T00:00:00Z", + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"200"); // Remove the description let content = json!({ "description": serde_json::Value::Null }); let (response, code) = server.patch_api_key(&uid, content).await; - assert_eq!(200, code, "{:?}", &response); - assert!(response["key"].is_string()); - assert!(response["expiresAt"].is_string()); - assert!(response["createdAt"].is_string()); - - let expected = json!({ - "indexes": ["products"], - "actions": [ - "search", - "documents.add", - "documents.get", - "documents.delete", - "indexes.create", - "indexes.get", - "indexes.update", - "indexes.delete", - "stats.get", - "dumps.create", - ], - "expiresAt": "2050-11-13T00:00:00Z" - }); - - assert_json_include!(actual: response, expected: expected); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }, @r###" + { + "name": null, + "description": null, + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "search", + "documents.add", + "documents.get", + "documents.delete", + "indexes.create", + "indexes.get", + "indexes.update", + "indexes.delete", + "stats.get", + "dumps.create" + ], + "indexes": [ + "products" + ], + "expiresAt": "2050-11-13T00:00:00Z", + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"200"); } #[actix_rt::test] @@ -1027,11 +1210,33 @@ async fn patch_api_key_name() { }); let (response, code) = server.add_api_key(content).await; - // must pass if add_valid_api_key test passes. - assert_eq!(201, code, "{:?}", &response); - assert!(response["key"].is_string()); - assert!(response["createdAt"].is_string()); - assert!(response["updatedAt"].is_string()); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }, @r###" + { + "name": null, + "description": null, + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "search", + "documents.add", + "documents.get", + "documents.delete", + "indexes.create", + "indexes.get", + "indexes.update", + "indexes.delete", + "stats.get", + "dumps.create" + ], + "indexes": [ + "products" + ], + "expiresAt": "2050-11-13T00:00:00Z", + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"201"); let uid = response["uid"].as_str().unwrap(); let created_at = response["createdAt"].as_str().unwrap(); @@ -1042,89 +1247,100 @@ async fn patch_api_key_name() { thread::sleep(time::Duration::new(1, 0)); let (response, code) = server.patch_api_key(&uid, content).await; - assert_eq!(200, code, "{:?}", &response); - assert!(response["key"].is_string()); - assert!(response["expiresAt"].is_string()); - assert!(response["createdAt"].is_string()); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }, @r###" + { + "name": "Indexing API key", + "description": null, + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "search", + "documents.add", + "documents.get", + "documents.delete", + "indexes.create", + "indexes.get", + "indexes.update", + "indexes.delete", + "stats.get", + "dumps.create" + ], + "indexes": [ + "products" + ], + "expiresAt": "2050-11-13T00:00:00Z", + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"200"); + assert_ne!(response["updatedAt"].as_str().unwrap(), updated_at); assert_eq!(response["createdAt"].as_str().unwrap(), created_at); - let expected = json!({ - "name": "Indexing API key", - "indexes": ["products"], - "actions": [ - "search", - "documents.add", - "documents.get", - "documents.delete", - "indexes.create", - "indexes.get", - "indexes.update", - "indexes.delete", - "stats.get", - "dumps.create", - ], - "expiresAt": "2050-11-13T00:00:00Z" - }); - - assert_json_include!(actual: response, expected: expected); - // Change the name let content = json!({ "name": "Product API key" }); let (response, code) = server.patch_api_key(&uid, content).await; - assert_eq!(200, code, "{:?}", &response); - assert!(response["key"].is_string()); - assert!(response["expiresAt"].is_string()); - assert!(response["createdAt"].is_string()); - - let expected = json!({ - "name": "Product API key", - "indexes": ["products"], - "actions": [ - "search", - "documents.add", - "documents.get", - "documents.delete", - "indexes.create", - "indexes.get", - "indexes.update", - "indexes.delete", - "stats.get", - "dumps.create", - ], - "expiresAt": "2050-11-13T00:00:00Z" - }); - - assert_json_include!(actual: response, expected: expected); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }, @r###" + { + "name": "Product API key", + "description": null, + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "search", + "documents.add", + "documents.get", + "documents.delete", + "indexes.create", + "indexes.get", + "indexes.update", + "indexes.delete", + "stats.get", + "dumps.create" + ], + "indexes": [ + "products" + ], + "expiresAt": "2050-11-13T00:00:00Z", + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"200"); // Remove the name let content = json!({ "name": serde_json::Value::Null }); let (response, code) = server.patch_api_key(&uid, content).await; - assert_eq!(200, code, "{:?}", &response); - assert!(response["key"].is_string()); - assert!(response["expiresAt"].is_string()); - assert!(response["createdAt"].is_string()); - - let expected = json!({ - "indexes": ["products"], - "actions": [ - "search", - "documents.add", - "documents.get", - "documents.delete", - "indexes.create", - "indexes.get", - "indexes.update", - "indexes.delete", - "stats.get", - "dumps.create", - ], - "expiresAt": "2050-11-13T00:00:00Z" - }); - - assert_json_include!(actual: response, expected: expected); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }, @r###" + { + "name": null, + "description": null, + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "search", + "documents.add", + "documents.get", + "documents.delete", + "indexes.create", + "indexes.get", + "indexes.update", + "indexes.delete", + "stats.get", + "dumps.create" + ], + "indexes": [ + "products" + ], + "expiresAt": "2050-11-13T00:00:00Z", + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"200"); } #[actix_rt::test] @@ -1151,11 +1367,33 @@ async fn error_patch_api_key_indexes() { }); let (response, code) = server.add_api_key(content).await; - // must pass if add_valid_api_key test passes. - assert_eq!(201, code, "{:?}", &response); - assert!(response["key"].is_string()); - assert!(response["createdAt"].is_string()); - assert!(response["updatedAt"].is_string()); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }, @r###" + { + "name": null, + "description": "Indexing API key", + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "search", + "documents.add", + "documents.get", + "documents.delete", + "indexes.create", + "indexes.get", + "indexes.update", + "indexes.delete", + "stats.get", + "dumps.create" + ], + "indexes": [ + "products" + ], + "expiresAt": "2050-11-13T00:00:00Z", + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"201"); let uid = response["uid"].as_str().unwrap(); @@ -1163,15 +1401,15 @@ async fn error_patch_api_key_indexes() { thread::sleep(time::Duration::new(1, 0)); let (response, code) = server.patch_api_key(&uid, content).await; - assert_eq!(400, code, "{:?}", &response); - - let expected = json!({"message": "The `indexes` field cannot be modified for the given resource.", - "code": "immutable_field", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#immutable-field" - }); - - assert_json_include!(actual: response, expected: expected); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "Json deserialize error: unknown field `indexes`, expected one of `description`, `name` at ``.", + "code": "immutable_field", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#immutable-field" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"400"); } #[actix_rt::test] @@ -1198,11 +1436,33 @@ async fn error_patch_api_key_actions() { }); let (response, code) = server.add_api_key(content).await; - // must pass if add_valid_api_key test passes. - assert_eq!(201, code, "{:?}", &response); - assert!(response["key"].is_string()); - assert!(response["createdAt"].is_string()); - assert!(response["updatedAt"].is_string()); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }, @r###" + { + "name": null, + "description": "Indexing API key", + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "search", + "documents.add", + "documents.get", + "documents.delete", + "indexes.create", + "indexes.get", + "indexes.update", + "indexes.delete", + "stats.get", + "dumps.create" + ], + "indexes": [ + "products" + ], + "expiresAt": "2050-11-13T00:00:00Z", + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"201"); let uid = response["uid"].as_str().unwrap(); @@ -1218,15 +1478,15 @@ async fn error_patch_api_key_actions() { thread::sleep(time::Duration::new(1, 0)); let (response, code) = server.patch_api_key(&uid, content).await; - assert_eq!(400, code, "{:?}", &response); - - let expected = json!({"message": "The `actions` field cannot be modified for the given resource.", - "code": "immutable_field", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#immutable-field" - }); - - assert_json_include!(actual: response, expected: expected); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "Json deserialize error: unknown field `actions`, expected one of `description`, `name` at ``.", + "code": "immutable_field", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#immutable-field" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"400"); } #[actix_rt::test] @@ -1253,11 +1513,33 @@ async fn error_patch_api_key_expiration_date() { }); let (response, code) = server.add_api_key(content).await; - // must pass if add_valid_api_key test passes. - assert_eq!(201, code, "{:?}", &response); - assert!(response["key"].is_string()); - assert!(response["createdAt"].is_string()); - assert!(response["updatedAt"].is_string()); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }, @r###" + { + "name": null, + "description": "Indexing API key", + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "search", + "documents.add", + "documents.get", + "documents.delete", + "indexes.create", + "indexes.get", + "indexes.update", + "indexes.delete", + "stats.get", + "dumps.create" + ], + "indexes": [ + "products" + ], + "expiresAt": "2050-11-13T00:00:00Z", + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"201"); let uid = response["uid"].as_str().unwrap(); @@ -1265,15 +1547,15 @@ async fn error_patch_api_key_expiration_date() { thread::sleep(time::Duration::new(1, 0)); let (response, code) = server.patch_api_key(&uid, content).await; - assert_eq!(400, code, "{:?}", &response); - - let expected = json!({"message": "The `expiresAt` field cannot be modified for the given resource.", - "code": "immutable_field", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#immutable-field" - }); - - assert_json_include!(actual: response, expected: expected); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "Json deserialize error: unknown field `expiresAt`, expected one of `description`, `name` at ``.", + "code": "immutable_field", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#immutable-field" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"400"); } #[actix_rt::test] @@ -1286,16 +1568,16 @@ async fn error_patch_api_key_no_header() { json!({}), ) .await; - assert_eq!(401, code, "{:?}", &response); - let expected_response = json!({ - "message": "The Authorization header is missing. It must use the bearer authorization method.", - "code": "missing_authorization_header", - "type": "auth", - "link": "https://docs.meilisearch.com/errors#missing-authorization-header" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "The Authorization header is missing. It must use the bearer authorization method.", + "code": "missing_authorization_header", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#missing-authorization-header" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"401"); } #[actix_rt::test] @@ -1309,16 +1591,16 @@ async fn error_patch_api_key_bad_key() { json!({}), ) .await; - assert_eq!(403, code, "{:?}", &response); - let expected_response = json!({ - "message": "The provided API key is invalid.", - "code": "invalid_api_key", - "type": "auth", - "link": "https://docs.meilisearch.com/errors#invalid-api-key" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "The provided API key is invalid.", + "code": "invalid_api_key", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#invalid-api-key" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"403"); } #[actix_rt::test] @@ -1332,16 +1614,16 @@ async fn error_patch_api_key_not_found() { json!({}), ) .await; - assert_eq!(404, code, "{:?}", &response); - let expected_response = json!({ - "message": "API key `d0552b41d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4` not found.", - "code": "api_key_not_found", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#api-key-not-found" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "API key `d0552b41d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4` not found.", + "code": "api_key_not_found", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#api-key-not-found" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"404"); } #[actix_rt::test] @@ -1359,9 +1641,24 @@ async fn error_patch_api_key_indexes_invalid_parameters() { }); let (response, code) = server.add_api_key(content).await; - // must pass if add_valid_api_key test passes. - assert_eq!(201, code, "{:?}", &response); - assert!(response["key"].is_string()); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }, @r###" + { + "name": null, + "description": "Indexing API key", + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "search" + ], + "indexes": [ + "products" + ], + "expiresAt": "2050-11-13T00:00:00Z", + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"201"); let uid = response["uid"].as_str().unwrap(); @@ -1371,16 +1668,15 @@ async fn error_patch_api_key_indexes_invalid_parameters() { }); let (response, code) = server.patch_api_key(&uid, content).await; - assert_eq!(400, code, "{:?}", &response); - - let expected_response = json!({ - "message": "`description` field value `13` is invalid. It should be a string or specified as a null value.", - "code": "invalid_api_key_description", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-api-key-description" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "invalid type: Integer `13`, expected a String at `.description`.", + "code": "invalid_api_key_description", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-api-key-description" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"400"); // invalid name let content = json!({ @@ -1388,77 +1684,108 @@ async fn error_patch_api_key_indexes_invalid_parameters() { }); let (response, code) = server.patch_api_key(&uid, content).await; - assert_eq!(400, code, "{:?}", &response); - - let expected_response = json!({ - "message": "`name` field value `13` is invalid. It should be a string or specified as a null value.", - "code": "invalid_api_key_name", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-api-key-name" - }); - - assert_eq!(response, expected_response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "invalid type: Integer `13`, expected a String at `.name`.", + "code": "invalid_api_key_name", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-api-key-name" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"400"); } #[actix_rt::test] async fn error_access_api_key_routes_no_master_key_set() { let mut server = Server::new().await; - let expected_response = json!({ - "message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.", - "code": "missing_master_key", - "type": "auth", - "link": "https://docs.meilisearch.com/errors#missing-master-key" - }); - let expected_code = 401; - let (response, code) = server.add_api_key(json!({})).await; - - assert_eq!(expected_code, code, "{:?}", &response); - assert_eq!(response, expected_response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.", + "code": "missing_master_key", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#missing-master-key" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"401"); let (response, code) = server.patch_api_key("content", json!({})).await; - - assert_eq!(expected_code, code, "{:?}", &response); - assert_eq!(response, expected_response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.", + "code": "missing_master_key", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#missing-master-key" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"401"); let (response, code) = server.get_api_key("content").await; - - assert_eq!(expected_code, code, "{:?}", &response); - assert_eq!(response, expected_response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.", + "code": "missing_master_key", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#missing-master-key" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"401"); let (response, code) = server.list_api_keys().await; - - assert_eq!(expected_code, code, "{:?}", &response); - assert_eq!(response, expected_response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.", + "code": "missing_master_key", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#missing-master-key" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"401"); server.use_api_key("MASTER_KEY"); - let expected_response = json!({ - "message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.", - "code": "missing_master_key", - "type": "auth", - "link": "https://docs.meilisearch.com/errors#missing-master-key" - }); - let expected_code = 401; - let (response, code) = server.add_api_key(json!({})).await; - - assert_eq!(expected_code, code, "{:?}", &response); - assert_eq!(response, expected_response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.", + "code": "missing_master_key", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#missing-master-key" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"401"); let (response, code) = server.patch_api_key("content", json!({})).await; - - assert_eq!(expected_code, code, "{:?}", &response); - assert_eq!(response, expected_response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.", + "code": "missing_master_key", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#missing-master-key" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"401"); let (response, code) = server.get_api_key("content").await; - - assert_eq!(expected_code, code, "{:?}", &response); - assert_eq!(response, expected_response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.", + "code": "missing_master_key", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#missing-master-key" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"401"); let (response, code) = server.list_api_keys().await; - - assert_eq!(expected_code, code, "{:?}", &response); - assert_eq!(response, expected_response); + meili_snap::insta::assert_json_snapshot!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }, @r###" + { + "message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.", + "code": "missing_master_key", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#missing-master-key" + } + "###); + meili_snap::insta::assert_debug_snapshot!(code, @"401"); } diff --git a/meilisearch/tests/settings/get_settings.rs b/meilisearch/tests/settings/get_settings.rs index 8ec10db39..1146fc013 100644 --- a/meilisearch/tests/settings/get_settings.rs +++ b/meilisearch/tests/settings/get_settings.rs @@ -278,22 +278,16 @@ async fn error_set_invalid_ranking_rules() { let index = server.index("test"); index.create(None).await; - let (_response, _code) = - index.update_settings(json!({ "rankingRules": [ "manyTheFish"]})).await; - index.wait_task(1).await; - let (response, code) = index.get_task(1).await; - - assert_eq!(code, 200); - assert_eq!(response["status"], "failed"); - - let expected_error = json!({ - "message": r#"`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" - }); - - assert_eq!(response["error"], expected_error); + let (response, code) = index.update_settings(json!({ "rankingRules": [ "manyTheFish"]})).await; + meili_snap::insta::assert_debug_snapshot!(code, @"400"); + meili_snap::insta::assert_json_snapshot!(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]`.", + "code": "invalid_settings_ranking_rules", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-settings-ranking-rules" + } + "###); } #[actix_rt::test] diff --git a/meilisearch/tests/tasks/mod.rs b/meilisearch/tests/tasks/mod.rs index 5add71475..9516770fa 100644 --- a/meilisearch/tests/tasks/mod.rs +++ b/meilisearch/tests/tasks/mod.rs @@ -1,4 +1,4 @@ -use meili_snap::insta::{self, assert_json_snapshot}; +use meili_snap::insta::{self, assert_debug_snapshot, assert_json_snapshot}; use serde_json::json; use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; @@ -517,46 +517,26 @@ async fn test_summarized_settings_update() { let server = Server::new().await; let index = server.index("test"); // here we should find my payload even in the failed task. - index.update_settings(json!({ "rankingRules": ["custom"] })).await; + let (response, code) = index.update_settings(json!({ "rankingRules": ["custom"] })).await; + assert_debug_snapshot!(code, @"400"); + assert_json_snapshot!(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]`.", + "code": "invalid_settings_ranking_rules", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-settings-ranking-rules" + } + "###); + + index.update_settings(json!({ "displayedAttributes": ["doggos", "name"], "filterableAttributes": ["age", "nb_paw_pads"], "sortableAttributes": ["iq"] })).await; index.wait_task(0).await; let (task, _) = index.get_task(0).await; - dbg!(&task); assert_json_snapshot!(task, { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, @r###" { "uid": 0, "indexUid": "test", - "status": "failed", - "type": "settingsUpdate", - "canceledBy": null, - "details": { - "rankingRules": [ - "custom" - ] - }, - "error": { - "message": "`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" - }, - "duration": "[duration]", - "enqueuedAt": "[date]", - "startedAt": "[date]", - "finishedAt": "[date]" - } - "###); - - index.update_settings(json!({ "displayedAttributes": ["doggos", "name"], "filterableAttributes": ["age", "nb_paw_pads"], "sortableAttributes": ["iq"] })).await; - index.wait_task(1).await; - let (task, _) = index.get_task(1).await; - assert_json_snapshot!(task, - { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, - @r###" - { - "uid": 1, - "indexUid": "test", "status": "succeeded", "type": "settingsUpdate", "canceledBy": null,