diff --git a/Cargo.lock b/Cargo.lock index 64012c10e..f4954ca86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,7 +46,7 @@ dependencies = [ "actix-tls", "actix-utils", "ahash", - "base64", + "base64 0.13.1", "bitflags", "brotli", "bytes", @@ -331,9 +331,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d1d8ab452a3936018a687b20e6f7cf5363d713b732b8884001317b0e48aa3" +checksum = "705339e0e4a9690e2908d2b3d049d85682cf19fbd5782494498fbf7003a6a282" dependencies = [ "proc-macro2 1.0.49", "quote 1.0.23", @@ -387,6 +387,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" + [[package]] name = "base64ct" version = "1.5.3" @@ -1018,9 +1024,9 @@ dependencies = [ [[package]] name = "deserr" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb73133def0ebeb6f2e911a8ea3495cde53b00a5337dddc49bdb4b6c450ac8c7" +checksum = "86290491a2b5c21a1a5083da8dae831006761258fabd5617309c3eebc5f89468" dependencies = [ "deserr-internal", "serde-cs", @@ -1029,9 +1035,9 @@ dependencies = [ [[package]] name = "deserr-internal" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f290f1f52fbf7d0afe91e6f71f3c831ae5b223a55cc396e819c5748ba73a7bfa" +checksum = "7131de1c27581bc376a22166c9f570be91b76cb096be2f6aecf224c27bf7c49a" dependencies = [ "convert_case 0.5.0", "proc-macro2 1.0.49", @@ -1309,8 +1315,8 @@ dependencies = [ [[package]] name = "filter-parser" -version = "0.38.0" -source = "git+https://github.com/meilisearch/milli.git?tag=v0.38.0#c3f4835e8e102586bd6d5eb1e55c4bba5e92f994" +version = "0.39.0" +source = "git+https://github.com/meilisearch/milli.git?tag=v0.39.0#e6bea999740b153871f665abce869ffbb5aa94c5" dependencies = [ "nom", "nom_locate", @@ -1328,8 +1334,8 @@ dependencies = [ [[package]] name = "flatten-serde-json" -version = "0.38.0" -source = "git+https://github.com/meilisearch/milli.git?tag=v0.38.0#c3f4835e8e102586bd6d5eb1e55c4bba5e92f994" +version = "0.39.0" +source = "git+https://github.com/meilisearch/milli.git?tag=v0.39.0#e6bea999740b153871f665abce869ffbb5aa94c5" dependencies = [ "serde_json", ] @@ -1513,9 +1519,9 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "grenad" @@ -1786,9 +1792,9 @@ dependencies = [ [[package]] name = "insta" -version = "1.24.1" +version = "1.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5686bd8e9239eabe90bb30a0c341bffd6fdc177fb556708f2cb792bf00352d" +checksum = "f6f0f08b46e4379744de2ab67aa8f7de3ffd1da3e275adc41fcc82053ede46ff" dependencies = [ "console", "lazy_static", @@ -1821,9 +1827,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11b0d96e660696543b251e58030cf9787df56da39dab19ad60eae7353040917e" +checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" [[package]] name = "is-terminal" @@ -1893,8 +1899,8 @@ dependencies = [ [[package]] name = "json-depth-checker" -version = "0.38.0" -source = "git+https://github.com/meilisearch/milli.git?tag=v0.38.0#c3f4835e8e102586bd6d5eb1e55c4bba5e92f994" +version = "0.39.0" +source = "git+https://github.com/meilisearch/milli.git?tag=v0.39.0#e6bea999740b153871f665abce869ffbb5aa94c5" dependencies = [ "serde_json", ] @@ -1905,7 +1911,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09f4f04699947111ec1733e71778d763555737579e44b85844cae8e1940a1828" dependencies = [ - "base64", + "base64 0.13.1", "pem", "ring", "serde", @@ -1942,9 +1948,9 @@ checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] name = "libgit2-sys" -version = "0.14.0+1.5.0" +version = "0.14.1+1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47a00859c70c8a4f7218e6d1cc32875c4b55f6799445b842b0d8ed5e4c3d959b" +checksum = "4a07fb2692bc3593bda59de45a502bb3071659f2c515e28c71e728306b038e17" dependencies = [ "cc", "libc", @@ -1960,9 +1966,9 @@ checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" [[package]] name = "libmimalloc-sys" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04d1c67deb83e6b75fa4fe3309e09cfeade12e7721d95322af500d3814ea60c9" +checksum = "dd8c7cbf8b89019683667e347572e6d55a7df7ea36b0c4ce69961b0cde67b174" dependencies = [ "cc", "libc", @@ -2371,7 +2377,7 @@ dependencies = [ name = "meilisearch-auth" version = "1.0.0" dependencies = [ - "base64", + "base64 0.13.1", "enum-iterator", "hmac", "meilisearch-types", @@ -2442,8 +2448,8 @@ dependencies = [ [[package]] name = "milli" -version = "0.38.0" -source = "git+https://github.com/meilisearch/milli.git?tag=v0.38.0#c3f4835e8e102586bd6d5eb1e55c4bba5e92f994" +version = "0.39.0" +source = "git+https://github.com/meilisearch/milli.git?tag=v0.39.0#e6bea999740b153871f665abce869ffbb5aa94c5" dependencies = [ "bimap", "bincode", @@ -2453,6 +2459,7 @@ dependencies = [ "concat-arrays", "crossbeam-channel", "csv", + "deserr", "either", "filter-parser", "flatten-serde-json", @@ -2487,9 +2494,9 @@ dependencies = [ [[package]] name = "mimalloc" -version = "0.1.32" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2374e2999959a7b583e1811a1ddbf1d3a4b9496eceb9746f1192a59d871eca" +checksum = "9dcb174b18635f7561a0c6c9fc2ce57218ac7523cf72c50af80e2d79ab8f3ba1" dependencies = [ "libmimalloc-sys", ] @@ -2615,9 +2622,9 @@ dependencies = [ [[package]] name = "object" -version = "0.30.1" +version = "0.30.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d864c91689fdc196779b98dba0aceac6118594c2df6ee5d943eb6a8df4d107a" +checksum = "2b8c786513eb403643f2a88c244c2aaa270ef2153f55094587d0c48a3cf22a83" dependencies = [ "memchr", ] @@ -2748,7 +2755,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4" dependencies = [ - "base64", + "base64 0.13.1", ] [[package]] @@ -2767,9 +2774,9 @@ dependencies = [ [[package]] name = "pest" -version = "2.5.2" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f6e86fb9e7026527a0d46bc308b841d73170ef8f443e1807f6ef88526a816d4" +checksum = "4257b4a04d91f7e9e6290be5d3da4804dd5784fafde3a497d73eb2b4a158c30a" dependencies = [ "thiserror", "ucd-trie", @@ -2777,9 +2784,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.5.2" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96504449aa860c8dcde14f9fba5c58dc6658688ca1fe363589d6327b8662c603" +checksum = "241cda393b0cdd65e62e07e12454f1f25d57017dcc514b1514cd3c4645e3a0a6" dependencies = [ "pest", "pest_generator", @@ -2787,9 +2794,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.5.2" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "798e0220d1111ae63d66cb66a5dcb3fc2d986d520b98e49e1852bfdb11d7c5e7" +checksum = "46b53634d8c8196302953c74d5352f33d0c512a9499bd2ce468fc9f4128fa27c" dependencies = [ "pest", "pest_meta", @@ -2800,13 +2807,13 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.5.2" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "984298b75898e30a843e278a9f2452c31e349a073a0ce6fd950a12a74464e065" +checksum = "0ef4f1332a8d4678b41966bb4cc1d0676880e84183a1ecc3f4b69f03e99c7a51" dependencies = [ "once_cell", "pest", - "sha1", + "sha2", ] [[package]] @@ -3108,9 +3115,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" dependencies = [ "aho-corasick", "memchr", @@ -3144,7 +3151,7 @@ version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c" dependencies = [ - "base64", + "base64 0.13.1", "bytes", "encoding_rs", "futures-core", @@ -3265,11 +3272,11 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" +checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" dependencies = [ - "base64", + "base64 0.21.0", ] [[package]] @@ -3323,9 +3330,9 @@ dependencies = [ [[package]] name = "segment" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24fc91c898e0487ff3e471d0849bbaf7d38a00ff5e3531009d386b0bab9b6b12" +checksum = "2bb93f3f738322ce8f33c4e80c251fb1560ca81f3a241355271fcb912eeb48e3" dependencies = [ "async-trait", "reqwest", @@ -3607,9 +3614,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.26.8" +version = "0.26.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ddf41e393a9133c81d5f0974195366bd57082deac6e0eb02ed39b8341c2bb6" +checksum = "5c18a6156d1f27a9592ee18c1a846ca8dd5c258b7179fc193ae87c74ebb666f5" dependencies = [ "cfg-if", "core-foundation-sys", @@ -3733,9 +3740,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.24.0" +version = "1.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7125661431c26622a80ca5051a2f936c9a678318e0351007b0cc313143024e5c" +checksum = "1d9f76183f91ecfb55e1d7d5602bd1d979e38a3a522fe900241cf195624d67ae" dependencies = [ "autocfg", "bytes", @@ -3836,9 +3843,9 @@ dependencies = [ [[package]] name = "try-lock" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "typenum" @@ -4310,10 +4317,11 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.4+zstd.1.5.2" +version = "2.0.5+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa202f2ef00074143e219d15b62ffc317d17cc33909feac471c044087cad7b0" +checksum = "edc50ffce891ad571e9f9afe5039c4837bede781ac4bb13052ed7ae695518596" dependencies = [ "cc", "libc", + "pkg-config", ] 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 0216d490e..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; @@ -254,14 +256,14 @@ impl From> for v6::Setting { impl From for v6::ResponseError { fn from(error: v5::ResponseError) -> Self { let code = match error.error_code.as_ref() { - "index_creation_failed" => v6::Code::CreateIndex, + "index_creation_failed" => v6::Code::IndexCreationFailed, "index_already_exists" => v6::Code::IndexAlreadyExists, "index_not_found" => v6::Code::IndexNotFound, "invalid_index_uid" => v6::Code::InvalidIndexUid, "invalid_min_word_length_for_typo" => v6::Code::InvalidMinWordLengthForTypo, "invalid_state" => v6::Code::InvalidState, - "primary_key_inference_failed" => v6::Code::NoPrimaryKeyCandidateFound, - "index_primary_key_already_exists" => v6::Code::PrimaryKeyAlreadyPresent, + "primary_key_inference_failed" => v6::Code::IndexPrimaryKeyNoCandidateFound, + "index_primary_key_already_exists" => v6::Code::IndexPrimaryKeyAlreadyExists, "max_fields_limit_exceeded" => v6::Code::MaxFieldsLimitExceeded, "missing_document_id" => v6::Code::MissingDocumentId, "invalid_document_id" => v6::Code::InvalidDocumentId, @@ -274,16 +276,16 @@ impl From for v6::ResponseError { "internal" => v6::Code::Internal, "invalid_geo_field" => v6::Code::InvalidDocumentGeoField, "invalid_ranking_rule" => v6::Code::InvalidSettingsRankingRules, - "invalid_store_file" => v6::Code::InvalidStore, - "invalid_api_key" => v6::Code::InvalidToken, + "invalid_store_file" => v6::Code::InvalidStoreFile, + "invalid_api_key" => v6::Code::InvalidApiKey, "missing_authorization_header" => v6::Code::MissingAuthorizationHeader, "no_space_left_on_device" => v6::Code::NoSpaceLeftOnDevice, "dump_not_found" => v6::Code::DumpNotFound, "task_not_found" => v6::Code::TaskNotFound, "payload_too_large" => v6::Code::PayloadTooLarge, - "unretrievable_document" => v6::Code::DocumentNotFound, + "unretrievable_document" => v6::Code::UnretrievableDocument, "unsupported_media_type" => v6::Code::UnsupportedMediaType, - "dump_already_processing" => v6::Code::DumpAlreadyInProgress, + "dump_already_processing" => v6::Code::DumpAlreadyProcessing, "dump_process_failed" => v6::Code::DumpProcessFailed, "invalid_content_type" => v6::Code::InvalidContentType, "missing_content_type" => v6::Code::MissingContentType, @@ -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/index-scheduler/src/error.rs b/index-scheduler/src/error.rs index 0be6e4e32..013bcf595 100644 --- a/index-scheduler/src/error.rs +++ b/index-scheduler/src/error.rs @@ -139,8 +139,8 @@ impl ErrorCode for Error { match self { Error::IndexNotFound(_) => Code::IndexNotFound, Error::IndexAlreadyExists(_) => Code::IndexAlreadyExists, - Error::SwapDuplicateIndexesFound(_) => Code::InvalidDuplicateIndexesFound, - Error::SwapDuplicateIndexFound(_) => Code::InvalidDuplicateIndexesFound, + Error::SwapDuplicateIndexesFound(_) => Code::InvalidSwapDuplicateIndexFound, + Error::SwapDuplicateIndexFound(_) => Code::InvalidSwapDuplicateIndexFound, Error::SwapIndexNotFound(_) => Code::InvalidSwapIndexes, Error::SwapIndexesNotFound(_) => Code::InvalidSwapIndexes, Error::InvalidTaskDate { field, .. } => (*field).into(), @@ -150,8 +150,8 @@ impl ErrorCode for Error { Error::InvalidTaskCanceledBy { .. } => Code::InvalidTaskCanceledBy, Error::InvalidIndexUid { .. } => Code::InvalidIndexUid, Error::TaskNotFound(_) => Code::TaskNotFound, - Error::TaskDeletionWithEmptyQuery => Code::TaskDeletionWithEmptyQuery, - Error::TaskCancelationWithEmptyQuery => Code::TaskCancelationWithEmptyQuery, + Error::TaskDeletionWithEmptyQuery => Code::MissingTaskFilters, + Error::TaskCancelationWithEmptyQuery => Code::MissingTaskFilters, Error::Dump(e) => e.error_code(), Error::Milli(e) => e.error_code(), Error::ProcessBatchPanicked => Code::Internal, 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..8d4a7f2b7 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,18 @@ 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, create_key: CreateApiKey) -> Result { + 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..379ac26aa 100644 --- a/meilisearch-types/Cargo.toml +++ b/meilisearch-types/Cargo.toml @@ -9,14 +9,14 @@ actix-web = { version = "4.2.1", default-features = false } anyhow = "1.0.65" convert_case = "0.6.0" csv = "1.1.6" -deserr = { version = "0.1.2", features = ["serde-json"] } +deserr = { version = "0.1.4", features = ["serde-json"] } either = { version = "1.6.1", features = ["serde"] } enum-iterator = "1.1.3" file-store = { path = "../file-store" } flate2 = "1.0.24" fst = "0.4.7" memmap2 = "0.5.7" -milli = { git = "https://github.com/meilisearch/milli.git", tag = "v0.38.0", default-features = false } +milli = { git = "https://github.com/meilisearch/milli.git", tag = "v0.39.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 ced817cb7..b4a8b786e 100644 --- a/meilisearch-types/src/error.rs +++ b/meilisearch-types/src/error.rs @@ -1,12 +1,17 @@ +use std::convert::Infallible; +use std::marker::PhantomData; use std::{fmt, io}; use actix_web::http::StatusCode; use actix_web::{self as aweb, HttpResponseBuilder}; use aweb::rt::task::JoinError; use convert_case::Casing; +use deserr::{DeserializeError, IntoValue, MergeWithError, ValuePointerRef}; use 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))] @@ -31,7 +36,7 @@ impl ResponseError { Self { code: code.http(), message, - error_code: code.err_code().error_name.to_string(), + error_code: code.err_code().error_name, error_type: code.type_(), error_link: code.url(), } @@ -48,7 +53,7 @@ impl std::error::Error for ResponseError {} impl From for ResponseError where - T: ErrorCode, + T: std::error::Error + ErrorCode, { fn from(other: T) -> Self { Self::from_msg(other.to_string(), other.error_code()) @@ -66,7 +71,7 @@ impl aweb::error::ResponseError for ResponseError { } } -pub trait ErrorCode: std::error::Error { +pub trait ErrorCode { fn error_code(&self) -> Code; /// returns the HTTP status code associated with the error @@ -111,461 +116,186 @@ impl fmt::Display for ErrorType { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Code { - // error related to your setup - IoError, - NoSpaceLeftOnDevice, - TooManyOpenFiles, - - // index related error - CreateIndex, - IndexAlreadyExists, - InvalidIndexPrimaryKey, - IndexNotFound, - InvalidIndexUid, - MissingIndexUid, - InvalidMinWordLengthForTypo, - InvalidIndexLimit, - InvalidIndexOffset, - - DuplicateIndexFound, - - // invalid state error - InvalidState, - NoPrimaryKeyCandidateFound, - MultiplePrimaryKeyCandidatesFound, - PrimaryKeyAlreadyPresent, - - MaxFieldsLimitExceeded, - MissingDocumentId, - InvalidDocumentId, - - // Invalid swap-indexes - InvalidSwapIndexes, - InvalidDuplicateIndexesFound, - - // Invalid settings update request - InvalidSettingsDisplayedAttributes, - InvalidSettingsSearchableAttributes, - InvalidSettingsFilterableAttributes, - InvalidSettingsSortableAttributes, - InvalidSettingsRankingRules, - InvalidSettingsStopWords, - InvalidSettingsSynonyms, - InvalidSettingsDistinctAttribute, - InvalidSettingsTypoTolerance, - InvalidSettingsFaceting, - InvalidSettingsPagination, - - // Invalid search request - InvalidSearchQ, - InvalidSearchOffset, - InvalidSearchLimit, - InvalidSearchPage, - InvalidSearchHitsPerPage, - InvalidSearchAttributesToRetrieve, - InvalidSearchAttributesToCrop, - InvalidSearchCropLength, - InvalidSearchAttributesToHighlight, - InvalidSearchShowMatchesPosition, - InvalidSearchFilter, - InvalidSearchSort, - InvalidSearchFacets, - InvalidSearchHighlightPreTag, - InvalidSearchHighlightPostTag, - InvalidSearchCropMarker, - InvalidSearchMatchingStrategy, - - // Related to the tasks - InvalidTaskUids, - InvalidTaskTypes, - InvalidTaskStatuses, - InvalidTaskCanceledBy, - InvalidTaskLimit, - InvalidTaskFrom, - InvalidTaskBeforeEnqueuedAt, - InvalidTaskAfterEnqueuedAt, - InvalidTaskBeforeStartedAt, - InvalidTaskAfterStartedAt, - InvalidTaskBeforeFinishedAt, - InvalidTaskAfterFinishedAt, - - // Documents API - InvalidDocumentFields, - InvalidDocumentLimit, - InvalidDocumentOffset, - - BadParameter, - BadRequest, - DatabaseSizeLimitReached, - DocumentNotFound, - Internal, - InvalidDocumentGeoField, - InvalidStore, - InvalidToken, - MissingAuthorizationHeader, - MissingMasterKey, - DumpNotFound, - TaskNotFound, - TaskDeletionWithEmptyQuery, - TaskCancelationWithEmptyQuery, - PayloadTooLarge, - UnsupportedMediaType, - - DumpAlreadyInProgress, - DumpProcessFailed, - // Only used when importing a dump - UnretrievableErrorCode, - - InvalidContentType, - MissingContentType, - MalformedPayload, - MissingPayload, - - ApiKeyNotFound, - - MissingApiKeyActions, - MissingApiKeyExpiresAt, - MissingApiKeyIndexes, - - InvalidApiKeyOffset, - InvalidApiKeyLimit, - InvalidApiKeyActions, - InvalidApiKeyIndexes, - InvalidApiKeyExpiresAt, - InvalidApiKeyDescription, - InvalidApiKeyName, - InvalidApiKeyUid, - ImmutableField, - ApiKeyAlreadyExists, -} - -impl Code { - /// associate a `Code` variant to the actual ErrCode - fn err_code(&self) -> ErrCode { - use Code::*; - - match self { - // related to the setup - IoError => ErrCode::system("io_error", StatusCode::INTERNAL_SERVER_ERROR), - TooManyOpenFiles => { - ErrCode::system("too_many_open_files", StatusCode::INTERNAL_SERVER_ERROR) +macro_rules! make_error_codes { + ($($code_ident:ident, $err_type:ident, $status:ident);*) => { + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum Code { + $($code_ident),* + } + impl Code { + /// associate a `Code` variant to the actual ErrCode + fn err_code(&self) -> ErrCode { + match self { + $( + Code::$code_ident => { + ErrCode::$err_type( stringify!($code_ident).to_case(convert_case::Case::Snake), StatusCode::$status) + } + )* + } } - NoSpaceLeftOnDevice => { - ErrCode::system("no_space_left_on_device", StatusCode::INTERNAL_SERVER_ERROR) + /// return the HTTP status code associated with the `Code` + fn http(&self) -> StatusCode { + self.err_code().status_code } - // index related errors - // create index is thrown on internal error while creating an index. - CreateIndex => { - ErrCode::internal("index_creation_failed", StatusCode::INTERNAL_SERVER_ERROR) - } - IndexAlreadyExists => ErrCode::invalid("index_already_exists", StatusCode::CONFLICT), - // thrown when requesting an unexisting index - IndexNotFound => ErrCode::invalid("index_not_found", StatusCode::NOT_FOUND), - InvalidIndexUid => ErrCode::invalid("invalid_index_uid", StatusCode::BAD_REQUEST), - MissingIndexUid => ErrCode::invalid("missing_index_uid", StatusCode::BAD_REQUEST), - InvalidIndexPrimaryKey => { - ErrCode::invalid("invalid_index_primary_key", StatusCode::BAD_REQUEST) - } - InvalidIndexLimit => ErrCode::invalid("invalid_index_limit", StatusCode::BAD_REQUEST), - InvalidIndexOffset => ErrCode::invalid("invalid_index_offset", StatusCode::BAD_REQUEST), - - // invalid state error - InvalidState => ErrCode::internal("invalid_state", StatusCode::INTERNAL_SERVER_ERROR), - // thrown when no primary key has been set - NoPrimaryKeyCandidateFound => { - ErrCode::invalid("index_primary_key_no_candidate_found", StatusCode::BAD_REQUEST) - } - MultiplePrimaryKeyCandidatesFound => ErrCode::invalid( - "index_primary_key_multiple_candidates_found", - StatusCode::BAD_REQUEST, - ), - // error thrown when trying to set an already existing primary key - PrimaryKeyAlreadyPresent => { - ErrCode::invalid("index_primary_key_already_exists", StatusCode::BAD_REQUEST) + /// return error name, used as error code + fn name(&self) -> String { + self.err_code().error_name.to_string() } - // invalid database - InvalidStore => { - ErrCode::internal("invalid_store_file", StatusCode::INTERNAL_SERVER_ERROR) + /// return the error type + fn type_(&self) -> String { + self.err_code().error_type.to_string() } - // invalid document - MaxFieldsLimitExceeded => { - ErrCode::invalid("max_fields_limit_exceeded", StatusCode::BAD_REQUEST) - } - MissingDocumentId => ErrCode::invalid("missing_document_id", StatusCode::BAD_REQUEST), - InvalidDocumentId => ErrCode::invalid("invalid_document_id", StatusCode::BAD_REQUEST), - - BadParameter => ErrCode::invalid("bad_parameter", StatusCode::BAD_REQUEST), - BadRequest => ErrCode::invalid("bad_request", StatusCode::BAD_REQUEST), - DatabaseSizeLimitReached => { - ErrCode::internal("database_size_limit_reached", StatusCode::INTERNAL_SERVER_ERROR) - } - DocumentNotFound => ErrCode::invalid("document_not_found", StatusCode::NOT_FOUND), - Internal => ErrCode::internal("internal", StatusCode::INTERNAL_SERVER_ERROR), - InvalidDocumentGeoField => { - ErrCode::invalid("invalid_document_geo_field", StatusCode::BAD_REQUEST) - } - InvalidToken => ErrCode::authentication("invalid_api_key", StatusCode::FORBIDDEN), - MissingAuthorizationHeader => { - ErrCode::authentication("missing_authorization_header", StatusCode::UNAUTHORIZED) - } - MissingMasterKey => { - ErrCode::authentication("missing_master_key", StatusCode::UNAUTHORIZED) - } - TaskNotFound => ErrCode::invalid("task_not_found", StatusCode::NOT_FOUND), - TaskDeletionWithEmptyQuery => { - ErrCode::invalid("missing_task_filters", StatusCode::BAD_REQUEST) - } - TaskCancelationWithEmptyQuery => { - ErrCode::invalid("missing_task_filters", StatusCode::BAD_REQUEST) - } - DumpNotFound => ErrCode::invalid("dump_not_found", StatusCode::NOT_FOUND), - PayloadTooLarge => ErrCode::invalid("payload_too_large", StatusCode::PAYLOAD_TOO_LARGE), - UnsupportedMediaType => { - ErrCode::invalid("unsupported_media_type", StatusCode::UNSUPPORTED_MEDIA_TYPE) - } - - // error related to dump - DumpAlreadyInProgress => { - ErrCode::invalid("dump_already_processing", StatusCode::CONFLICT) - } - DumpProcessFailed => { - ErrCode::internal("dump_process_failed", StatusCode::INTERNAL_SERVER_ERROR) - } - MissingContentType => { - ErrCode::invalid("missing_content_type", StatusCode::UNSUPPORTED_MEDIA_TYPE) - } - MalformedPayload => ErrCode::invalid("malformed_payload", StatusCode::BAD_REQUEST), - InvalidContentType => { - ErrCode::invalid("invalid_content_type", StatusCode::UNSUPPORTED_MEDIA_TYPE) - } - MissingPayload => ErrCode::invalid("missing_payload", StatusCode::BAD_REQUEST), - // This one can only happen when importing a dump and encountering an unknown code in the task queue. - UnretrievableErrorCode => { - ErrCode::invalid("unretrievable_error_code", StatusCode::BAD_REQUEST) - } - - // error related to keys - ApiKeyNotFound => ErrCode::invalid("api_key_not_found", StatusCode::NOT_FOUND), - - MissingApiKeyExpiresAt => { - ErrCode::invalid("missing_api_key_expires_at", StatusCode::BAD_REQUEST) - } - - MissingApiKeyActions => { - ErrCode::invalid("missing_api_key_actions", StatusCode::BAD_REQUEST) - } - - MissingApiKeyIndexes => { - ErrCode::invalid("missing_api_key_indexes", StatusCode::BAD_REQUEST) - } - - InvalidApiKeyOffset => { - ErrCode::invalid("invalid_api_key_offset", StatusCode::BAD_REQUEST) - } - InvalidApiKeyLimit => { - ErrCode::invalid("invalid_api_key_limit", StatusCode::BAD_REQUEST) - } - InvalidApiKeyActions => { - ErrCode::invalid("invalid_api_key_actions", StatusCode::BAD_REQUEST) - } - InvalidApiKeyIndexes => { - ErrCode::invalid("invalid_api_key_indexes", StatusCode::BAD_REQUEST) - } - InvalidApiKeyExpiresAt => { - ErrCode::invalid("invalid_api_key_expires_at", StatusCode::BAD_REQUEST) - } - InvalidApiKeyDescription => { - ErrCode::invalid("invalid_api_key_description", StatusCode::BAD_REQUEST) - } - InvalidApiKeyName => ErrCode::invalid("invalid_api_key_name", StatusCode::BAD_REQUEST), - InvalidApiKeyUid => ErrCode::invalid("invalid_api_key_uid", StatusCode::BAD_REQUEST), - ApiKeyAlreadyExists => ErrCode::invalid("api_key_already_exists", StatusCode::CONFLICT), - ImmutableField => ErrCode::invalid("immutable_field", StatusCode::BAD_REQUEST), - InvalidMinWordLengthForTypo => { - ErrCode::invalid("invalid_min_word_length_for_typo", StatusCode::BAD_REQUEST) - } - DuplicateIndexFound => { - ErrCode::invalid("duplicate_index_found", StatusCode::BAD_REQUEST) - } - - // Swap indexes error - InvalidSwapIndexes => ErrCode::invalid("invalid_swap_indexes", StatusCode::BAD_REQUEST), - InvalidDuplicateIndexesFound => { - ErrCode::invalid("invalid_swap_duplicate_index_found", StatusCode::BAD_REQUEST) - } - - // Invalid settings - InvalidSettingsDisplayedAttributes => { - ErrCode::invalid("invalid_settings_displayed_attributes", StatusCode::BAD_REQUEST) - } - InvalidSettingsSearchableAttributes => { - ErrCode::invalid("invalid_settings_searchable_attributes", StatusCode::BAD_REQUEST) - } - InvalidSettingsFilterableAttributes => { - ErrCode::invalid("invalid_settings_filterable_attributes", StatusCode::BAD_REQUEST) - } - InvalidSettingsSortableAttributes => { - ErrCode::invalid("invalid_settings_sortable_attributes", StatusCode::BAD_REQUEST) - } - InvalidSettingsRankingRules => { - ErrCode::invalid("invalid_settings_ranking_rules", StatusCode::BAD_REQUEST) - } - InvalidSettingsStopWords => { - ErrCode::invalid("invalid_settings_stop_words", StatusCode::BAD_REQUEST) - } - InvalidSettingsSynonyms => { - ErrCode::invalid("invalid_settings_synonyms", StatusCode::BAD_REQUEST) - } - InvalidSettingsDistinctAttribute => { - ErrCode::invalid("invalid_settings_distinct_attribute", StatusCode::BAD_REQUEST) - } - InvalidSettingsTypoTolerance => { - ErrCode::invalid("invalid_settings_typo_tolerance", StatusCode::BAD_REQUEST) - } - InvalidSettingsFaceting => { - ErrCode::invalid("invalid_settings_faceting", StatusCode::BAD_REQUEST) - } - InvalidSettingsPagination => { - ErrCode::invalid("invalid_settings_pagination", StatusCode::BAD_REQUEST) - } - - // Invalid search - InvalidSearchQ => ErrCode::invalid("invalid_search_q", StatusCode::BAD_REQUEST), - InvalidSearchOffset => { - ErrCode::invalid("invalid_search_offset", StatusCode::BAD_REQUEST) - } - InvalidSearchLimit => ErrCode::invalid("invalid_search_limit", StatusCode::BAD_REQUEST), - InvalidSearchPage => ErrCode::invalid("invalid_search_page", StatusCode::BAD_REQUEST), - InvalidSearchHitsPerPage => { - ErrCode::invalid("invalid_search_hits_per_page", StatusCode::BAD_REQUEST) - } - InvalidSearchAttributesToRetrieve => { - ErrCode::invalid("invalid_search_attributes_to_retrieve", StatusCode::BAD_REQUEST) - } - InvalidSearchAttributesToCrop => { - ErrCode::invalid("invalid_search_attributes_to_crop", StatusCode::BAD_REQUEST) - } - InvalidSearchCropLength => { - ErrCode::invalid("invalid_search_crop_length", StatusCode::BAD_REQUEST) - } - InvalidSearchAttributesToHighlight => { - ErrCode::invalid("invalid_search_attributes_to_highlight", StatusCode::BAD_REQUEST) - } - InvalidSearchShowMatchesPosition => { - ErrCode::invalid("invalid_search_show_matches_position", StatusCode::BAD_REQUEST) - } - InvalidSearchFilter => { - ErrCode::invalid("invalid_search_filter", StatusCode::BAD_REQUEST) - } - InvalidSearchSort => ErrCode::invalid("invalid_search_sort", StatusCode::BAD_REQUEST), - InvalidSearchFacets => { - ErrCode::invalid("invalid_search_facets", StatusCode::BAD_REQUEST) - } - InvalidSearchHighlightPreTag => { - ErrCode::invalid("invalid_search_highlight_pre_tag", StatusCode::BAD_REQUEST) - } - InvalidSearchHighlightPostTag => { - ErrCode::invalid("invalid_search_highlight_post_tag", StatusCode::BAD_REQUEST) - } - InvalidSearchCropMarker => { - ErrCode::invalid("invalid_search_crop_marker", StatusCode::BAD_REQUEST) - } - InvalidSearchMatchingStrategy => { - ErrCode::invalid("invalid_search_matching_strategy", StatusCode::BAD_REQUEST) - } - - // Related to the tasks - InvalidTaskUids => ErrCode::invalid("invalid_task_uids", StatusCode::BAD_REQUEST), - InvalidTaskTypes => ErrCode::invalid("invalid_task_types", StatusCode::BAD_REQUEST), - InvalidTaskStatuses => { - ErrCode::invalid("invalid_task_statuses", StatusCode::BAD_REQUEST) - } - InvalidTaskCanceledBy => { - ErrCode::invalid("invalid_task_canceled_by", StatusCode::BAD_REQUEST) - } - InvalidTaskLimit => ErrCode::invalid("invalid_task_limit", StatusCode::BAD_REQUEST), - InvalidTaskFrom => ErrCode::invalid("invalid_task_from", StatusCode::BAD_REQUEST), - InvalidTaskBeforeEnqueuedAt => { - ErrCode::invalid("invalid_task_before_enqueued_at", StatusCode::BAD_REQUEST) - } - InvalidTaskAfterEnqueuedAt => { - ErrCode::invalid("invalid_task_after_enqueued_at", StatusCode::BAD_REQUEST) - } - InvalidTaskBeforeStartedAt => { - ErrCode::invalid("invalid_task_before_started_at", StatusCode::BAD_REQUEST) - } - InvalidTaskAfterStartedAt => { - ErrCode::invalid("invalid_task_after_started_at", StatusCode::BAD_REQUEST) - } - InvalidTaskBeforeFinishedAt => { - ErrCode::invalid("invalid_task_before_finished_at", StatusCode::BAD_REQUEST) - } - InvalidTaskAfterFinishedAt => { - ErrCode::invalid("invalid_task_after_finished_at", StatusCode::BAD_REQUEST) - } - - InvalidDocumentFields => { - ErrCode::invalid("invalid_document_fields", StatusCode::BAD_REQUEST) - } - InvalidDocumentLimit => { - ErrCode::invalid("invalid_document_limit", StatusCode::BAD_REQUEST) - } - InvalidDocumentOffset => { - ErrCode::invalid("invalid_document_offset", StatusCode::BAD_REQUEST) + /// return the doc url associated with the error + fn url(&self) -> String { + format!( + "https://docs.meilisearch.com/errors#{}", + self.name().to_case(convert_case::Case::Kebab) + ) } } + pub mod deserr_codes { + use super::{Code, ErrorCode}; + $( + #[derive(Default)] + pub struct $code_ident; + impl ErrorCode for $code_ident { + fn error_code(&self) -> Code { + Code::$code_ident + } + } + )* + } } - - /// return the HTTP status code associated with the `Code` - fn http(&self) -> StatusCode { - self.err_code().status_code - } - - /// return error name, used as error code - fn name(&self) -> String { - self.err_code().error_name.to_string() - } - - /// return the error type - fn type_(&self) -> String { - self.err_code().error_type.to_string() - } - - /// return the doc url associated with the error - fn url(&self) -> String { - format!( - "https://docs.meilisearch.com/errors#{}", - self.name().to_case(convert_case::Case::Kebab) - ) - } +} +make_error_codes! { +ApiKeyAlreadyExists , invalid , CONFLICT ; +ApiKeyNotFound , invalid , NOT_FOUND ; +BadParameter , invalid , BAD_REQUEST; +BadRequest , invalid , BAD_REQUEST; +DatabaseSizeLimitReached , internal , INTERNAL_SERVER_ERROR; +DocumentNotFound , invalid , NOT_FOUND; +DumpAlreadyProcessing , invalid , CONFLICT; +DumpNotFound , invalid , NOT_FOUND; +DumpProcessFailed , internal , INTERNAL_SERVER_ERROR; +DuplicateIndexFound , invalid , BAD_REQUEST; +ImmutableField , invalid , BAD_REQUEST; +IndexAlreadyExists , invalid , CONFLICT ; +IndexCreationFailed , internal , INTERNAL_SERVER_ERROR; +IndexNotFound , invalid , NOT_FOUND; +IndexPrimaryKeyAlreadyExists , invalid , BAD_REQUEST ; +IndexPrimaryKeyNoCandidateFound , invalid , BAD_REQUEST ; +IndexPrimaryKeyMultipleCandidatesFound, invalid , BAD_REQUEST; +Internal , internal , INTERNAL_SERVER_ERROR ; +InvalidApiKeyActions , invalid , BAD_REQUEST ; +InvalidApiKeyDescription , invalid , BAD_REQUEST ; +InvalidApiKeyExpiresAt , invalid , BAD_REQUEST ; +InvalidApiKeyIndexes , invalid , BAD_REQUEST ; +InvalidApiKeyLimit , invalid , BAD_REQUEST ; +InvalidApiKeyName , invalid , BAD_REQUEST ; +InvalidApiKeyOffset , invalid , BAD_REQUEST ; +InvalidApiKeyUid , invalid , BAD_REQUEST ; +InvalidApiKey , authentication, FORBIDDEN ; +InvalidContentType , invalid , UNSUPPORTED_MEDIA_TYPE ; +InvalidDocumentFields , invalid , BAD_REQUEST ; +InvalidDocumentGeoField , invalid , BAD_REQUEST ; +InvalidDocumentId , invalid , BAD_REQUEST ; +InvalidDocumentLimit , invalid , BAD_REQUEST ; +InvalidDocumentOffset , invalid , BAD_REQUEST ; +InvalidIndexLimit , invalid , BAD_REQUEST ; +InvalidIndexOffset , invalid , BAD_REQUEST ; +InvalidIndexPrimaryKey , invalid , BAD_REQUEST ; +InvalidIndexUid , invalid , BAD_REQUEST ; +InvalidMinWordLengthForTypo , invalid , BAD_REQUEST ; +InvalidSearchAttributesToCrop , invalid , BAD_REQUEST ; +InvalidSearchAttributesToHighlight , invalid , BAD_REQUEST ; +InvalidSearchAttributesToRetrieve , invalid , BAD_REQUEST ; +InvalidSearchCropLength , invalid , BAD_REQUEST ; +InvalidSearchCropMarker , invalid , BAD_REQUEST ; +InvalidSearchFacets , invalid , BAD_REQUEST ; +InvalidSearchFilter , invalid , BAD_REQUEST ; +InvalidSearchHighlightPostTag , invalid , BAD_REQUEST ; +InvalidSearchHighlightPreTag , invalid , BAD_REQUEST ; +InvalidSearchHitsPerPage , invalid , BAD_REQUEST ; +InvalidSearchLimit , invalid , BAD_REQUEST ; +InvalidSearchMatchingStrategy , invalid , BAD_REQUEST ; +InvalidSearchOffset , invalid , BAD_REQUEST ; +InvalidSearchPage , invalid , BAD_REQUEST ; +InvalidSearchQ , invalid , BAD_REQUEST ; +InvalidSearchShowMatchesPosition , invalid , BAD_REQUEST ; +InvalidSearchSort , invalid , BAD_REQUEST ; +InvalidSettingsDisplayedAttributes , invalid , BAD_REQUEST ; +InvalidSettingsDistinctAttribute , invalid , BAD_REQUEST ; +InvalidSettingsFaceting , invalid , BAD_REQUEST ; +InvalidSettingsFilterableAttributes , invalid , BAD_REQUEST ; +InvalidSettingsPagination , invalid , BAD_REQUEST ; +InvalidSettingsRankingRules , invalid , BAD_REQUEST ; +InvalidSettingsSearchableAttributes , invalid , BAD_REQUEST ; +InvalidSettingsSortableAttributes , invalid , BAD_REQUEST ; +InvalidSettingsStopWords , invalid , BAD_REQUEST ; +InvalidSettingsSynonyms , invalid , BAD_REQUEST ; +InvalidSettingsTypoTolerance , invalid , BAD_REQUEST ; +InvalidState , internal , INTERNAL_SERVER_ERROR ; +InvalidStoreFile , internal , INTERNAL_SERVER_ERROR ; +InvalidSwapDuplicateIndexFound , invalid , BAD_REQUEST ; +InvalidSwapIndexes , invalid , BAD_REQUEST ; +InvalidTaskAfterEnqueuedAt , invalid , BAD_REQUEST ; +InvalidTaskAfterFinishedAt , invalid , BAD_REQUEST ; +InvalidTaskAfterStartedAt , invalid , BAD_REQUEST ; +InvalidTaskBeforeEnqueuedAt , invalid , BAD_REQUEST ; +InvalidTaskBeforeFinishedAt , invalid , BAD_REQUEST ; +InvalidTaskBeforeStartedAt , invalid , BAD_REQUEST ; +InvalidTaskCanceledBy , invalid , BAD_REQUEST ; +InvalidTaskFrom , invalid , BAD_REQUEST ; +InvalidTaskLimit , invalid , BAD_REQUEST ; +InvalidTaskStatuses , invalid , BAD_REQUEST ; +InvalidTaskTypes , invalid , BAD_REQUEST ; +InvalidTaskUids , invalid , BAD_REQUEST ; +IoError , system , UNPROCESSABLE_ENTITY; +MalformedPayload , invalid , BAD_REQUEST ; +MaxFieldsLimitExceeded , invalid , BAD_REQUEST ; +MissingApiKeyActions , invalid , BAD_REQUEST ; +MissingApiKeyExpiresAt , invalid , BAD_REQUEST ; +MissingApiKeyIndexes , invalid , BAD_REQUEST ; +MissingAuthorizationHeader , authentication, UNAUTHORIZED ; +MissingContentType , invalid , UNSUPPORTED_MEDIA_TYPE ; +MissingDocumentId , invalid , BAD_REQUEST ; +MissingIndexUid , invalid , BAD_REQUEST ; +MissingMasterKey , authentication, UNAUTHORIZED ; +MissingPayload , invalid , BAD_REQUEST ; +MissingTaskFilters , invalid , BAD_REQUEST ; +NoSpaceLeftOnDevice , system , UNPROCESSABLE_ENTITY; +PayloadTooLarge , invalid , PAYLOAD_TOO_LARGE ; +TaskNotFound , invalid , NOT_FOUND ; +TooManyOpenFiles , system , UNPROCESSABLE_ENTITY ; +UnretrievableDocument , internal , BAD_REQUEST ; +UnretrievableErrorCode , invalid , BAD_REQUEST ; +UnsupportedMediaType , invalid , UNSUPPORTED_MEDIA_TYPE } /// Internal structure providing a convenient way to create error codes struct ErrCode { status_code: StatusCode, error_type: ErrorType, - error_name: &'static str, + error_name: String, } impl ErrCode { - fn authentication(error_name: &'static str, status_code: StatusCode) -> ErrCode { + fn authentication(error_name: String, status_code: StatusCode) -> ErrCode { ErrCode { status_code, error_name, error_type: ErrorType::AuthenticationError } } - fn internal(error_name: &'static str, status_code: StatusCode) -> ErrCode { + fn internal(error_name: String, status_code: StatusCode) -> ErrCode { ErrCode { status_code, error_name, error_type: ErrorType::InternalError } } - fn invalid(error_name: &'static str, status_code: StatusCode) -> ErrCode { + fn invalid(error_name: String, status_code: StatusCode) -> ErrCode { ErrCode { status_code, error_name, error_type: ErrorType::InvalidRequestError } } - fn system(error_name: &'static str, status_code: StatusCode) -> ErrCode { + fn system(error_name: String, status_code: StatusCode) -> ErrCode { ErrCode { status_code, error_name, error_type: ErrorType::System } } } @@ -591,7 +321,7 @@ impl ErrorCode for milli::Error { | UserError::DocumentLimitReached | UserError::AccessingSoftDeletedDocument { .. } | UserError::UnknownInternalDocumentId { .. } => Code::Internal, - UserError::InvalidStoreFile => Code::InvalidStore, + UserError::InvalidStoreFile => Code::InvalidStoreFile, UserError::NoSpaceLeftOnDevice => Code::NoSpaceLeftOnDevice, UserError::MaxDatabaseSizeReached => Code::DatabaseSizeLimitReached, UserError::AttributeLimitReached => Code::MaxFieldsLimitExceeded, @@ -600,11 +330,11 @@ impl ErrorCode for milli::Error { UserError::InvalidDocumentId { .. } | UserError::TooManyDocumentIds { .. } => { Code::InvalidDocumentId } - UserError::NoPrimaryKeyCandidateFound => Code::NoPrimaryKeyCandidateFound, + UserError::NoPrimaryKeyCandidateFound => Code::IndexPrimaryKeyNoCandidateFound, UserError::MultiplePrimaryKeyCandidatesFound { .. } => { - Code::MultiplePrimaryKeyCandidatesFound + Code::IndexPrimaryKeyMultipleCandidatesFound } - UserError::PrimaryKeyCannotBeChanged(_) => Code::PrimaryKeyAlreadyPresent, + UserError::PrimaryKeyCannotBeChanged(_) => Code::IndexPrimaryKeyAlreadyExists, UserError::SortRankingRuleMissing => Code::InvalidSearchSort, UserError::InvalidFacetsDistribution { .. } => Code::BadRequest, UserError::InvalidSortableAttribute { .. } => Code::InvalidSearchSort, @@ -639,7 +369,7 @@ impl ErrorCode for HeedError { fn error_code(&self) -> Code { match self { HeedError::Mdb(MdbError::MapFull) => Code::DatabaseSizeLimitReached, - HeedError::Mdb(MdbError::Invalid) => Code::InvalidStore, + HeedError::Mdb(MdbError::Invalid) => Code::InvalidStoreFile, HeedError::Io(e) => e.error_code(), HeedError::Mdb(_) | HeedError::Encoding @@ -680,6 +410,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..00d1a33eb 100644 --- a/meilisearch-types/src/keys.rs +++ b/meilisearch-types/src/keys.rs @@ -1,22 +1,105 @@ +use std::convert::Infallible; +use std::fmt::Display; use std::hash::Hash; -use std::str::FromStr; +use deserr::{DeserializeError, DeserializeFromValue, MergeWithError, 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 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, + ) + } +} + +fn parse_uuid_from_str(s: &str) -> Result> { + Uuid::parse_str(s).map_err(TakeErrorMessage) +} + +#[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, from(&String) = parse_uuid_from_str -> TakeErrorMessage)] + pub uid: KeyId, + #[deserr(error = DeserrError)] + 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 +118,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 +149,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 +366,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..3169920a6 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::deserr_codes::*; +use crate::error::{unwrap_any, DeserrError}; /// 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/Cargo.toml b/meilisearch/Cargo.toml index ae866a39b..4ed1843db 100644 --- a/meilisearch/Cargo.toml +++ b/meilisearch/Cargo.toml @@ -19,7 +19,7 @@ byte-unit = { version = "4.0.14", default-features = false, features = ["std", " bytes = "1.2.1" clap = { version = "4.0.9", features = ["derive", "env"] } crossbeam-channel = "0.5.6" -deserr = { version = "0.1.2", features = ["serde-json"] } +deserr = { version = "0.1.4", features = ["serde-json"] } dump = { path = "../dump" } either = "1.8.0" env_logger = "0.9.1" 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..76912bbaa 100644 --- a/meilisearch/src/routes/api_key.rs +++ b/meilisearch/src/routes/api_key.rs @@ -1,20 +1,21 @@ -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::*; +use meilisearch_types::error::{Code, DeserrError, ResponseError, TakeErrorMessage}; +use meilisearch_types::keys::{Action, CreateApiKey, Key, PatchApiKey}; use serde::{Deserialize, Serialize}; -use serde_json::Value; use time::OffsetDateTime; use uuid::Uuid; +use super::indexes::search::parse_usize_take_error_message; +use super::PAGINATION_DEFAULT_LIMIT; use crate::extractors::authentication::policies::*; use crate::extractors::authentication::GuardedData; +use crate::extractors::json::ValidatedJson; use crate::extractors::query_parameters::QueryParameter; use crate::extractors::sequential_extractor::SeqHandler; use crate::routes::Pagination; @@ -35,7 +36,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 +50,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 +106,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..3a54bba6a 100644 --- a/meilisearch/src/routes/indexes/documents.rs +++ b/meilisearch/src/routes/indexes/documents.rs @@ -1,19 +1,17 @@ -use std::convert::Infallible; -use std::fmt; use std::io::ErrorKind; use std::num::ParseIntError; -use std::str::FromStr; 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::*; +use meilisearch_types::error::{DeserrError, ResponseError, TakeErrorMessage}; use meilisearch_types::heed::RoTxn; use meilisearch_types::index_uid::IndexUid; use meilisearch_types::milli::update::IndexDocumentsMethod; @@ -29,6 +27,7 @@ use tempfile::tempfile; use tokio::fs::File; use tokio::io::{AsyncSeekExt, AsyncWriteExt, BufWriter}; +use super::search::parse_usize_take_error_message; use crate::analytics::{Analytics, DocumentDeletionKind}; use crate::error::MeilisearchHttpError; use crate::error::PayloadError::ReceivePayload; @@ -83,61 +82,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 +119,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 +148,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 +185,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..19a4dfa6c 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, TakeErrorMessage}; use meilisearch_types::index_uid::IndexUid; use meilisearch_types::milli::{self, FieldDistribution, Index}; use meilisearch_types::tasks::KindWithContent; @@ -16,7 +12,8 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use time::OffsetDateTime; -use super::{Pagination, SummarizedTaskView}; +use self::search::parse_usize_take_error_message; +use super::{Pagination, SummarizedTaskView, PAGINATION_DEFAULT_LIMIT}; use crate::analytics::Analytics; use crate::extractors::authentication::policies::*; use crate::extractors::authentication::{AuthenticationError, GuardedData}; @@ -72,9 +69,26 @@ impl IndexView { } } +#[derive(DeserializeFromValue, Deserialize, Debug, Clone, Copy)] +#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ListIndexes { + #[serde(default)] + #[deserr(error = DeserrError, default, from(&String) = parse_usize_take_error_message -> TakeErrorMessage)] + 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 ListIndexes { + fn as_pagination(self) -> Pagination { + Pagination { offset: self.offset, limit: self.limit } + } +} + 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 +98,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 +140,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 +162,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 +186,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..13c280d63 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; @@ -19,7 +16,7 @@ use crate::routes::SummarizedTaskView; #[macro_export] macro_rules! make_setting_route { - ($route:literal, $update_verb:ident, $type:ty, $attr:ident, $camelcase_attr:literal, $analytics_var:ident, $analytics:expr) => { + ($route:literal, $update_verb:ident, $type:ty, $err_ty:ty, $attr:ident, $camelcase_attr:literal, $analytics_var:ident, $analytics:expr) => { pub mod $attr { use actix_web::web::Data; use actix_web::{web, HttpRequest, HttpResponse, Resource}; @@ -68,7 +65,7 @@ macro_rules! make_setting_route { Data, >, index_uid: actix_web::web::Path, - body: actix_web::web::Json>, + body: $crate::routes::indexes::ValidatedJson, $err_ty>, req: HttpRequest, $analytics_var: web::Data, ) -> std::result::Result { @@ -133,6 +130,9 @@ make_setting_route!( "/filterable-attributes", put, std::collections::BTreeSet, + meilisearch_types::error::DeserrError< + meilisearch_types::error::deserr_codes::InvalidSettingsFilterableAttributes, + >, filterable_attributes, "filterableAttributes", analytics, @@ -156,6 +156,9 @@ make_setting_route!( "/sortable-attributes", put, std::collections::BTreeSet, + meilisearch_types::error::DeserrError< + meilisearch_types::error::deserr_codes::InvalidSettingsSortableAttributes, + >, sortable_attributes, "sortableAttributes", analytics, @@ -179,6 +182,9 @@ make_setting_route!( "/displayed-attributes", put, Vec, + meilisearch_types::error::DeserrError< + meilisearch_types::error::deserr_codes::InvalidSettingsDisplayedAttributes, + >, displayed_attributes, "displayedAttributes", analytics, @@ -202,6 +208,9 @@ make_setting_route!( "/typo-tolerance", patch, meilisearch_types::settings::TypoSettings, + meilisearch_types::error::DeserrError< + meilisearch_types::error::deserr_codes::InvalidSettingsTypoTolerance, + >, typo_tolerance, "typoTolerance", analytics, @@ -212,7 +221,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())), @@ -244,6 +253,9 @@ make_setting_route!( "/searchable-attributes", put, Vec, + meilisearch_types::error::DeserrError< + meilisearch_types::error::deserr_codes::InvalidSettingsSearchableAttributes, + >, searchable_attributes, "searchableAttributes", analytics, @@ -267,6 +279,9 @@ make_setting_route!( "/stop-words", put, std::collections::BTreeSet, + meilisearch_types::error::DeserrError< + meilisearch_types::error::deserr_codes::InvalidSettingsStopWords, + >, stop_words, "stopWords", analytics, @@ -289,6 +304,9 @@ make_setting_route!( "/synonyms", put, std::collections::BTreeMap>, + meilisearch_types::error::DeserrError< + meilisearch_types::error::deserr_codes::InvalidSettingsSynonyms, + >, synonyms, "synonyms", analytics, @@ -311,6 +329,9 @@ make_setting_route!( "/distinct-attribute", put, String, + meilisearch_types::error::DeserrError< + meilisearch_types::error::deserr_codes::InvalidSettingsDistinctAttribute, + >, distinct_attribute, "distinctAttribute", analytics, @@ -331,24 +352,27 @@ make_setting_route!( make_setting_route!( "/ranking-rules", put, - Vec, + Vec, + meilisearch_types::error::DeserrError< + meilisearch_types::error::deserr_codes::InvalidSettingsRankingRules, + >, 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), @@ -360,6 +384,9 @@ make_setting_route!( "/faceting", patch, meilisearch_types::settings::FacetingSettings, + meilisearch_types::error::DeserrError< + meilisearch_types::error::deserr_codes::InvalidSettingsFaceting, + >, faceting, "faceting", analytics, @@ -382,6 +409,9 @@ make_setting_route!( "/pagination", patch, meilisearch_types::settings::PaginationSettings, + meilisearch_types::error::DeserrError< + meilisearch_types::error::deserr_codes::InvalidSettingsPagination, + >, pagination, "pagination", analytics, @@ -428,66 +458,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 +471,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..2e619540a 100644 --- a/meilisearch/src/routes/mod.rs +++ b/meilisearch/src/routes/mod.rs @@ -3,10 +3,9 @@ use std::str::FromStr; use actix_web::web::Data; 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::{ResponseError, TakeErrorMessage}; use meilisearch_types::settings::{Settings, Unchecked}; use meilisearch_types::star_or::StarOr; use meilisearch_types::tasks::{Kind, Status, Task, TaskId}; @@ -57,6 +56,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,16 +90,8 @@ impl From for SummarizedTaskView { } } } - -#[derive(DeserializeFromValue, Deserialize, Debug, Clone, Copy)] -#[deserr(rename_all = camelCase, deny_unknown_fields)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct Pagination { - #[serde(default)] - #[deserr(default, from(&String) = FromStr::from_str -> std::num::ParseIntError)] pub offset: usize, - #[serde(default = "PAGINATION_DEFAULT_LIMIT")] - #[deserr(default = PAGINATION_DEFAULT_LIMIT(), from(&String) = FromStr::from_str -> std::num::ParseIntError)] pub limit: usize, } 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..129137859 100644 --- a/meilisearch/src/search.rs +++ b/meilisearch/src/search.rs @@ -1,16 +1,12 @@ 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::*; +use meilisearch_types::error::DeserrError; use meilisearch_types::settings::DEFAULT_PAGINATION_MAX_TOTAL_HITS; use meilisearch_types::{milli, Document}; use milli::tokenizer::TokenizerBuilder; @@ -34,32 +30,41 @@ pub const DEFAULT_HIGHLIGHT_PRE_TAG: fn() -> String = || "".to_string(); pub const DEFAULT_HIGHLIGHT_POST_TAG: fn() -> String = || "".to_string(); #[derive(Debug, Clone, Default, PartialEq, DeserializeFromValue)] -#[deserr(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 +99,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..485d1b551 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::snapshot!(code, @"201 Created"); + meili_snap::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"201 Created"); + meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }), @r###" + { + "name": null, + "description": "Indexing API key", + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "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::snapshot!(code, @"201 Created"); + meili_snap::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"201 Created"); + meili_snap::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"401 Unauthorized"); + meili_snap::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"403 Forbidden"); + meili_snap::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"400 Bad Request"); + meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" + { + "message": "Json deserialize error: missing field `indexes` at ``", + "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::snapshot!(code, @"400 Bad Request"); + meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" + { + "message": "Json deserialize error: missing field `actions` at ``", + "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::snapshot!(code, @"201 Created"); + meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }), @r###" + { + "name": null, + "description": "Indexing API key", + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "documents.add" + ], + "indexes": [ + "products" + ], + "expiresAt": null, + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + } + "###); } #[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::snapshot!(code, @"400 Bad Request"); + meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" + { + "message": "invalid type: Map `{\"name\":\"products\"}`, expected a String at `.description`.", + "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::snapshot!(code, @"400 Bad Request"); + meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" + { + "message": "invalid type: Map `{\"name\":\"products\"}`, expected a String at `.name`.", + "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::snapshot!(code, @"400 Bad Request"); + meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" + { + "message": "invalid type: Map `{\"name\":\"products\"}`, expected a Sequence at `.indexes`.", + "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::snapshot!(code, @"400 Bad Request"); + meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" + { + "message": "`invalid index # / \\name with spaces` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_). at `.indexes[0]`.", + "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::snapshot!(code, @"400 Bad Request"); + meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" + { + "message": "invalid type: Map `{\"name\":\"products\"}`, expected a Sequence at `.actions`.", + "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::snapshot!(code, @"400 Bad Request"); + meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" + { + "message": "Json deserialize error: unknown value `doc.add`, expected one of `*`, `search`, `documents.*`, `documents.add`, `documents.get`, `documents.delete`, `indexes.*`, `indexes.create`, `indexes.get`, `indexes.update`, `indexes.delete`, `indexes.swap`, `tasks.*`, `tasks.cancel`, `tasks.delete`, `tasks.get`, `settings.*`, `settings.get`, `settings.update`, `stats.*`, `stats.get`, `metrics.*`, `metrics.get`, `dumps.*`, `dumps.create`, `version`, `keys.create`, `keys.get`, `keys.update`, `keys.delete` at `.actions[0]`.", + "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::snapshot!(code, @"400 Bad Request"); + meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" + { + "message": "invalid type: Map `{\"name\":\"products\"}`, expected a String at `.expiresAt`.", + "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::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" + { + "message": "`2010-11-13T00:00:00Z` is not a valid date. It should follow the RFC 3339 format to represents a date or datetime in the future or specified as a null value. e.g. 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'.\n at `.expiresAt`.", + "code": "invalid_api_key_expires_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-api-key-expires-at" + } + "###); + meili_snap::snapshot!(code, @"400 Bad Request"); } #[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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"400 Bad Request"); } #[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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"201 Created"); // 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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"409 Conflict"); } #[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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"201 Created"); 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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"200 OK"); // 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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"200 OK"); } #[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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"401 Unauthorized"); } #[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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"403 Forbidden"); } #[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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"404 Not Found"); } #[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::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }), @r###" + { + "name": null, + "description": "Indexing API key", + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "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::snapshot!(code, @"201 Created"); 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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"200 OK"); } #[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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"401 Unauthorized"); } #[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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"403 Forbidden"); } #[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::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }), @r###" + { + "name": null, + "description": "Indexing API key", + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "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::snapshot!(code, @"201 Created"); let uid = response["uid"].as_str().unwrap(); let (response, code) = server.delete_api_key(&uid).await; - assert_eq!(204, code, "{:?}", &response); + meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @"null"); + meili_snap::snapshot!(code, @"204 No Content"); // check if API key no longer exist. let (response, code) = server.get_api_key(&uid).await; - assert_eq!(404, code, "{:?}", &response); + meili_snap::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"404 Not Found"); } #[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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"401 Unauthorized"); } #[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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"403 Forbidden"); } #[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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"404 Not Found"); } #[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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"201 Created"); 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::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }), @r###" + { + "name": null, + "description": "Indexing API key", + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "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::snapshot!(code, @"200 OK"); // 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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"200 OK"); // 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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"200 OK"); } #[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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"201 Created"); 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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"200 OK"); + 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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"200 OK"); // 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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"200 OK"); } #[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::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }), @r###" + { + "name": null, + "description": "Indexing API key", + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "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::snapshot!(code, @"201 Created"); 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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"400 Bad Request"); } #[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::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }), @r###" + { + "name": null, + "description": "Indexing API key", + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "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::snapshot!(code, @"201 Created"); 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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"400 Bad Request"); } #[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::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }), @r###" + { + "name": null, + "description": "Indexing API key", + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "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::snapshot!(code, @"201 Created"); 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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"400 Bad Request"); } #[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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"401 Unauthorized"); } #[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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"403 Forbidden"); } #[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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"404 Not Found"); } #[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::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }), @r###" + { + "name": null, + "description": "Indexing API key", + "key": "[ignored]", + "uid": "[ignored]", + "actions": [ + "search" + ], + "indexes": [ + "products" + ], + "expiresAt": "2050-11-13T00:00:00Z", + "createdAt": "[ignored]", + "updatedAt": "[ignored]" + } + "###); + meili_snap::snapshot!(code, @"201 Created"); 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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"400 Bad Request"); // 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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"400 Bad Request"); } #[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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"401 Unauthorized"); let (response, code) = server.patch_api_key("content", json!({})).await; - - assert_eq!(expected_code, code, "{:?}", &response); - assert_eq!(response, expected_response); + meili_snap::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"401 Unauthorized"); let (response, code) = server.get_api_key("content").await; - - assert_eq!(expected_code, code, "{:?}", &response); - assert_eq!(response, expected_response); + meili_snap::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"401 Unauthorized"); let (response, code) = server.list_api_keys().await; - - assert_eq!(expected_code, code, "{:?}", &response); - assert_eq!(response, expected_response); + meili_snap::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"401 Unauthorized"); 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::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"401 Unauthorized"); let (response, code) = server.patch_api_key("content", json!({})).await; - - assert_eq!(expected_code, code, "{:?}", &response); - assert_eq!(response, expected_response); + meili_snap::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"401 Unauthorized"); let (response, code) = server.get_api_key("content").await; - - assert_eq!(expected_code, code, "{:?}", &response); - assert_eq!(response, expected_response); + meili_snap::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"401 Unauthorized"); let (response, code) = server.list_api_keys().await; - - assert_eq!(expected_code, code, "{:?}", &response); - assert_eq!(response, expected_response); + meili_snap::snapshot!(meili_snap::json_string!(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::snapshot!(code, @"401 Unauthorized"); } diff --git a/meilisearch/tests/common/index.rs b/meilisearch/tests/common/index.rs index a7779e5ad..d42f18d2c 100644 --- a/meilisearch/tests/common/index.rs +++ b/meilisearch/tests/common/index.rs @@ -197,6 +197,76 @@ impl Index<'_> { self.service.patch_encoded(url, settings, self.encoder).await } + pub async fn update_settings_displayed_attributes( + &self, + settings: Value, + ) -> (Value, StatusCode) { + let url = + format!("/indexes/{}/settings/displayed-attributes", urlencode(self.uid.as_ref())); + self.service.put_encoded(url, settings, self.encoder).await + } + + pub async fn update_settings_searchable_attributes( + &self, + settings: Value, + ) -> (Value, StatusCode) { + let url = + format!("/indexes/{}/settings/searchable-attributes", urlencode(self.uid.as_ref())); + self.service.put_encoded(url, settings, self.encoder).await + } + + pub async fn update_settings_filterable_attributes( + &self, + settings: Value, + ) -> (Value, StatusCode) { + let url = + format!("/indexes/{}/settings/filterable-attributes", urlencode(self.uid.as_ref())); + self.service.put_encoded(url, settings, self.encoder).await + } + + pub async fn update_settings_sortable_attributes( + &self, + settings: Value, + ) -> (Value, StatusCode) { + let url = format!("/indexes/{}/settings/sortable-attributes", urlencode(self.uid.as_ref())); + self.service.put_encoded(url, settings, self.encoder).await + } + + pub async fn update_settings_ranking_rules(&self, settings: Value) -> (Value, StatusCode) { + let url = format!("/indexes/{}/settings/ranking-rules", urlencode(self.uid.as_ref())); + self.service.put_encoded(url, settings, self.encoder).await + } + + pub async fn update_settings_stop_words(&self, settings: Value) -> (Value, StatusCode) { + let url = format!("/indexes/{}/settings/stop-words", urlencode(self.uid.as_ref())); + self.service.put_encoded(url, settings, self.encoder).await + } + + pub async fn update_settings_synonyms(&self, settings: Value) -> (Value, StatusCode) { + let url = format!("/indexes/{}/settings/synonyms", urlencode(self.uid.as_ref())); + self.service.put_encoded(url, settings, self.encoder).await + } + + pub async fn update_settings_distinct_attribute(&self, settings: Value) -> (Value, StatusCode) { + let url = format!("/indexes/{}/settings/distinct-attribute", urlencode(self.uid.as_ref())); + self.service.put_encoded(url, settings, self.encoder).await + } + + pub async fn update_settings_typo_tolerance(&self, settings: Value) -> (Value, StatusCode) { + let url = format!("/indexes/{}/settings/typo-tolerance", urlencode(self.uid.as_ref())); + self.service.patch_encoded(url, settings, self.encoder).await + } + + pub async fn update_settings_faceting(&self, settings: Value) -> (Value, StatusCode) { + let url = format!("/indexes/{}/settings/faceting", urlencode(self.uid.as_ref())); + self.service.patch_encoded(url, settings, self.encoder).await + } + + pub async fn update_settings_pagination(&self, settings: Value) -> (Value, StatusCode) { + let url = format!("/indexes/{}/settings/pagination", urlencode(self.uid.as_ref())); + self.service.patch_encoded(url, settings, self.encoder).await + } + pub async fn delete_settings(&self) -> (Value, StatusCode) { let url = format!("/indexes/{}/settings", urlencode(self.uid.as_ref())); self.service.delete(url).await diff --git a/meilisearch/tests/documents/add_documents.rs b/meilisearch/tests/documents/add_documents.rs index c27b899c6..4af365a7e 100644 --- a/meilisearch/tests/documents/add_documents.rs +++ b/meilisearch/tests/documents/add_documents.rs @@ -926,7 +926,7 @@ async fn error_primary_key_inference() { "indexedDocuments": 1 }, "error": { - "message": "The primary key inference process failed because the engine did not find any field ending with `id` in its name. Please specify the primary key manually using the `primaryKey` query parameter.", + "message": "The primary key inference failed as the engine did not find any field ending with `id` in its name. Please specify the primary key manually using the `primaryKey` query parameter.", "code": "index_primary_key_no_candidate_found", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#index-primary-key-no-candidate-found" @@ -966,7 +966,7 @@ async fn error_primary_key_inference() { "indexedDocuments": 1 }, "error": { - "message": "The primary key inference process failed because the engine found 3 fields ending with `id` in their name, such as 'id' and 'object_id'. Please specify the primary key manually using the `primaryKey` query parameter.", + "message": "The primary key inference failed as the engine found 3 fields ending with `id` in their names: 'id' and 'object_id'. Please specify the primary key manually using the `primaryKey` query parameter.", "code": "index_primary_key_multiple_candidates_found", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#index-primary-key-multiple-candidates-found" diff --git a/meilisearch/tests/search/errors.rs b/meilisearch/tests/search/errors.rs index 511e17eb5..d582a3672 100644 --- a/meilisearch/tests/search/errors.rs +++ b/meilisearch/tests/search/errors.rs @@ -384,7 +384,7 @@ async fn search_bad_matching_strategy() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "Incorrect tag value at `.matchingStrategy`.", + "message": "Json deserialize error: unknown value `doggo`, expected one of `last`, `all` at `.matchingStrategy`.", "code": "invalid_search_matching_strategy", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-search-matching-strategy" @@ -395,7 +395,7 @@ async fn search_bad_matching_strategy() { snapshot!(code, @"400 Bad Request"); snapshot!(json_string!(response), @r###" { - "message": "Incorrect tag value at `.matchingStrategy`.", + "message": "Json deserialize error: unknown value `doggo`, expected one of `last`, `all` at `.matchingStrategy`.", "code": "invalid_search_matching_strategy", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-search-matching-strategy" diff --git a/meilisearch/tests/settings/errors.rs b/meilisearch/tests/settings/errors.rs index b1a846637..77e62303a 100644 --- a/meilisearch/tests/settings/errors.rs +++ b/meilisearch/tests/settings/errors.rs @@ -18,6 +18,17 @@ async fn settings_bad_displayed_attributes() { "link": "https://docs.meilisearch.com/errors#invalid-settings-displayed-attributes" } "###); + + let (response, code) = index.update_settings_displayed_attributes(json!("doggo")).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Sequence at ``.", + "code": "invalid_settings_displayed_attributes", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-settings-displayed-attributes" + } + "###); } #[actix_rt::test] @@ -35,6 +46,17 @@ async fn settings_bad_searchable_attributes() { "link": "https://docs.meilisearch.com/errors#invalid-settings-searchable-attributes" } "###); + + let (response, code) = index.update_settings_searchable_attributes(json!("doggo")).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Sequence at ``.", + "code": "invalid_settings_searchable_attributes", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-settings-searchable-attributes" + } + "###); } #[actix_rt::test] @@ -52,6 +74,17 @@ async fn settings_bad_filterable_attributes() { "link": "https://docs.meilisearch.com/errors#invalid-settings-filterable-attributes" } "###); + + let (response, code) = index.update_settings_filterable_attributes(json!("doggo")).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Sequence at ``.", + "code": "invalid_settings_filterable_attributes", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-settings-filterable-attributes" + } + "###); } #[actix_rt::test] @@ -69,6 +102,17 @@ async fn settings_bad_sortable_attributes() { "link": "https://docs.meilisearch.com/errors#invalid-settings-sortable-attributes" } "###); + + let (response, code) = index.update_settings_sortable_attributes(json!("doggo")).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Sequence at ``.", + "code": "invalid_settings_sortable_attributes", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-settings-sortable-attributes" + } + "###); } #[actix_rt::test] @@ -86,6 +130,17 @@ async fn settings_bad_ranking_rules() { "link": "https://docs.meilisearch.com/errors#invalid-settings-ranking-rules" } "###); + + let (response, code) = index.update_settings_ranking_rules(json!("doggo")).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Sequence at ``.", + "code": "invalid_settings_ranking_rules", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-settings-ranking-rules" + } + "###); } #[actix_rt::test] @@ -103,6 +158,17 @@ async fn settings_bad_stop_words() { "link": "https://docs.meilisearch.com/errors#invalid-settings-stop-words" } "###); + + let (response, code) = index.update_settings_stop_words(json!("doggo")).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Sequence at ``.", + "code": "invalid_settings_stop_words", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-settings-stop-words" + } + "###); } #[actix_rt::test] @@ -120,6 +186,17 @@ async fn settings_bad_synonyms() { "link": "https://docs.meilisearch.com/errors#invalid-settings-synonyms" } "###); + + let (response, code) = index.update_settings_synonyms(json!("doggo")).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Map at ``.", + "code": "invalid_settings_synonyms", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-settings-synonyms" + } + "###); } #[actix_rt::test] @@ -137,6 +214,17 @@ async fn settings_bad_distinct_attribute() { "link": "https://docs.meilisearch.com/errors#invalid-settings-distinct-attribute" } "###); + + let (response, code) = index.update_settings_distinct_attribute(json!(["doggo"])).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: Sequence `[\"doggo\"]`, expected a String at ``.", + "code": "invalid_settings_distinct_attribute", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-settings-distinct-attribute" + } + "###); } #[actix_rt::test] @@ -154,6 +242,17 @@ async fn settings_bad_typo_tolerance() { "link": "https://docs.meilisearch.com/errors#invalid-settings-typo-tolerance" } "###); + + let (response, code) = index.update_settings_typo_tolerance(json!("doggo")).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Map at ``.", + "code": "invalid_settings_typo_tolerance", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-settings-typo-tolerance" + } + "###); } #[actix_rt::test] @@ -171,6 +270,17 @@ async fn settings_bad_faceting() { "link": "https://docs.meilisearch.com/errors#invalid-settings-faceting" } "###); + + let (response, code) = index.update_settings_faceting(json!("doggo")).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Map at ``.", + "code": "invalid_settings_faceting", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-settings-faceting" + } + "###); } #[actix_rt::test] @@ -188,4 +298,15 @@ async fn settings_bad_pagination() { "link": "https://docs.meilisearch.com/errors#invalid-settings-pagination" } "###); + + let (response, code) = index.update_settings_pagination(json!("doggo")).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Map at ``.", + "code": "invalid_settings_pagination", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-settings-pagination" + } + "###); } diff --git a/meilisearch/tests/settings/get_settings.rs b/meilisearch/tests/settings/get_settings.rs index 8ec10db39..3ac7d3801 100644 --- a/meilisearch/tests/settings/get_settings.rs +++ b/meilisearch/tests/settings/get_settings.rs @@ -179,15 +179,15 @@ async fn error_update_setting_unexisting_index_invalid_uid() { let server = Server::new().await; let index = server.index("test##! "); let (response, code) = index.update_settings(json!({})).await; - assert_eq!(code, 400); - - let expected = json!({ - "message": "`test##! ` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", - "code": "invalid_index_uid", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-index-uid"}); - - assert_eq!(response, expected); + meili_snap::snapshot!(code, @"400 Bad Request"); + meili_snap::snapshot!(meili_snap::json_string!(response), @r###" + { + "message": "`test##! ` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", + "code": "invalid_index_uid", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-index-uid" + } + "###); } macro_rules! test_setting_routes { @@ -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::snapshot!(code, @"400 Bad Request"); + meili_snap::snapshot!(meili_snap::json_string!(response), @r###" + { + "message": "`manyTheFish` ranking rule is invalid. Valid ranking rules are words, typo, sort, proximity, attribute, exactness and custom ranking rules. at `.rankingRules[0]`.", + "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..a47c1ad9a 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::assert_json_snapshot; use serde_json::json; use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; @@ -179,7 +179,7 @@ async fn get_task_filter_error() { let (response, code) = server.tasks_filter(json!( { "lol": "pied" })).await; assert_eq!(code, 400, "{}", response); - insta::assert_json_snapshot!(response, @r###" + meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { "message": "Query deserialize error: unknown field `lol`", "code": "bad_request", @@ -190,7 +190,7 @@ async fn get_task_filter_error() { let (response, code) = server.tasks_filter(json!( { "uids": "pied" })).await; assert_eq!(code, 400, "{}", response); - insta::assert_json_snapshot!(response, @r###" + meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { "message": "Task uid `pied` is invalid. It should only contain numeric characters.", "code": "invalid_task_uids", @@ -201,7 +201,7 @@ async fn get_task_filter_error() { let (response, code) = server.tasks_filter(json!( { "from": "pied" })).await; assert_eq!(code, 400, "{}", response); - insta::assert_json_snapshot!(response, @r###" + meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { "message": "Query deserialize error: invalid digit found in string", "code": "bad_request", @@ -212,7 +212,7 @@ async fn get_task_filter_error() { let (response, code) = server.tasks_filter(json!( { "beforeStartedAt": "pied" })).await; assert_eq!(code, 400, "{}", response); - insta::assert_json_snapshot!(response, @r###" + meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { "message": "Task `beforeStartedAt` `pied` is invalid. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "code": "invalid_task_before_started_at", @@ -228,7 +228,7 @@ async fn delete_task_filter_error() { let (response, code) = server.delete_tasks(json!(null)).await; assert_eq!(code, 400, "{}", response); - insta::assert_json_snapshot!(response, @r###" + meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { "message": "Query parameters to filter the tasks to delete are missing. Available query parameters are: `uids`, `indexUids`, `statuses`, `types`, `beforeEnqueuedAt`, `afterEnqueuedAt`, `beforeStartedAt`, `afterStartedAt`, `beforeFinishedAt`, `afterFinishedAt`.", "code": "missing_task_filters", @@ -239,7 +239,7 @@ async fn delete_task_filter_error() { let (response, code) = server.delete_tasks(json!({ "lol": "pied" })).await; assert_eq!(code, 400, "{}", response); - insta::assert_json_snapshot!(response, @r###" + meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { "message": "Query deserialize error: unknown field `lol`", "code": "bad_request", @@ -250,7 +250,7 @@ async fn delete_task_filter_error() { let (response, code) = server.delete_tasks(json!({ "uids": "pied" })).await; assert_eq!(code, 400, "{}", response); - insta::assert_json_snapshot!(response, @r###" + meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { "message": "Task uid `pied` is invalid. It should only contain numeric characters.", "code": "invalid_task_uids", @@ -266,7 +266,7 @@ async fn cancel_task_filter_error() { let (response, code) = server.cancel_tasks(json!(null)).await; assert_eq!(code, 400, "{}", response); - insta::assert_json_snapshot!(response, @r###" + meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { "message": "Query parameters to filter the tasks to cancel are missing. Available query parameters are: `uids`, `indexUids`, `statuses`, `types`, `beforeEnqueuedAt`, `afterEnqueuedAt`, `beforeStartedAt`, `afterStartedAt`, `beforeFinishedAt`, `afterFinishedAt`.", "code": "missing_task_filters", @@ -277,7 +277,7 @@ async fn cancel_task_filter_error() { let (response, code) = server.cancel_tasks(json!({ "lol": "pied" })).await; assert_eq!(code, 400, "{}", response); - insta::assert_json_snapshot!(response, @r###" + meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { "message": "Query deserialize error: unknown field `lol`", "code": "bad_request", @@ -288,7 +288,7 @@ async fn cancel_task_filter_error() { let (response, code) = server.cancel_tasks(json!({ "uids": "pied" })).await; assert_eq!(code, 400, "{}", response); - insta::assert_json_snapshot!(response, @r###" + meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { "message": "Task uid `pied` is invalid. It should only contain numeric characters.", "code": "invalid_task_uids", @@ -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; + meili_snap::snapshot!(code, @"400 Bad Request"); + meili_snap::snapshot!(meili_snap::json_string!(response), @r###" + { + "message": "`custom` ranking rule is invalid. Valid ranking rules are words, typo, sort, proximity, attribute, exactness and custom ranking rules. at `.rankingRules[0]`.", + "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,