diff --git a/.github/workflows/latest-git-tag.yml b/.github/workflows/latest-git-tag.yml index 0adf0a1c6..2b5657fd5 100644 --- a/.github/workflows/latest-git-tag.yml +++ b/.github/workflows/latest-git-tag.yml @@ -3,7 +3,7 @@ name: Update latest git tag on: workflow_dispatch: release: - types: [published] + types: [released] jobs: check-version: @@ -17,6 +17,7 @@ jobs: update-latest-tag: runs-on: ubuntu-latest + needs: check-version steps: - uses: actions/checkout@v3 - uses: rickstaa/action-create-tag@v1 diff --git a/.github/workflows/publish-deb-brew-pkg.yml b/.github/workflows/publish-deb-brew-pkg.yml index a276b5a6e..4dea6e305 100644 --- a/.github/workflows/publish-deb-brew-pkg.yml +++ b/.github/workflows/publish-deb-brew-pkg.yml @@ -2,7 +2,7 @@ name: Publish to APT repository & Homebrew on: release: - types: [published] + types: [released] jobs: check-version: diff --git a/Cargo.lock b/Cargo.lock index a3152498c..ab9c0732c 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", @@ -337,9 +337,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", @@ -383,7 +383,7 @@ dependencies = [ "cfg-if", "libc", "miniz_oxide", - "object 0.30.1", + "object 0.30.2", "rustc-demangle", ] @@ -393,6 +393,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" @@ -1404,8 +1410,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", @@ -1433,8 +1439,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", ] @@ -1679,9 +1685,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" @@ -1958,9 +1964,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", @@ -1993,9 +1999,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" @@ -2065,8 +2071,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", ] @@ -2085,7 +2091,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", @@ -2122,9 +2128,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", @@ -2140,9 +2146,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", @@ -2551,7 +2557,7 @@ dependencies = [ name = "meilisearch-auth" version = "1.0.0" dependencies = [ - "base64", + "base64 0.13.1", "enum-iterator", "hmac", "meilisearch-types", @@ -2582,7 +2588,7 @@ dependencies = [ "insta", "meili-snap", "memmap2", - "milli 0.38.0", + "milli 0.39.0", "proptest", "proptest-derive", "roaring", @@ -2622,8 +2628,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", @@ -2633,16 +2639,17 @@ dependencies = [ "concat-arrays", "crossbeam-channel", "csv", + "deserr", "either", - "filter-parser 0.38.0", - "flatten-serde-json 0.38.0", + "filter-parser 0.39.0", + "flatten-serde-json 0.39.0", "fst", "fxhash", "geoutils", "grenad", "heed", "itertools", - "json-depth-checker 0.38.0", + "json-depth-checker 0.39.0", "levenshtein_automata", "log", "logging_timer", @@ -2718,9 +2725,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", ] @@ -2865,9 +2872,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", ] @@ -3010,7 +3017,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4" dependencies = [ - "base64", + "base64 0.13.1", ] [[package]] @@ -3029,9 +3036,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", @@ -3039,9 +3046,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", @@ -3049,9 +3056,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", @@ -3062,13 +3069,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]] @@ -3398,9 +3405,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", @@ -3434,7 +3441,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", @@ -3555,11 +3562,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]] @@ -3613,9 +3620,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", @@ -3897,9 +3904,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", @@ -4033,9 +4040,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", @@ -4136,9 +4143,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" @@ -4627,10 +4634,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 571490980..237381414 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,51 +256,50 @@ 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, - "invalid_filter" => v6::Code::Filter, - "invalid_sort" => v6::Code::Sort, + "invalid_filter" => v6::Code::InvalidSettingsFilterableAttributes, + "invalid_sort" => v6::Code::InvalidSettingsSortableAttributes, "bad_parameter" => v6::Code::BadParameter, "bad_request" => v6::Code::BadRequest, "database_size_limit_reached" => v6::Code::DatabaseSizeLimitReached, "document_not_found" => v6::Code::DocumentNotFound, "internal" => v6::Code::Internal, "invalid_geo_field" => v6::Code::InvalidDocumentGeoField, - "invalid_ranking_rule" => v6::Code::InvalidRankingRule, - "invalid_store_file" => v6::Code::InvalidStore, - "invalid_api_key" => v6::Code::InvalidToken, + "invalid_ranking_rule" => v6::Code::InvalidSettingsRankingRules, + "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::RetrieveDocument, - "search_error" => v6::Code::SearchDocuments, + "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, "malformed_payload" => v6::Code::MalformedPayload, "missing_payload" => v6::Code::MissingPayload, "api_key_not_found" => v6::Code::ApiKeyNotFound, - "missing_parameter" => v6::Code::UnretrievableErrorCode, + "missing_parameter" => v6::Code::BadRequest, "invalid_api_key_actions" => v6::Code::InvalidApiKeyActions, "invalid_api_key_indexes" => v6::Code::InvalidApiKeyIndexes, "invalid_api_key_expires_at" => v6::Code::InvalidApiKeyExpiresAt, "invalid_api_key_description" => v6::Code::InvalidApiKeyDescription, "invalid_api_key_name" => v6::Code::InvalidApiKeyName, "invalid_api_key_uid" => v6::Code::InvalidApiKeyUid, - "immutable_field" => v6::Code::ImmutableField, + "immutable_field" => v6::Code::BadRequest, "api_key_already_exists" => v6::Code::ApiKeyAlreadyExists, other => { log::warn!("Unknown error code {}", other); @@ -316,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..bd596ba2d 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 = "0.1.4" 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 bd06e69e9..bc29f9e82 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,478 +116,198 @@ 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, - - Filter, - Sort, - - // 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, - InvalidRankingRule, - InvalidStore, - InvalidToken, - MissingAuthorizationHeader, - MissingMasterKey, - DumpNotFound, - TaskNotFound, - TaskDeletionWithEmptyQuery, - TaskCancelationWithEmptyQuery, - PayloadTooLarge, - RetrieveDocument, - SearchDocuments, - 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::UNPROCESSABLE_ENTITY), - TooManyOpenFiles => { - ErrCode::system("too_many_open_files", StatusCode::UNPROCESSABLE_ENTITY) +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::UNPROCESSABLE_ENTITY) + /// 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) - } - // invalid ranking rule - InvalidRankingRule => ErrCode::invalid("invalid_ranking_rule", StatusCode::BAD_REQUEST), - - // invalid database - InvalidStore => { - ErrCode::internal("invalid_store_file", StatusCode::INTERNAL_SERVER_ERROR) + /// return error name, used as error code + fn name(&self) -> String { + self.err_code().error_name.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), - - // error related to filters - Filter => ErrCode::invalid("invalid_filter", StatusCode::BAD_REQUEST), - // error related to sorts - Sort => ErrCode::invalid("invalid_sort", 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), - RetrieveDocument => { - ErrCode::internal("unretrievable_document", StatusCode::BAD_REQUEST) - } - SearchDocuments => ErrCode::internal("search_error", StatusCode::BAD_REQUEST), - UnsupportedMediaType => { - ErrCode::invalid("unsupported_media_type", StatusCode::UNSUPPORTED_MEDIA_TYPE) + /// return the error type + fn type_(&self) -> String { + self.err_code().error_type.to_string() } - // 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 + } + } + )* + } } +} +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; - /// return the HTTP status code associated with the `Code` - fn http(&self) -> StatusCode { - self.err_code().status_code - } +ImmutableApiKeyUid , invalid , BAD_REQUEST; +ImmutableApiKeyKey , invalid , BAD_REQUEST; +ImmutableApiKeyActions , invalid , BAD_REQUEST; +ImmutableApiKeyIndexes , invalid , BAD_REQUEST; +ImmutableApiKeyExpiresAt , invalid , BAD_REQUEST; +ImmutableApiKeyCreatedAt , invalid , BAD_REQUEST; +ImmutableApiKeyUpdatedAt , invalid , BAD_REQUEST; - /// return error name, used as error code - fn name(&self) -> String { - self.err_code().error_name.to_string() - } +ImmutableIndexUid , invalid , BAD_REQUEST; +ImmutableIndexCreatedAt , invalid , BAD_REQUEST; +ImmutableIndexUpdatedAt , invalid , BAD_REQUEST; - /// 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) - ) - } +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 } } } @@ -608,26 +333,26 @@ 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, - UserError::InvalidFilter(_) => Code::Filter, + UserError::InvalidFilter(_) => Code::InvalidSearchFilter, UserError::MissingDocumentId { .. } => Code::MissingDocumentId, 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::SortRankingRuleMissing => Code::Sort, + UserError::PrimaryKeyCannotBeChanged(_) => Code::IndexPrimaryKeyAlreadyExists, + UserError::SortRankingRuleMissing => Code::InvalidSearchSort, UserError::InvalidFacetsDistribution { .. } => Code::BadRequest, - UserError::InvalidSortableAttribute { .. } => Code::Sort, - UserError::CriterionError(_) => Code::InvalidRankingRule, + UserError::InvalidSortableAttribute { .. } => Code::InvalidSearchSort, + UserError::CriterionError(_) => Code::InvalidSettingsRankingRules, UserError::InvalidGeoField { .. } => Code::InvalidDocumentGeoField, - UserError::SortError(_) => Code::Sort, + UserError::SortError(_) => Code::InvalidSearchSort, UserError::InvalidMinTypoWordLenSetting(_, _) => { Code::InvalidMinWordLengthForTypo } @@ -656,7 +381,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 @@ -697,6 +422,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..b41bb06b6 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::ImmutableApiKeyUid, + "actions" => Code::ImmutableApiKeyActions, + "indexes" => Code::ImmutableApiKeyIndexes, + "expiresAt" => Code::ImmutableApiKeyExpiresAt, + "createdAt" => Code::ImmutableApiKeyCreatedAt, + "updatedAt" => Code::ImmutableApiKeyUpdatedAt, + _ => 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 cda61a25f..a42e5cc7b 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 = "0.1.4" dump = { path = "../dump" } either = "1.8.0" env_logger = "0.9.1" @@ -109,5 +109,5 @@ japanese = ["meilisearch-types/japanese"] thai = ["meilisearch-types/thai"] [package.metadata.mini-dashboard] -assets-url = "https://github.com/meilisearch/mini-dashboard/releases/download/v0.2.4/build.zip" -sha1 = "b53c2edb51d4ce1984d5586333b91c4ad3a1b4e4" +assets-url = "https://github.com/meilisearch/mini-dashboard/releases/download/v0.2.5/build.zip" +sha1 = "6fe959b78511b32e9ff857fd9fd31740633b9fce" diff --git a/meilisearch/src/analytics/mock_analytics.rs b/meilisearch/src/analytics/mock_analytics.rs index ad45a1ac8..092e35424 100644 --- a/meilisearch/src/analytics/mock_analytics.rs +++ b/meilisearch/src/analytics/mock_analytics.rs @@ -7,7 +7,7 @@ use serde_json::Value; use super::{find_user_id, Analytics, DocumentDeletionKind}; use crate::routes::indexes::documents::UpdateDocumentsQuery; -use crate::routes::tasks::TasksFilterQueryRaw; +use crate::routes::tasks::TasksFilterQuery; use crate::Opt; pub struct MockAnalytics { @@ -58,6 +58,6 @@ impl Analytics for MockAnalytics { _request: &HttpRequest, ) { } - fn get_tasks(&self, _query: &TasksFilterQueryRaw, _request: &HttpRequest) {} + fn get_tasks(&self, _query: &TasksFilterQuery, _request: &HttpRequest) {} fn health_seen(&self, _request: &HttpRequest) {} } diff --git a/meilisearch/src/analytics/mod.rs b/meilisearch/src/analytics/mod.rs index 1effe6692..ec15707bf 100644 --- a/meilisearch/src/analytics/mod.rs +++ b/meilisearch/src/analytics/mod.rs @@ -15,7 +15,7 @@ use platform_dirs::AppDirs; use serde_json::Value; use crate::routes::indexes::documents::UpdateDocumentsQuery; -use crate::routes::tasks::TasksFilterQueryRaw; +use crate::routes::tasks::TasksFilterQuery; // if we are in debug mode OR the analytics feature is disabled // the `SegmentAnalytics` point to the mock instead of the real analytics @@ -94,7 +94,7 @@ pub trait Analytics: Sync + Send { ); // this method should be called to aggregate the get tasks requests. - fn get_tasks(&self, query: &TasksFilterQueryRaw, request: &HttpRequest); + fn get_tasks(&self, query: &TasksFilterQuery, request: &HttpRequest); // this method should be called to aggregate a add documents request fn health_seen(&self, request: &HttpRequest); diff --git a/meilisearch/src/analytics/segment_analytics.rs b/meilisearch/src/analytics/segment_analytics.rs index 8af927811..1b5a1d73f 100644 --- a/meilisearch/src/analytics/segment_analytics.rs +++ b/meilisearch/src/analytics/segment_analytics.rs @@ -27,7 +27,7 @@ use super::{config_user_id_path, DocumentDeletionKind, MEILISEARCH_CONFIG_PATH}; use crate::analytics::Analytics; use crate::option::{default_http_addr, IndexerOpts, MaxMemory, MaxThreads, ScheduleSnapshot}; use crate::routes::indexes::documents::UpdateDocumentsQuery; -use crate::routes::tasks::TasksFilterQueryRaw; +use crate::routes::tasks::TasksFilterQuery; use crate::routes::{create_all_stats, Stats}; use crate::search::{ SearchQuery, SearchResult, DEFAULT_CROP_LENGTH, DEFAULT_CROP_MARKER, @@ -195,7 +195,7 @@ impl super::Analytics for SegmentAnalytics { let _ = self.sender.try_send(AnalyticsMsg::AggregateUpdateDocuments(aggregate)); } - fn get_tasks(&self, query: &TasksFilterQueryRaw, request: &HttpRequest) { + fn get_tasks(&self, query: &TasksFilterQuery, request: &HttpRequest) { let aggregate = TasksAggregator::from_query(query, request); let _ = self.sender.try_send(AnalyticsMsg::AggregateTasks(aggregate)); } @@ -868,21 +868,21 @@ pub struct TasksAggregator { } impl TasksAggregator { - pub fn from_query(query: &TasksFilterQueryRaw, request: &HttpRequest) -> Self { + pub fn from_query(query: &TasksFilterQuery, request: &HttpRequest) -> Self { Self { timestamp: Some(OffsetDateTime::now_utc()), user_agents: extract_user_agents(request).into_iter().collect(), - filtered_by_uid: query.common.uids.is_some(), - filtered_by_index_uid: query.common.index_uids.is_some(), - filtered_by_type: query.common.types.is_some(), - filtered_by_status: query.common.statuses.is_some(), - filtered_by_canceled_by: query.common.canceled_by.is_some(), - filtered_by_before_enqueued_at: query.dates.before_enqueued_at.is_some(), - filtered_by_after_enqueued_at: query.dates.after_enqueued_at.is_some(), - filtered_by_before_started_at: query.dates.before_started_at.is_some(), - filtered_by_after_started_at: query.dates.after_started_at.is_some(), - filtered_by_before_finished_at: query.dates.before_finished_at.is_some(), - filtered_by_after_finished_at: query.dates.after_finished_at.is_some(), + filtered_by_uid: query.uids.is_some(), + filtered_by_index_uid: query.index_uids.is_some(), + filtered_by_type: query.types.is_some(), + filtered_by_status: query.statuses.is_some(), + filtered_by_canceled_by: query.canceled_by.is_some(), + filtered_by_before_enqueued_at: query.before_enqueued_at.is_some(), + filtered_by_after_enqueued_at: query.after_enqueued_at.is_some(), + filtered_by_before_started_at: query.before_started_at.is_some(), + filtered_by_after_started_at: query.after_started_at.is_some(), + filtered_by_before_finished_at: query.before_finished_at.is_some(), + filtered_by_after_finished_at: query.after_finished_at.is_some(), total_received: 1, } } diff --git a/meilisearch/src/error.rs b/meilisearch/src/error.rs index bec233971..23a101080 100644 --- a/meilisearch/src/error.rs +++ b/meilisearch/src/error.rs @@ -55,7 +55,7 @@ impl ErrorCode for MeilisearchHttpError { MeilisearchHttpError::MissingPayload(_) => Code::MissingPayload, MeilisearchHttpError::InvalidContentType(_, _) => Code::InvalidContentType, MeilisearchHttpError::DocumentNotFound(_) => Code::DocumentNotFound, - MeilisearchHttpError::InvalidExpression(_, _) => Code::Filter, + MeilisearchHttpError::InvalidExpression(_, _) => Code::InvalidSearchFilter, MeilisearchHttpError::PayloadTooLarge => Code::PayloadTooLarge, MeilisearchHttpError::SwapIndexPayloadWrongLength(_) => Code::InvalidSwapIndexes, MeilisearchHttpError::IndexUid(e) => e.error_code(), 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..7a3a97c1f 100644 --- a/meilisearch/src/routes/indexes/mod.rs +++ b/meilisearch/src/routes/indexes/mod.rs @@ -1,14 +1,12 @@ 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::{DeserializeError, DeserializeFromValue, ValuePointerRef}; 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::{unwrap_any, Code, DeserrError, ResponseError, TakeErrorMessage}; use meilisearch_types::index_uid::IndexUid; use meilisearch_types::milli::{self, FieldDistribution, Index}; use meilisearch_types::tasks::KindWithContent; @@ -16,7 +14,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 +71,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 +100,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 +142,29 @@ pub async fn create_index( } } -#[derive(Debug)] -pub struct CreateIndexesDeserrError { - error: String, - code: Code, +fn deny_immutable_fields_index( + 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::ImmutableIndexUid, + "createdAt" => Code::ImmutableIndexCreatedAt, + "updatedAt" => Code::ImmutableIndexUpdatedAt, + _ => Code::BadRequest, + }; + error } - -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 = deny_immutable_fields_index)] pub struct UpdateIndexRequest { + #[deserr(error = DeserrError)] primary_key: Option, } @@ -254,7 +183,7 @@ pub async fn get_index( pub async fn update_index( index_scheduler: GuardedData, Data>, path: web::Path, - body: ValidatedJson, + body: ValidatedJson, req: HttpRequest, analytics: web::Data, ) -> Result { @@ -278,51 +207,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/routes/tasks.rs b/meilisearch/src/routes/tasks.rs index d25707530..09723623f 100644 --- a/meilisearch/src/routes/tasks.rs +++ b/meilisearch/src/routes/tasks.rs @@ -1,10 +1,12 @@ +use std::num::ParseIntError; use std::str::FromStr; use actix_web::web::Data; use actix_web::{web, HttpRequest, HttpResponse}; -use index_scheduler::error::DateField; +use deserr::DeserializeFromValue; use index_scheduler::{IndexScheduler, Query, TaskId}; -use meilisearch_types::error::ResponseError; +use meilisearch_types::error::deserr_codes::*; +use meilisearch_types::error::{DeserrError, ResponseError, TakeErrorMessage}; use meilisearch_types::index_uid::IndexUid; use meilisearch_types::settings::{Settings, Unchecked}; use meilisearch_types::star_or::StarOr; @@ -14,14 +16,16 @@ use meilisearch_types::tasks::{ use serde::{Deserialize, Serialize}; use serde_cs::vec::CS; use serde_json::json; -use time::{Duration, OffsetDateTime}; +use time::format_description::well_known::Rfc3339; +use time::macros::format_description; +use time::{Date, Duration, OffsetDateTime, Time}; use tokio::task; -use self::date_deserializer::{deserialize_date, DeserializeDateOption}; use super::{fold_star_or, SummarizedTaskView}; use crate::analytics::Analytics; use crate::extractors::authentication::policies::*; use crate::extractors::authentication::GuardedData; +use crate::extractors::query_parameters::QueryParameter; use crate::extractors::sequential_extractor::SeqHandler; const DEFAULT_LIMIT: fn() -> u32 = || 20; @@ -160,307 +164,124 @@ impl From
for DetailsView { } } -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct TaskCommonQueryRaw { - pub uids: Option>, - pub canceled_by: Option>, - pub types: Option>>, - pub statuses: Option>>, - pub index_uids: Option>>, +fn parse_option_cs( + s: Option>, +) -> Result>, TakeErrorMessage> { + if let Some(s) = s { + s.into_iter() + .map(|s| T::from_str(&s)) + .collect::, T::Err>>() + .map_err(TakeErrorMessage) + .map(Some) + } else { + Ok(None) + } } - -impl TaskCommonQueryRaw { - fn validate(self) -> Result { - let Self { uids, canceled_by, types, statuses, index_uids } = self; - let uids = if let Some(uids) = uids { - Some( - uids.into_iter() - .map(|uid_string| { - uid_string.parse::().map_err(|_e| { - index_scheduler::Error::InvalidTaskUids { task_uid: uid_string }.into() - }) - }) - .collect::, ResponseError>>()?, - ) - } else { - None - }; - let canceled_by = if let Some(canceled_by) = canceled_by { - Some( - canceled_by - .into_iter() - .map(|canceled_by_string| { - canceled_by_string.parse::().map_err(|_e| { - index_scheduler::Error::InvalidTaskCanceledBy { - canceled_by: canceled_by_string, - } - .into() - }) - }) - .collect::, ResponseError>>()?, - ) - } else { - None - }; - - let types = if let Some(types) = types.and_then(fold_star_or) as Option> { - Some( - types - .into_iter() - .map(|type_string| { - Kind::from_str(&type_string).map_err(|_e| { - index_scheduler::Error::InvalidTaskTypes { type_: type_string }.into() - }) - }) - .collect::, ResponseError>>()?, - ) - } else { - None - }; - let statuses = if let Some(statuses) = - statuses.and_then(fold_star_or) as Option> - { - Some( - statuses - .into_iter() - .map(|status_string| { - Status::from_str(&status_string).map_err(|_e| { - index_scheduler::Error::InvalidTaskStatuses { status: status_string } - .into() - }) - }) - .collect::, ResponseError>>()?, - ) - } else { - None - }; - - let index_uids = - if let Some(index_uids) = index_uids.and_then(fold_star_or) as Option> { - Some( - index_uids - .into_iter() - .map(|index_uid_string| { - IndexUid::from_str(&index_uid_string) - .map(|index_uid| index_uid.to_string()) - .map_err(|_e| { - index_scheduler::Error::InvalidIndexUid { - index_uid: index_uid_string, - } - .into() - }) - }) - .collect::, ResponseError>>()?, - ) - } else { - None - }; - Ok(TaskCommonQuery { types, uids, canceled_by, statuses, index_uids }) +fn parse_option_cs_star_or( + s: Option>>, +) -> Result>, TakeErrorMessage> { + if let Some(s) = s.and_then(fold_star_or) as Option> { + s.into_iter() + .map(|s| T::from_str(&s)) + .collect::, T::Err>>() + .map_err(TakeErrorMessage) + .map(Some) + } else { + Ok(None) + } +} +fn parse_option_str(s: Option) -> Result, TakeErrorMessage> { + if let Some(s) = s { + T::from_str(&s).map_err(TakeErrorMessage).map(Some) + } else { + Ok(None) } } -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct TaskDateQueryRaw { - pub after_enqueued_at: Option, - pub before_enqueued_at: Option, - pub after_started_at: Option, - pub before_started_at: Option, - pub after_finished_at: Option, - pub before_finished_at: Option, -} -impl TaskDateQueryRaw { - fn validate(self) -> Result { - let Self { - after_enqueued_at, - before_enqueued_at, - after_started_at, - before_started_at, - after_finished_at, - before_finished_at, - } = self; - - let mut query = TaskDateQuery { - after_enqueued_at: None, - before_enqueued_at: None, - after_started_at: None, - before_started_at: None, - after_finished_at: None, - before_finished_at: None, - }; - - for (field_name, string_value, before_or_after, dest) in [ - ( - DateField::AfterEnqueuedAt, - after_enqueued_at, - DeserializeDateOption::After, - &mut query.after_enqueued_at, - ), - ( - DateField::BeforeEnqueuedAt, - before_enqueued_at, - DeserializeDateOption::Before, - &mut query.before_enqueued_at, - ), - ( - DateField::AfterStartedAt, - after_started_at, - DeserializeDateOption::After, - &mut query.after_started_at, - ), - ( - DateField::BeforeStartedAt, - before_started_at, - DeserializeDateOption::Before, - &mut query.before_started_at, - ), - ( - DateField::AfterFinishedAt, - after_finished_at, - DeserializeDateOption::After, - &mut query.after_finished_at, - ), - ( - DateField::BeforeFinishedAt, - before_finished_at, - DeserializeDateOption::Before, - &mut query.before_finished_at, - ), - ] { - if let Some(string_value) = string_value { - *dest = Some(deserialize_date(field_name, &string_value, before_or_after)?); - } - } - - Ok(query) - } +fn parse_str(s: String) -> Result> { + T::from_str(&s).map_err(TakeErrorMessage) } -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct TasksFilterQueryRaw { - #[serde(flatten)] - pub common: TaskCommonQueryRaw, - #[serde(default = "DEFAULT_LIMIT")] - pub limit: u32, - pub from: Option, - #[serde(flatten)] - pub dates: TaskDateQueryRaw, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct TaskDeletionOrCancelationQueryRaw { - #[serde(flatten)] - pub common: TaskCommonQueryRaw, - #[serde(flatten)] - pub dates: TaskDateQueryRaw, -} - -impl TasksFilterQueryRaw { - fn validate(self) -> Result { - let Self { common, limit, from, dates } = self; - let common = common.validate()?; - let dates = dates.validate()?; - - Ok(TasksFilterQuery { common, limit, from, dates }) - } -} - -impl TaskDeletionOrCancelationQueryRaw { - fn validate(self) -> Result { - let Self { common, dates } = self; - let common = common.validate()?; - let dates = dates.validate()?; - - Ok(TaskDeletionOrCancelationQuery { common, dates }) - } -} - -#[derive(Serialize, Debug)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct TaskDateQuery { - #[serde( - default, - skip_serializing_if = "Option::is_none", - serialize_with = "time::serde::rfc3339::option::serialize" - )] - after_enqueued_at: Option, - #[serde( - default, - skip_serializing_if = "Option::is_none", - serialize_with = "time::serde::rfc3339::option::serialize" - )] - before_enqueued_at: Option, - #[serde( - default, - skip_serializing_if = "Option::is_none", - serialize_with = "time::serde::rfc3339::option::serialize" - )] - after_started_at: Option, - #[serde( - default, - skip_serializing_if = "Option::is_none", - serialize_with = "time::serde::rfc3339::option::serialize" - )] - before_started_at: Option, - #[serde( - default, - skip_serializing_if = "Option::is_none", - serialize_with = "time::serde::rfc3339::option::serialize" - )] - after_finished_at: Option, - #[serde( - default, - skip_serializing_if = "Option::is_none", - serialize_with = "time::serde::rfc3339::option::serialize" - )] - before_finished_at: Option, -} - -#[derive(Debug)] -pub struct TaskCommonQuery { - types: Option>, - uids: Option>, - canceled_by: Option>, - statuses: Option>, - index_uids: Option>, -} - -#[derive(Debug)] +#[derive(Debug, DeserializeFromValue)] +#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] pub struct TasksFilterQuery { - limit: u32, - from: Option, - common: TaskCommonQuery, - dates: TaskDateQuery, + #[deserr(error = DeserrError, default = DEFAULT_LIMIT(), from(String) = parse_str:: -> TakeErrorMessage)] + pub limit: u32, + #[deserr(error = DeserrError, from(Option) = parse_option_str:: -> TakeErrorMessage)] + pub from: Option, + + #[deserr(error = DeserrError, from(Option>) = parse_option_cs:: -> TakeErrorMessage)] + pub uids: Option>, + #[deserr(error = DeserrError, from(Option>) = parse_option_cs:: -> TakeErrorMessage)] + pub canceled_by: Option>, + #[deserr(error = DeserrError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] + pub types: Option>, + #[deserr(error = DeserrError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] + pub statuses: Option>, + #[deserr(error = DeserrError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] + pub index_uids: Option>, + + #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] + pub after_enqueued_at: Option, + #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] + pub before_enqueued_at: Option, + #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] + pub after_started_at: Option, + #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] + pub before_started_at: Option, + #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] + pub after_finished_at: Option, + #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] + pub before_finished_at: Option, } -#[derive(Debug)] +#[derive(Deserialize, Debug, DeserializeFromValue)] +#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] pub struct TaskDeletionOrCancelationQuery { - common: TaskCommonQuery, - dates: TaskDateQuery, + #[deserr(error = DeserrError, from(Option>) = parse_option_cs:: -> TakeErrorMessage)] + pub uids: Option>, + #[deserr(error = DeserrError, from(Option>) = parse_option_cs:: -> TakeErrorMessage)] + pub canceled_by: Option>, + #[deserr(error = DeserrError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] + pub types: Option>, + #[deserr(error = DeserrError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] + pub statuses: Option>, + #[deserr(error = DeserrError, default = None, from(Option>>) = parse_option_cs_star_or:: -> TakeErrorMessage)] + pub index_uids: Option>, + + #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] + pub after_enqueued_at: Option, + #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] + pub before_enqueued_at: Option, + #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] + pub after_started_at: Option, + #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] + pub before_started_at: Option, + #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_after -> TakeErrorMessage)] + pub after_finished_at: Option, + #[deserr(error = DeserrError, default = None, from(Option) = deserialize_date_before -> TakeErrorMessage)] + pub before_finished_at: Option, } async fn cancel_tasks( index_scheduler: GuardedData, Data>, - params: web::Query, + params: QueryParameter, req: HttpRequest, analytics: web::Data, ) -> Result { - let query = params.into_inner().validate()?; let TaskDeletionOrCancelationQuery { - common: TaskCommonQuery { types, uids, canceled_by, statuses, index_uids }, - dates: - TaskDateQuery { - after_enqueued_at, - before_enqueued_at, - after_started_at, - before_started_at, - after_finished_at, - before_finished_at, - }, - } = query; + types, + uids, + canceled_by, + statuses, + index_uids, + after_enqueued_at, + before_enqueued_at, + after_started_at, + before_started_at, + after_finished_at, + before_finished_at, + } = params.into_inner(); analytics.publish( "Tasks Canceled".to_string(), @@ -485,7 +306,7 @@ async fn cancel_tasks( from: None, statuses, types, - index_uids, + index_uids: index_uids.map(|xs| xs.into_iter().map(|s| s.to_string()).collect()), uids, canceled_by, before_enqueued_at, @@ -516,22 +337,24 @@ async fn cancel_tasks( async fn delete_tasks( index_scheduler: GuardedData, Data>, - params: web::Query, + params: QueryParameter, req: HttpRequest, analytics: web::Data, ) -> Result { let TaskDeletionOrCancelationQuery { - common: TaskCommonQuery { types, uids, canceled_by, statuses, index_uids }, - dates: - TaskDateQuery { - after_enqueued_at, - before_enqueued_at, - after_started_at, - before_started_at, - after_finished_at, - before_finished_at, - }, - } = params.into_inner().validate()?; + types, + uids, + canceled_by, + statuses, + index_uids, + + after_enqueued_at, + before_enqueued_at, + after_started_at, + before_started_at, + after_finished_at, + before_finished_at, + } = params.into_inner(); analytics.publish( "Tasks Deleted".to_string(), @@ -556,7 +379,7 @@ async fn delete_tasks( from: None, statuses, types, - index_uids, + index_uids: index_uids.map(|xs| xs.into_iter().map(|s| s.to_string()).collect()), uids, canceled_by, after_enqueued_at, @@ -595,26 +418,28 @@ pub struct AllTasks { async fn get_tasks( index_scheduler: GuardedData, Data>, - params: web::Query, + params: QueryParameter, req: HttpRequest, analytics: web::Data, ) -> Result { + let params = params.into_inner(); analytics.get_tasks(¶ms, &req); let TasksFilterQuery { - common: TaskCommonQuery { types, uids, canceled_by, statuses, index_uids }, + types, + uids, + canceled_by, + statuses, + index_uids, limit, from, - dates: - TaskDateQuery { - after_enqueued_at, - before_enqueued_at, - after_started_at, - before_started_at, - after_finished_at, - before_finished_at, - }, - } = params.into_inner().validate()?; + after_enqueued_at, + before_enqueued_at, + after_started_at, + before_started_at, + after_finished_at, + before_finished_at, + } = params; // We +1 just to know if there is more after this "page" or not. let limit = limit.saturating_add(1); @@ -624,7 +449,7 @@ async fn get_tasks( from, statuses, types, - index_uids, + index_uids: index_uids.map(|xs| xs.into_iter().map(|s| s.to_string()).collect()), uids, canceled_by, before_enqueued_at, @@ -691,333 +516,275 @@ async fn get_task( } } -pub(crate) mod date_deserializer { - use index_scheduler::error::DateField; - use meilisearch_types::error::ResponseError; - use time::format_description::well_known::Rfc3339; - use time::macros::format_description; - use time::{Date, Duration, OffsetDateTime, Time}; +pub enum DeserializeDateOption { + Before, + After, +} - pub enum DeserializeDateOption { - Before, - After, - } - - pub fn deserialize_date( - field_name: DateField, - value: &str, - option: DeserializeDateOption, - ) -> std::result::Result { - // We can't parse using time's rfc3339 format, since then we won't know what part of the - // datetime was not explicitly specified, and thus we won't be able to increment it to the - // next step. - if let Ok(datetime) = OffsetDateTime::parse(value, &Rfc3339) { - // fully specified up to the second - // we assume that the subseconds are 0 if not specified, and we don't increment to the next second - Ok(datetime) - } else if let Ok(datetime) = Date::parse( - value, - format_description!("[year repr:full base:calendar]-[month repr:numerical]-[day]"), - ) { - let datetime = datetime.with_time(Time::MIDNIGHT).assume_utc(); - // add one day since the time was not specified - match option { - DeserializeDateOption::Before => Ok(datetime), - DeserializeDateOption::After => { - let datetime = datetime.checked_add(Duration::days(1)).unwrap_or(datetime); - Ok(datetime) - } +pub fn deserialize_date( + value: &str, + option: DeserializeDateOption, +) -> std::result::Result> { + // We can't parse using time's rfc3339 format, since then we won't know what part of the + // datetime was not explicitly specified, and thus we won't be able to increment it to the + // next step. + if let Ok(datetime) = OffsetDateTime::parse(value, &Rfc3339) { + // fully specified up to the second + // we assume that the subseconds are 0 if not specified, and we don't increment to the next second + Ok(datetime) + } else if let Ok(datetime) = Date::parse( + value, + format_description!("[year repr:full base:calendar]-[month repr:numerical]-[day]"), + ) { + let datetime = datetime.with_time(Time::MIDNIGHT).assume_utc(); + // add one day since the time was not specified + match option { + DeserializeDateOption::Before => Ok(datetime), + DeserializeDateOption::After => { + let datetime = datetime.checked_add(Duration::days(1)).unwrap_or(datetime); + Ok(datetime) } - } else { - Err(index_scheduler::Error::InvalidTaskDate { - field: field_name, - date: value.to_string(), - } - .into()) } + } else { + Err(TakeErrorMessage(InvalidTaskDateError(value.to_owned()))) } } +pub fn deserialize_date_before( + value: Option, +) -> std::result::Result, TakeErrorMessage> { + if let Some(value) = value { + let date = deserialize_date(&value, DeserializeDateOption::Before)?; + Ok(Some(date)) + } else { + Ok(None) + } +} +pub fn deserialize_date_after( + value: Option, +) -> std::result::Result, TakeErrorMessage> { + if let Some(value) = value { + let date = deserialize_date(&value, DeserializeDateOption::After)?; + Ok(Some(date)) + } else { + Ok(None) + } +} + +#[derive(Debug)] +pub struct InvalidTaskDateError(String); +impl std::fmt::Display for InvalidTaskDateError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "`{}` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", self.0) + } +} +impl std::error::Error for InvalidTaskDateError {} + #[cfg(test)] mod tests { + use deserr::DeserializeFromValue; use meili_snap::snapshot; + use meilisearch_types::error::DeserrError; - use crate::routes::tasks::{TaskDeletionOrCancelationQueryRaw, TasksFilterQueryRaw}; + use crate::extractors::query_parameters::QueryParameter; + use crate::routes::tasks::{TaskDeletionOrCancelationQuery, TasksFilterQuery}; + + fn deserr_query_params(j: &str) -> Result + where + T: DeserializeFromValue, + { + QueryParameter::::from_query(j).map(|p| p.0) + } #[test] fn deserialize_task_filter_dates() { { - let json = r#" { - "afterEnqueuedAt": "2021-12-03", - "beforeEnqueuedAt": "2021-12-03", - "afterStartedAt": "2021-12-03", - "beforeStartedAt": "2021-12-03", - "afterFinishedAt": "2021-12-03", - "beforeFinishedAt": "2021-12-03" - } "#; - let query = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap(); - snapshot!(format!("{:?}", query.dates.after_enqueued_at.unwrap()), @"2021-12-04 0:00:00.0 +00:00:00"); - snapshot!(format!("{:?}", query.dates.before_enqueued_at.unwrap()), @"2021-12-03 0:00:00.0 +00:00:00"); - snapshot!(format!("{:?}", query.dates.after_started_at.unwrap()), @"2021-12-04 0:00:00.0 +00:00:00"); - snapshot!(format!("{:?}", query.dates.before_started_at.unwrap()), @"2021-12-03 0:00:00.0 +00:00:00"); - snapshot!(format!("{:?}", query.dates.after_finished_at.unwrap()), @"2021-12-04 0:00:00.0 +00:00:00"); - snapshot!(format!("{:?}", query.dates.before_finished_at.unwrap()), @"2021-12-03 0:00:00.0 +00:00:00"); + let params = "afterEnqueuedAt=2021-12-03&beforeEnqueuedAt=2021-12-03&afterStartedAt=2021-12-03&beforeStartedAt=2021-12-03&afterFinishedAt=2021-12-03&beforeFinishedAt=2021-12-03"; + let query = deserr_query_params::(params).unwrap(); + + snapshot!(format!("{:?}", query.after_enqueued_at.unwrap()), @"2021-12-04 0:00:00.0 +00:00:00"); + snapshot!(format!("{:?}", query.before_enqueued_at.unwrap()), @"2021-12-03 0:00:00.0 +00:00:00"); + snapshot!(format!("{:?}", query.after_started_at.unwrap()), @"2021-12-04 0:00:00.0 +00:00:00"); + snapshot!(format!("{:?}", query.before_started_at.unwrap()), @"2021-12-03 0:00:00.0 +00:00:00"); + snapshot!(format!("{:?}", query.after_finished_at.unwrap()), @"2021-12-04 0:00:00.0 +00:00:00"); + snapshot!(format!("{:?}", query.before_finished_at.unwrap()), @"2021-12-03 0:00:00.0 +00:00:00"); } { - let json = r#" { "afterEnqueuedAt": "2021-12-03T23:45:23Z", "beforeEnqueuedAt": "2021-12-03T23:45:23Z" } "#; - let query = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap(); - snapshot!(format!("{:?}", query.dates.after_enqueued_at.unwrap()), @"2021-12-03 23:45:23.0 +00:00:00"); - snapshot!(format!("{:?}", query.dates.before_enqueued_at.unwrap()), @"2021-12-03 23:45:23.0 +00:00:00"); + let params = + "afterEnqueuedAt=2021-12-03T23:45:23Z&beforeEnqueuedAt=2021-12-03T23:45:23Z"; + let query = deserr_query_params::(params).unwrap(); + snapshot!(format!("{:?}", query.after_enqueued_at.unwrap()), @"2021-12-03 23:45:23.0 +00:00:00"); + snapshot!(format!("{:?}", query.before_enqueued_at.unwrap()), @"2021-12-03 23:45:23.0 +00:00:00"); } { - let json = r#" { "afterEnqueuedAt": "1997-11-12T09:55:06-06:20" } "#; - let query = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap(); - snapshot!(format!("{:?}", query.dates.after_enqueued_at.unwrap()), @"1997-11-12 9:55:06.0 -06:20:00"); + let params = "afterEnqueuedAt=1997-11-12T09:55:06-06:20"; + let query = deserr_query_params::(params).unwrap(); + snapshot!(format!("{:?}", query.after_enqueued_at.unwrap()), @"1997-11-12 9:55:06.0 -06:20:00"); } { - let json = r#" { "afterEnqueuedAt": "1997-11-12T09:55:06+00:00" } "#; - let query = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap(); - snapshot!(format!("{:?}", query.dates.after_enqueued_at.unwrap()), @"1997-11-12 9:55:06.0 +00:00:00"); + let params = "afterEnqueuedAt=1997-11-12T09:55:06%2B00:00"; + let query = deserr_query_params::(params).unwrap(); + snapshot!(format!("{:?}", query.after_enqueued_at.unwrap()), @"1997-11-12 9:55:06.0 +00:00:00"); } { - let json = r#" { "afterEnqueuedAt": "1997-11-12T09:55:06.200000300Z" } "#; - let query = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap(); - snapshot!(format!("{:?}", query.dates.after_enqueued_at.unwrap()), @"1997-11-12 9:55:06.2000003 +00:00:00"); + let params = "afterEnqueuedAt=1997-11-12T09:55:06.200000300Z"; + let query = deserr_query_params::(params).unwrap(); + snapshot!(format!("{:?}", query.after_enqueued_at.unwrap()), @"1997-11-12 9:55:06.2000003 +00:00:00"); } { - let json = r#" { "afterFinishedAt": "2021" } "#; - let err = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap_err(); - snapshot!(format!("{err}"), @"Task `afterFinishedAt` `2021` is invalid. It should follow the YYYY-MM-DD or RFC 3339 date-time format."); + let params = "afterFinishedAt=2021"; + let err = deserr_query_params::(params).unwrap_err(); + snapshot!(format!("{err}"), @"`2021` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterFinishedAt`."); } { - let json = r#" { "beforeFinishedAt": "2021" } "#; - let err = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap_err(); - snapshot!(format!("{err}"), @"Task `beforeFinishedAt` `2021` is invalid. It should follow the YYYY-MM-DD or RFC 3339 date-time format."); + let params = "beforeFinishedAt=2021"; + let err = deserr_query_params::(params).unwrap_err(); + snapshot!(format!("{err}"), @"`2021` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeFinishedAt`."); } { - let json = r#" { "afterEnqueuedAt": "2021-12" } "#; - let err = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap_err(); - snapshot!(format!("{err}"), @"Task `afterEnqueuedAt` `2021-12` is invalid. It should follow the YYYY-MM-DD or RFC 3339 date-time format."); + let params = "afterEnqueuedAt=2021-12"; + let err = deserr_query_params::(params).unwrap_err(); + snapshot!(format!("{err}"), @"`2021-12` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterEnqueuedAt`."); } { - let json = r#" { "beforeEnqueuedAt": "2021-12-03T23" } "#; - let err = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap_err(); - snapshot!(format!("{err}"), @"Task `beforeEnqueuedAt` `2021-12-03T23` is invalid. It should follow the YYYY-MM-DD or RFC 3339 date-time format."); + let params = "beforeEnqueuedAt=2021-12-03T23"; + let err = deserr_query_params::(params).unwrap_err(); + snapshot!(format!("{err}"), @"`2021-12-03T23` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeEnqueuedAt`."); } { - let json = r#" { "afterStartedAt": "2021-12-03T23:45" } "#; - let err = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap_err(); - snapshot!(format!("{err}"), @"Task `afterStartedAt` `2021-12-03T23:45` is invalid. It should follow the YYYY-MM-DD or RFC 3339 date-time format."); - - let json = r#" { "beforeStartedAt": "2021-12-03T23:45" } "#; - let err = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap_err(); - snapshot!(format!("{err}"), @"Task `beforeStartedAt` `2021-12-03T23:45` is invalid. It should follow the YYYY-MM-DD or RFC 3339 date-time format."); + let params = "afterStartedAt=2021-12-03T23:45"; + let err = deserr_query_params::(params).unwrap_err(); + snapshot!(format!("{err}"), @"`2021-12-03T23:45` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterStartedAt`."); + } + { + let params = "beforeStartedAt=2021-12-03T23:45"; + let err = deserr_query_params::(params).unwrap_err(); + snapshot!(format!("{err}"), @"`2021-12-03T23:45` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeStartedAt`."); } } #[test] fn deserialize_task_filter_uids() { { - let json = r#" { "uids": "78,1,12,73" } "#; - let query = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap(); - snapshot!(format!("{:?}", query.common.uids.unwrap()), @"[78, 1, 12, 73]"); + let params = "uids=78,1,12,73"; + let query = deserr_query_params::(params).unwrap(); + snapshot!(format!("{:?}", query.uids.unwrap()), @"[78, 1, 12, 73]"); } { - let json = r#" { "uids": "1" } "#; - let query = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap(); - snapshot!(format!("{:?}", query.common.uids.unwrap()), @"[1]"); + let params = "uids=1"; + let query = deserr_query_params::(params).unwrap(); + snapshot!(format!("{:?}", query.uids.unwrap()), @"[1]"); } { - let json = r#" { "uids": "78,hello,world" } "#; - let err = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap_err(); - snapshot!(format!("{err}"), @"Task uid `hello` is invalid. It should only contain numeric characters."); + let params = "uids=78,hello,world"; + let err = deserr_query_params::(params).unwrap_err(); + snapshot!(format!("{err}"), @"invalid digit found in string at `.uids`."); } { - let json = r#" { "uids": "cat" } "#; - let err = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap_err(); - snapshot!(format!("{err}"), @"Task uid `cat` is invalid. It should only contain numeric characters."); + let params = "uids=cat"; + let err = deserr_query_params::(params).unwrap_err(); + snapshot!(format!("{err}"), @"invalid digit found in string at `.uids`."); } } #[test] fn deserialize_task_filter_status() { { - let json = r#" { "statuses": "succeeded,failed,enqueued,processing,canceled" } "#; - let query = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap(); - snapshot!(format!("{:?}", query.common.statuses.unwrap()), @"[Succeeded, Failed, Enqueued, Processing, Canceled]"); + let params = "statuses=succeeded,failed,enqueued,processing,canceled"; + let query = deserr_query_params::(params).unwrap(); + snapshot!(format!("{:?}", query.statuses.unwrap()), @"[Succeeded, Failed, Enqueued, Processing, Canceled]"); } { - let json = r#" { "statuses": "enqueued" } "#; - let query = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap(); - snapshot!(format!("{:?}", query.common.statuses.unwrap()), @"[Enqueued]"); + let params = "statuses=enqueued"; + let query = deserr_query_params::(params).unwrap(); + snapshot!(format!("{:?}", query.statuses.unwrap()), @"[Enqueued]"); } { - let json = r#" { "statuses": "finished" } "#; - let err = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap_err(); - snapshot!(format!("{err}"), @"Task status `finished` is invalid. Available task statuses are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`."); + let params = "statuses=finished"; + let err = deserr_query_params::(params).unwrap_err(); + snapshot!(format!("{err}"), @"`finished` is not a status. Available status are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`. at `.statuses`."); } } #[test] fn deserialize_task_filter_types() { { - let json = r#" { "types": "documentAdditionOrUpdate,documentDeletion,settingsUpdate,indexCreation,indexDeletion,indexUpdate,indexSwap,taskCancelation,taskDeletion,dumpCreation,snapshotCreation" }"#; - let query = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap(); - snapshot!(format!("{:?}", query.common.types.unwrap()), @"[DocumentAdditionOrUpdate, DocumentDeletion, SettingsUpdate, IndexCreation, IndexDeletion, IndexUpdate, IndexSwap, TaskCancelation, TaskDeletion, DumpCreation, SnapshotCreation]"); + let params = "types=documentAdditionOrUpdate,documentDeletion,settingsUpdate,indexCreation,indexDeletion,indexUpdate,indexSwap,taskCancelation,taskDeletion,dumpCreation,snapshotCreation"; + let query = deserr_query_params::(params).unwrap(); + snapshot!(format!("{:?}", query.types.unwrap()), @"[DocumentAdditionOrUpdate, DocumentDeletion, SettingsUpdate, IndexCreation, IndexDeletion, IndexUpdate, IndexSwap, TaskCancelation, TaskDeletion, DumpCreation, SnapshotCreation]"); } { - let json = r#" { "types": "settingsUpdate" } "#; - let query = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap(); - snapshot!(format!("{:?}", query.common.types.unwrap()), @"[SettingsUpdate]"); + let params = "types=settingsUpdate"; + let query = deserr_query_params::(params).unwrap(); + snapshot!(format!("{:?}", query.types.unwrap()), @"[SettingsUpdate]"); } { - let json = r#" { "types": "createIndex" } "#; - let err = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap_err(); - snapshot!(format!("{err}"), @"Task type `createIndex` is invalid. Available task types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`"); + let params = "types=createIndex"; + let err = deserr_query_params::(params).unwrap_err(); + snapshot!(format!("{err}"), @"`createIndex` is not a type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`. at `.types`."); } } #[test] fn deserialize_task_filter_index_uids() { { - let json = r#" { "indexUids": "toto,tata-78" }"#; - let query = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap(); - snapshot!(format!("{:?}", query.common.index_uids.unwrap()), @r###"["toto", "tata-78"]"###); + let params = "indexUids=toto,tata-78"; + let query = deserr_query_params::(params).unwrap(); + snapshot!(format!("{:?}", query.index_uids.unwrap()), @r###"[IndexUid("toto"), IndexUid("tata-78")]"###); } { - let json = r#" { "indexUids": "index_a" } "#; - let query = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap(); - snapshot!(format!("{:?}", query.common.index_uids.unwrap()), @r###"["index_a"]"###); + let params = "indexUids=index_a"; + let query = deserr_query_params::(params).unwrap(); + snapshot!(format!("{:?}", query.index_uids.unwrap()), @r###"[IndexUid("index_a")]"###); } { - let json = r#" { "indexUids": "1,hé" } "#; - let err = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap_err(); - snapshot!(format!("{err}"), @"hé is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_)."); + let params = "indexUids=1,hé"; + let err = deserr_query_params::(params).unwrap_err(); + snapshot!(format!("{err}"), @"`hé` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_). at `.indexUids`."); } { - let json = r#" { "indexUids": "hé" } "#; - let err = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap_err(); - snapshot!(format!("{err}"), @"hé is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_)."); + let params = "indexUids=hé"; + let err = deserr_query_params::(params).unwrap_err(); + snapshot!(format!("{err}"), @"`hé` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_). at `.indexUids`."); } } #[test] fn deserialize_task_filter_general() { { - let json = r#" { "from": 12, "limit": 15, "indexUids": "toto,tata-78", "statuses": "succeeded,enqueued", "afterEnqueuedAt": "2012-04-23", "uids": "1,2,3" }"#; - let query = - serde_json::from_str::(json).unwrap().validate().unwrap(); - snapshot!(format!("{:?}", query), @r###"TasksFilterQuery { limit: 15, from: Some(12), common: TaskCommonQuery { types: None, uids: Some([1, 2, 3]), canceled_by: None, statuses: Some([Succeeded, Enqueued]), index_uids: Some(["toto", "tata-78"]) }, dates: TaskDateQuery { after_enqueued_at: Some(2012-04-24 0:00:00.0 +00:00:00), before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None } }"###); + let params = "from=12&limit=15&indexUids=toto,tata-78&statuses=succeeded,enqueued&afterEnqueuedAt=2012-04-23&uids=1,2,3"; + let query = deserr_query_params::(params).unwrap(); + snapshot!(format!("{:?}", query), @r###"TasksFilterQuery { limit: 15, from: Some(12), uids: Some([1, 2, 3]), canceled_by: None, types: None, statuses: Some([Succeeded, Enqueued]), index_uids: Some([IndexUid("toto"), IndexUid("tata-78")]), after_enqueued_at: Some(2012-04-24 0:00:00.0 +00:00:00), before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None }"###); } { // Stars should translate to `None` in the query // Verify value of the default limit - let json = r#" { "indexUids": "*", "statuses": "succeeded,*", "afterEnqueuedAt": "2012-04-23", "uids": "1,2,3" }"#; - let query = - serde_json::from_str::(json).unwrap().validate().unwrap(); - snapshot!(format!("{:?}", query), @"TasksFilterQuery { limit: 20, from: None, common: TaskCommonQuery { types: None, uids: Some([1, 2, 3]), canceled_by: None, statuses: None, index_uids: None }, dates: TaskDateQuery { after_enqueued_at: Some(2012-04-24 0:00:00.0 +00:00:00), before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None } }"); + let params = "indexUids=*&statuses=succeeded,*&afterEnqueuedAt=2012-04-23&uids=1,2,3"; + let query = deserr_query_params::(params).unwrap(); + snapshot!(format!("{:?}", query), @"TasksFilterQuery { limit: 20, from: None, uids: Some([1, 2, 3]), canceled_by: None, types: None, statuses: None, index_uids: None, after_enqueued_at: Some(2012-04-24 0:00:00.0 +00:00:00), before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None }"); } { // Stars should also translate to `None` in task deletion/cancelation queries - let json = r#" { "indexUids": "*", "statuses": "succeeded,*", "afterEnqueuedAt": "2012-04-23", "uids": "1,2,3" }"#; - let query = serde_json::from_str::(json) - .unwrap() - .validate() - .unwrap(); - snapshot!(format!("{:?}", query), @"TaskDeletionOrCancelationQuery { common: TaskCommonQuery { types: None, uids: Some([1, 2, 3]), canceled_by: None, statuses: None, index_uids: None }, dates: TaskDateQuery { after_enqueued_at: Some(2012-04-24 0:00:00.0 +00:00:00), before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None } }"); + let params = "indexUids=*&statuses=succeeded,*&afterEnqueuedAt=2012-04-23&uids=1,2,3"; + let query = deserr_query_params::(params).unwrap(); + snapshot!(format!("{:?}", query), @"TaskDeletionOrCancelationQuery { uids: Some([1, 2, 3]), canceled_by: None, types: None, statuses: None, index_uids: None, after_enqueued_at: Some(2012-04-24 0:00:00.0 +00:00:00), before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None }"); } { // Stars in uids not allowed - let json = r#" { "uids": "*" }"#; - let err = - serde_json::from_str::(json).unwrap().validate().unwrap_err(); - snapshot!(format!("{err}"), @"Task uid `*` is invalid. It should only contain numeric characters."); + let params = "uids=*"; + let err = deserr_query_params::(params).unwrap_err(); + snapshot!(format!("{err}"), @"invalid digit found in string at `.uids`."); } { // From not allowed in task deletion/cancelation queries - let json = r#" { "from": 12 }"#; - let err = serde_json::from_str::(json).unwrap_err(); - snapshot!(format!("{err}"), @"unknown field `from` at line 1 column 15"); + let params = "from=12"; + let err = deserr_query_params::(params).unwrap_err(); + snapshot!(format!("{err}"), @"Json deserialize error: unknown field `from`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``."); } { // Limit not allowed in task deletion/cancelation queries - let json = r#" { "limit": 12 }"#; - let err = serde_json::from_str::(json).unwrap_err(); - snapshot!(format!("{err}"), @"unknown field `limit` at line 1 column 16"); + let params = "limit=12"; + let err = deserr_query_params::(params).unwrap_err(); + snapshot!(format!("{err}"), @"Json deserialize error: unknown field `limit`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``."); } } } diff --git a/meilisearch/src/search.rs b/meilisearch/src/search.rs index 5044bd611..a3972cd38 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, Eq, 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)] @@ -695,7 +610,7 @@ fn parse_filter(facets: &Value) -> Result, MeilisearchHttpError> Ok(condition) } Value::Array(arr) => parse_filter_array(arr), - v => Err(MeilisearchHttpError::InvalidExpression(&["Array"], v.clone())), + v => Err(MeilisearchHttpError::InvalidExpression(&["String", "Array"], v.clone())), } } diff --git a/meilisearch/tests/auth/api_keys.rs b/meilisearch/tests/auth/api_keys.rs index 72f7cdff1..0a14107a8 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_api_key_indexes", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#immutable-api-key-indexes" + } + "###); + 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_api_key_actions", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#immutable-api-key-actions" + } + "###); + 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_api_key_expires_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#immutable-api-key-expires-at" + } + "###); + 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 163a63d6f..d582a3672 100644 --- a/meilisearch/tests/search/errors.rs +++ b/meilisearch/tests/search/errors.rs @@ -1,3 +1,4 @@ +use meili_snap::*; use serde_json::json; use super::DOCUMENTS; @@ -37,104 +38,368 @@ async fn search_unexisting_parameter() { } #[actix_rt::test] -async fn search_invalid_crop_marker() { +async fn search_bad_q() { let server = Server::new().await; let index = server.index("test"); - // object - let response = index.search_post(json!({"cropMarker": { "marker": "" }})).await; - meili_snap::snapshot!(format!("{:#?}", response), @r###" - ( - Object { - "message": String("invalid type: Map `{\"marker\":\"\"}`, expected a String at `.cropMarker`."), - "code": String("invalid_search_crop_marker"), - "type": String("invalid_request"), - "link": String("https://docs.meilisearch.com/errors#invalid-search-crop-marker"), - }, - 400, - ) + let (response, code) = index.search_post(json!({"q": ["doggo"]})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: Sequence `[\"doggo\"]`, expected a String at `.q`.", + "code": "invalid_search_q", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-search-q" + } + "###); + // Can't make the `q` fail with a get search since it'll accept anything as a string. +} + +#[actix_rt::test] +async fn search_bad_offset() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.search_post(json!({"offset": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Integer at `.offset`.", + "code": "invalid_search_offset", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-search-offset" + } "###); - // array - let response = index.search_post(json!({"cropMarker": ["marker", ""]})).await; - meili_snap::snapshot!(format!("{:#?}", response), @r###" - ( - Object { - "message": String("invalid type: Sequence `[\"marker\",\"\"]`, expected a String at `.cropMarker`."), - "code": String("invalid_search_crop_marker"), - "type": String("invalid_request"), - "link": String("https://docs.meilisearch.com/errors#invalid-search-crop-marker"), - }, - 400, - ) + let (response, code) = index.search_get(json!({"offset": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid digit found in string at `.offset`.", + "code": "invalid_search_offset", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-search-offset" + } "###); } #[actix_rt::test] -async fn search_invalid_highlight_pre_tag() { +async fn search_bad_limit() { let server = Server::new().await; let index = server.index("test"); - // object - let response = index.search_post(json!({"highlightPreTag": { "marker": "" }})).await; - meili_snap::snapshot!(format!("{:#?}", response), @r###" - ( - Object { - "message": String("invalid type: Map `{\"marker\":\"\"}`, expected a String at `.highlightPreTag`."), - "code": String("invalid_search_highlight_pre_tag"), - "type": String("invalid_request"), - "link": String("https://docs.meilisearch.com/errors#invalid-search-highlight-pre-tag"), - }, - 400, - ) + let (response, code) = index.search_post(json!({"limit": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Integer at `.limit`.", + "code": "invalid_search_limit", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-search-limit" + } "###); - // array - let response = index.search_post(json!({"highlightPreTag": ["marker", ""]})).await; - meili_snap::snapshot!(format!("{:#?}", response), @r###" - ( - Object { - "message": String("invalid type: Sequence `[\"marker\",\"\"]`, expected a String at `.highlightPreTag`."), - "code": String("invalid_search_highlight_pre_tag"), - "type": String("invalid_request"), - "link": String("https://docs.meilisearch.com/errors#invalid-search-highlight-pre-tag"), - }, - 400, - ) + let (response, code) = index.search_get(json!({"limit": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid digit found in string at `.limit`.", + "code": "invalid_search_limit", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-search-limit" + } "###); } #[actix_rt::test] -async fn search_invalid_highlight_post_tag() { +async fn search_bad_page() { let server = Server::new().await; let index = server.index("test"); - // object - let response = index.search_post(json!({"highlightPostTag": { "marker": "" }})).await; - meili_snap::snapshot!(format!("{:#?}", response), @r###" - ( - Object { - "message": String("invalid type: Map `{\"marker\":\"\"}`, expected a String at `.highlightPostTag`."), - "code": String("invalid_search_highlight_post_tag"), - "type": String("invalid_request"), - "link": String("https://docs.meilisearch.com/errors#invalid-search-highlight-post-tag"), - }, - 400, - ) + let (response, code) = index.search_post(json!({"page": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Integer at `.page`.", + "code": "invalid_search_page", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-search-page" + } "###); - // array - let response = index.search_post(json!({"highlightPostTag": ["marker", ""]})).await; - meili_snap::snapshot!(format!("{:#?}", response), @r###" - ( - Object { - "message": String("invalid type: Sequence `[\"marker\",\"\"]`, expected a String at `.highlightPostTag`."), - "code": String("invalid_search_highlight_post_tag"), - "type": String("invalid_request"), - "link": String("https://docs.meilisearch.com/errors#invalid-search-highlight-post-tag"), - }, - 400, - ) + let (response, code) = index.search_get(json!({"page": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid digit found in string at `.page`.", + "code": "invalid_search_page", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-search-page" + } + "###); +} + +#[actix_rt::test] +async fn search_bad_hits_per_page() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.search_post(json!({"hitsPerPage": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Integer at `.hitsPerPage`.", + "code": "invalid_search_hits_per_page", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-search-hits-per-page" + } + "###); + + let (response, code) = index.search_get(json!({"hitsPerPage": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid digit found in string at `.hitsPerPage`.", + "code": "invalid_search_hits_per_page", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-search-hits-per-page" + } + "###); +} + +#[actix_rt::test] +async fn search_bad_attributes_to_crop() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.search_post(json!({"attributesToCrop": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.attributesToCrop`.", + "code": "invalid_search_attributes_to_crop", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-search-attributes-to-crop" + } + "###); + // Can't make the `attributes_to_crop` fail with a get search since it'll accept anything as an array of strings. +} + +#[actix_rt::test] +async fn search_bad_crop_length() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.search_post(json!({"cropLength": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Integer at `.cropLength`.", + "code": "invalid_search_crop_length", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-search-crop-length" + } + "###); + + let (response, code) = index.search_get(json!({"cropLength": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid digit found in string at `.cropLength`.", + "code": "invalid_search_crop_length", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-search-crop-length" + } + "###); +} + +#[actix_rt::test] +async fn search_bad_attributes_to_highlight() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.search_post(json!({"attributesToHighlight": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.attributesToHighlight`.", + "code": "invalid_search_attributes_to_highlight", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-search-attributes-to-highlight" + } + "###); + // Can't make the `attributes_to_highlight` fail with a get search since it'll accept anything as an array of strings. +} + +#[actix_rt::test] +async fn search_bad_filter() { + // Since a filter is deserialized as a json Value it will never fail to deserialize. + // Thus the error message is not generated by deserr but written by us. + let server = Server::new().await; + let index = server.index("test"); + // Also, to trigger the error message we need to effectively create the index or else it'll throw an + // index does not exists error. + let (_, code) = index.create(None).await; + server.wait_task(0).await; + + snapshot!(code, @"202 Accepted"); + + let (response, code) = index.search_post(json!({ "filter": true })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Invalid syntax for the filter parameter: `expected String, Array, found: true`.", + "code": "invalid_search_filter", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-search-filter" + } + "###); + // Can't make the `filter` fail with a get search since it'll accept anything as a strings. +} + +#[actix_rt::test] +async fn search_bad_sort() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.search_post(json!({"sort": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.sort`.", + "code": "invalid_search_sort", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-search-sort" + } + "###); + // Can't make the `sort` fail with a get search since it'll accept anything as a strings. +} + +#[actix_rt::test] +async fn search_bad_show_matches_position() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.search_post(json!({"showMatchesPosition": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Boolean at `.showMatchesPosition`.", + "code": "invalid_search_show_matches_position", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-search-show-matches-position" + } + "###); + + let (response, code) = index.search_get(json!({"showMatchesPosition": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "provided string was not `true` or `false` at `.showMatchesPosition`.", + "code": "invalid_search_show_matches_position", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-search-show-matches-position" + } + "###); +} + +#[actix_rt::test] +async fn search_bad_facets() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.search_post(json!({"facets": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.facets`.", + "code": "invalid_search_facets", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-search-facets" + } + "###); + // Can't make the `attributes_to_highlight` fail with a get search since it'll accept anything as an array of strings. +} + +#[actix_rt::test] +async fn search_bad_highlight_pre_tag() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.search_post(json!({"highlightPreTag": ["doggo"]})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: Sequence `[\"doggo\"]`, expected a String at `.highlightPreTag`.", + "code": "invalid_search_highlight_pre_tag", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-search-highlight-pre-tag" + } + "###); + // Can't make the `highlight_pre_tag` fail with a get search since it'll accept anything as a strings. +} + +#[actix_rt::test] +async fn search_bad_highlight_post_tag() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.search_post(json!({"highlightPostTag": ["doggo"]})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: Sequence `[\"doggo\"]`, expected a String at `.highlightPostTag`.", + "code": "invalid_search_highlight_post_tag", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-search-highlight-post-tag" + } + "###); + // Can't make the `highlight_post_tag` fail with a get search since it'll accept anything as a strings. +} + +#[actix_rt::test] +async fn search_bad_crop_marker() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.search_post(json!({"cropMarker": ["doggo"]})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: Sequence `[\"doggo\"]`, expected a String at `.cropMarker`.", + "code": "invalid_search_crop_marker", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-search-crop-marker" + } + "###); + // Can't make the `crop_marker` fail with a get search since it'll accept anything as a strings. +} + +#[actix_rt::test] +async fn search_bad_matching_strategy() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.search_post(json!({"matchingStrategy": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Json deserialize error: unknown value `doggo`, expected one of `last`, `all` at `.matchingStrategy`.", + "code": "invalid_search_matching_strategy", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-search-matching-strategy" + } + "###); + + let (response, code) = index.search_get(json!({"matchingStrategy": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "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" + } "###); } @@ -151,9 +416,9 @@ async fn filter_invalid_syntax_object() { let expected_response = json!({ "message": "Was expecting an operation `=`, `!=`, `>=`, `>`, `<=`, `<`, `IN`, `NOT IN`, `TO`, `EXISTS`, `NOT EXISTS`, or `_geoRadius` at `title & Glass`.\n1:14 title & Glass", - "code": "invalid_filter", + "code": "invalid_search_filter", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-filter" + "link": "https://docs.meilisearch.com/errors#invalid-search-filter" }); index .search(json!({"filter": "title & Glass"}), |response, code| { @@ -176,9 +441,9 @@ async fn filter_invalid_syntax_array() { let expected_response = json!({ "message": "Was expecting an operation `=`, `!=`, `>=`, `>`, `<=`, `<`, `IN`, `NOT IN`, `TO`, `EXISTS`, `NOT EXISTS`, or `_geoRadius` at `title & Glass`.\n1:14 title & Glass", - "code": "invalid_filter", + "code": "invalid_search_filter", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-filter" + "link": "https://docs.meilisearch.com/errors#invalid-search-filter" }); index .search(json!({"filter": ["title & Glass"]}), |response, code| { @@ -201,9 +466,9 @@ async fn filter_invalid_syntax_string() { let expected_response = json!({ "message": "Found unexpected characters at the end of the filter: `XOR title = Glass`. You probably forgot an `OR` or an `AND` rule.\n15:32 title = Glass XOR title = Glass", - "code": "invalid_filter", + "code": "invalid_search_filter", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-filter" + "link": "https://docs.meilisearch.com/errors#invalid-search-filter" }); index .search(json!({"filter": "title = Glass XOR title = Glass"}), |response, code| { @@ -226,9 +491,9 @@ async fn filter_invalid_attribute_array() { let expected_response = json!({ "message": "Attribute `many` is not filterable. Available filterable attributes are: `title`.\n1:5 many = Glass", - "code": "invalid_filter", + "code": "invalid_search_filter", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-filter" + "link": "https://docs.meilisearch.com/errors#invalid-search-filter" }); index .search(json!({"filter": ["many = Glass"]}), |response, code| { @@ -251,9 +516,9 @@ async fn filter_invalid_attribute_string() { let expected_response = json!({ "message": "Attribute `many` is not filterable. Available filterable attributes are: `title`.\n1:5 many = Glass", - "code": "invalid_filter", + "code": "invalid_search_filter", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-filter" + "link": "https://docs.meilisearch.com/errors#invalid-search-filter" }); index .search(json!({"filter": "many = Glass"}), |response, code| { @@ -276,9 +541,9 @@ async fn filter_reserved_geo_attribute_array() { let expected_response = json!({ "message": "`_geo` is a reserved keyword and thus can't be used as a filter expression. Use the _geoRadius(latitude, longitude, distance) built-in rule to filter on _geo field coordinates.\n1:5 _geo = Glass", - "code": "invalid_filter", + "code": "invalid_search_filter", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-filter" + "link": "https://docs.meilisearch.com/errors#invalid-search-filter" }); index .search(json!({"filter": ["_geo = Glass"]}), |response, code| { @@ -301,9 +566,9 @@ async fn filter_reserved_geo_attribute_string() { let expected_response = json!({ "message": "`_geo` is a reserved keyword and thus can't be used as a filter expression. Use the _geoRadius(latitude, longitude, distance) built-in rule to filter on _geo field coordinates.\n1:5 _geo = Glass", - "code": "invalid_filter", + "code": "invalid_search_filter", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-filter" + "link": "https://docs.meilisearch.com/errors#invalid-search-filter" }); index .search(json!({"filter": "_geo = Glass"}), |response, code| { @@ -326,9 +591,9 @@ async fn filter_reserved_attribute_array() { let expected_response = json!({ "message": "`_geoDistance` is a reserved keyword and thus can't be used as a filter expression.\n1:13 _geoDistance = Glass", - "code": "invalid_filter", + "code": "invalid_search_filter", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-filter" + "link": "https://docs.meilisearch.com/errors#invalid-search-filter" }); index .search(json!({"filter": ["_geoDistance = Glass"]}), |response, code| { @@ -351,9 +616,9 @@ async fn filter_reserved_attribute_string() { let expected_response = json!({ "message": "`_geoDistance` is a reserved keyword and thus can't be used as a filter expression.\n1:13 _geoDistance = Glass", - "code": "invalid_filter", + "code": "invalid_search_filter", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-filter" + "link": "https://docs.meilisearch.com/errors#invalid-search-filter" }); index .search(json!({"filter": "_geoDistance = Glass"}), |response, code| { @@ -376,9 +641,9 @@ async fn sort_geo_reserved_attribute() { let expected_response = json!({ "message": "`_geo` is a reserved keyword and thus can't be used as a sort expression. Use the _geoPoint(latitude, longitude) built-in rule to sort on _geo field coordinates.", - "code": "invalid_sort", + "code": "invalid_search_sort", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-sort" + "link": "https://docs.meilisearch.com/errors#invalid-search-sort" }); index .search( @@ -406,9 +671,9 @@ async fn sort_reserved_attribute() { let expected_response = json!({ "message": "`_geoDistance` is a reserved keyword and thus can't be used as a sort expression.", - "code": "invalid_sort", + "code": "invalid_search_sort", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-sort" + "link": "https://docs.meilisearch.com/errors#invalid-search-sort" }); index .search( @@ -436,9 +701,9 @@ async fn sort_unsortable_attribute() { let expected_response = json!({ "message": "Attribute `title` is not sortable. Available sortable attributes are: `id`.", - "code": "invalid_sort", + "code": "invalid_search_sort", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-sort" + "link": "https://docs.meilisearch.com/errors#invalid-search-sort" }); index .search( @@ -466,9 +731,9 @@ async fn sort_invalid_syntax() { let expected_response = json!({ "message": "Invalid syntax for the sort parameter: expected expression ending by `:asc` or `:desc`, found `title`.", - "code": "invalid_sort", + "code": "invalid_search_sort", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-sort" + "link": "https://docs.meilisearch.com/errors#invalid-search-sort" }); index .search( @@ -500,9 +765,9 @@ async fn sort_unset_ranking_rule() { let expected_response = json!({ "message": "The sort ranking rule must be specified in the ranking rules settings to use the sort parameter at search time.", - "code": "invalid_sort", + "code": "invalid_search_sort", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-sort" + "link": "https://docs.meilisearch.com/errors#invalid-search-sort" }); index .search( diff --git a/meilisearch/tests/search/mod.rs b/meilisearch/tests/search/mod.rs index 44a4702d0..60ffa6cee 100644 --- a/meilisearch/tests/search/mod.rs +++ b/meilisearch/tests/search/mod.rs @@ -200,11 +200,14 @@ async fn search_with_filter_string_notation() { let server = Server::new().await; let index = server.index("test"); - index.update_settings(json!({"filterableAttributes": ["title"]})).await; + let (_, code) = index.update_settings(json!({"filterableAttributes": ["title"]})).await; + meili_snap::snapshot!(code, @"202 Accepted"); let documents = DOCUMENTS.clone(); - index.add_documents(documents, None).await; - index.wait_task(1).await; + let (_, code) = index.add_documents(documents, None).await; + meili_snap::snapshot!(code, @"202 Accepted"); + let res = index.wait_task(1).await; + meili_snap::snapshot!(res["status"], @r###""succeeded""###); index .search( @@ -220,11 +223,15 @@ async fn search_with_filter_string_notation() { let index = server.index("nested"); - index.update_settings(json!({"filterableAttributes": ["cattos", "doggos.age"]})).await; + let (_, code) = + index.update_settings(json!({"filterableAttributes": ["cattos", "doggos.age"]})).await; + meili_snap::snapshot!(code, @"202 Accepted"); let documents = NESTED_DOCUMENTS.clone(); - index.add_documents(documents, None).await; - index.wait_task(3).await; + let (_, code) = index.add_documents(documents, None).await; + meili_snap::snapshot!(code, @"202 Accepted"); + let res = index.wait_task(3).await; + meili_snap::snapshot!(res["status"], @r###""succeeded""###); index .search( diff --git a/meilisearch/tests/settings/errors.rs b/meilisearch/tests/settings/errors.rs new file mode 100644 index 000000000..77e62303a --- /dev/null +++ b/meilisearch/tests/settings/errors.rs @@ -0,0 +1,312 @@ +use meili_snap::*; +use serde_json::json; + +use crate::common::Server; + +#[actix_rt::test] +async fn settings_bad_displayed_attributes() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.update_settings(json!({ "displayedAttributes": "doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.displayedAttributes`.", + "code": "invalid_settings_displayed_attributes", + "type": "invalid_request", + "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] +async fn settings_bad_searchable_attributes() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.update_settings(json!({ "searchableAttributes": "doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.searchableAttributes`.", + "code": "invalid_settings_searchable_attributes", + "type": "invalid_request", + "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] +async fn settings_bad_filterable_attributes() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.update_settings(json!({ "filterableAttributes": "doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.filterableAttributes`.", + "code": "invalid_settings_filterable_attributes", + "type": "invalid_request", + "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] +async fn settings_bad_sortable_attributes() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.update_settings(json!({ "sortableAttributes": "doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.sortableAttributes`.", + "code": "invalid_settings_sortable_attributes", + "type": "invalid_request", + "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] +async fn settings_bad_ranking_rules() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.update_settings(json!({ "rankingRules": "doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.rankingRules`.", + "code": "invalid_settings_ranking_rules", + "type": "invalid_request", + "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] +async fn settings_bad_stop_words() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.update_settings(json!({ "stopWords": "doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Sequence at `.stopWords`.", + "code": "invalid_settings_stop_words", + "type": "invalid_request", + "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] +async fn settings_bad_synonyms() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.update_settings(json!({ "synonyms": "doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Map at `.synonyms`.", + "code": "invalid_settings_synonyms", + "type": "invalid_request", + "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] +async fn settings_bad_distinct_attribute() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.update_settings(json!({ "distinctAttribute": ["doggo"] })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: Sequence `[\"doggo\"]`, expected a String at `.distinctAttribute`.", + "code": "invalid_settings_distinct_attribute", + "type": "invalid_request", + "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] +async fn settings_bad_typo_tolerance() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.update_settings(json!({ "typoTolerance": "doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Map at `.typoTolerance`.", + "code": "invalid_settings_typo_tolerance", + "type": "invalid_request", + "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] +async fn settings_bad_faceting() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.update_settings(json!({ "faceting": "doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Map at `.faceting`.", + "code": "invalid_settings_faceting", + "type": "invalid_request", + "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] +async fn settings_bad_pagination() { + let server = Server::new().await; + let index = server.index("test"); + + let (response, code) = index.update_settings(json!({ "pagination": "doggo" })).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid type: String `\"doggo\"`, expected a Map at `.pagination`.", + "code": "invalid_settings_pagination", + "type": "invalid_request", + "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 0b71dac1c..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_ranking_rule", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-ranking-rule" - }); - - 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/settings/mod.rs b/meilisearch/tests/settings/mod.rs index 05339cb37..fa4df254c 100644 --- a/meilisearch/tests/settings/mod.rs +++ b/meilisearch/tests/settings/mod.rs @@ -1,2 +1,3 @@ mod distinct; +mod errors; mod get_settings; diff --git a/meilisearch/tests/tasks/errors.rs b/meilisearch/tests/tasks/errors.rs new file mode 100644 index 000000000..305ab8b9c --- /dev/null +++ b/meilisearch/tests/tasks/errors.rs @@ -0,0 +1,498 @@ +use meili_snap::*; +use serde_json::json; + +use crate::common::Server; + +#[actix_rt::test] +async fn task_bad_uids() { + let server = Server::new().await; + + let (response, code) = server.tasks_filter(json!({"uids": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid digit found in string at `.uids`.", + "code": "invalid_task_uids", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-uids" + } + "###); + + let (response, code) = server.cancel_tasks(json!({"uids": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid digit found in string at `.uids`.", + "code": "invalid_task_uids", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-uids" + } + "###); + + let (response, code) = server.delete_tasks(json!({"uids": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid digit found in string at `.uids`.", + "code": "invalid_task_uids", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-uids" + } + "###); +} + +#[actix_rt::test] +async fn task_bad_canceled_by() { + let server = Server::new().await; + + let (response, code) = server.tasks_filter(json!({"canceledBy": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid digit found in string at `.canceledBy`.", + "code": "invalid_task_canceled_by", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-canceled-by" + } + "###); + + let (response, code) = server.cancel_tasks(json!({"canceledBy": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid digit found in string at `.canceledBy`.", + "code": "invalid_task_canceled_by", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-canceled-by" + } + "###); + + let (response, code) = server.delete_tasks(json!({"canceledBy": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid digit found in string at `.canceledBy`.", + "code": "invalid_task_canceled_by", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-canceled-by" + } + "###); +} + +#[actix_rt::test] +async fn task_bad_types() { + let server = Server::new().await; + + let (response, code) = server.tasks_filter(json!({"types": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`doggo` is not a type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`. at `.types`.", + "code": "invalid_task_types", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-types" + } + "###); + + let (response, code) = server.cancel_tasks(json!({"types": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`doggo` is not a type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`. at `.types`.", + "code": "invalid_task_types", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-types" + } + "###); + + let (response, code) = server.delete_tasks(json!({"types": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`doggo` is not a type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`. at `.types`.", + "code": "invalid_task_types", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-types" + } + "###); +} + +#[actix_rt::test] +async fn task_bad_statuses() { + let server = Server::new().await; + + let (response, code) = server.tasks_filter(json!({"statuses": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`doggo` is not a status. Available status are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`. at `.statuses`.", + "code": "invalid_task_statuses", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-statuses" + } + "###); + + let (response, code) = server.cancel_tasks(json!({"statuses": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`doggo` is not a status. Available status are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`. at `.statuses`.", + "code": "invalid_task_statuses", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-statuses" + } + "###); + + let (response, code) = server.delete_tasks(json!({"statuses": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`doggo` is not a status. Available status are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`. at `.statuses`.", + "code": "invalid_task_statuses", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-statuses" + } + "###); +} + +#[actix_rt::test] +async fn task_bad_index_uids() { + let server = Server::new().await; + + let (response, code) = server.tasks_filter(json!({"indexUids": "the good doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`the good doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_). at `.indexUids`.", + "code": "invalid_index_uid", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-index-uid" + } + "###); + + let (response, code) = server.cancel_tasks(json!({"indexUids": "the good doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`the good doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_). at `.indexUids`.", + "code": "invalid_index_uid", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-index-uid" + } + "###); + + let (response, code) = server.delete_tasks(json!({"indexUids": "the good doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`the good doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_). at `.indexUids`.", + "code": "invalid_index_uid", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-index-uid" + } + "###); +} + +#[actix_rt::test] +async fn task_bad_limit() { + let server = Server::new().await; + + let (response, code) = server.tasks_filter(json!({"limit": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid digit found in string at `.limit`.", + "code": "invalid_task_limit", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-limit" + } + "###); + + let (response, code) = server.cancel_tasks(json!({"limit": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Json deserialize error: unknown field `limit`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", + "code": "bad_request", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#bad-request" + } + "###); + + let (response, code) = server.delete_tasks(json!({"limit": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Json deserialize error: unknown field `limit`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", + "code": "bad_request", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#bad-request" + } + "###); +} + +#[actix_rt::test] +async fn task_bad_from() { + let server = Server::new().await; + + let (response, code) = server.tasks_filter(json!({"from": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "invalid digit found in string at `.from`.", + "code": "invalid_task_from", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-from" + } + "###); + + let (response, code) = server.cancel_tasks(json!({"from": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Json deserialize error: unknown field `from`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", + "code": "bad_request", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#bad-request" + } + "###); + + let (response, code) = server.delete_tasks(json!({"from": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Json deserialize error: unknown field `from`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", + "code": "bad_request", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#bad-request" + } + "###); +} + +#[actix_rt::test] +async fn task_bad_after_enqueued_at() { + let server = Server::new().await; + + let (response, code) = server.tasks_filter(json!({"afterEnqueuedAt": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterEnqueuedAt`.", + "code": "invalid_task_after_enqueued_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-after-enqueued-at" + } + "###); + + let (response, code) = server.cancel_tasks(json!({"afterEnqueuedAt": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterEnqueuedAt`.", + "code": "invalid_task_after_enqueued_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-after-enqueued-at" + } + "###); + + let (response, code) = server.delete_tasks(json!({"afterEnqueuedAt": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterEnqueuedAt`.", + "code": "invalid_task_after_enqueued_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-after-enqueued-at" + } + "###); +} + +#[actix_rt::test] +async fn task_bad_before_enqueued_at() { + let server = Server::new().await; + + let (response, code) = server.tasks_filter(json!({"beforeEnqueuedAt": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeEnqueuedAt`.", + "code": "invalid_task_before_enqueued_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-before-enqueued-at" + } + "###); + + let (response, code) = server.cancel_tasks(json!({"beforeEnqueuedAt": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeEnqueuedAt`.", + "code": "invalid_task_before_enqueued_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-before-enqueued-at" + } + "###); + + let (response, code) = server.delete_tasks(json!({"beforeEnqueuedAt": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeEnqueuedAt`.", + "code": "invalid_task_before_enqueued_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-before-enqueued-at" + } + "###); +} + +#[actix_rt::test] +async fn task_bad_after_started_at() { + let server = Server::new().await; + + let (response, code) = server.tasks_filter(json!({"afterStartedAt": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterStartedAt`.", + "code": "invalid_task_after_started_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-after-started-at" + } + "###); + + let (response, code) = server.cancel_tasks(json!({"afterStartedAt": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterStartedAt`.", + "code": "invalid_task_after_started_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-after-started-at" + } + "###); + + let (response, code) = server.delete_tasks(json!({"afterStartedAt": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterStartedAt`.", + "code": "invalid_task_after_started_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-after-started-at" + } + "###); +} + +#[actix_rt::test] +async fn task_bad_before_started_at() { + let server = Server::new().await; + + let (response, code) = server.tasks_filter(json!({"beforeStartedAt": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeStartedAt`.", + "code": "invalid_task_before_started_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-before-started-at" + } + "###); + + let (response, code) = server.cancel_tasks(json!({"beforeStartedAt": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeStartedAt`.", + "code": "invalid_task_before_started_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-before-started-at" + } + "###); + + let (response, code) = server.delete_tasks(json!({"beforeStartedAt": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeStartedAt`.", + "code": "invalid_task_before_started_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-before-started-at" + } + "###); +} + +#[actix_rt::test] +async fn task_bad_after_finished_at() { + let server = Server::new().await; + + let (response, code) = server.tasks_filter(json!({"afterFinishedAt": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterFinishedAt`.", + "code": "invalid_task_after_finished_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-after-finished-at" + } + "###); + + let (response, code) = server.cancel_tasks(json!({"afterFinishedAt": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterFinishedAt`.", + "code": "invalid_task_after_finished_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-after-finished-at" + } + "###); + + let (response, code) = server.delete_tasks(json!({"afterFinishedAt": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterFinishedAt`.", + "code": "invalid_task_after_finished_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-after-finished-at" + } + "###); +} + +#[actix_rt::test] +async fn task_bad_before_finished_at() { + let server = Server::new().await; + + let (response, code) = server.tasks_filter(json!({"beforeFinishedAt": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeFinishedAt`.", + "code": "invalid_task_before_finished_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-before-finished-at" + } + "###); + + let (response, code) = server.cancel_tasks(json!({"beforeFinishedAt": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeFinishedAt`.", + "code": "invalid_task_before_finished_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-before-finished-at" + } + "###); + + let (response, code) = server.delete_tasks(json!({"beforeFinishedAt": "doggo"})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeFinishedAt`.", + "code": "invalid_task_before_finished_at", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid-task-before-finished-at" + } + "###); +} diff --git a/meilisearch/tests/tasks/mod.rs b/meilisearch/tests/tasks/mod.rs index 4505d7fe5..46775e05f 100644 --- a/meilisearch/tests/tasks/mod.rs +++ b/meilisearch/tests/tasks/mod.rs @@ -1,4 +1,6 @@ -use meili_snap::insta::{self, assert_json_snapshot}; +mod errors; + +use meili_snap::insta::assert_json_snapshot; use serde_json::json; use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; @@ -179,9 +181,9 @@ 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`", + "message": "Json deserialize error: unknown field `lol`, expected one of `limit`, `from`, `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", "code": "bad_request", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#bad-request" @@ -190,9 +192,9 @@ 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.", + "message": "invalid digit found in string at `.uids`.", "code": "invalid_task_uids", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-uids" @@ -201,20 +203,20 @@ 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", + "message": "invalid digit found in string at `.from`.", + "code": "invalid_task_from", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#bad-request" + "link": "https://docs.meilisearch.com/errors#invalid-task-from" } "###); 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.", + "message": "`pied` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeStartedAt`.", "code": "invalid_task_before_started_at", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-before-started-at" @@ -228,7 +230,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,9 +241,9 @@ 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`", + "message": "Json deserialize error: unknown field `lol`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", "code": "bad_request", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#bad-request" @@ -250,9 +252,9 @@ 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.", + "message": "invalid digit found in string at `.uids`.", "code": "invalid_task_uids", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-uids" @@ -266,7 +268,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,9 +279,9 @@ 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`", + "message": "Json deserialize error: unknown field `lol`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", "code": "bad_request", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#bad-request" @@ -288,9 +290,9 @@ 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.", + "message": "invalid digit found in string at `.uids`.", "code": "invalid_task_uids", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid-task-uids" @@ -517,46 +519,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_ranking_rule", - "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid-ranking-rule" - }, - "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,