3339: Continued deserr integration r=irevoire a=loiclec

Fix https://github.com/meilisearch/meilisearch/issues/3337
Fix https://github.com/meilisearch/meilisearch/issues/3338

1. Add new error codes that should have been implemented earlier:
- `MissingApiKeyActions`
- `MissingApiKeyExpiresAt`
- `MissingApiKeyIndexes`
- `MissingSwapIndexes`

2. Fix a bug where it was possible to create an API key without specifying the value of `expiresAt`

3. Improve the error messages generated by deserr. Have specific error messages for JSON and QueryParam deserialisation errors.

4. Improve error tests by passing query params as arguments to `GET` routes directly instead of using an intermediary JSON object

5. [Use invalid_index_uid error code in more places](e225608337)

Co-authored-by: Loïc Lecrenier <loic.lecrenier@me.com>
This commit is contained in:
bors[bot] 2023-01-17 14:41:22 +00:00 committed by GitHub
commit 41a970247e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1957 additions and 1377 deletions

329
Cargo.lock generated
View File

@ -77,8 +77,8 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6"
dependencies = [ dependencies = [
"quote 1.0.23", "quote",
"syn 1.0.107", "syn",
] ]
[[package]] [[package]]
@ -211,9 +211,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa9362663c8643d67b2d5eafba49e4cb2c8a053a29ed00a0bea121f17c76b13" checksum = "1fa9362663c8643d67b2d5eafba49e4cb2c8a053a29ed00a0bea121f17c76b13"
dependencies = [ dependencies = [
"actix-router", "actix-router",
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"syn 1.0.107", "syn",
] ]
[[package]] [[package]]
@ -324,9 +324,9 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27"
dependencies = [ dependencies = [
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"syn 1.0.107", "syn",
] ]
[[package]] [[package]]
@ -335,9 +335,9 @@ version = "0.1.61"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "705339e0e4a9690e2908d2b3d049d85682cf19fbd5782494498fbf7003a6a282" checksum = "705339e0e4a9690e2908d2b3d049d85682cf19fbd5782494498fbf7003a6a282"
dependencies = [ dependencies = [
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"syn 1.0.107", "syn",
] ]
[[package]] [[package]]
@ -423,21 +423,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "bit-set"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@ -541,9 +526,9 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fe233b960f12f8007e3db2d136e3cb1c291bfd7396e384ee76025fc1a3932b4" checksum = "5fe233b960f12f8007e3db2d136e3cb1c291bfd7396e384ee76025fc1a3932b4"
dependencies = [ dependencies = [
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"syn 1.0.107", "syn",
] ]
[[package]] [[package]]
@ -702,9 +687,9 @@ checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro-error", "proc-macro-error",
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"syn 1.0.107", "syn",
] ]
[[package]] [[package]]
@ -715,9 +700,9 @@ checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro-error", "proc-macro-error",
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"syn 1.0.107", "syn",
] ]
[[package]] [[package]]
@ -744,9 +729,9 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1df715824eb382e34b7afb7463b0247bf41538aeba731fba05241ecdb5dc3747" checksum = "1df715824eb382e34b7afb7463b0247bf41538aeba731fba05241ecdb5dc3747"
dependencies = [ dependencies = [
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"syn 1.0.107", "syn",
] ]
[[package]] [[package]]
@ -961,10 +946,10 @@ checksum = "a784d2ccaf7c98501746bf0be29b2022ba41fd62a2e622af997a03e9f972859f"
dependencies = [ dependencies = [
"fnv", "fnv",
"ident_case", "ident_case",
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"strsim", "strsim",
"syn 1.0.107", "syn",
] ]
[[package]] [[package]]
@ -974,8 +959,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e" checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"quote 1.0.23", "quote",
"syn 1.0.107", "syn",
] ]
[[package]] [[package]]
@ -994,9 +979,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4" checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4"
dependencies = [ dependencies = [
"darling", "darling",
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"syn 1.0.107", "syn",
] ]
[[package]] [[package]]
@ -1006,7 +991,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68" checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68"
dependencies = [ dependencies = [
"derive_builder_core", "derive_builder_core",
"syn 1.0.107", "syn",
] ]
[[package]] [[package]]
@ -1016,17 +1001,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
dependencies = [ dependencies = [
"convert_case 0.4.0", "convert_case 0.4.0",
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"rustc_version", "rustc_version",
"syn 1.0.107", "syn",
] ]
[[package]] [[package]]
name = "deserr" name = "deserr"
version = "0.1.4" version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86290491a2b5c21a1a5083da8dae831006761258fabd5617309c3eebc5f89468" checksum = "5d3c6417f0bf7561774690e3d47f9659b0cbc3614c7af7bfda404fda7a2c11d3"
dependencies = [ dependencies = [
"deserr-internal", "deserr-internal",
"serde-cs", "serde-cs",
@ -1035,14 +1020,14 @@ dependencies = [
[[package]] [[package]]
name = "deserr-internal" name = "deserr-internal"
version = "0.1.4" version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7131de1c27581bc376a22166c9f570be91b76cb096be2f6aecf224c27bf7c49a" checksum = "196415cbd3b782cddecbdd69da18cd9b19e1bb0bdbb649e87b5afd83fa8d322b"
dependencies = [ dependencies = [
"convert_case 0.5.0", "convert_case 0.5.0",
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"syn 1.0.107", "syn",
] ]
[[package]] [[package]]
@ -1220,9 +1205,9 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "828de45d0ca18782232dfb8f3ea9cc428e8ced380eb26a520baaacfc70de39ce" checksum = "828de45d0ca18782232dfb8f3ea9cc428e8ced380eb26a520baaacfc70de39ce"
dependencies = [ dependencies = [
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"syn 1.0.107", "syn",
] ]
[[package]] [[package]]
@ -1285,9 +1270,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35c9bb4a2c13ffb3a93a39902aaf4e7190a1706a4779b6db0449aee433d26c4a" checksum = "35c9bb4a2c13ffb3a93a39902aaf4e7190a1706a4779b6db0449aee433d26c4a"
dependencies = [ dependencies = [
"darling", "darling",
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"syn 1.0.107", "syn",
"uuid 0.8.2", "uuid 0.8.2",
] ]
@ -1315,8 +1300,8 @@ dependencies = [
[[package]] [[package]]
name = "filter-parser" name = "filter-parser"
version = "0.39.0" version = "0.39.1"
source = "git+https://github.com/meilisearch/milli.git?tag=v0.39.0#e6bea999740b153871f665abce869ffbb5aa94c5" source = "git+https://github.com/meilisearch/milli.git?tag=v0.39.1#0c7d1f761e5db6d086f27d3f0f47a97c7f4a5f08"
dependencies = [ dependencies = [
"nom", "nom",
"nom_locate", "nom_locate",
@ -1334,8 +1319,8 @@ dependencies = [
[[package]] [[package]]
name = "flatten-serde-json" name = "flatten-serde-json"
version = "0.39.0" version = "0.39.1"
source = "git+https://github.com/meilisearch/milli.git?tag=v0.39.0#e6bea999740b153871f665abce869ffbb5aa94c5" source = "git+https://github.com/meilisearch/milli.git?tag=v0.39.1#0c7d1f761e5db6d086f27d3f0f47a97c7f4a5f08"
dependencies = [ dependencies = [
"serde_json", "serde_json",
] ]
@ -1415,9 +1400,9 @@ version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d"
dependencies = [ dependencies = [
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"syn 1.0.107", "syn",
] ]
[[package]] [[package]]
@ -1493,9 +1478,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9" checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9"
dependencies = [ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"syn 1.0.107", "syn",
] ]
[[package]] [[package]]
@ -1899,8 +1884,8 @@ dependencies = [
[[package]] [[package]]
name = "json-depth-checker" name = "json-depth-checker"
version = "0.39.0" version = "0.39.1"
source = "git+https://github.com/meilisearch/milli.git?tag=v0.39.0#e6bea999740b153871f665abce869ffbb5aa94c5" source = "git+https://github.com/meilisearch/milli.git?tag=v0.39.1#0c7d1f761e5db6d086f27d3f0f47a97c7f4a5f08"
dependencies = [ dependencies = [
"serde_json", "serde_json",
] ]
@ -2238,9 +2223,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a9062912d7952c5588cc474795e0b9ee008e7e6781127945b85413d4b99d81" checksum = "10a9062912d7952c5588cc474795e0b9ee008e7e6781127945b85413d4b99d81"
dependencies = [ dependencies = [
"log", "log",
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"syn 1.0.107", "syn",
] ]
[[package]] [[package]]
@ -2260,9 +2245,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f08150cf2bab1fc47c2196f4f41173a27fcd0f684165e5458c0046b53a472e2f" checksum = "f08150cf2bab1fc47c2196f4f41173a27fcd0f684165e5458c0046b53a472e2f"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"syn 1.0.107", "syn",
] ]
[[package]] [[package]]
@ -2348,7 +2333,6 @@ dependencies = [
"rustls-pemfile", "rustls-pemfile",
"segment", "segment",
"serde", "serde",
"serde-cs",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"sha-1", "sha-1",
@ -2409,10 +2393,9 @@ dependencies = [
"meili-snap", "meili-snap",
"memmap2", "memmap2",
"milli", "milli",
"proptest",
"proptest-derive",
"roaring", "roaring",
"serde", "serde",
"serde-cs",
"serde_json", "serde_json",
"tar", "tar",
"tempfile", "tempfile",
@ -2448,8 +2431,8 @@ dependencies = [
[[package]] [[package]]
name = "milli" name = "milli"
version = "0.39.0" version = "0.39.1"
source = "git+https://github.com/meilisearch/milli.git?tag=v0.39.0#e6bea999740b153871f665abce869ffbb5aa94c5" source = "git+https://github.com/meilisearch/milli.git?tag=v0.39.1#0c7d1f761e5db6d086f27d3f0f47a97c7f4a5f08"
dependencies = [ dependencies = [
"bimap", "bimap",
"bincode", "bincode",
@ -2800,9 +2783,9 @@ checksum = "46b53634d8c8196302953c74d5352f33d0c512a9499bd2ce468fc9f4128fa27c"
dependencies = [ dependencies = [
"pest", "pest",
"pest_meta", "pest_meta",
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"syn 1.0.107", "syn",
] ]
[[package]] [[package]]
@ -2900,9 +2883,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [ dependencies = [
"proc-macro-error-attr", "proc-macro-error-attr",
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"syn 1.0.107", "syn",
"version_check", "version_check",
] ]
@ -2912,20 +2895,11 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [ dependencies = [
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"version_check", "version_check",
] ]
[[package]]
name = "proc-macro2"
version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
dependencies = [
"unicode-xid 0.1.0",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.49" version = "1.0.49"
@ -2965,71 +2939,19 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "proptest"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0d9cc07f18492d879586c92b485def06bc850da3118075cd45d50e9c95b0e5"
dependencies = [
"bit-set",
"bitflags",
"byteorder",
"lazy_static",
"num-traits",
"quick-error 2.0.1",
"rand",
"rand_chacha",
"rand_xorshift",
"regex-syntax",
"rusty-fork",
"tempfile",
]
[[package]]
name = "proptest-derive"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90b46295382dc76166cb7cf2bb4a97952464e4b7ed5a43e6cd34e1fec3349ddc"
dependencies = [
"proc-macro2 0.4.30",
"quote 0.6.13",
"syn 0.15.44",
]
[[package]] [[package]]
name = "protobuf" name = "protobuf"
version = "2.28.0" version = "2.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94"
[[package]]
name = "quick-error"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quick-error"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quote"
version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
dependencies = [
"proc-macro2 0.4.30",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.23" version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
dependencies = [ dependencies = [
"proc-macro2 1.0.49", "proc-macro2",
] ]
[[package]] [[package]]
@ -3062,15 +2984,6 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "rand_xorshift"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f"
dependencies = [
"rand_core",
]
[[package]] [[package]]
name = "rayon" name = "rayon"
version = "1.6.1" version = "1.6.1"
@ -3285,18 +3198,6 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70"
[[package]]
name = "rusty-fork"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f"
dependencies = [
"fnv",
"quick-error 1.2.3",
"tempfile",
"wait-timeout",
]
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.12" version = "1.0.12"
@ -3372,9 +3273,9 @@ version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
dependencies = [ dependencies = [
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"syn 1.0.107", "syn",
] ]
[[package]] [[package]]
@ -3569,25 +3470,14 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "syn"
version = "0.15.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5"
dependencies = [
"proc-macro2 0.4.30",
"quote 0.6.13",
"unicode-xid 0.1.0",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.107" version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
dependencies = [ dependencies = [
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"unicode-ident", "unicode-ident",
] ]
@ -3606,10 +3496,10 @@ version = "0.12.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
dependencies = [ dependencies = [
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"syn 1.0.107", "syn",
"unicode-xid 0.2.4", "unicode-xid",
] ]
[[package]] [[package]]
@ -3691,9 +3581,9 @@ version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
dependencies = [ dependencies = [
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"syn 1.0.107", "syn",
] ]
[[package]] [[package]]
@ -3764,9 +3654,9 @@ version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
dependencies = [ dependencies = [
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"syn 1.0.107", "syn",
] ]
[[package]] [[package]]
@ -3895,12 +3785,6 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a"
[[package]]
name = "unicode-xid"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.4" version = "0.2.4"
@ -3983,15 +3867,6 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wait-timeout"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "walkdir" name = "walkdir"
version = "2.3.2" version = "2.3.2"
@ -4038,9 +3913,9 @@ dependencies = [
"bumpalo", "bumpalo",
"log", "log",
"once_cell", "once_cell",
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"syn 1.0.107", "syn",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -4062,7 +3937,7 @@ version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
dependencies = [ dependencies = [
"quote 1.0.23", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
] ]
@ -4072,9 +3947,9 @@ version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
dependencies = [ dependencies = [
"proc-macro2 1.0.49", "proc-macro2",
"quote 1.0.23", "quote",
"syn 1.0.107", "syn",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -4271,8 +4146,8 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d498dbd1fd7beb83c86709ae1c33ca50942889473473d287d56ce4770a18edfb" checksum = "d498dbd1fd7beb83c86709ae1c33ca50942889473473d287d56ce4770a18edfb"
dependencies = [ dependencies = [
"proc-macro2 1.0.49", "proc-macro2",
"syn 1.0.107", "syn",
"synstructure", "synstructure",
] ]

View File

@ -5,10 +5,8 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "test-traits", derive(proptest_derive::Arbitrary))]
pub struct ResponseError { pub struct ResponseError {
#[serde(skip)] #[serde(skip)]
#[cfg_attr(feature = "test-traits", proptest(strategy = "strategy::status_code_strategy()"))]
pub code: StatusCode, pub code: StatusCode,
pub message: String, pub message: String,
#[serde(rename = "code")] #[serde(rename = "code")]

View File

@ -5,7 +5,6 @@ use serde::Deserialize;
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] #[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "test-traits", derive(proptest_derive::Arbitrary))]
#[cfg_attr(test, derive(serde::Serialize))] #[cfg_attr(test, derive(serde::Serialize))]
pub struct ResponseError { pub struct ResponseError {
#[serde(skip)] #[serde(skip)]

View File

@ -3,7 +3,6 @@ pub mod error;
mod store; mod store;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::ops::Deref;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
@ -86,15 +85,13 @@ impl AuthController {
key.indexes key.indexes
.into_iter() .into_iter()
.filter_map(|index| { .filter_map(|index| {
search_rules.get_index_search_rules(index.deref()).map( search_rules.get_index_search_rules(&format!("{index}")).map(
|index_search_rules| { |index_search_rules| (index.to_string(), Some(index_search_rules)),
(String::from(index), Some(index_search_rules))
},
) )
}) })
.collect(), .collect(),
), ),
None => SearchRules::Set(key.indexes.into_iter().map(String::from).collect()), None => SearchRules::Set(key.indexes.into_iter().map(|x| x.to_string()).collect()),
}; };
} else if let Some(search_rules) = search_rules { } else if let Some(search_rules) = search_rules {
filters.search_rules = search_rules; filters.search_rules = search_rules;

View File

@ -3,7 +3,6 @@ use std::cmp::Reverse;
use std::collections::HashSet; use std::collections::HashSet;
use std::convert::{TryFrom, TryInto}; use std::convert::{TryFrom, TryInto};
use std::fs::create_dir_all; use std::fs::create_dir_all;
use std::ops::Deref;
use std::path::Path; use std::path::Path;
use std::str; use std::str;
use std::sync::Arc; use std::sync::Arc;
@ -135,7 +134,7 @@ impl HeedAuthStore {
for index in key.indexes.iter() { for index in key.indexes.iter() {
db.put( db.put(
&mut wtxn, &mut wtxn,
&(&uid, &action, Some(index.deref().as_bytes())), &(&uid, &action, Some(index.to_string().as_bytes())),
&key.expires_at, &key.expires_at,
)?; )?;
} }

View File

@ -9,18 +9,17 @@ actix-web = { version = "4.2.1", default-features = false }
anyhow = "1.0.65" anyhow = "1.0.65"
convert_case = "0.6.0" convert_case = "0.6.0"
csv = "1.1.6" csv = "1.1.6"
deserr = "0.1.4" deserr = "0.1.5"
either = { version = "1.6.1", features = ["serde"] } either = { version = "1.6.1", features = ["serde"] }
enum-iterator = "1.1.3" enum-iterator = "1.1.3"
file-store = { path = "../file-store" } file-store = { path = "../file-store" }
flate2 = "1.0.24" flate2 = "1.0.24"
fst = "0.4.7" fst = "0.4.7"
memmap2 = "0.5.7" memmap2 = "0.5.7"
milli = { git = "https://github.com/meilisearch/milli.git", tag = "v0.39.0", default-features = false } milli = { git = "https://github.com/meilisearch/milli.git", tag = "v0.39.1", 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"] } roaring = { version = "0.10.0", features = ["serde"] }
serde = { version = "1.0.145", features = ["derive"] } serde = { version = "1.0.145", features = ["derive"] }
serde-cs = "0.2.4"
serde_json = "1.0.85" serde_json = "1.0.85"
tar = "0.4.38" tar = "0.4.38"
tempfile = "3.3.0" tempfile = "3.3.0"
@ -32,8 +31,6 @@ uuid = { version = "1.1.2", features = ["serde", "v4"] }
[dev-dependencies] [dev-dependencies]
insta = "1.19.1" insta = "1.19.1"
meili-snap = { path = "../meili-snap" } meili-snap = { path = "../meili-snap" }
proptest = "1.0.0"
proptest-derive = "0.3.0"
[features] [features]
# all specialized tokenizations # all specialized tokenizations
@ -47,4 +44,3 @@ hebrew = ["milli/hebrew"]
japanese = ["milli/japanese"] japanese = ["milli/japanese"]
# thai specialized tokenization # thai specialized tokenization
thai = ["milli/thai"] thai = ["milli/thai"]
test-traits = ["proptest", "proptest-derive"]

View File

@ -0,0 +1,315 @@
/*!
This module implements the error messages of deserialization errors.
We try to:
1. Give a human-readable description of where the error originated.
2. Use the correct terms depending on the format of the request (json/query param)
3. Categorise the type of the error (e.g. missing field, wrong value type, unexpected error, etc.)
*/
use deserr::{ErrorKind, IntoValue, ValueKind, ValuePointerRef};
use super::{DeserrJsonError, DeserrQueryParamError};
use crate::error::ErrorCode;
/// Return a description of the given location in a Json, preceded by the given article.
/// e.g. `at .key1[8].key2`. If the location is the origin, the given article will not be
/// included in the description.
pub fn location_json_description(location: ValuePointerRef, article: &str) -> String {
fn rec(location: ValuePointerRef) -> String {
match location {
ValuePointerRef::Origin => String::new(),
ValuePointerRef::Key { key, prev } => rec(*prev) + "." + key,
ValuePointerRef::Index { index, prev } => format!("{}[{index}]", rec(*prev)),
}
}
match location {
ValuePointerRef::Origin => String::new(),
_ => {
format!("{article} `{}`", rec(location))
}
}
}
/// Return a description of the list of value kinds for a Json payload.
fn value_kinds_description_json(kinds: &[ValueKind]) -> String {
// Rank each value kind so that they can be sorted (and deduplicated)
// Having a predictable order helps with pattern matching
fn order(kind: &ValueKind) -> u8 {
match kind {
ValueKind::Null => 0,
ValueKind::Boolean => 1,
ValueKind::Integer => 2,
ValueKind::NegativeInteger => 3,
ValueKind::Float => 4,
ValueKind::String => 5,
ValueKind::Sequence => 6,
ValueKind::Map => 7,
}
}
// Return a description of a single value kind, preceded by an article
fn single_description(kind: &ValueKind) -> &'static str {
match kind {
ValueKind::Null => "null",
ValueKind::Boolean => "a boolean",
ValueKind::Integer => "a positive integer",
ValueKind::NegativeInteger => "an integer",
ValueKind::Float => "a number",
ValueKind::String => "a string",
ValueKind::Sequence => "an array",
ValueKind::Map => "an object",
}
}
fn description_rec(kinds: &[ValueKind], count_items: &mut usize, message: &mut String) {
let (msg_part, rest): (_, &[ValueKind]) = match kinds {
[] => (String::new(), &[]),
[ValueKind::Integer | ValueKind::NegativeInteger, ValueKind::Float, rest @ ..] => {
("a number".to_owned(), rest)
}
[ValueKind::Integer, ValueKind::NegativeInteger, ValueKind::Float, rest @ ..] => {
("a number".to_owned(), rest)
}
[ValueKind::Integer, ValueKind::NegativeInteger, rest @ ..] => {
("an integer".to_owned(), rest)
}
[a] => (single_description(a).to_owned(), &[]),
[a, rest @ ..] => (single_description(a).to_owned(), rest),
};
if rest.is_empty() {
if *count_items == 0 {
message.push_str(&msg_part);
} else if *count_items == 1 {
message.push_str(&format!(" or {msg_part}"));
} else {
message.push_str(&format!(", or {msg_part}"));
}
} else {
if *count_items == 0 {
message.push_str(&msg_part);
} else {
message.push_str(&format!(", {msg_part}"));
}
*count_items += 1;
description_rec(rest, count_items, message);
}
}
let mut kinds = kinds.to_owned();
kinds.sort_by_key(order);
kinds.dedup();
if kinds.is_empty() {
// Should not happen ideally
"a different value".to_owned()
} else {
let mut message = String::new();
description_rec(kinds.as_slice(), &mut 0, &mut message);
message
}
}
/// Return the JSON string of the value preceded by a description of its kind
fn value_description_with_kind_json(v: &serde_json::Value) -> String {
match v.kind() {
ValueKind::Null => "null".to_owned(),
kind => {
format!(
"{}: `{}`",
value_kinds_description_json(&[kind]),
serde_json::to_string(v).unwrap()
)
}
}
}
impl<C: Default + ErrorCode> deserr::DeserializeError for DeserrJsonError<C> {
fn error<V: IntoValue>(
_self_: Option<Self>,
error: deserr::ErrorKind<V>,
location: ValuePointerRef,
) -> Result<Self, Self> {
let mut message = String::new();
message.push_str(&match error {
ErrorKind::IncorrectValueKind { actual, accepted } => {
let expected = value_kinds_description_json(accepted);
let received = value_description_with_kind_json(&serde_json::Value::from(actual));
let location = location_json_description(location, " at");
format!("Invalid value type{location}: expected {expected}, but found {received}")
}
ErrorKind::MissingField { field } => {
let location = location_json_description(location, " inside");
format!("Missing field `{field}`{location}")
}
ErrorKind::UnknownKey { key, accepted } => {
let location = location_json_description(location, " inside");
format!(
"Unknown field `{}`{location}: expected one of {}",
key,
accepted
.iter()
.map(|accepted| format!("`{}`", accepted))
.collect::<Vec<String>>()
.join(", ")
)
}
ErrorKind::UnknownValue { value, accepted } => {
let location = location_json_description(location, " at");
format!(
"Unknown value `{}`{location}: expected one of {}",
value,
accepted
.iter()
.map(|accepted| format!("`{}`", accepted))
.collect::<Vec<String>>()
.join(", "),
)
}
ErrorKind::Unexpected { msg } => {
let location = location_json_description(location, " at");
format!("Invalid value{location}: {msg}")
}
});
Err(DeserrJsonError::new(message, C::default().error_code()))
}
}
/// Return a description of the given location in query parameters, preceded by the
/// given article. e.g. `at key5[2]`. If the location is the origin, the given article
/// will not be included in the description.
pub fn location_query_param_description(location: ValuePointerRef, article: &str) -> String {
fn rec(location: ValuePointerRef) -> String {
match location {
ValuePointerRef::Origin => String::new(),
ValuePointerRef::Key { key, prev } => {
if matches!(prev, ValuePointerRef::Origin) {
key.to_owned()
} else {
rec(*prev) + "." + key
}
}
ValuePointerRef::Index { index, prev } => format!("{}[{index}]", rec(*prev)),
}
}
match location {
ValuePointerRef::Origin => String::new(),
_ => {
format!("{article} `{}`", rec(location))
}
}
}
impl<C: Default + ErrorCode> deserr::DeserializeError for DeserrQueryParamError<C> {
fn error<V: IntoValue>(
_self_: Option<Self>,
error: deserr::ErrorKind<V>,
location: ValuePointerRef,
) -> Result<Self, Self> {
let mut message = String::new();
message.push_str(&match error {
ErrorKind::IncorrectValueKind { actual, accepted } => {
let expected = value_kinds_description_query_param(accepted);
let received = value_description_with_kind_query_param(actual);
let location = location_query_param_description(location, " for parameter");
format!("Invalid value type{location}: expected {expected}, but found {received}")
}
ErrorKind::MissingField { field } => {
let location = location_query_param_description(location, " inside");
format!("Missing parameter `{field}`{location}")
}
ErrorKind::UnknownKey { key, accepted } => {
let location = location_query_param_description(location, " inside");
format!(
"Unknown parameter `{}`{location}: expected one of {}",
key,
accepted
.iter()
.map(|accepted| format!("`{}`", accepted))
.collect::<Vec<String>>()
.join(", ")
)
}
ErrorKind::UnknownValue { value, accepted } => {
let location = location_query_param_description(location, " for parameter");
format!(
"Unknown value `{}`{location}: expected one of {}",
value,
accepted
.iter()
.map(|accepted| format!("`{}`", accepted))
.collect::<Vec<String>>()
.join(", "),
)
}
ErrorKind::Unexpected { msg } => {
let location = location_query_param_description(location, " in parameter");
format!("Invalid value{location}: {msg}")
}
});
Err(DeserrQueryParamError::new(message, C::default().error_code()))
}
}
/// Return a description of the list of value kinds for query parameters
/// Since query parameters are always treated as strings, we always return
/// "a string" for now.
fn value_kinds_description_query_param(_accepted: &[ValueKind]) -> String {
"a string".to_owned()
}
fn value_description_with_kind_query_param<V: IntoValue>(actual: deserr::Value<V>) -> String {
match actual {
deserr::Value::Null => "null".to_owned(),
deserr::Value::Boolean(x) => format!("a boolean: `{x}`"),
deserr::Value::Integer(x) => format!("an integer: `{x}`"),
deserr::Value::NegativeInteger(x) => {
format!("an integer: `{x}`")
}
deserr::Value::Float(x) => {
format!("a number: `{x}`")
}
deserr::Value::String(x) => {
format!("a string: `{x}`")
}
deserr::Value::Sequence(_) => "multiple values".to_owned(),
deserr::Value::Map(_) => "multiple parameters".to_owned(),
}
}
#[cfg(test)]
mod tests {
use deserr::ValueKind;
use crate::deserr::error_messages::value_kinds_description_json;
#[test]
fn test_value_kinds_description_json() {
insta::assert_display_snapshot!(value_kinds_description_json(&[]), @"a different value");
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Boolean]), @"a boolean");
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer]), @"a positive integer");
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::NegativeInteger]), @"an integer");
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer]), @"a positive integer");
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::String]), @"a string");
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Sequence]), @"an array");
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Map]), @"an object");
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer, ValueKind::Boolean]), @"a boolean or a positive integer");
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Null, ValueKind::Integer]), @"null or a positive integer");
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Sequence, ValueKind::NegativeInteger]), @"an integer or an array");
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer, ValueKind::Float]), @"a number");
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer, ValueKind::Float, ValueKind::NegativeInteger]), @"a number");
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer, ValueKind::Float, ValueKind::NegativeInteger, ValueKind::Null]), @"null or a number");
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Boolean, ValueKind::Integer, ValueKind::Float, ValueKind::NegativeInteger, ValueKind::Null]), @"null, a boolean, or a number");
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Null, ValueKind::Boolean, ValueKind::Integer, ValueKind::Float, ValueKind::NegativeInteger, ValueKind::Null]), @"null, a boolean, or a number");
}
}

View File

@ -0,0 +1,134 @@
use std::convert::Infallible;
use std::fmt;
use std::marker::PhantomData;
use deserr::{DeserializeError, MergeWithError, ValuePointerRef};
use crate::error::deserr_codes::{self, *};
use crate::error::{
unwrap_any, Code, DeserrParseBoolError, DeserrParseIntError, ErrorCode, InvalidTaskDateError,
ParseOffsetDateTimeError,
};
use crate::index_uid::IndexUidFormatError;
use crate::tasks::{ParseTaskKindError, ParseTaskStatusError};
pub mod error_messages;
pub mod query_params;
/// Marker type for the Json format
pub struct DeserrJson;
/// Marker type for the Query Parameter format
pub struct DeserrQueryParam;
pub type DeserrJsonError<C = deserr_codes::BadRequest> = DeserrError<DeserrJson, C>;
pub type DeserrQueryParamError<C = deserr_codes::BadRequest> = DeserrError<DeserrQueryParam, C>;
/// A request deserialization error.
///
/// The first generic paramater is a marker type describing the format of the request: either json (e.g. [`DeserrJson`] or [`DeserrQueryParam`]).
/// The second generic parameter is the default error code for the deserialization error, in case it is not given.
pub struct DeserrError<Format, C: Default + ErrorCode> {
pub msg: String,
pub code: Code,
_phantom: PhantomData<(Format, C)>,
}
impl<Format, C: Default + ErrorCode> DeserrError<Format, C> {
pub fn new(msg: String, code: Code) -> Self {
Self { msg, code, _phantom: PhantomData }
}
}
impl<Format, C: Default + ErrorCode> std::fmt::Debug for DeserrError<Format, C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DeserrError").field("msg", &self.msg).field("code", &self.code).finish()
}
}
impl<Format, C: Default + ErrorCode> std::fmt::Display for DeserrError<Format, C> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.msg)
}
}
impl<Format, C: Default + ErrorCode> std::error::Error for DeserrError<Format, C> {}
impl<Format, C: Default + ErrorCode> ErrorCode for DeserrError<Format, C> {
fn error_code(&self) -> Code {
self.code
}
}
// For now, we don't accumulate errors. Only one deserialisation error is ever returned at a time.
impl<Format, C1: Default + ErrorCode, C2: Default + ErrorCode>
MergeWithError<DeserrError<Format, C2>> for DeserrError<Format, C1>
{
fn merge(
_self_: Option<Self>,
other: DeserrError<Format, C2>,
_merge_location: ValuePointerRef,
) -> Result<Self, Self> {
Err(DeserrError { msg: other.msg, code: other.code, _phantom: PhantomData })
}
}
impl<Format, C: Default + ErrorCode> MergeWithError<Infallible> for DeserrError<Format, C> {
fn merge(
_self_: Option<Self>,
_other: Infallible,
_merge_location: ValuePointerRef,
) -> Result<Self, Self> {
unreachable!()
}
}
// Implement a convenience function to build a `missing_field` error
macro_rules! make_missing_field_convenience_builder {
($err_code:ident, $fn_name:ident) => {
impl DeserrJsonError<$err_code> {
pub fn $fn_name(field: &str, location: ValuePointerRef) -> Self {
let x = unwrap_any(Self::error::<Infallible>(
None,
deserr::ErrorKind::MissingField { field },
location,
));
Self { msg: x.msg, code: $err_code.error_code(), _phantom: PhantomData }
}
}
};
}
make_missing_field_convenience_builder!(MissingIndexUid, missing_index_uid);
make_missing_field_convenience_builder!(MissingApiKeyActions, missing_api_key_actions);
make_missing_field_convenience_builder!(MissingApiKeyExpiresAt, missing_api_key_expires_at);
make_missing_field_convenience_builder!(MissingApiKeyIndexes, missing_api_key_indexes);
make_missing_field_convenience_builder!(MissingSwapIndexes, missing_swap_indexes);
// Integrate a sub-error into a [`DeserrError`] by taking its error message but using
// the default error code (C) from `Self`
macro_rules! merge_with_error_impl_take_error_message {
($err_type:ty) => {
impl<Format, C: Default + ErrorCode> MergeWithError<$err_type> for DeserrError<Format, C>
where
DeserrError<Format, C>: deserr::DeserializeError,
{
fn merge(
_self_: Option<Self>,
other: $err_type,
merge_location: ValuePointerRef,
) -> Result<Self, Self> {
DeserrError::<Format, C>::error::<Infallible>(
None,
deserr::ErrorKind::Unexpected { msg: other.to_string() },
merge_location,
)
}
}
};
}
// All these errors can be merged into a `DeserrError`
merge_with_error_impl_take_error_message!(DeserrParseIntError);
merge_with_error_impl_take_error_message!(DeserrParseBoolError);
merge_with_error_impl_take_error_message!(uuid::Error);
merge_with_error_impl_take_error_message!(InvalidTaskDateError);
merge_with_error_impl_take_error_message!(ParseOffsetDateTimeError);
merge_with_error_impl_take_error_message!(ParseTaskKindError);
merge_with_error_impl_take_error_message!(ParseTaskStatusError);
merge_with_error_impl_take_error_message!(IndexUidFormatError);

View File

@ -0,0 +1,115 @@
/*!
This module provides helper traits, types, and functions to deserialize query parameters.
The source of the problem is that query parameters only give us a string to work with.
This means `deserr` is never given a sequence or numbers, and thus the default deserialization
code for common types such as `usize` or `Vec<T>` does not work. To work around it, we create a
wrapper type called `Param<T>`, which is deserialised using the `from_query_param` method of the trait
`FromQueryParameter`.
We also use other helper types such as `CS` (i.e. comma-separated) from `serde_cs` as well as
`StarOr`, `OptionStarOr`, and `OptionStarOrList`.
*/
use std::convert::Infallible;
use std::ops::Deref;
use std::str::FromStr;
use deserr::{DeserializeError, DeserializeFromValue, MergeWithError, ValueKind};
use super::{DeserrParseBoolError, DeserrParseIntError};
use crate::error::unwrap_any;
use crate::index_uid::IndexUid;
use crate::tasks::{Kind, Status};
/// A wrapper type indicating that the inner value should be
/// deserialised from a query parameter string.
///
/// Note that if the field is optional, it is better to use
/// `Option<Param<T>>` instead of `Param<Option<T>>`.
#[derive(Default, Debug, Clone, Copy)]
pub struct Param<T>(pub T);
impl<T> Deref for Param<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T, E> DeserializeFromValue<E> for Param<T>
where
E: DeserializeError + MergeWithError<T::Err>,
T: FromQueryParameter,
{
fn deserialize_from_value<V: deserr::IntoValue>(
value: deserr::Value<V>,
location: deserr::ValuePointerRef,
) -> Result<Self, E> {
match value {
deserr::Value::String(s) => match T::from_query_param(&s) {
Ok(x) => Ok(Param(x)),
Err(e) => Err(unwrap_any(E::merge(None, e, location))),
},
_ => Err(unwrap_any(E::error(
None,
deserr::ErrorKind::IncorrectValueKind {
actual: value,
accepted: &[ValueKind::String],
},
location,
))),
}
}
}
/// Parse a value from a query parameter string.
///
/// This trait is functionally equivalent to `FromStr`.
/// Having a separate trait trait allows us to return better
/// deserializatio error messages.
pub trait FromQueryParameter: Sized {
type Err;
fn from_query_param(p: &str) -> Result<Self, Self::Err>;
}
/// Implement `FromQueryParameter` for the given type using its `FromStr`
/// trait implementation.
macro_rules! impl_from_query_param_from_str {
($type:ty) => {
impl FromQueryParameter for $type {
type Err = <$type as FromStr>::Err;
fn from_query_param(p: &str) -> Result<Self, Self::Err> {
p.parse()
}
}
};
}
impl_from_query_param_from_str!(Kind);
impl_from_query_param_from_str!(Status);
impl_from_query_param_from_str!(IndexUid);
/// Implement `FromQueryParameter` for the given type using its `FromStr`
/// trait implementation, replacing the returned error with a struct
/// that wraps the original query parameter.
macro_rules! impl_from_query_param_wrap_original_value_in_error {
($type:ty, $err_type:path) => {
impl FromQueryParameter for $type {
type Err = $err_type;
fn from_query_param(p: &str) -> Result<Self, Self::Err> {
p.parse().map_err(|_| $err_type(p.to_owned()))
}
}
};
}
impl_from_query_param_wrap_original_value_in_error!(usize, DeserrParseIntError);
impl_from_query_param_wrap_original_value_in_error!(u32, DeserrParseIntError);
impl_from_query_param_wrap_original_value_in_error!(bool, DeserrParseBoolError);
impl FromQueryParameter for String {
type Err = Infallible;
fn from_query_param(p: &str) -> Result<Self, Infallible> {
Ok(p.to_owned())
}
}

View File

@ -1,23 +1,16 @@
use std::convert::Infallible;
use std::marker::PhantomData;
use std::{fmt, io}; use std::{fmt, io};
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
use actix_web::{self as aweb, HttpResponseBuilder}; use actix_web::{self as aweb, HttpResponseBuilder};
use aweb::rt::task::JoinError; use aweb::rt::task::JoinError;
use convert_case::Casing; use convert_case::Casing;
use deserr::{DeserializeError, IntoValue, MergeWithError, ValuePointerRef};
use milli::heed::{Error as HeedError, MdbError}; use milli::heed::{Error as HeedError, MdbError};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use self::deserr_codes::MissingIndexUid;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "test-traits", derive(proptest_derive::Arbitrary))]
pub struct ResponseError { pub struct ResponseError {
#[serde(skip)] #[serde(skip)]
#[cfg_attr(feature = "test-traits", proptest(strategy = "strategy::status_code_strategy()"))]
code: StatusCode, code: StatusCode,
message: String, message: String,
#[serde(rename = "code")] #[serde(rename = "code")]
@ -36,7 +29,7 @@ impl ResponseError {
Self { Self {
code: code.http(), code: code.http(),
message, message,
error_code: code.err_code().error_name, error_code: code.name(),
error_type: code.type_(), error_type: code.type_(),
error_link: code.url(), error_link: code.url(),
} }
@ -97,9 +90,9 @@ pub trait ErrorCode {
#[allow(clippy::enum_variant_names)] #[allow(clippy::enum_variant_names)]
enum ErrorType { enum ErrorType {
InternalError, Internal,
InvalidRequestError, InvalidRequest,
AuthenticationError, Auth,
System, System,
} }
@ -108,14 +101,24 @@ impl fmt::Display for ErrorType {
use ErrorType::*; use ErrorType::*;
match self { match self {
InternalError => write!(f, "internal"), Internal => write!(f, "internal"),
InvalidRequestError => write!(f, "invalid_request"), InvalidRequest => write!(f, "invalid_request"),
AuthenticationError => write!(f, "auth"), Auth => write!(f, "auth"),
System => write!(f, "system"), System => write!(f, "system"),
} }
} }
} }
/// Implement all the error codes.
///
/// 1. Make an enum `Code` where each error code is a variant
/// 2. Implement the `http`, `name`, and `type_` method on the enum
/// 3. Make a unit type for each error code in the module `deserr_codes`.
///
/// The unit type's purpose is to be used as a marker type parameter, e.g.
/// `DeserrJsonError<MyErrorCode>`. It implements `Default` and `ErrorCode`,
/// so we can get a value of the `Code` enum with the correct variant by calling
/// `MyErrorCode::default().error_code()`.
macro_rules! make_error_codes { macro_rules! make_error_codes {
($($code_ident:ident, $err_type:ident, $status:ident);*) => { ($($code_ident:ident, $err_type:ident, $status:ident);*) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -123,29 +126,31 @@ macro_rules! make_error_codes {
$($code_ident),* $($code_ident),*
} }
impl Code { impl Code {
/// associate a `Code` variant to the actual ErrCode
fn err_code(&self) -> ErrCode {
match self {
$(
Code::$code_ident => {
ErrCode::$err_type( stringify!($code_ident).to_case(convert_case::Case::Snake), StatusCode::$status)
}
)*
}
}
/// return the HTTP status code associated with the `Code` /// return the HTTP status code associated with the `Code`
fn http(&self) -> StatusCode { fn http(&self) -> StatusCode {
self.err_code().status_code match self {
$(
Code::$code_ident => StatusCode::$status
),*
}
} }
/// return error name, used as error code /// return error name, used as error code
fn name(&self) -> String { fn name(&self) -> String {
self.err_code().error_name.to_string() match self {
$(
Code::$code_ident => stringify!($code_ident).to_case(convert_case::Case::Snake)
),*
}
} }
/// return the error type /// return the error type
fn type_(&self) -> String { fn type_(&self) -> String {
self.err_code().error_type.to_string() match self {
$(
Code::$code_ident => ErrorType::$err_type.to_string()
),*
}
} }
/// return the doc url associated with the error /// return the doc url associated with the error
@ -170,146 +175,121 @@ macro_rules! make_error_codes {
} }
} }
} }
// An exhaustive list of all the error codes used by meilisearch.
make_error_codes! { make_error_codes! {
ApiKeyAlreadyExists , invalid , CONFLICT ; ApiKeyAlreadyExists , InvalidRequest , CONFLICT ;
ApiKeyNotFound , invalid , NOT_FOUND ; ApiKeyNotFound , InvalidRequest , NOT_FOUND ;
BadParameter , invalid , BAD_REQUEST; BadParameter , InvalidRequest , BAD_REQUEST;
BadRequest , invalid , BAD_REQUEST; BadRequest , InvalidRequest , BAD_REQUEST;
DatabaseSizeLimitReached , internal , INTERNAL_SERVER_ERROR; DatabaseSizeLimitReached , Internal , INTERNAL_SERVER_ERROR;
DocumentNotFound , invalid , NOT_FOUND; DocumentNotFound , InvalidRequest , NOT_FOUND;
DumpAlreadyProcessing , invalid , CONFLICT; DumpAlreadyProcessing , InvalidRequest , CONFLICT;
DumpNotFound , invalid , NOT_FOUND; DumpNotFound , InvalidRequest , NOT_FOUND;
DumpProcessFailed , internal , INTERNAL_SERVER_ERROR; DumpProcessFailed , Internal , INTERNAL_SERVER_ERROR;
DuplicateIndexFound , invalid , BAD_REQUEST; DuplicateIndexFound , InvalidRequest , BAD_REQUEST;
ImmutableApiKeyActions , InvalidRequest , BAD_REQUEST;
ImmutableApiKeyUid , invalid , BAD_REQUEST; ImmutableApiKeyCreatedAt , InvalidRequest , BAD_REQUEST;
ImmutableApiKeyKey , invalid , BAD_REQUEST; ImmutableApiKeyExpiresAt , InvalidRequest , BAD_REQUEST;
ImmutableApiKeyActions , invalid , BAD_REQUEST; ImmutableApiKeyIndexes , InvalidRequest , BAD_REQUEST;
ImmutableApiKeyIndexes , invalid , BAD_REQUEST; ImmutableApiKeyKey , InvalidRequest , BAD_REQUEST;
ImmutableApiKeyExpiresAt , invalid , BAD_REQUEST; ImmutableApiKeyUid , InvalidRequest , BAD_REQUEST;
ImmutableApiKeyCreatedAt , invalid , BAD_REQUEST; ImmutableApiKeyUpdatedAt , InvalidRequest , BAD_REQUEST;
ImmutableApiKeyUpdatedAt , invalid , BAD_REQUEST; ImmutableIndexCreatedAt , InvalidRequest , BAD_REQUEST;
ImmutableIndexUid , InvalidRequest , BAD_REQUEST;
ImmutableIndexUid , invalid , BAD_REQUEST; ImmutableIndexUpdatedAt , InvalidRequest , BAD_REQUEST;
ImmutableIndexCreatedAt , invalid , BAD_REQUEST; IndexAlreadyExists , InvalidRequest , CONFLICT ;
ImmutableIndexUpdatedAt , invalid , BAD_REQUEST; IndexCreationFailed , Internal , INTERNAL_SERVER_ERROR;
IndexNotFound , InvalidRequest , NOT_FOUND;
IndexAlreadyExists , invalid , CONFLICT ; IndexPrimaryKeyAlreadyExists , InvalidRequest , BAD_REQUEST ;
IndexCreationFailed , internal , INTERNAL_SERVER_ERROR; IndexPrimaryKeyMultipleCandidatesFound, InvalidRequest , BAD_REQUEST;
IndexNotFound , invalid , NOT_FOUND; IndexPrimaryKeyNoCandidateFound , InvalidRequest , BAD_REQUEST ;
IndexPrimaryKeyAlreadyExists , invalid , BAD_REQUEST ; Internal , Internal , INTERNAL_SERVER_ERROR ;
IndexPrimaryKeyNoCandidateFound , invalid , BAD_REQUEST ; InvalidApiKey , Auth , FORBIDDEN ;
IndexPrimaryKeyMultipleCandidatesFound, invalid , BAD_REQUEST; InvalidApiKeyActions , InvalidRequest , BAD_REQUEST ;
Internal , internal , INTERNAL_SERVER_ERROR ; InvalidApiKeyDescription , InvalidRequest , BAD_REQUEST ;
InvalidApiKeyActions , invalid , BAD_REQUEST ; InvalidApiKeyExpiresAt , InvalidRequest , BAD_REQUEST ;
InvalidApiKeyDescription , invalid , BAD_REQUEST ; InvalidApiKeyIndexes , InvalidRequest , BAD_REQUEST ;
InvalidApiKeyExpiresAt , invalid , BAD_REQUEST ; InvalidApiKeyLimit , InvalidRequest , BAD_REQUEST ;
InvalidApiKeyIndexes , invalid , BAD_REQUEST ; InvalidApiKeyName , InvalidRequest , BAD_REQUEST ;
InvalidApiKeyLimit , invalid , BAD_REQUEST ; InvalidApiKeyOffset , InvalidRequest , BAD_REQUEST ;
InvalidApiKeyName , invalid , BAD_REQUEST ; InvalidApiKeyUid , InvalidRequest , BAD_REQUEST ;
InvalidApiKeyOffset , invalid , BAD_REQUEST ; InvalidContentType , InvalidRequest , UNSUPPORTED_MEDIA_TYPE ;
InvalidApiKeyUid , invalid , BAD_REQUEST ; InvalidDocumentFields , InvalidRequest , BAD_REQUEST ;
InvalidApiKey , authentication, FORBIDDEN ; InvalidDocumentGeoField , InvalidRequest , BAD_REQUEST ;
InvalidContentType , invalid , UNSUPPORTED_MEDIA_TYPE ; InvalidDocumentId , InvalidRequest , BAD_REQUEST ;
InvalidDocumentFields , invalid , BAD_REQUEST ; InvalidDocumentLimit , InvalidRequest , BAD_REQUEST ;
InvalidDocumentGeoField , invalid , BAD_REQUEST ; InvalidDocumentOffset , InvalidRequest , BAD_REQUEST ;
InvalidDocumentId , invalid , BAD_REQUEST ; InvalidIndexLimit , InvalidRequest , BAD_REQUEST ;
InvalidDocumentLimit , invalid , BAD_REQUEST ; InvalidIndexOffset , InvalidRequest , BAD_REQUEST ;
InvalidDocumentOffset , invalid , BAD_REQUEST ; InvalidIndexPrimaryKey , InvalidRequest , BAD_REQUEST ;
InvalidIndexLimit , invalid , BAD_REQUEST ; InvalidIndexUid , InvalidRequest , BAD_REQUEST ;
InvalidIndexOffset , invalid , BAD_REQUEST ; InvalidMinWordLengthForTypo , InvalidRequest , BAD_REQUEST ;
InvalidIndexPrimaryKey , invalid , BAD_REQUEST ; InvalidSearchAttributesToCrop , InvalidRequest , BAD_REQUEST ;
InvalidIndexUid , invalid , BAD_REQUEST ; InvalidSearchAttributesToHighlight , InvalidRequest , BAD_REQUEST ;
InvalidMinWordLengthForTypo , invalid , BAD_REQUEST ; InvalidSearchAttributesToRetrieve , InvalidRequest , BAD_REQUEST ;
InvalidSearchAttributesToCrop , invalid , BAD_REQUEST ; InvalidSearchCropLength , InvalidRequest , BAD_REQUEST ;
InvalidSearchAttributesToHighlight , invalid , BAD_REQUEST ; InvalidSearchCropMarker , InvalidRequest , BAD_REQUEST ;
InvalidSearchAttributesToRetrieve , invalid , BAD_REQUEST ; InvalidSearchFacets , InvalidRequest , BAD_REQUEST ;
InvalidSearchCropLength , invalid , BAD_REQUEST ; InvalidSearchFilter , InvalidRequest , BAD_REQUEST ;
InvalidSearchCropMarker , invalid , BAD_REQUEST ; InvalidSearchHighlightPostTag , InvalidRequest , BAD_REQUEST ;
InvalidSearchFacets , invalid , BAD_REQUEST ; InvalidSearchHighlightPreTag , InvalidRequest , BAD_REQUEST ;
InvalidSearchFilter , invalid , BAD_REQUEST ; InvalidSearchHitsPerPage , InvalidRequest , BAD_REQUEST ;
InvalidSearchHighlightPostTag , invalid , BAD_REQUEST ; InvalidSearchLimit , InvalidRequest , BAD_REQUEST ;
InvalidSearchHighlightPreTag , invalid , BAD_REQUEST ; InvalidSearchMatchingStrategy , InvalidRequest , BAD_REQUEST ;
InvalidSearchHitsPerPage , invalid , BAD_REQUEST ; InvalidSearchOffset , InvalidRequest , BAD_REQUEST ;
InvalidSearchLimit , invalid , BAD_REQUEST ; InvalidSearchPage , InvalidRequest , BAD_REQUEST ;
InvalidSearchMatchingStrategy , invalid , BAD_REQUEST ; InvalidSearchQ , InvalidRequest , BAD_REQUEST ;
InvalidSearchOffset , invalid , BAD_REQUEST ; InvalidSearchShowMatchesPosition , InvalidRequest , BAD_REQUEST ;
InvalidSearchPage , invalid , BAD_REQUEST ; InvalidSearchSort , InvalidRequest , BAD_REQUEST ;
InvalidSearchQ , invalid , BAD_REQUEST ; InvalidSettingsDisplayedAttributes , InvalidRequest , BAD_REQUEST ;
InvalidSearchShowMatchesPosition , invalid , BAD_REQUEST ; InvalidSettingsDistinctAttribute , InvalidRequest , BAD_REQUEST ;
InvalidSearchSort , invalid , BAD_REQUEST ; InvalidSettingsFaceting , InvalidRequest , BAD_REQUEST ;
InvalidSettingsDisplayedAttributes , invalid , BAD_REQUEST ; InvalidSettingsFilterableAttributes , InvalidRequest , BAD_REQUEST ;
InvalidSettingsDistinctAttribute , invalid , BAD_REQUEST ; InvalidSettingsPagination , InvalidRequest , BAD_REQUEST ;
InvalidSettingsFaceting , invalid , BAD_REQUEST ; InvalidSettingsRankingRules , InvalidRequest , BAD_REQUEST ;
InvalidSettingsFilterableAttributes , invalid , BAD_REQUEST ; InvalidSettingsSearchableAttributes , InvalidRequest , BAD_REQUEST ;
InvalidSettingsPagination , invalid , BAD_REQUEST ; InvalidSettingsSortableAttributes , InvalidRequest , BAD_REQUEST ;
InvalidSettingsRankingRules , invalid , BAD_REQUEST ; InvalidSettingsStopWords , InvalidRequest , BAD_REQUEST ;
InvalidSettingsSearchableAttributes , invalid , BAD_REQUEST ; InvalidSettingsSynonyms , InvalidRequest , BAD_REQUEST ;
InvalidSettingsSortableAttributes , invalid , BAD_REQUEST ; InvalidSettingsTypoTolerance , InvalidRequest , BAD_REQUEST ;
InvalidSettingsStopWords , invalid , BAD_REQUEST ; InvalidState , Internal , INTERNAL_SERVER_ERROR ;
InvalidSettingsSynonyms , invalid , BAD_REQUEST ; InvalidStoreFile , Internal , INTERNAL_SERVER_ERROR ;
InvalidSettingsTypoTolerance , invalid , BAD_REQUEST ; InvalidSwapDuplicateIndexFound , InvalidRequest , BAD_REQUEST ;
InvalidState , internal , INTERNAL_SERVER_ERROR ; InvalidSwapIndexes , InvalidRequest , BAD_REQUEST ;
InvalidStoreFile , internal , INTERNAL_SERVER_ERROR ; InvalidTaskAfterEnqueuedAt , InvalidRequest , BAD_REQUEST ;
InvalidSwapDuplicateIndexFound , invalid , BAD_REQUEST ; InvalidTaskAfterFinishedAt , InvalidRequest , BAD_REQUEST ;
InvalidSwapIndexes , invalid , BAD_REQUEST ; InvalidTaskAfterStartedAt , InvalidRequest , BAD_REQUEST ;
InvalidTaskAfterEnqueuedAt , invalid , BAD_REQUEST ; InvalidTaskBeforeEnqueuedAt , InvalidRequest , BAD_REQUEST ;
InvalidTaskAfterFinishedAt , invalid , BAD_REQUEST ; InvalidTaskBeforeFinishedAt , InvalidRequest , BAD_REQUEST ;
InvalidTaskAfterStartedAt , invalid , BAD_REQUEST ; InvalidTaskBeforeStartedAt , InvalidRequest , BAD_REQUEST ;
InvalidTaskBeforeEnqueuedAt , invalid , BAD_REQUEST ; InvalidTaskCanceledBy , InvalidRequest , BAD_REQUEST ;
InvalidTaskBeforeFinishedAt , invalid , BAD_REQUEST ; InvalidTaskFrom , InvalidRequest , BAD_REQUEST ;
InvalidTaskBeforeStartedAt , invalid , BAD_REQUEST ; InvalidTaskLimit , InvalidRequest , BAD_REQUEST ;
InvalidTaskCanceledBy , invalid , BAD_REQUEST ; InvalidTaskStatuses , InvalidRequest , BAD_REQUEST ;
InvalidTaskFrom , invalid , BAD_REQUEST ; InvalidTaskTypes , InvalidRequest , BAD_REQUEST ;
InvalidTaskLimit , invalid , BAD_REQUEST ; InvalidTaskUids , InvalidRequest , BAD_REQUEST ;
InvalidTaskStatuses , invalid , BAD_REQUEST ; IoError , System , UNPROCESSABLE_ENTITY;
InvalidTaskTypes , invalid , BAD_REQUEST ; MalformedPayload , InvalidRequest , BAD_REQUEST ;
InvalidTaskUids , invalid , BAD_REQUEST ; MaxFieldsLimitExceeded , InvalidRequest , BAD_REQUEST ;
IoError , system , UNPROCESSABLE_ENTITY; MissingApiKeyActions , InvalidRequest , BAD_REQUEST ;
MalformedPayload , invalid , BAD_REQUEST ; MissingApiKeyExpiresAt , InvalidRequest , BAD_REQUEST ;
MaxFieldsLimitExceeded , invalid , BAD_REQUEST ; MissingApiKeyIndexes , InvalidRequest , BAD_REQUEST ;
MissingApiKeyActions , invalid , BAD_REQUEST ; MissingAuthorizationHeader , Auth , UNAUTHORIZED ;
MissingApiKeyExpiresAt , invalid , BAD_REQUEST ; MissingContentType , InvalidRequest , UNSUPPORTED_MEDIA_TYPE ;
MissingApiKeyIndexes , invalid , BAD_REQUEST ; MissingDocumentId , InvalidRequest , BAD_REQUEST ;
MissingAuthorizationHeader , authentication, UNAUTHORIZED ; MissingIndexUid , InvalidRequest , BAD_REQUEST ;
MissingContentType , invalid , UNSUPPORTED_MEDIA_TYPE ; MissingMasterKey , Auth , UNAUTHORIZED ;
MissingDocumentId , invalid , BAD_REQUEST ; MissingPayload , InvalidRequest , BAD_REQUEST ;
MissingIndexUid , invalid , BAD_REQUEST ; MissingSwapIndexes , InvalidRequest , BAD_REQUEST ;
MissingMasterKey , authentication, UNAUTHORIZED ; MissingTaskFilters , InvalidRequest , BAD_REQUEST ;
MissingPayload , invalid , BAD_REQUEST ; NoSpaceLeftOnDevice , System , UNPROCESSABLE_ENTITY;
MissingTaskFilters , invalid , BAD_REQUEST ; PayloadTooLarge , InvalidRequest , PAYLOAD_TOO_LARGE ;
NoSpaceLeftOnDevice , system , UNPROCESSABLE_ENTITY; TaskNotFound , InvalidRequest , NOT_FOUND ;
PayloadTooLarge , invalid , PAYLOAD_TOO_LARGE ; TooManyOpenFiles , System , UNPROCESSABLE_ENTITY ;
TaskNotFound , invalid , NOT_FOUND ; UnretrievableDocument , Internal , BAD_REQUEST ;
TooManyOpenFiles , system , UNPROCESSABLE_ENTITY ; UnretrievableErrorCode , InvalidRequest , BAD_REQUEST ;
UnretrievableDocument , internal , BAD_REQUEST ; UnsupportedMediaType , InvalidRequest , UNSUPPORTED_MEDIA_TYPE
UnretrievableErrorCode , invalid , BAD_REQUEST ;
UnsupportedMediaType , invalid , UNSUPPORTED_MEDIA_TYPE
}
/// Internal structure providing a convenient way to create error codes
struct ErrCode {
status_code: StatusCode,
error_type: ErrorType,
error_name: String,
}
impl ErrCode {
fn authentication(error_name: String, status_code: StatusCode) -> ErrCode {
ErrCode { status_code, error_name, error_type: ErrorType::AuthenticationError }
}
fn internal(error_name: String, status_code: StatusCode) -> ErrCode {
ErrCode { status_code, error_name, error_type: ErrorType::InternalError }
}
fn invalid(error_name: String, status_code: StatusCode) -> ErrCode {
ErrCode { status_code, error_name, error_type: ErrorType::InvalidRequestError }
}
fn system(error_name: String, status_code: StatusCode) -> ErrCode {
ErrCode { status_code, error_name, error_type: ErrorType::System }
}
} }
impl ErrorCode for JoinError { impl ErrorCode for JoinError {
@ -404,6 +384,7 @@ impl ErrorCode for io::Error {
} }
} }
/// Unwrap a result, either its Ok or Err value.
pub fn unwrap_any<T>(any: Result<T, T>) -> T { pub fn unwrap_any<T>(any: Result<T, T>) -> T {
match any { match any {
Ok(any) => any, Ok(any) => any,
@ -411,90 +392,41 @@ pub fn unwrap_any<T>(any: Result<T, T>) -> T {
} }
} }
#[cfg(feature = "test-traits")] /// Deserialization when `deserr` cannot parse an API key date.
mod strategy { #[derive(Debug)]
use proptest::strategy::Strategy; pub struct ParseOffsetDateTimeError(pub String);
impl fmt::Display for ParseOffsetDateTimeError {
use super::*;
pub(super) fn status_code_strategy() -> impl Strategy<Value = StatusCode> {
(100..999u16).prop_map(|i| StatusCode::from_u16(i).unwrap())
}
}
pub struct DeserrError<C: ErrorCode = deserr_codes::BadRequest> {
pub msg: String,
pub code: Code,
_phantom: PhantomData<C>,
}
impl<C: ErrorCode> std::fmt::Debug for DeserrError<C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DeserrError").field("msg", &self.msg).field("code", &self.code).finish()
}
}
impl<C: ErrorCode> std::fmt::Display for DeserrError<C> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.msg) 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<C: ErrorCode> std::error::Error for DeserrError<C> {} /// Deserialization when `deserr` cannot parse a task date.
impl<C: ErrorCode> ErrorCode for DeserrError<C> { #[derive(Debug)]
fn error_code(&self) -> Code { pub struct InvalidTaskDateError(pub String);
self.code 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<C1: ErrorCode, C2: ErrorCode> MergeWithError<DeserrError<C2>> for DeserrError<C1> { /// Deserialization error when `deserr` cannot parse a String
fn merge( /// into a bool.
_self_: Option<Self>, #[derive(Debug)]
other: DeserrError<C2>, pub struct DeserrParseBoolError(pub String);
_merge_location: ValuePointerRef, impl fmt::Display for DeserrParseBoolError {
) -> Result<Self, Self> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Err(DeserrError { msg: other.msg, code: other.code, _phantom: PhantomData }) write!(f, "could not parse `{}` as a boolean, expected either `true` or `false`", self.0)
} }
} }
impl DeserrError<MissingIndexUid> { /// Deserialization error when `deserr` cannot parse a String
pub fn missing_index_uid(field: &str, location: ValuePointerRef) -> Self { /// into an integer.
let x = unwrap_any(Self::error::<Infallible>( #[derive(Debug)]
None, pub struct DeserrParseIntError(pub String);
deserr::ErrorKind::MissingField { field }, impl fmt::Display for DeserrParseIntError {
location, fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
)); write!(f, "could not parse `{}` as a positive integer", self.0)
Self { msg: x.msg, code: MissingIndexUid.error_code(), _phantom: PhantomData }
}
}
impl<C: Default + ErrorCode> deserr::DeserializeError for DeserrError<C> {
fn error<V: IntoValue>(
_self_: Option<Self>,
error: deserr::ErrorKind<V>,
location: ValuePointerRef,
) -> Result<Self, Self> {
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<T>(pub T);
impl<C: Default + ErrorCode, T> MergeWithError<TakeErrorMessage<T>> for DeserrError<C>
where
T: std::error::Error,
{
fn merge(
_self_: Option<Self>,
other: TakeErrorMessage<T>,
merge_location: ValuePointerRef,
) -> Result<Self, Self> {
DeserrError::error::<Infallible>(
None,
deserr::ErrorKind::Unexpected { msg: other.0.to_string() },
merge_location,
)
} }
} }

View File

@ -2,17 +2,15 @@ use std::error::Error;
use std::fmt; use std::fmt;
use std::str::FromStr; use std::str::FromStr;
use serde::{Deserialize, Serialize}; use deserr::DeserializeFromValue;
use crate::error::{Code, ErrorCode}; use crate::error::{Code, ErrorCode};
/// An index uid is composed of only ascii alphanumeric characters, - and _, between 1 and 400 /// An index uid is composed of only ascii alphanumeric characters, - and _, between 1 and 400
/// bytes long /// bytes long
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromValue)]
#[cfg_attr(feature = "test-traits", derive(proptest_derive::Arbitrary))] #[deserr(from(String) = IndexUid::try_from -> IndexUidFormatError)]
pub struct IndexUid( pub struct IndexUid(String);
#[cfg_attr(feature = "test-traits", proptest(regex("[a-zA-Z0-9_-]{1,400}")))] String,
);
impl IndexUid { impl IndexUid {
pub fn new_unchecked(s: impl AsRef<str>) -> Self { pub fn new_unchecked(s: impl AsRef<str>) -> Self {
@ -29,6 +27,12 @@ impl IndexUid {
} }
} }
impl fmt::Display for IndexUid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl std::ops::Deref for IndexUid { impl std::ops::Deref for IndexUid {
type Target = str; type Target = str;

View File

@ -1,8 +1,8 @@
use std::convert::Infallible; use std::convert::Infallible;
use std::fmt::Display;
use std::hash::Hash; use std::hash::Hash;
use std::str::FromStr;
use deserr::{DeserializeError, DeserializeFromValue, MergeWithError, ValuePointerRef}; use deserr::{DeserializeError, DeserializeFromValue, ValuePointerRef};
use enum_iterator::Sequence; use enum_iterator::Sequence;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use time::format_description::well_known::Rfc3339; use time::format_description::well_known::Rfc3339;
@ -10,45 +10,28 @@ use time::macros::{format_description, time};
use time::{Date, OffsetDateTime, PrimitiveDateTime}; use time::{Date, OffsetDateTime, PrimitiveDateTime};
use uuid::Uuid; use uuid::Uuid;
use crate::deserr::DeserrJsonError;
use crate::error::deserr_codes::*; use crate::error::deserr_codes::*;
use crate::error::{unwrap_any, Code, DeserrError, ErrorCode, TakeErrorMessage}; use crate::error::{unwrap_any, Code, ParseOffsetDateTimeError};
use crate::index_uid::{IndexUid, IndexUidFormatError}; use crate::index_uid::IndexUid;
use crate::star_or::StarOr; use crate::star_or::StarOr;
pub type KeyId = Uuid; pub type KeyId = Uuid;
impl<C: Default + ErrorCode> MergeWithError<IndexUidFormatError> for DeserrError<C> {
fn merge(
_self_: Option<Self>,
other: IndexUidFormatError,
merge_location: deserr::ValuePointerRef,
) -> std::result::Result<Self, Self> {
DeserrError::error::<Infallible>(
None,
deserr::ErrorKind::Unexpected { msg: other.to_string() },
merge_location,
)
}
}
fn parse_uuid_from_str(s: &str) -> Result<Uuid, TakeErrorMessage<uuid::Error>> {
Uuid::parse_str(s).map_err(TakeErrorMessage)
}
#[derive(Debug, DeserializeFromValue)] #[derive(Debug, DeserializeFromValue)]
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
pub struct CreateApiKey { pub struct CreateApiKey {
#[deserr(error = DeserrError<InvalidApiKeyDescription>)] #[deserr(default, error = DeserrJsonError<InvalidApiKeyDescription>)]
pub description: Option<String>, pub description: Option<String>,
#[deserr(error = DeserrError<InvalidApiKeyName>)] #[deserr(default, error = DeserrJsonError<InvalidApiKeyName>)]
pub name: Option<String>, pub name: Option<String>,
#[deserr(default = Uuid::new_v4(), error = DeserrError<InvalidApiKeyUid>, from(&String) = parse_uuid_from_str -> TakeErrorMessage<uuid::Error>)] #[deserr(default = Uuid::new_v4(), error = DeserrJsonError<InvalidApiKeyUid>, from(&String) = Uuid::from_str -> uuid::Error)]
pub uid: KeyId, pub uid: KeyId,
#[deserr(error = DeserrError<InvalidApiKeyActions>)] #[deserr(error = DeserrJsonError<InvalidApiKeyActions>, missing_field_error = DeserrJsonError::missing_api_key_actions)]
pub actions: Vec<Action>, pub actions: Vec<Action>,
#[deserr(error = DeserrError<InvalidApiKeyIndexes>)] #[deserr(error = DeserrJsonError<InvalidApiKeyIndexes>, missing_field_error = DeserrJsonError::missing_api_key_indexes)]
pub indexes: Vec<StarOr<IndexUid>>, pub indexes: Vec<StarOr<IndexUid>>,
#[deserr(error = DeserrError<InvalidApiKeyExpiresAt>, default = None, from(&String) = parse_expiration_date -> TakeErrorMessage<ParseOffsetDateTimeError>)] #[deserr(error = DeserrJsonError<InvalidApiKeyExpiresAt>, from(Option<String>) = parse_expiration_date -> ParseOffsetDateTimeError, missing_field_error = DeserrJsonError::missing_api_key_expires_at)]
pub expires_at: Option<OffsetDateTime>, pub expires_at: Option<OffsetDateTime>,
} }
impl CreateApiKey { impl CreateApiKey {
@ -72,8 +55,8 @@ fn deny_immutable_fields_api_key(
field: &str, field: &str,
accepted: &[&str], accepted: &[&str],
location: ValuePointerRef, location: ValuePointerRef,
) -> DeserrError { ) -> DeserrJsonError {
let mut error = unwrap_any(DeserrError::<BadRequest>::error::<Infallible>( let mut error = unwrap_any(DeserrJsonError::<BadRequest>::error::<Infallible>(
None, None,
deserr::ErrorKind::UnknownKey { key: field, accepted }, deserr::ErrorKind::UnknownKey { key: field, accepted },
location, location,
@ -92,11 +75,11 @@ fn deny_immutable_fields_api_key(
} }
#[derive(Debug, DeserializeFromValue)] #[derive(Debug, DeserializeFromValue)]
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_api_key)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_api_key)]
pub struct PatchApiKey { pub struct PatchApiKey {
#[deserr(error = DeserrError<InvalidApiKeyDescription>)] #[deserr(default, error = DeserrJsonError<InvalidApiKeyDescription>)]
pub description: Option<String>, pub description: Option<String>,
#[deserr(error = DeserrError<InvalidApiKeyName>)] #[deserr(default, error = DeserrJsonError<InvalidApiKeyName>)]
pub name: Option<String>, pub name: Option<String>,
} }
@ -149,46 +132,40 @@ impl Key {
} }
} }
#[derive(Debug)]
pub struct ParseOffsetDateTimeError(String);
impl Display for ParseOffsetDateTimeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "`{original}` is not a valid date. It should follow the RFC 3339 format to represents a date or datetime in the future or specified as a null value. e.g. 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'.", original = self.0)
}
}
impl std::error::Error for ParseOffsetDateTimeError {}
fn parse_expiration_date( fn parse_expiration_date(
string: &str, string: Option<String>,
) -> std::result::Result<Option<OffsetDateTime>, TakeErrorMessage<ParseOffsetDateTimeError>> { ) -> std::result::Result<Option<OffsetDateTime>, ParseOffsetDateTimeError> {
let datetime = if let Ok(datetime) = OffsetDateTime::parse(string, &Rfc3339) { let Some(string) = string else {
return Ok(None)
};
let datetime = if let Ok(datetime) = OffsetDateTime::parse(&string, &Rfc3339) {
datetime datetime
} else if let Ok(primitive_datetime) = PrimitiveDateTime::parse( } else if let Ok(primitive_datetime) = PrimitiveDateTime::parse(
string, &string,
format_description!( format_description!(
"[year repr:full base:calendar]-[month repr:numerical]-[day]T[hour]:[minute]:[second]" "[year repr:full base:calendar]-[month repr:numerical]-[day]T[hour]:[minute]:[second]"
), ),
) { ) {
primitive_datetime.assume_utc() primitive_datetime.assume_utc()
} else if let Ok(primitive_datetime) = PrimitiveDateTime::parse( } else if let Ok(primitive_datetime) = PrimitiveDateTime::parse(
string, &string,
format_description!( format_description!(
"[year repr:full base:calendar]-[month repr:numerical]-[day] [hour]:[minute]:[second]" "[year repr:full base:calendar]-[month repr:numerical]-[day] [hour]:[minute]:[second]"
), ),
) { ) {
primitive_datetime.assume_utc() primitive_datetime.assume_utc()
} else if let Ok(date) = Date::parse( } else if let Ok(date) = Date::parse(
string, &string,
format_description!("[year repr:full base:calendar]-[month repr:numerical]-[day]"), format_description!("[year repr:full base:calendar]-[month repr:numerical]-[day]"),
) { ) {
PrimitiveDateTime::new(date, time!(00:00)).assume_utc() PrimitiveDateTime::new(date, time!(00:00)).assume_utc()
} else { } else {
return Err(TakeErrorMessage(ParseOffsetDateTimeError(string.to_owned()))); return Err(ParseOffsetDateTimeError(string));
}; };
if datetime > OffsetDateTime::now_utc() { if datetime > OffsetDateTime::now_utc() {
Ok(Some(datetime)) Ok(Some(datetime))
} else { } else {
Err(TakeErrorMessage(ParseOffsetDateTimeError(string.to_owned()))) Err(ParseOffsetDateTimeError(string))
} }
} }

View File

@ -1,4 +1,5 @@
pub mod compression; pub mod compression;
pub mod deserr;
pub mod document_formats; pub mod document_formats;
pub mod error; pub mod error;
pub mod index_uid; pub mod index_uid;
@ -7,11 +8,10 @@ pub mod settings;
pub mod star_or; pub mod star_or;
pub mod tasks; pub mod tasks;
pub mod versioning; pub mod versioning;
pub use milli;
pub use milli::{heed, Index}; pub use milli::{heed, Index};
use uuid::Uuid; use uuid::Uuid;
pub use versioning::VERSION_FILE_NAME; pub use versioning::VERSION_FILE_NAME;
pub use {milli, serde_cs};
pub type Document = serde_json::Map<String, serde_json::Value>; pub type Document = serde_json::Map<String, serde_json::Value>;
pub type InstanceUid = Uuid; pub type InstanceUid = Uuid;

View File

@ -11,8 +11,9 @@ use milli::update::Setting;
use milli::{Criterion, CriterionError, Index, DEFAULT_VALUES_PER_FACET}; use milli::{Criterion, CriterionError, Index, DEFAULT_VALUES_PER_FACET};
use serde::{Deserialize, Serialize, Serializer}; use serde::{Deserialize, Serialize, Serializer};
use crate::deserr::DeserrJsonError;
use crate::error::deserr_codes::*; use crate::error::deserr_codes::*;
use crate::error::{unwrap_any, DeserrError}; use crate::error::unwrap_any;
/// The maximimum number of results that the engine /// The maximimum number of results that the engine
/// will be able to return in one search call. /// will be able to return in one search call.
@ -66,26 +67,31 @@ fn validate_min_word_size_for_typo_setting<E: DeserializeError>(
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)] #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)]
#[serde(deny_unknown_fields, rename_all = "camelCase")] #[serde(deny_unknown_fields, rename_all = "camelCase")]
#[deserr(deny_unknown_fields, rename_all = camelCase, validate = validate_min_word_size_for_typo_setting -> DeserrError<InvalidMinWordLengthForTypo>)] #[deserr(deny_unknown_fields, rename_all = camelCase, validate = validate_min_word_size_for_typo_setting -> DeserrJsonError<InvalidMinWordLengthForTypo>)]
pub struct MinWordSizeTyposSetting { pub struct MinWordSizeTyposSetting {
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
pub one_typo: Setting<u8>, pub one_typo: Setting<u8>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
pub two_typos: Setting<u8>, pub two_typos: Setting<u8>,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)] #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)]
#[serde(deny_unknown_fields, rename_all = "camelCase")] #[serde(deny_unknown_fields, rename_all = "camelCase")]
#[deserr(deny_unknown_fields, rename_all = camelCase, where_predicate = __Deserr_E: deserr::MergeWithError<DeserrError<InvalidMinWordLengthForTypo>>)] #[deserr(deny_unknown_fields, rename_all = camelCase, where_predicate = __Deserr_E: deserr::MergeWithError<DeserrJsonError<InvalidMinWordLengthForTypo>>)]
pub struct TypoSettings { pub struct TypoSettings {
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
pub enabled: Setting<bool>, pub enabled: Setting<bool>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(error = DeserrError<InvalidMinWordLengthForTypo>)] #[deserr(default, error = DeserrJsonError<InvalidMinWordLengthForTypo>)]
pub min_word_size_for_typos: Setting<MinWordSizeTyposSetting>, pub min_word_size_for_typos: Setting<MinWordSizeTyposSetting>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
pub disable_on_words: Setting<BTreeSet<String>>, pub disable_on_words: Setting<BTreeSet<String>>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
pub disable_on_attributes: Setting<BTreeSet<String>>, pub disable_on_attributes: Setting<BTreeSet<String>>,
} }
@ -94,6 +100,7 @@ pub struct TypoSettings {
#[deserr(rename_all = camelCase, deny_unknown_fields)] #[deserr(rename_all = camelCase, deny_unknown_fields)]
pub struct FacetingSettings { pub struct FacetingSettings {
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
pub max_values_per_facet: Setting<usize>, pub max_values_per_facet: Setting<usize>,
} }
@ -102,10 +109,11 @@ pub struct FacetingSettings {
#[deserr(rename_all = camelCase, deny_unknown_fields)] #[deserr(rename_all = camelCase, deny_unknown_fields)]
pub struct PaginationSettings { pub struct PaginationSettings {
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
pub max_total_hits: Setting<usize>, pub max_total_hits: Setting<usize>,
} }
impl MergeWithError<milli::CriterionError> for DeserrError<InvalidSettingsRankingRules> { impl MergeWithError<milli::CriterionError> for DeserrJsonError<InvalidSettingsRankingRules> {
fn merge( fn merge(
_self_: Option<Self>, _self_: Option<Self>,
other: milli::CriterionError, other: milli::CriterionError,
@ -128,14 +136,14 @@ impl MergeWithError<milli::CriterionError> for DeserrError<InvalidSettingsRankin
rename_all = "camelCase", rename_all = "camelCase",
bound(serialize = "T: Serialize", deserialize = "T: Deserialize<'static>") bound(serialize = "T: Serialize", deserialize = "T: Deserialize<'static>")
)] )]
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
pub struct Settings<T> { pub struct Settings<T> {
#[serde( #[serde(
default, default,
serialize_with = "serialize_with_wildcard", serialize_with = "serialize_with_wildcard",
skip_serializing_if = "Setting::is_not_set" skip_serializing_if = "Setting::is_not_set"
)] )]
#[deserr(error = DeserrError<InvalidSettingsDisplayedAttributes>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsDisplayedAttributes>)]
pub displayed_attributes: Setting<Vec<String>>, pub displayed_attributes: Setting<Vec<String>>,
#[serde( #[serde(
@ -143,35 +151,35 @@ pub struct Settings<T> {
serialize_with = "serialize_with_wildcard", serialize_with = "serialize_with_wildcard",
skip_serializing_if = "Setting::is_not_set" skip_serializing_if = "Setting::is_not_set"
)] )]
#[deserr(error = DeserrError<InvalidSettingsSearchableAttributes>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsSearchableAttributes>)]
pub searchable_attributes: Setting<Vec<String>>, pub searchable_attributes: Setting<Vec<String>>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(error = DeserrError<InvalidSettingsFilterableAttributes>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsFilterableAttributes>)]
pub filterable_attributes: Setting<BTreeSet<String>>, pub filterable_attributes: Setting<BTreeSet<String>>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(error = DeserrError<InvalidSettingsSortableAttributes>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsSortableAttributes>)]
pub sortable_attributes: Setting<BTreeSet<String>>, pub sortable_attributes: Setting<BTreeSet<String>>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(error = DeserrError<InvalidSettingsRankingRules>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsRankingRules>)]
pub ranking_rules: Setting<Vec<RankingRuleView>>, pub ranking_rules: Setting<Vec<RankingRuleView>>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(error = DeserrError<InvalidSettingsStopWords>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsStopWords>)]
pub stop_words: Setting<BTreeSet<String>>, pub stop_words: Setting<BTreeSet<String>>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(error = DeserrError<InvalidSettingsSynonyms>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsSynonyms>)]
pub synonyms: Setting<BTreeMap<String, Vec<String>>>, pub synonyms: Setting<BTreeMap<String, Vec<String>>>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(error = DeserrError<InvalidSettingsDistinctAttribute>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsDistinctAttribute>)]
pub distinct_attribute: Setting<String>, pub distinct_attribute: Setting<String>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(error = DeserrError<InvalidSettingsTypoTolerance>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsTypoTolerance>)]
pub typo_tolerance: Setting<TypoSettings>, pub typo_tolerance: Setting<TypoSettings>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(error = DeserrError<InvalidSettingsFaceting>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsFaceting>)]
pub faceting: Setting<FacetingSettings>, pub faceting: Setting<FacetingSettings>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(error = DeserrError<InvalidSettingsPagination>)] #[deserr(default, error = DeserrJsonError<InvalidSettingsPagination>)]
pub pagination: Setting<PaginationSettings>, pub pagination: Setting<PaginationSettings>,
#[serde(skip)] #[serde(skip)]

View File

@ -1,12 +1,12 @@
use std::fmt::{Display, Formatter}; use std::fmt;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::ops::Deref;
use std::str::FromStr; use std::str::FromStr;
use deserr::{DeserializeError, DeserializeFromValue, MergeWithError, ValueKind}; use deserr::{DeserializeError, DeserializeFromValue, MergeWithError, ValueKind};
use serde::de::Visitor; use serde::de::Visitor;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::deserr::query_params::FromQueryParameter;
use crate::error::unwrap_any; use crate::error::unwrap_any;
/// A type that tries to match either a star (*) or /// A type that tries to match either a star (*) or
@ -17,35 +17,6 @@ pub enum StarOr<T> {
Other(T), Other(T),
} }
impl<E: DeserializeError, T> DeserializeFromValue<E> for StarOr<T>
where
T: FromStr,
E: MergeWithError<T::Err>,
{
fn deserialize_from_value<V: deserr::IntoValue>(
value: deserr::Value<V>,
location: deserr::ValuePointerRef,
) -> Result<Self, E> {
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::<V>(
None,
deserr::ErrorKind::IncorrectValueKind {
actual: value,
accepted: &[ValueKind::String],
},
location,
))),
}
}
}
impl<T: FromStr> FromStr for StarOr<T> { impl<T: FromStr> FromStr for StarOr<T> {
type Err = T::Err; type Err = T::Err;
@ -57,23 +28,11 @@ impl<T: FromStr> FromStr for StarOr<T> {
} }
} }
} }
impl<T: fmt::Display> fmt::Display for StarOr<T> {
impl<T: Deref<Target = str>> Deref for StarOr<T> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
type Target = str;
fn deref(&self) -> &Self::Target {
match self { match self {
Self::Star => "*", StarOr::Star => write!(f, "*"),
Self::Other(t) => t.deref(), StarOr::Other(x) => fmt::Display::fmt(x, f),
}
}
}
impl<T: Into<String>> From<StarOr<T>> for String {
fn from(s: StarOr<T>) -> Self {
match s {
StarOr::Star => "*".to_string(),
StarOr::Other(t) => t.into(),
} }
} }
} }
@ -93,7 +52,7 @@ impl<T: PartialEq + Eq> Eq for StarOr<T> {}
impl<'de, T, E> Deserialize<'de> for StarOr<T> impl<'de, T, E> Deserialize<'de> for StarOr<T>
where where
T: FromStr<Err = E>, T: FromStr<Err = E>,
E: Display, E: fmt::Display,
{ {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where where
@ -109,11 +68,11 @@ where
impl<'de, T, FE> Visitor<'de> for StarOrVisitor<T> impl<'de, T, FE> Visitor<'de> for StarOrVisitor<T>
where where
T: FromStr<Err = FE>, T: FromStr<Err = FE>,
FE: Display, FE: fmt::Display,
{ {
type Value = StarOr<T>; type Value = StarOr<T>;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { fn expecting(&self, formatter: &mut fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string") formatter.write_str("a string")
} }
@ -139,7 +98,7 @@ where
impl<T> Serialize for StarOr<T> impl<T> Serialize for StarOr<T>
where where
T: Deref<Target = str>, T: ToString,
{ {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where where
@ -147,7 +106,222 @@ where
{ {
match self { match self {
StarOr::Star => serializer.serialize_str("*"), StarOr::Star => serializer.serialize_str("*"),
StarOr::Other(other) => serializer.serialize_str(other.deref()), StarOr::Other(other) => serializer.serialize_str(&other.to_string()),
}
}
}
impl<T, E> DeserializeFromValue<E> for StarOr<T>
where
T: FromStr,
E: DeserializeError + MergeWithError<T::Err>,
{
fn deserialize_from_value<V: deserr::IntoValue>(
value: deserr::Value<V>,
location: deserr::ValuePointerRef,
) -> Result<Self, E> {
match value {
deserr::Value::String(v) => {
if v == "*" {
Ok(StarOr::Star)
} else {
match T::from_str(&v) {
Ok(parsed) => Ok(StarOr::Other(parsed)),
Err(e) => Err(unwrap_any(E::merge(None, e, location))),
}
}
}
_ => Err(unwrap_any(E::error::<V>(
None,
deserr::ErrorKind::IncorrectValueKind {
actual: value,
accepted: &[ValueKind::String],
},
location,
))),
}
}
}
/// A type representing the content of a query parameter that can either not exist,
/// be equal to a star (*), or another value
///
/// It is a convenient alternative to `Option<StarOr<T>>`.
#[derive(Debug, Default, Clone, Copy)]
pub enum OptionStarOr<T> {
#[default]
None,
Star,
Other(T),
}
impl<T> OptionStarOr<T> {
pub fn is_some(&self) -> bool {
match self {
Self::None => false,
Self::Star => false,
Self::Other(_) => true,
}
}
pub fn merge_star_and_none(self) -> Option<T> {
match self {
Self::None | Self::Star => None,
Self::Other(x) => Some(x),
}
}
pub fn try_map<U, E, F: Fn(T) -> Result<U, E>>(self, map_f: F) -> Result<OptionStarOr<U>, E> {
match self {
OptionStarOr::None => Ok(OptionStarOr::None),
OptionStarOr::Star => Ok(OptionStarOr::Star),
OptionStarOr::Other(x) => map_f(x).map(OptionStarOr::Other),
}
}
}
impl<T> FromQueryParameter for OptionStarOr<T>
where
T: FromQueryParameter,
{
type Err = T::Err;
fn from_query_param(p: &str) -> Result<Self, Self::Err> {
match p {
"*" => Ok(OptionStarOr::Star),
s => T::from_query_param(s).map(OptionStarOr::Other),
}
}
}
impl<T, E> DeserializeFromValue<E> for OptionStarOr<T>
where
E: DeserializeError + MergeWithError<T::Err>,
T: FromQueryParameter,
{
fn deserialize_from_value<V: deserr::IntoValue>(
value: deserr::Value<V>,
location: deserr::ValuePointerRef,
) -> Result<Self, E> {
match value {
deserr::Value::String(s) => match s.as_str() {
"*" => Ok(OptionStarOr::Star),
s => match T::from_query_param(s) {
Ok(x) => Ok(OptionStarOr::Other(x)),
Err(e) => Err(unwrap_any(E::merge(None, e, location))),
},
},
_ => Err(unwrap_any(E::error::<V>(
None,
deserr::ErrorKind::IncorrectValueKind {
actual: value,
accepted: &[ValueKind::String],
},
location,
))),
}
}
}
/// A type representing the content of a query parameter that can either not exist, be equal to a star (*), or represent a list of other values
#[derive(Debug, Default, Clone)]
pub enum OptionStarOrList<T> {
#[default]
None,
Star,
List(Vec<T>),
}
impl<T> OptionStarOrList<T> {
pub fn is_some(&self) -> bool {
match self {
Self::None => false,
Self::Star => false,
Self::List(_) => true,
}
}
pub fn map<U, F: Fn(T) -> U>(self, map_f: F) -> OptionStarOrList<U> {
match self {
Self::None => OptionStarOrList::None,
Self::Star => OptionStarOrList::Star,
Self::List(xs) => OptionStarOrList::List(xs.into_iter().map(map_f).collect()),
}
}
pub fn try_map<U, E, F: Fn(T) -> Result<U, E>>(
self,
map_f: F,
) -> Result<OptionStarOrList<U>, E> {
match self {
Self::None => Ok(OptionStarOrList::None),
Self::Star => Ok(OptionStarOrList::Star),
Self::List(xs) => {
xs.into_iter().map(map_f).collect::<Result<Vec<_>, _>>().map(OptionStarOrList::List)
}
}
}
pub fn merge_star_and_none(self) -> Option<Vec<T>> {
match self {
Self::None | Self::Star => None,
Self::List(xs) => Some(xs),
}
}
pub fn push(&mut self, el: T) {
match self {
Self::None => *self = Self::List(vec![el]),
Self::Star => (),
Self::List(xs) => xs.push(el),
}
}
}
impl<T, E> DeserializeFromValue<E> for OptionStarOrList<T>
where
E: DeserializeError + MergeWithError<T::Err>,
T: FromQueryParameter,
{
fn deserialize_from_value<V: deserr::IntoValue>(
value: deserr::Value<V>,
location: deserr::ValuePointerRef,
) -> Result<Self, E> {
match value {
deserr::Value::String(s) => {
let mut error = None;
let mut is_star = false;
// CS::<String>::from_str is infaillible
let cs = serde_cs::vec::CS::<String>::from_str(&s).unwrap();
let len_cs = cs.0.len();
let mut els = vec![];
for (i, el_str) in cs.into_iter().enumerate() {
if el_str == "*" {
is_star = true;
} else {
match T::from_query_param(&el_str) {
Ok(el) => {
els.push(el);
}
Err(e) => {
let location =
if len_cs > 1 { location.push_index(i) } else { location };
error = Some(E::merge(error, e, location)?);
}
}
}
}
if let Some(error) = error {
return Err(error);
}
if is_star {
Ok(OptionStarOrList::Star)
} else {
Ok(OptionStarOrList::List(els))
}
}
_ => Err(unwrap_any(E::error::<V>(
None,
deserr::ErrorKind::IncorrectValueKind {
actual: value,
accepted: &[ValueKind::String],
},
location,
))),
} }
} }
} }

View File

@ -1,3 +1,4 @@
use core::fmt;
use std::collections::HashSet; use std::collections::HashSet;
use std::fmt::{Display, Write}; use std::fmt::{Display, Write};
use std::str::FromStr; use std::str::FromStr;
@ -9,7 +10,7 @@ use serde::{Deserialize, Serialize, Serializer};
use time::{Duration, OffsetDateTime}; use time::{Duration, OffsetDateTime};
use uuid::Uuid; use uuid::Uuid;
use crate::error::{Code, ResponseError}; use crate::error::ResponseError;
use crate::keys::Key; use crate::keys::Key;
use crate::settings::{Settings, Unchecked}; use crate::settings::{Settings, Unchecked};
use crate::InstanceUid; use crate::InstanceUid;
@ -332,7 +333,7 @@ impl Display for Status {
} }
impl FromStr for Status { impl FromStr for Status {
type Err = ResponseError; type Err = ParseTaskStatusError;
fn from_str(status: &str) -> Result<Self, Self::Err> { fn from_str(status: &str) -> Result<Self, Self::Err> {
if status.eq_ignore_ascii_case("enqueued") { if status.eq_ignore_ascii_case("enqueued") {
@ -346,21 +347,28 @@ impl FromStr for Status {
} else if status.eq_ignore_ascii_case("canceled") { } else if status.eq_ignore_ascii_case("canceled") {
Ok(Status::Canceled) Ok(Status::Canceled)
} else { } else {
Err(ResponseError::from_msg( Err(ParseTaskStatusError(status.to_owned()))
format!(
"`{}` is not a status. Available status are {}.",
status,
enum_iterator::all::<Status>()
.map(|s| format!("`{s}`"))
.collect::<Vec<String>>()
.join(", ")
),
Code::BadRequest,
))
} }
} }
} }
#[derive(Debug)]
pub struct ParseTaskStatusError(pub String);
impl fmt::Display for ParseTaskStatusError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"`{}` is not a valid task status. Available statuses are {}.",
self.0,
enum_iterator::all::<Status>()
.map(|s| format!("`{s}`"))
.collect::<Vec<String>>()
.join(", ")
)
}
}
impl std::error::Error for ParseTaskStatusError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Sequence)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Sequence)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub enum Kind { pub enum Kind {
@ -412,7 +420,7 @@ impl Display for Kind {
} }
} }
impl FromStr for Kind { impl FromStr for Kind {
type Err = ResponseError; type Err = ParseTaskKindError;
fn from_str(kind: &str) -> Result<Self, Self::Err> { fn from_str(kind: &str) -> Result<Self, Self::Err> {
if kind.eq_ignore_ascii_case("indexCreation") { if kind.eq_ignore_ascii_case("indexCreation") {
@ -438,25 +446,32 @@ impl FromStr for Kind {
} else if kind.eq_ignore_ascii_case("snapshotCreation") { } else if kind.eq_ignore_ascii_case("snapshotCreation") {
Ok(Kind::SnapshotCreation) Ok(Kind::SnapshotCreation)
} else { } else {
Err(ResponseError::from_msg( Err(ParseTaskKindError(kind.to_owned()))
format!(
"`{}` is not a type. Available types are {}.",
kind,
enum_iterator::all::<Kind>()
.map(|k| format!(
"`{}`",
// by default serde is going to insert `"` around the value.
serde_json::to_string(&k).unwrap().trim_matches('"')
))
.collect::<Vec<String>>()
.join(", ")
),
Code::BadRequest,
))
} }
} }
} }
#[derive(Debug)]
pub struct ParseTaskKindError(pub String);
impl fmt::Display for ParseTaskKindError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"`{}` is not a valid task type. Available types are {}.",
self.0,
enum_iterator::all::<Kind>()
.map(|k| format!(
"`{}`",
// by default serde is going to insert `"` around the value.
serde_json::to_string(&k).unwrap().trim_matches('"')
))
.collect::<Vec<String>>()
.join(", ")
)
}
}
impl std::error::Error for ParseTaskKindError {}
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub enum Details { pub enum Details {
DocumentAdditionOrUpdate { received_documents: u64, indexed_documents: Option<u64> }, DocumentAdditionOrUpdate { received_documents: u64, indexed_documents: Option<u64> },

View File

@ -19,7 +19,7 @@ byte-unit = { version = "4.0.14", default-features = false, features = ["std", "
bytes = "1.2.1" bytes = "1.2.1"
clap = { version = "4.0.9", features = ["derive", "env"] } clap = { version = "4.0.9", features = ["derive", "env"] }
crossbeam-channel = "0.5.6" crossbeam-channel = "0.5.6"
deserr = "0.1.4" deserr = "0.1.5"
dump = { path = "../dump" } dump = { path = "../dump" }
either = "1.8.0" either = "1.8.0"
env_logger = "0.9.1" env_logger = "0.9.1"
@ -55,7 +55,6 @@ rustls = "0.20.6"
rustls-pemfile = "1.0.1" rustls-pemfile = "1.0.1"
segment = { version = "0.2.1", optional = true } segment = { version = "0.2.1", optional = true }
serde = { version = "1.0.145", features = ["derive"] } serde = { version = "1.0.145", features = ["derive"] }
serde-cs = "0.2.4"
serde_json = { version = "1.0.85", features = ["preserve_order"] } serde_json = { version = "1.0.85", features = ["preserve_order"] }
sha2 = "0.10.6" sha2 = "0.10.6"
siphasher = "0.3.10" siphasher = "0.3.10"

View File

@ -2,7 +2,7 @@ use actix_web as aweb;
use aweb::error::{JsonPayloadError, QueryPayloadError}; use aweb::error::{JsonPayloadError, QueryPayloadError};
use meilisearch_types::document_formats::{DocumentFormatError, PayloadType}; use meilisearch_types::document_formats::{DocumentFormatError, PayloadType};
use meilisearch_types::error::{Code, ErrorCode, ResponseError}; use meilisearch_types::error::{Code, ErrorCode, ResponseError};
use meilisearch_types::index_uid::IndexUidFormatError; use meilisearch_types::index_uid::{IndexUid, IndexUidFormatError};
use serde_json::Value; use serde_json::Value;
use tokio::task::JoinError; use tokio::task::JoinError;
@ -27,7 +27,7 @@ pub enum MeilisearchHttpError {
#[error("Two indexes must be given for each swap. The list `{:?}` contains {} indexes.", #[error("Two indexes must be given for each swap. The list `{:?}` contains {} indexes.",
.0, .0.len() .0, .0.len()
)] )]
SwapIndexPayloadWrongLength(Vec<String>), SwapIndexPayloadWrongLength(Vec<IndexUid>),
#[error(transparent)] #[error(transparent)]
IndexUid(#[from] IndexUidFormatError), IndexUid(#[from] IndexUidFormatError),
#[error(transparent)] #[error(transparent)]

View File

@ -4,14 +4,15 @@ use actix_web::{web, HttpRequest, HttpResponse};
use deserr::DeserializeFromValue; use deserr::DeserializeFromValue;
use meilisearch_auth::error::AuthControllerError; use meilisearch_auth::error::AuthControllerError;
use meilisearch_auth::AuthController; use meilisearch_auth::AuthController;
use meilisearch_types::deserr::query_params::Param;
use meilisearch_types::deserr::{DeserrJsonError, DeserrQueryParamError};
use meilisearch_types::error::deserr_codes::*; use meilisearch_types::error::deserr_codes::*;
use meilisearch_types::error::{Code, DeserrError, ResponseError, TakeErrorMessage}; use meilisearch_types::error::{Code, ResponseError};
use meilisearch_types::keys::{Action, CreateApiKey, Key, PatchApiKey}; use meilisearch_types::keys::{Action, CreateApiKey, Key, PatchApiKey};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use time::OffsetDateTime; use time::OffsetDateTime;
use uuid::Uuid; use uuid::Uuid;
use super::indexes::search::parse_usize_take_error_message;
use super::PAGINATION_DEFAULT_LIMIT; use super::PAGINATION_DEFAULT_LIMIT;
use crate::extractors::authentication::policies::*; use crate::extractors::authentication::policies::*;
use crate::extractors::authentication::GuardedData; use crate::extractors::authentication::GuardedData;
@ -36,7 +37,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
pub async fn create_api_key( pub async fn create_api_key(
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_CREATE }>, AuthController>, auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_CREATE }>, AuthController>,
body: ValidatedJson<CreateApiKey, DeserrError>, body: ValidatedJson<CreateApiKey, DeserrJsonError>,
_req: HttpRequest, _req: HttpRequest,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let v = body.into_inner(); let v = body.into_inner();
@ -50,26 +51,23 @@ pub async fn create_api_key(
Ok(HttpResponse::Created().json(res)) Ok(HttpResponse::Created().json(res))
} }
#[derive(DeserializeFromValue, Deserialize, Debug, Clone, Copy)] #[derive(DeserializeFromValue, Debug, Clone, Copy)]
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ListApiKeys { pub struct ListApiKeys {
#[serde(default)] #[deserr(default, error = DeserrQueryParamError<InvalidApiKeyOffset>)]
#[deserr(error = DeserrError<InvalidApiKeyOffset>, default, from(&String) = parse_usize_take_error_message -> TakeErrorMessage<std::num::ParseIntError>)] pub offset: Param<usize>,
pub offset: usize, #[deserr(default = Param(PAGINATION_DEFAULT_LIMIT), error = DeserrQueryParamError<InvalidApiKeyLimit>)]
#[serde(default = "PAGINATION_DEFAULT_LIMIT")] pub limit: Param<usize>,
#[deserr(error = DeserrError<InvalidApiKeyLimit>, default = PAGINATION_DEFAULT_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage<std::num::ParseIntError>)]
pub limit: usize,
} }
impl ListApiKeys { impl ListApiKeys {
fn as_pagination(self) -> Pagination { fn as_pagination(self) -> Pagination {
Pagination { offset: self.offset, limit: self.limit } Pagination { offset: self.offset.0, limit: self.limit.0 }
} }
} }
pub async fn list_api_keys( pub async fn list_api_keys(
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_GET }>, AuthController>, auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_GET }>, AuthController>,
list_api_keys: QueryParameter<ListApiKeys, DeserrError>, list_api_keys: QueryParameter<ListApiKeys, DeserrQueryParamError>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let paginate = list_api_keys.into_inner().as_pagination(); let paginate = list_api_keys.into_inner().as_pagination();
let page_view = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> { let page_view = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> {
@ -106,7 +104,7 @@ pub async fn get_api_key(
pub async fn patch_api_key( pub async fn patch_api_key(
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_UPDATE }>, AuthController>, auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_UPDATE }>, AuthController>,
body: ValidatedJson<PatchApiKey, DeserrError>, body: ValidatedJson<PatchApiKey, DeserrJsonError>,
path: web::Path<AuthParam>, path: web::Path<AuthParam>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let key = path.into_inner().key; let key = path.into_inner().key;
@ -172,7 +170,7 @@ impl KeyView {
key: generated_key, key: generated_key,
uid: key.uid, uid: key.uid,
actions: key.actions, actions: key.actions,
indexes: key.indexes.into_iter().map(String::from).collect(), indexes: key.indexes.into_iter().map(|x| x.to_string()).collect(),
expires_at: key.expires_at, expires_at: key.expires_at,
created_at: key.created_at, created_at: key.created_at,
updated_at: key.updated_at, updated_at: key.updated_at,

View File

@ -1,5 +1,4 @@
use std::io::ErrorKind; use std::io::ErrorKind;
use std::num::ParseIntError;
use actix_web::http::header::CONTENT_TYPE; use actix_web::http::header::CONTENT_TYPE;
use actix_web::web::Data; use actix_web::web::Data;
@ -9,25 +8,25 @@ use deserr::DeserializeFromValue;
use futures::StreamExt; use futures::StreamExt;
use index_scheduler::IndexScheduler; use index_scheduler::IndexScheduler;
use log::debug; use log::debug;
use meilisearch_types::deserr::query_params::Param;
use meilisearch_types::deserr::{DeserrJsonError, DeserrQueryParamError};
use meilisearch_types::document_formats::{read_csv, read_json, read_ndjson, PayloadType}; use meilisearch_types::document_formats::{read_csv, read_json, read_ndjson, PayloadType};
use meilisearch_types::error::deserr_codes::*; use meilisearch_types::error::deserr_codes::*;
use meilisearch_types::error::{DeserrError, ResponseError, TakeErrorMessage}; use meilisearch_types::error::ResponseError;
use meilisearch_types::heed::RoTxn; use meilisearch_types::heed::RoTxn;
use meilisearch_types::index_uid::IndexUid; use meilisearch_types::index_uid::IndexUid;
use meilisearch_types::milli::update::IndexDocumentsMethod; use meilisearch_types::milli::update::IndexDocumentsMethod;
use meilisearch_types::star_or::StarOr; use meilisearch_types::star_or::OptionStarOrList;
use meilisearch_types::tasks::KindWithContent; use meilisearch_types::tasks::KindWithContent;
use meilisearch_types::{milli, Document, Index}; use meilisearch_types::{milli, Document, Index};
use mime::Mime; use mime::Mime;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::Deserialize; use serde::Deserialize;
use serde_cs::vec::CS;
use serde_json::Value; use serde_json::Value;
use tempfile::tempfile; use tempfile::tempfile;
use tokio::fs::File; use tokio::fs::File;
use tokio::io::{AsyncSeekExt, AsyncWriteExt, BufWriter}; use tokio::io::{AsyncSeekExt, AsyncWriteExt, BufWriter};
use super::search::parse_usize_take_error_message;
use crate::analytics::{Analytics, DocumentDeletionKind}; use crate::analytics::{Analytics, DocumentDeletionKind};
use crate::error::MeilisearchHttpError; use crate::error::MeilisearchHttpError;
use crate::error::PayloadError::ReceivePayload; use crate::error::PayloadError::ReceivePayload;
@ -36,7 +35,7 @@ use crate::extractors::authentication::GuardedData;
use crate::extractors::payload::Payload; use crate::extractors::payload::Payload;
use crate::extractors::query_parameters::QueryParameter; use crate::extractors::query_parameters::QueryParameter;
use crate::extractors::sequential_extractor::SeqHandler; use crate::extractors::sequential_extractor::SeqHandler;
use crate::routes::{fold_star_or, PaginationView, SummarizedTaskView}; use crate::routes::{PaginationView, SummarizedTaskView, PAGINATION_DEFAULT_LIMIT};
static ACCEPTED_CONTENT_TYPE: Lazy<Vec<String>> = Lazy::new(|| { static ACCEPTED_CONTENT_TYPE: Lazy<Vec<String>> = Lazy::new(|| {
vec!["application/json".to_string(), "application/x-ndjson".to_string(), "text/csv".to_string()] vec!["application/json".to_string(), "application/x-ndjson".to_string(), "text/csv".to_string()]
@ -81,23 +80,26 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
); );
} }
#[derive(Deserialize, Debug, DeserializeFromValue)] #[derive(Debug, DeserializeFromValue)]
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
pub struct GetDocument { pub struct GetDocument {
#[deserr(error = DeserrError<InvalidDocumentFields>)] #[deserr(default, error = DeserrQueryParamError<InvalidDocumentFields>)]
fields: Option<CS<StarOr<String>>>, fields: OptionStarOrList<String>,
} }
pub async fn get_document( pub async fn get_document(
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_GET }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_GET }>, Data<IndexScheduler>>,
path: web::Path<DocumentParam>, document_param: web::Path<DocumentParam>,
params: QueryParameter<GetDocument, DeserrError>, params: QueryParameter<GetDocument, DeserrQueryParamError>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let GetDocument { fields } = params.into_inner(); let DocumentParam { index_uid, document_id } = document_param.into_inner();
let attributes_to_retrieve = fields.and_then(fold_star_or); let index_uid = IndexUid::try_from(index_uid)?;
let index = index_scheduler.index(&path.index_uid)?; let GetDocument { fields } = params.into_inner();
let document = retrieve_document(&index, &path.document_id, attributes_to_retrieve)?; let attributes_to_retrieve = fields.merge_star_and_none();
let index = index_scheduler.index(&index_uid)?;
let document = retrieve_document(&index, &document_id, attributes_to_retrieve)?;
debug!("returns: {:?}", document); debug!("returns: {:?}", document);
Ok(HttpResponse::Ok().json(document)) Ok(HttpResponse::Ok().json(document))
} }
@ -108,60 +110,68 @@ pub async fn delete_document(
req: HttpRequest, req: HttpRequest,
analytics: web::Data<dyn Analytics>, analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let DocumentParam { index_uid, document_id } = path.into_inner();
let index_uid = IndexUid::try_from(index_uid)?;
analytics.delete_documents(DocumentDeletionKind::PerDocumentId, &req); analytics.delete_documents(DocumentDeletionKind::PerDocumentId, &req);
let DocumentParam { document_id, index_uid } = path.into_inner(); let task = KindWithContent::DocumentDeletion {
let task = KindWithContent::DocumentDeletion { index_uid, documents_ids: vec![document_id] }; index_uid: index_uid.to_string(),
documents_ids: vec![document_id],
};
let task: SummarizedTaskView = let task: SummarizedTaskView =
tokio::task::spawn_blocking(move || index_scheduler.register(task)).await??.into(); tokio::task::spawn_blocking(move || index_scheduler.register(task)).await??.into();
debug!("returns: {:?}", task); debug!("returns: {:?}", task);
Ok(HttpResponse::Accepted().json(task)) Ok(HttpResponse::Accepted().json(task))
} }
#[derive(Deserialize, Debug, DeserializeFromValue)] #[derive(Debug, DeserializeFromValue)]
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
pub struct BrowseQuery { pub struct BrowseQuery {
#[deserr(error = DeserrError<InvalidDocumentFields>, default, from(&String) = parse_usize_take_error_message -> TakeErrorMessage<ParseIntError>)] #[deserr(default, error = DeserrQueryParamError<InvalidDocumentFields>)]
offset: usize, offset: Param<usize>,
#[deserr(error = DeserrError<InvalidDocumentLimit>, default = crate::routes::PAGINATION_DEFAULT_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage<ParseIntError>)] #[deserr(default = Param(PAGINATION_DEFAULT_LIMIT), error = DeserrQueryParamError<InvalidDocumentLimit>)]
limit: usize, limit: Param<usize>,
#[deserr(error = DeserrError<InvalidDocumentLimit>)] #[deserr(default, error = DeserrQueryParamError<InvalidDocumentLimit>)]
fields: Option<CS<StarOr<String>>>, fields: OptionStarOrList<String>,
} }
pub async fn get_all_documents( pub async fn get_all_documents(
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_GET }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_GET }>, Data<IndexScheduler>>,
index_uid: web::Path<String>, index_uid: web::Path<String>,
params: QueryParameter<BrowseQuery, DeserrError>, params: QueryParameter<BrowseQuery, DeserrQueryParamError>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let index_uid = IndexUid::try_from(index_uid.into_inner())?;
debug!("called with params: {:?}", params); debug!("called with params: {:?}", params);
let BrowseQuery { limit, offset, fields } = params.into_inner(); let BrowseQuery { limit, offset, fields } = params.into_inner();
let attributes_to_retrieve = fields.and_then(fold_star_or); let attributes_to_retrieve = fields.merge_star_and_none();
let index = index_scheduler.index(&index_uid)?; let index = index_scheduler.index(&index_uid)?;
let (total, documents) = retrieve_documents(&index, offset, limit, attributes_to_retrieve)?; let (total, documents) = retrieve_documents(&index, offset.0, limit.0, attributes_to_retrieve)?;
let ret = PaginationView::new(offset, limit, total as usize, documents); let ret = PaginationView::new(offset.0, limit.0, total as usize, documents);
debug!("returns: {:?}", ret); debug!("returns: {:?}", ret);
Ok(HttpResponse::Ok().json(ret)) Ok(HttpResponse::Ok().json(ret))
} }
#[derive(Deserialize, Debug, DeserializeFromValue)] #[derive(Deserialize, Debug, DeserializeFromValue)]
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
pub struct UpdateDocumentsQuery { pub struct UpdateDocumentsQuery {
#[deserr(error = DeserrError<InvalidIndexPrimaryKey>)] #[deserr(default, error = DeserrJsonError<InvalidIndexPrimaryKey>)]
pub primary_key: Option<String>, pub primary_key: Option<String>,
} }
pub async fn add_documents( pub async fn add_documents(
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_ADD }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_ADD }>, Data<IndexScheduler>>,
index_uid: web::Path<String>, index_uid: web::Path<String>,
params: QueryParameter<UpdateDocumentsQuery, DeserrError>, params: QueryParameter<UpdateDocumentsQuery, DeserrJsonError>,
body: Payload, body: Payload,
req: HttpRequest, req: HttpRequest,
analytics: web::Data<dyn Analytics>, analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let index_uid = IndexUid::try_from(index_uid.into_inner())?;
debug!("called with params: {:?}", params); debug!("called with params: {:?}", params);
let params = params.into_inner(); let params = params.into_inner();
@ -171,7 +181,7 @@ pub async fn add_documents(
let task = document_addition( let task = document_addition(
extract_mime_type(&req)?, extract_mime_type(&req)?,
index_scheduler, index_scheduler,
index_uid.into_inner(), index_uid,
params.primary_key, params.primary_key,
body, body,
IndexDocumentsMethod::ReplaceDocuments, IndexDocumentsMethod::ReplaceDocuments,
@ -184,14 +194,15 @@ pub async fn add_documents(
pub async fn update_documents( pub async fn update_documents(
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_ADD }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_ADD }>, Data<IndexScheduler>>,
path: web::Path<String>, index_uid: web::Path<String>,
params: QueryParameter<UpdateDocumentsQuery, DeserrError>, params: QueryParameter<UpdateDocumentsQuery, DeserrJsonError>,
body: Payload, body: Payload,
req: HttpRequest, req: HttpRequest,
analytics: web::Data<dyn Analytics>, analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let index_uid = IndexUid::try_from(index_uid.into_inner())?;
debug!("called with params: {:?}", params); debug!("called with params: {:?}", params);
let index_uid = path.into_inner();
analytics.update_documents(&params, index_scheduler.index(&index_uid).is_err(), &req); analytics.update_documents(&params, index_scheduler.index(&index_uid).is_err(), &req);
@ -213,7 +224,7 @@ pub async fn update_documents(
async fn document_addition( async fn document_addition(
mime_type: Option<Mime>, mime_type: Option<Mime>,
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_ADD }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_ADD }>, Data<IndexScheduler>>,
index_uid: String, index_uid: IndexUid,
primary_key: Option<String>, primary_key: Option<String>,
mut body: Payload, mut body: Payload,
method: IndexDocumentsMethod, method: IndexDocumentsMethod,
@ -234,9 +245,6 @@ async fn document_addition(
} }
}; };
// is your indexUid valid?
let index_uid = IndexUid::try_from(index_uid)?.into_inner();
let (uuid, mut update_file) = index_scheduler.create_update_file()?; let (uuid, mut update_file) = index_scheduler.create_update_file()?;
let temp_file = match tempfile() { let temp_file = match tempfile() {
@ -312,7 +320,7 @@ async fn document_addition(
documents_count, documents_count,
primary_key, primary_key,
allow_index_creation, allow_index_creation,
index_uid, index_uid: index_uid.to_string(),
}; };
let scheduler = index_scheduler.clone(); let scheduler = index_scheduler.clone();
@ -330,12 +338,13 @@ async fn document_addition(
pub async fn delete_documents( pub async fn delete_documents(
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_DELETE }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_DELETE }>, Data<IndexScheduler>>,
path: web::Path<String>, index_uid: web::Path<String>,
body: web::Json<Vec<Value>>, body: web::Json<Vec<Value>>,
req: HttpRequest, req: HttpRequest,
analytics: web::Data<dyn Analytics>, analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
debug!("called with params: {:?}", body); debug!("called with params: {:?}", body);
let index_uid = IndexUid::try_from(index_uid.into_inner())?;
analytics.delete_documents(DocumentDeletionKind::PerBatch, &req); analytics.delete_documents(DocumentDeletionKind::PerBatch, &req);
@ -345,7 +354,7 @@ pub async fn delete_documents(
.collect(); .collect();
let task = let task =
KindWithContent::DocumentDeletion { index_uid: path.into_inner(), documents_ids: ids }; KindWithContent::DocumentDeletion { index_uid: index_uid.to_string(), documents_ids: ids };
let task: SummarizedTaskView = let task: SummarizedTaskView =
tokio::task::spawn_blocking(move || index_scheduler.register(task)).await??.into(); tokio::task::spawn_blocking(move || index_scheduler.register(task)).await??.into();
@ -355,13 +364,14 @@ pub async fn delete_documents(
pub async fn clear_all_documents( pub async fn clear_all_documents(
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_DELETE }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_DELETE }>, Data<IndexScheduler>>,
path: web::Path<String>, index_uid: web::Path<String>,
req: HttpRequest, req: HttpRequest,
analytics: web::Data<dyn Analytics>, analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let index_uid = IndexUid::try_from(index_uid.into_inner())?;
analytics.delete_documents(DocumentDeletionKind::ClearAll, &req); analytics.delete_documents(DocumentDeletionKind::ClearAll, &req);
let task = KindWithContent::DocumentClear { index_uid: path.into_inner() }; let task = KindWithContent::DocumentClear { index_uid: index_uid.to_string() };
let task: SummarizedTaskView = let task: SummarizedTaskView =
tokio::task::spawn_blocking(move || index_scheduler.register(task)).await??.into(); tokio::task::spawn_blocking(move || index_scheduler.register(task)).await??.into();

View File

@ -5,16 +5,17 @@ use actix_web::{web, HttpRequest, HttpResponse};
use deserr::{DeserializeError, DeserializeFromValue, ValuePointerRef}; use deserr::{DeserializeError, DeserializeFromValue, ValuePointerRef};
use index_scheduler::IndexScheduler; use index_scheduler::IndexScheduler;
use log::debug; use log::debug;
use meilisearch_types::deserr::query_params::Param;
use meilisearch_types::deserr::{DeserrJsonError, DeserrQueryParamError};
use meilisearch_types::error::deserr_codes::*; use meilisearch_types::error::deserr_codes::*;
use meilisearch_types::error::{unwrap_any, Code, DeserrError, ResponseError, TakeErrorMessage}; use meilisearch_types::error::{unwrap_any, Code, ResponseError};
use meilisearch_types::index_uid::IndexUid; use meilisearch_types::index_uid::IndexUid;
use meilisearch_types::milli::{self, FieldDistribution, Index}; use meilisearch_types::milli::{self, FieldDistribution, Index};
use meilisearch_types::tasks::KindWithContent; use meilisearch_types::tasks::KindWithContent;
use serde::{Deserialize, Serialize}; use serde::Serialize;
use serde_json::json; use serde_json::json;
use time::OffsetDateTime; use time::OffsetDateTime;
use self::search::parse_usize_take_error_message;
use super::{Pagination, SummarizedTaskView, PAGINATION_DEFAULT_LIMIT}; use super::{Pagination, SummarizedTaskView, PAGINATION_DEFAULT_LIMIT};
use crate::analytics::Analytics; use crate::analytics::Analytics;
use crate::extractors::authentication::policies::*; use crate::extractors::authentication::policies::*;
@ -48,7 +49,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
); );
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct IndexView { pub struct IndexView {
pub uid: String, pub uid: String,
@ -71,26 +72,23 @@ impl IndexView {
} }
} }
#[derive(DeserializeFromValue, Deserialize, Debug, Clone, Copy)] #[derive(DeserializeFromValue, Debug, Clone, Copy)]
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ListIndexes { pub struct ListIndexes {
#[serde(default)] #[deserr(default, error = DeserrQueryParamError<InvalidIndexOffset>)]
#[deserr(error = DeserrError<InvalidIndexOffset>, default, from(&String) = parse_usize_take_error_message -> TakeErrorMessage<std::num::ParseIntError>)] pub offset: Param<usize>,
pub offset: usize, #[deserr(default = Param(PAGINATION_DEFAULT_LIMIT), error = DeserrQueryParamError<InvalidIndexLimit>)]
#[serde(default = "PAGINATION_DEFAULT_LIMIT")] pub limit: Param<usize>,
#[deserr(error = DeserrError<InvalidIndexLimit>, default = PAGINATION_DEFAULT_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage<std::num::ParseIntError>)]
pub limit: usize,
} }
impl ListIndexes { impl ListIndexes {
fn as_pagination(self) -> Pagination { fn as_pagination(self) -> Pagination {
Pagination { offset: self.offset, limit: self.limit } Pagination { offset: self.offset.0, limit: self.limit.0 }
} }
} }
pub async fn list_indexes( pub async fn list_indexes(
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_GET }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_GET }>, Data<IndexScheduler>>,
paginate: QueryParameter<ListIndexes, DeserrError>, paginate: QueryParameter<ListIndexes, DeserrQueryParamError>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let search_rules = &index_scheduler.filters().search_rules; let search_rules = &index_scheduler.filters().search_rules;
let indexes: Vec<_> = index_scheduler.indexes()?; let indexes: Vec<_> = index_scheduler.indexes()?;
@ -107,22 +105,21 @@ pub async fn list_indexes(
} }
#[derive(DeserializeFromValue, Debug)] #[derive(DeserializeFromValue, Debug)]
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
pub struct IndexCreateRequest { pub struct IndexCreateRequest {
#[deserr(error = DeserrError<InvalidIndexUid>, missing_field_error = DeserrError::missing_index_uid)] #[deserr(error = DeserrJsonError<InvalidIndexUid>, missing_field_error = DeserrJsonError::missing_index_uid)]
uid: String, uid: IndexUid,
#[deserr(error = DeserrError<InvalidIndexPrimaryKey>)] #[deserr(default, error = DeserrJsonError<InvalidIndexPrimaryKey>)]
primary_key: Option<String>, primary_key: Option<String>,
} }
pub async fn create_index( pub async fn create_index(
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_CREATE }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_CREATE }>, Data<IndexScheduler>>,
body: ValidatedJson<IndexCreateRequest, DeserrError>, body: ValidatedJson<IndexCreateRequest, DeserrJsonError>,
req: HttpRequest, req: HttpRequest,
analytics: web::Data<dyn Analytics>, analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let IndexCreateRequest { primary_key, uid } = body.into_inner(); let IndexCreateRequest { primary_key, uid } = body.into_inner();
let uid = IndexUid::try_from(uid)?.into_inner();
let allow_index_creation = index_scheduler.filters().search_rules.is_index_authorized(&uid); let allow_index_creation = index_scheduler.filters().search_rules.is_index_authorized(&uid);
if allow_index_creation { if allow_index_creation {
@ -132,7 +129,7 @@ pub async fn create_index(
Some(&req), Some(&req),
); );
let task = KindWithContent::IndexCreation { index_uid: uid, primary_key }; let task = KindWithContent::IndexCreation { index_uid: uid.to_string(), primary_key };
let task: SummarizedTaskView = let task: SummarizedTaskView =
tokio::task::spawn_blocking(move || index_scheduler.register(task)).await??.into(); tokio::task::spawn_blocking(move || index_scheduler.register(task)).await??.into();
@ -146,8 +143,8 @@ fn deny_immutable_fields_index(
field: &str, field: &str,
accepted: &[&str], accepted: &[&str],
location: ValuePointerRef, location: ValuePointerRef,
) -> DeserrError { ) -> DeserrJsonError {
let mut error = unwrap_any(DeserrError::<BadRequest>::error::<Infallible>( let mut error = unwrap_any(DeserrJsonError::<BadRequest>::error::<Infallible>(
None, None,
deserr::ErrorKind::UnknownKey { key: field, accepted }, deserr::ErrorKind::UnknownKey { key: field, accepted },
location, location,
@ -162,9 +159,9 @@ fn deny_immutable_fields_index(
error error
} }
#[derive(DeserializeFromValue, Debug)] #[derive(DeserializeFromValue, Debug)]
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_index)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_index)]
pub struct UpdateIndexRequest { pub struct UpdateIndexRequest {
#[deserr(error = DeserrError<InvalidIndexPrimaryKey>)] #[deserr(default, error = DeserrJsonError<InvalidIndexPrimaryKey>)]
primary_key: Option<String>, primary_key: Option<String>,
} }
@ -172,6 +169,8 @@ pub async fn get_index(
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_GET }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_GET }>, Data<IndexScheduler>>,
index_uid: web::Path<String>, index_uid: web::Path<String>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let index_uid = IndexUid::try_from(index_uid.into_inner())?;
let index = index_scheduler.index(&index_uid)?; let index = index_scheduler.index(&index_uid)?;
let index_view = IndexView::new(index_uid.into_inner(), &index)?; let index_view = IndexView::new(index_uid.into_inner(), &index)?;
@ -182,12 +181,13 @@ pub async fn get_index(
pub async fn update_index( pub async fn update_index(
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_UPDATE }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_UPDATE }>, Data<IndexScheduler>>,
path: web::Path<String>, index_uid: web::Path<String>,
body: ValidatedJson<UpdateIndexRequest, DeserrError>, body: ValidatedJson<UpdateIndexRequest, DeserrJsonError>,
req: HttpRequest, req: HttpRequest,
analytics: web::Data<dyn Analytics>, analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
debug!("called with params: {:?}", body); debug!("called with params: {:?}", body);
let index_uid = IndexUid::try_from(index_uid.into_inner())?;
let body = body.into_inner(); let body = body.into_inner();
analytics.publish( analytics.publish(
"Index Updated".to_string(), "Index Updated".to_string(),
@ -196,7 +196,7 @@ pub async fn update_index(
); );
let task = KindWithContent::IndexUpdate { let task = KindWithContent::IndexUpdate {
index_uid: path.into_inner(), index_uid: index_uid.into_inner(),
primary_key: body.primary_key, primary_key: body.primary_key,
}; };
@ -211,6 +211,7 @@ pub async fn delete_index(
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_DELETE }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_DELETE }>, Data<IndexScheduler>>,
index_uid: web::Path<String>, index_uid: web::Path<String>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let index_uid = IndexUid::try_from(index_uid.into_inner())?;
let task = KindWithContent::IndexDeletion { index_uid: index_uid.into_inner() }; let task = KindWithContent::IndexDeletion { index_uid: index_uid.into_inner() };
let task: SummarizedTaskView = let task: SummarizedTaskView =
tokio::task::spawn_blocking(move || index_scheduler.register(task)).await??.into(); tokio::task::spawn_blocking(move || index_scheduler.register(task)).await??.into();
@ -224,6 +225,7 @@ pub async fn get_index_stats(
req: HttpRequest, req: HttpRequest,
analytics: web::Data<dyn Analytics>, analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let index_uid = IndexUid::try_from(index_uid.into_inner())?;
analytics.publish("Stats Seen".to_string(), json!({ "per_index_uid": true }), Some(&req)); analytics.publish("Stats Seen".to_string(), json!({ "per_index_uid": true }), Some(&req));
let stats = IndexStats::new((*index_scheduler).clone(), index_uid.into_inner())?; let stats = IndexStats::new((*index_scheduler).clone(), index_uid.into_inner())?;

View File

@ -1,13 +1,14 @@
use std::str::FromStr;
use actix_web::web::Data; use actix_web::web::Data;
use actix_web::{web, HttpRequest, HttpResponse}; use actix_web::{web, HttpRequest, HttpResponse};
use index_scheduler::IndexScheduler; use index_scheduler::IndexScheduler;
use log::debug; use log::debug;
use meilisearch_auth::IndexSearchRules; use meilisearch_auth::IndexSearchRules;
use meilisearch_types::deserr::query_params::Param;
use meilisearch_types::deserr::{DeserrJsonError, DeserrQueryParamError};
use meilisearch_types::error::deserr_codes::*; use meilisearch_types::error::deserr_codes::*;
use meilisearch_types::error::{DeserrError, ResponseError, TakeErrorMessage}; use meilisearch_types::error::ResponseError;
use serde_cs::vec::CS; use meilisearch_types::index_uid::IndexUid;
use meilisearch_types::serde_cs::vec::CS;
use serde_json::Value; use serde_json::Value;
use crate::analytics::{Analytics, SearchAggregator}; use crate::analytics::{Analytics, SearchAggregator};
@ -16,7 +17,6 @@ use crate::extractors::authentication::GuardedData;
use crate::extractors::json::ValidatedJson; use crate::extractors::json::ValidatedJson;
use crate::extractors::query_parameters::QueryParameter; use crate::extractors::query_parameters::QueryParameter;
use crate::extractors::sequential_extractor::SeqHandler; use crate::extractors::sequential_extractor::SeqHandler;
use crate::routes::from_string_to_option_take_error_message;
use crate::search::{ use crate::search::{
perform_search, MatchingStrategy, SearchQuery, DEFAULT_CROP_LENGTH, DEFAULT_CROP_MARKER, perform_search, MatchingStrategy, SearchQuery, DEFAULT_CROP_LENGTH, DEFAULT_CROP_MARKER,
DEFAULT_HIGHLIGHT_POST_TAG, DEFAULT_HIGHLIGHT_PRE_TAG, DEFAULT_SEARCH_LIMIT, DEFAULT_HIGHLIGHT_POST_TAG, DEFAULT_HIGHLIGHT_PRE_TAG, DEFAULT_SEARCH_LIMIT,
@ -31,54 +31,42 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
); );
} }
pub fn parse_usize_take_error_message(
s: &str,
) -> Result<usize, TakeErrorMessage<std::num::ParseIntError>> {
usize::from_str(s).map_err(TakeErrorMessage)
}
pub fn parse_bool_take_error_message(
s: &str,
) -> Result<bool, TakeErrorMessage<std::str::ParseBoolError>> {
s.parse().map_err(TakeErrorMessage)
}
#[derive(Debug, deserr::DeserializeFromValue)] #[derive(Debug, deserr::DeserializeFromValue)]
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
pub struct SearchQueryGet { pub struct SearchQueryGet {
#[deserr(error = DeserrError<InvalidSearchQ>)] #[deserr(default, error = DeserrQueryParamError<InvalidSearchQ>)]
q: Option<String>, q: Option<String>,
#[deserr(error = DeserrError<InvalidSearchOffset>, default = DEFAULT_SEARCH_OFFSET(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage<std::num::ParseIntError>)] #[deserr(default = Param(DEFAULT_SEARCH_OFFSET()), error = DeserrQueryParamError<InvalidSearchOffset>)]
offset: usize, offset: Param<usize>,
#[deserr(error = DeserrError<InvalidSearchLimit>, default = DEFAULT_SEARCH_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage<std::num::ParseIntError>)] #[deserr(default = Param(DEFAULT_SEARCH_LIMIT()), error = DeserrQueryParamError<InvalidSearchLimit>)]
limit: usize, limit: Param<usize>,
#[deserr(error = DeserrError<InvalidSearchPage>, from(&String) = from_string_to_option_take_error_message -> TakeErrorMessage<std::num::ParseIntError>)] #[deserr(default, error = DeserrQueryParamError<InvalidSearchPage>)]
page: Option<usize>, page: Option<Param<usize>>,
#[deserr(error = DeserrError<InvalidSearchHitsPerPage>, from(&String) = from_string_to_option_take_error_message -> TakeErrorMessage<std::num::ParseIntError>)] #[deserr(default, error = DeserrQueryParamError<InvalidSearchHitsPerPage>)]
hits_per_page: Option<usize>, hits_per_page: Option<Param<usize>>,
#[deserr(error = DeserrError<InvalidSearchAttributesToRetrieve>)] #[deserr(default, error = DeserrQueryParamError<InvalidSearchAttributesToRetrieve>)]
attributes_to_retrieve: Option<CS<String>>, attributes_to_retrieve: Option<CS<String>>,
#[deserr(error = DeserrError<InvalidSearchAttributesToCrop>)] #[deserr(default, error = DeserrQueryParamError<InvalidSearchAttributesToCrop>)]
attributes_to_crop: Option<CS<String>>, attributes_to_crop: Option<CS<String>>,
#[deserr(error = DeserrError<InvalidSearchCropLength>, default = DEFAULT_CROP_LENGTH(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage<std::num::ParseIntError>)] #[deserr(default = Param(DEFAULT_CROP_LENGTH()), error = DeserrQueryParamError<InvalidSearchCropLength>)]
crop_length: usize, crop_length: Param<usize>,
#[deserr(error = DeserrError<InvalidSearchAttributesToHighlight>)] #[deserr(default, error = DeserrQueryParamError<InvalidSearchAttributesToHighlight>)]
attributes_to_highlight: Option<CS<String>>, attributes_to_highlight: Option<CS<String>>,
#[deserr(error = DeserrError<InvalidSearchFilter>)] #[deserr(default, error = DeserrQueryParamError<InvalidSearchFilter>)]
filter: Option<String>, filter: Option<String>,
#[deserr(error = DeserrError<InvalidSearchSort>)] #[deserr(default, error = DeserrQueryParamError<InvalidSearchSort>)]
sort: Option<String>, sort: Option<String>,
#[deserr(error = DeserrError<InvalidSearchShowMatchesPosition>, default, from(&String) = parse_bool_take_error_message -> TakeErrorMessage<std::str::ParseBoolError>)] #[deserr(default, error = DeserrQueryParamError<InvalidSearchShowMatchesPosition>)]
show_matches_position: bool, show_matches_position: Param<bool>,
#[deserr(error = DeserrError<InvalidSearchFacets>)] #[deserr(default, error = DeserrQueryParamError<InvalidSearchFacets>)]
facets: Option<CS<String>>, facets: Option<CS<String>>,
#[deserr(error = DeserrError<InvalidSearchHighlightPreTag>, default = DEFAULT_HIGHLIGHT_PRE_TAG())] #[deserr( default = DEFAULT_HIGHLIGHT_PRE_TAG(), error = DeserrQueryParamError<InvalidSearchHighlightPreTag>)]
highlight_pre_tag: String, highlight_pre_tag: String,
#[deserr(error = DeserrError<InvalidSearchHighlightPostTag>, default = DEFAULT_HIGHLIGHT_POST_TAG())] #[deserr( default = DEFAULT_HIGHLIGHT_POST_TAG(), error = DeserrQueryParamError<InvalidSearchHighlightPostTag>)]
highlight_post_tag: String, highlight_post_tag: String,
#[deserr(error = DeserrError<InvalidSearchCropMarker>, default = DEFAULT_CROP_MARKER())] #[deserr(default = DEFAULT_CROP_MARKER(), error = DeserrQueryParamError<InvalidSearchCropMarker>)]
crop_marker: String, crop_marker: String,
#[deserr(error = DeserrError<InvalidSearchMatchingStrategy>, default)] #[deserr(default, error = DeserrQueryParamError<InvalidSearchMatchingStrategy>)]
matching_strategy: MatchingStrategy, matching_strategy: MatchingStrategy,
} }
@ -94,17 +82,17 @@ impl From<SearchQueryGet> for SearchQuery {
Self { Self {
q: other.q, q: other.q,
offset: other.offset, offset: other.offset.0,
limit: other.limit, limit: other.limit.0,
page: other.page, page: other.page.as_deref().copied(),
hits_per_page: other.hits_per_page, hits_per_page: other.hits_per_page.as_deref().copied(),
attributes_to_retrieve: other.attributes_to_retrieve.map(|o| o.into_iter().collect()), attributes_to_retrieve: other.attributes_to_retrieve.map(|o| o.into_iter().collect()),
attributes_to_crop: other.attributes_to_crop.map(|o| o.into_iter().collect()), attributes_to_crop: other.attributes_to_crop.map(|o| o.into_iter().collect()),
crop_length: other.crop_length, crop_length: other.crop_length.0,
attributes_to_highlight: other.attributes_to_highlight.map(|o| o.into_iter().collect()), attributes_to_highlight: other.attributes_to_highlight.map(|o| o.into_iter().collect()),
filter, filter,
sort: other.sort.map(|attr| fix_sort_query_parameters(&attr)), sort: other.sort.map(|attr| fix_sort_query_parameters(&attr)),
show_matches_position: other.show_matches_position, show_matches_position: other.show_matches_position.0,
facets: other.facets.map(|o| o.into_iter().collect()), facets: other.facets.map(|o| o.into_iter().collect()),
highlight_pre_tag: other.highlight_pre_tag, highlight_pre_tag: other.highlight_pre_tag,
highlight_post_tag: other.highlight_post_tag, highlight_post_tag: other.highlight_post_tag,
@ -162,11 +150,13 @@ fn fix_sort_query_parameters(sort_query: &str) -> Vec<String> {
pub async fn search_with_url_query( pub async fn search_with_url_query(
index_scheduler: GuardedData<ActionPolicy<{ actions::SEARCH }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::SEARCH }>, Data<IndexScheduler>>,
index_uid: web::Path<String>, index_uid: web::Path<String>,
params: QueryParameter<SearchQueryGet, DeserrError>, params: QueryParameter<SearchQueryGet, DeserrQueryParamError>,
req: HttpRequest, req: HttpRequest,
analytics: web::Data<dyn Analytics>, analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
debug!("called with params: {:?}", params); debug!("called with params: {:?}", params);
let index_uid = IndexUid::try_from(index_uid.into_inner())?;
let mut query: SearchQuery = params.into_inner().into(); let mut query: SearchQuery = params.into_inner().into();
// Tenant token search_rules. // Tenant token search_rules.
@ -194,10 +184,12 @@ pub async fn search_with_url_query(
pub async fn search_with_post( pub async fn search_with_post(
index_scheduler: GuardedData<ActionPolicy<{ actions::SEARCH }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::SEARCH }>, Data<IndexScheduler>>,
index_uid: web::Path<String>, index_uid: web::Path<String>,
params: ValidatedJson<SearchQuery, DeserrError>, params: ValidatedJson<SearchQuery, DeserrJsonError>,
req: HttpRequest, req: HttpRequest,
analytics: web::Data<dyn Analytics>, analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let index_uid = IndexUid::try_from(index_uid.into_inner())?;
let mut query = params.into_inner(); let mut query = params.into_inner();
debug!("search called with params: {:?}", query); debug!("search called with params: {:?}", query);

View File

@ -2,7 +2,8 @@ use actix_web::web::Data;
use actix_web::{web, HttpRequest, HttpResponse}; use actix_web::{web, HttpRequest, HttpResponse};
use index_scheduler::IndexScheduler; use index_scheduler::IndexScheduler;
use log::debug; use log::debug;
use meilisearch_types::error::{DeserrError, ResponseError}; use meilisearch_types::deserr::DeserrJsonError;
use meilisearch_types::error::ResponseError;
use meilisearch_types::index_uid::IndexUid; use meilisearch_types::index_uid::IndexUid;
use meilisearch_types::settings::{settings, RankingRuleView, Settings, Unchecked}; use meilisearch_types::settings::{settings, RankingRuleView, Settings, Unchecked};
use meilisearch_types::tasks::KindWithContent; use meilisearch_types::tasks::KindWithContent;
@ -40,12 +41,14 @@ macro_rules! make_setting_route {
>, >,
index_uid: web::Path<String>, index_uid: web::Path<String>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let index_uid = IndexUid::try_from(index_uid.into_inner())?;
let new_settings = Settings { $attr: Setting::Reset.into(), ..Default::default() }; let new_settings = Settings { $attr: Setting::Reset.into(), ..Default::default() };
let allow_index_creation = index_scheduler.filters().allow_index_creation; let allow_index_creation = index_scheduler.filters().allow_index_creation;
let index_uid = IndexUid::try_from(index_uid.into_inner())?.into_inner();
let task = KindWithContent::SettingsUpdate { let task = KindWithContent::SettingsUpdate {
index_uid, index_uid: index_uid.to_string(),
new_settings: Box::new(new_settings), new_settings: Box::new(new_settings),
is_deletion: true, is_deletion: true,
allow_index_creation, allow_index_creation,
@ -69,6 +72,8 @@ macro_rules! make_setting_route {
req: HttpRequest, req: HttpRequest,
$analytics_var: web::Data<dyn Analytics>, $analytics_var: web::Data<dyn Analytics>,
) -> std::result::Result<HttpResponse, ResponseError> { ) -> std::result::Result<HttpResponse, ResponseError> {
let index_uid = IndexUid::try_from(index_uid.into_inner())?;
let body = body.into_inner(); let body = body.into_inner();
$analytics(&body, &req); $analytics(&body, &req);
@ -82,9 +87,9 @@ macro_rules! make_setting_route {
}; };
let allow_index_creation = index_scheduler.filters().allow_index_creation; let allow_index_creation = index_scheduler.filters().allow_index_creation;
let index_uid = IndexUid::try_from(index_uid.into_inner())?.into_inner();
let task = KindWithContent::SettingsUpdate { let task = KindWithContent::SettingsUpdate {
index_uid, index_uid: index_uid.to_string(),
new_settings: Box::new(new_settings), new_settings: Box::new(new_settings),
is_deletion: false, is_deletion: false,
allow_index_creation, allow_index_creation,
@ -105,6 +110,8 @@ macro_rules! make_setting_route {
>, >,
index_uid: actix_web::web::Path<String>, index_uid: actix_web::web::Path<String>,
) -> std::result::Result<HttpResponse, ResponseError> { ) -> std::result::Result<HttpResponse, ResponseError> {
let index_uid = IndexUid::try_from(index_uid.into_inner())?;
let index = index_scheduler.index(&index_uid)?; let index = index_scheduler.index(&index_uid)?;
let rtxn = index.read_txn()?; let rtxn = index.read_txn()?;
let settings = settings(&index, &rtxn)?; let settings = settings(&index, &rtxn)?;
@ -130,7 +137,7 @@ make_setting_route!(
"/filterable-attributes", "/filterable-attributes",
put, put,
std::collections::BTreeSet<String>, std::collections::BTreeSet<String>,
meilisearch_types::error::DeserrError< meilisearch_types::deserr::DeserrJsonError<
meilisearch_types::error::deserr_codes::InvalidSettingsFilterableAttributes, meilisearch_types::error::deserr_codes::InvalidSettingsFilterableAttributes,
>, >,
filterable_attributes, filterable_attributes,
@ -156,7 +163,7 @@ make_setting_route!(
"/sortable-attributes", "/sortable-attributes",
put, put,
std::collections::BTreeSet<String>, std::collections::BTreeSet<String>,
meilisearch_types::error::DeserrError< meilisearch_types::deserr::DeserrJsonError<
meilisearch_types::error::deserr_codes::InvalidSettingsSortableAttributes, meilisearch_types::error::deserr_codes::InvalidSettingsSortableAttributes,
>, >,
sortable_attributes, sortable_attributes,
@ -182,7 +189,7 @@ make_setting_route!(
"/displayed-attributes", "/displayed-attributes",
put, put,
Vec<String>, Vec<String>,
meilisearch_types::error::DeserrError< meilisearch_types::deserr::DeserrJsonError<
meilisearch_types::error::deserr_codes::InvalidSettingsDisplayedAttributes, meilisearch_types::error::deserr_codes::InvalidSettingsDisplayedAttributes,
>, >,
displayed_attributes, displayed_attributes,
@ -208,7 +215,7 @@ make_setting_route!(
"/typo-tolerance", "/typo-tolerance",
patch, patch,
meilisearch_types::settings::TypoSettings, meilisearch_types::settings::TypoSettings,
meilisearch_types::error::DeserrError< meilisearch_types::deserr::DeserrJsonError<
meilisearch_types::error::deserr_codes::InvalidSettingsTypoTolerance, meilisearch_types::error::deserr_codes::InvalidSettingsTypoTolerance,
>, >,
typo_tolerance, typo_tolerance,
@ -253,7 +260,7 @@ make_setting_route!(
"/searchable-attributes", "/searchable-attributes",
put, put,
Vec<String>, Vec<String>,
meilisearch_types::error::DeserrError< meilisearch_types::deserr::DeserrJsonError<
meilisearch_types::error::deserr_codes::InvalidSettingsSearchableAttributes, meilisearch_types::error::deserr_codes::InvalidSettingsSearchableAttributes,
>, >,
searchable_attributes, searchable_attributes,
@ -279,7 +286,7 @@ make_setting_route!(
"/stop-words", "/stop-words",
put, put,
std::collections::BTreeSet<String>, std::collections::BTreeSet<String>,
meilisearch_types::error::DeserrError< meilisearch_types::deserr::DeserrJsonError<
meilisearch_types::error::deserr_codes::InvalidSettingsStopWords, meilisearch_types::error::deserr_codes::InvalidSettingsStopWords,
>, >,
stop_words, stop_words,
@ -304,7 +311,7 @@ make_setting_route!(
"/synonyms", "/synonyms",
put, put,
std::collections::BTreeMap<String, Vec<String>>, std::collections::BTreeMap<String, Vec<String>>,
meilisearch_types::error::DeserrError< meilisearch_types::deserr::DeserrJsonError<
meilisearch_types::error::deserr_codes::InvalidSettingsSynonyms, meilisearch_types::error::deserr_codes::InvalidSettingsSynonyms,
>, >,
synonyms, synonyms,
@ -329,7 +336,7 @@ make_setting_route!(
"/distinct-attribute", "/distinct-attribute",
put, put,
String, String,
meilisearch_types::error::DeserrError< meilisearch_types::deserr::DeserrJsonError<
meilisearch_types::error::deserr_codes::InvalidSettingsDistinctAttribute, meilisearch_types::error::deserr_codes::InvalidSettingsDistinctAttribute,
>, >,
distinct_attribute, distinct_attribute,
@ -353,7 +360,7 @@ make_setting_route!(
"/ranking-rules", "/ranking-rules",
put, put,
Vec<meilisearch_types::settings::RankingRuleView>, Vec<meilisearch_types::settings::RankingRuleView>,
meilisearch_types::error::DeserrError< meilisearch_types::deserr::DeserrJsonError<
meilisearch_types::error::deserr_codes::InvalidSettingsRankingRules, meilisearch_types::error::deserr_codes::InvalidSettingsRankingRules,
>, >,
ranking_rules, ranking_rules,
@ -384,7 +391,7 @@ make_setting_route!(
"/faceting", "/faceting",
patch, patch,
meilisearch_types::settings::FacetingSettings, meilisearch_types::settings::FacetingSettings,
meilisearch_types::error::DeserrError< meilisearch_types::deserr::DeserrJsonError<
meilisearch_types::error::deserr_codes::InvalidSettingsFaceting, meilisearch_types::error::deserr_codes::InvalidSettingsFaceting,
>, >,
faceting, faceting,
@ -409,7 +416,7 @@ make_setting_route!(
"/pagination", "/pagination",
patch, patch,
meilisearch_types::settings::PaginationSettings, meilisearch_types::settings::PaginationSettings,
meilisearch_types::error::DeserrError< meilisearch_types::deserr::DeserrJsonError<
meilisearch_types::error::deserr_codes::InvalidSettingsPagination, meilisearch_types::error::deserr_codes::InvalidSettingsPagination,
>, >,
pagination, pagination,
@ -461,10 +468,12 @@ generate_configure!(
pub async fn update_all( pub async fn update_all(
index_scheduler: GuardedData<ActionPolicy<{ actions::SETTINGS_UPDATE }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::SETTINGS_UPDATE }>, Data<IndexScheduler>>,
index_uid: web::Path<String>, index_uid: web::Path<String>,
body: ValidatedJson<Settings<Unchecked>, DeserrError>, body: ValidatedJson<Settings<Unchecked>, DeserrJsonError>,
req: HttpRequest, req: HttpRequest,
analytics: web::Data<dyn Analytics>, analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let index_uid = IndexUid::try_from(index_uid.into_inner())?;
let new_settings = body.into_inner(); let new_settings = body.into_inner();
analytics.publish( analytics.publish(
@ -570,6 +579,8 @@ pub async fn get_all(
index_scheduler: GuardedData<ActionPolicy<{ actions::SETTINGS_GET }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::SETTINGS_GET }>, Data<IndexScheduler>>,
index_uid: web::Path<String>, index_uid: web::Path<String>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let index_uid = IndexUid::try_from(index_uid.into_inner())?;
let index = index_scheduler.index(&index_uid)?; let index = index_scheduler.index(&index_uid)?;
let rtxn = index.read_txn()?; let rtxn = index.read_txn()?;
let new_settings = settings(&index, &rtxn)?; let new_settings = settings(&index, &rtxn)?;
@ -581,6 +592,8 @@ pub async fn delete_all(
index_scheduler: GuardedData<ActionPolicy<{ actions::SETTINGS_UPDATE }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::SETTINGS_UPDATE }>, Data<IndexScheduler>>,
index_uid: web::Path<String>, index_uid: web::Path<String>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let index_uid = IndexUid::try_from(index_uid.into_inner())?;
let new_settings = Settings::cleared().into_unchecked(); let new_settings = Settings::cleared().into_unchecked();
let allow_index_creation = index_scheduler.filters().allow_index_creation; let allow_index_creation = index_scheduler.filters().allow_index_creation;

View File

@ -1,13 +1,11 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::str::FromStr;
use actix_web::web::Data; use actix_web::web::Data;
use actix_web::{web, HttpRequest, HttpResponse}; use actix_web::{web, HttpRequest, HttpResponse};
use index_scheduler::{IndexScheduler, Query}; use index_scheduler::{IndexScheduler, Query};
use log::debug; use log::debug;
use meilisearch_types::error::{ResponseError, TakeErrorMessage}; use meilisearch_types::error::ResponseError;
use meilisearch_types::settings::{Settings, Unchecked}; use meilisearch_types::settings::{Settings, Unchecked};
use meilisearch_types::star_or::StarOr;
use meilisearch_types::tasks::{Kind, Status, Task, TaskId}; use meilisearch_types::tasks::{Kind, Status, Task, TaskId};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
@ -35,37 +33,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
.service(web::scope("/swap-indexes").configure(swap_indexes::configure)); .service(web::scope("/swap-indexes").configure(swap_indexes::configure));
} }
/// Extracts the raw values from the `StarOr` types and const PAGINATION_DEFAULT_LIMIT: usize = 20;
/// return None if a `StarOr::Star` is encountered.
pub fn fold_star_or<T, O>(content: impl IntoIterator<Item = StarOr<T>>) -> Option<O>
where
O: FromIterator<T>,
{
content
.into_iter()
.map(|value| match value {
StarOr::Star => None,
StarOr::Other(val) => Some(val),
})
.collect()
}
pub fn from_string_to_option<T, E>(input: &str) -> Result<Option<T>, E>
where
T: FromStr<Err = E>,
{
Ok(Some(input.parse()?))
}
pub fn from_string_to_option_take_error_message<T, E>(
input: &str,
) -> Result<Option<T>, TakeErrorMessage<E>>
where
T: FromStr<Err = E>,
{
Ok(Some(input.parse().map_err(TakeErrorMessage)?))
}
const PAGINATION_DEFAULT_LIMIT: fn() -> usize = || 20;
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]

View File

@ -2,8 +2,10 @@ use actix_web::web::Data;
use actix_web::{web, HttpRequest, HttpResponse}; use actix_web::{web, HttpRequest, HttpResponse};
use deserr::DeserializeFromValue; use deserr::DeserializeFromValue;
use index_scheduler::IndexScheduler; use index_scheduler::IndexScheduler;
use meilisearch_types::deserr::DeserrJsonError;
use meilisearch_types::error::deserr_codes::InvalidSwapIndexes; use meilisearch_types::error::deserr_codes::InvalidSwapIndexes;
use meilisearch_types::error::{DeserrError, ResponseError}; use meilisearch_types::error::ResponseError;
use meilisearch_types::index_uid::IndexUid;
use meilisearch_types::tasks::{IndexSwap, KindWithContent}; use meilisearch_types::tasks::{IndexSwap, KindWithContent};
use serde_json::json; use serde_json::json;
@ -20,15 +22,15 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
} }
#[derive(DeserializeFromValue, Debug, Clone, PartialEq, Eq)] #[derive(DeserializeFromValue, Debug, Clone, PartialEq, Eq)]
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
pub struct SwapIndexesPayload { pub struct SwapIndexesPayload {
#[deserr(error = DeserrError<InvalidSwapIndexes>)] #[deserr(error = DeserrJsonError<InvalidSwapIndexes>, missing_field_error = DeserrJsonError::missing_swap_indexes)]
indexes: Vec<String>, indexes: Vec<IndexUid>,
} }
pub async fn swap_indexes( pub async fn swap_indexes(
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_SWAP }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_SWAP }>, Data<IndexScheduler>>,
params: ValidatedJson<Vec<SwapIndexesPayload>, DeserrError>, params: ValidatedJson<Vec<SwapIndexesPayload>, DeserrJsonError>,
req: HttpRequest, req: HttpRequest,
analytics: web::Data<dyn Analytics>, analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
@ -44,6 +46,7 @@ pub async fn swap_indexes(
let mut swaps = vec![]; let mut swaps = vec![];
for SwapIndexesPayload { indexes } in params.into_iter() { for SwapIndexesPayload { indexes } in params.into_iter() {
// TODO: switch to deserr
let (lhs, rhs) = match indexes.as_slice() { let (lhs, rhs) = match indexes.as_slice() {
[lhs, rhs] => (lhs, rhs), [lhs, rhs] => (lhs, rhs),
_ => { _ => {
@ -53,7 +56,7 @@ pub async fn swap_indexes(
if !search_rules.is_index_authorized(lhs) || !search_rules.is_index_authorized(rhs) { if !search_rules.is_index_authorized(lhs) || !search_rules.is_index_authorized(rhs) {
return Err(AuthenticationError::InvalidToken.into()); return Err(AuthenticationError::InvalidToken.into());
} }
swaps.push(IndexSwap { indexes: (lhs.clone(), rhs.clone()) }); swaps.push(IndexSwap { indexes: (lhs.to_string(), rhs.to_string()) });
} }
let task = KindWithContent::IndexSwap { swaps }; let task = KindWithContent::IndexSwap { swaps };

View File

@ -1,34 +1,32 @@
use std::num::ParseIntError;
use std::str::FromStr;
use actix_web::web::Data; use actix_web::web::Data;
use actix_web::{web, HttpRequest, HttpResponse}; use actix_web::{web, HttpRequest, HttpResponse};
use deserr::DeserializeFromValue; use deserr::DeserializeFromValue;
use index_scheduler::{IndexScheduler, Query, TaskId}; use index_scheduler::{IndexScheduler, Query, TaskId};
use meilisearch_types::deserr::query_params::Param;
use meilisearch_types::deserr::DeserrQueryParamError;
use meilisearch_types::error::deserr_codes::*; use meilisearch_types::error::deserr_codes::*;
use meilisearch_types::error::{DeserrError, ResponseError, TakeErrorMessage}; use meilisearch_types::error::{InvalidTaskDateError, ResponseError};
use meilisearch_types::index_uid::IndexUid; use meilisearch_types::index_uid::IndexUid;
use meilisearch_types::settings::{Settings, Unchecked}; use meilisearch_types::settings::{Settings, Unchecked};
use meilisearch_types::star_or::StarOr; use meilisearch_types::star_or::{OptionStarOr, OptionStarOrList};
use meilisearch_types::tasks::{ use meilisearch_types::tasks::{
serialize_duration, Details, IndexSwap, Kind, KindWithContent, Status, Task, serialize_duration, Details, IndexSwap, Kind, KindWithContent, Status, Task,
}; };
use serde::{Deserialize, Serialize}; use serde::Serialize;
use serde_cs::vec::CS;
use serde_json::json; use serde_json::json;
use time::format_description::well_known::Rfc3339; use time::format_description::well_known::Rfc3339;
use time::macros::format_description; use time::macros::format_description;
use time::{Date, Duration, OffsetDateTime, Time}; use time::{Date, Duration, OffsetDateTime, Time};
use tokio::task; use tokio::task;
use super::{fold_star_or, SummarizedTaskView}; use super::SummarizedTaskView;
use crate::analytics::Analytics; use crate::analytics::Analytics;
use crate::extractors::authentication::policies::*; use crate::extractors::authentication::policies::*;
use crate::extractors::authentication::GuardedData; use crate::extractors::authentication::GuardedData;
use crate::extractors::query_parameters::QueryParameter; use crate::extractors::query_parameters::QueryParameter;
use crate::extractors::sequential_extractor::SeqHandler; use crate::extractors::sequential_extractor::SeqHandler;
const DEFAULT_LIMIT: fn() -> u32 = || 20; const DEFAULT_LIMIT: u32 = 20;
pub fn configure(cfg: &mut web::ServiceConfig) { pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service( cfg.service(
@ -164,162 +162,157 @@ impl From<Details> for DetailsView {
} }
} }
fn parse_option_cs<T: FromStr>( #[derive(Debug, DeserializeFromValue)]
s: Option<CS<String>>, #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
) -> Result<Option<Vec<T>>, TakeErrorMessage<T::Err>> { pub struct TasksFilterQuery {
if let Some(s) = s { #[deserr(default = Param(DEFAULT_LIMIT), error = DeserrQueryParamError<InvalidTaskLimit>)]
s.into_iter() pub limit: Param<u32>,
.map(|s| T::from_str(&s)) #[deserr(default, error = DeserrQueryParamError<InvalidTaskFrom>)]
.collect::<Result<Vec<T>, T::Err>>() pub from: Option<Param<TaskId>>,
.map_err(TakeErrorMessage)
.map(Some) #[deserr(default, error = DeserrQueryParamError<InvalidTaskUids>)]
} else { pub uids: OptionStarOrList<u32>,
Ok(None) #[deserr(default, error = DeserrQueryParamError<InvalidTaskCanceledBy>)]
} pub canceled_by: OptionStarOrList<u32>,
#[deserr(default, error = DeserrQueryParamError<InvalidTaskTypes>)]
pub types: OptionStarOrList<Kind>,
#[deserr(default, error = DeserrQueryParamError<InvalidTaskStatuses>)]
pub statuses: OptionStarOrList<Status>,
#[deserr(default, error = DeserrQueryParamError<InvalidIndexUid>)]
pub index_uids: OptionStarOrList<IndexUid>,
#[deserr(default, error = DeserrQueryParamError<InvalidTaskAfterEnqueuedAt>, from(OptionStarOr<String>) = deserialize_date_after -> InvalidTaskDateError)]
pub after_enqueued_at: OptionStarOr<OffsetDateTime>,
#[deserr(default, error = DeserrQueryParamError<InvalidTaskBeforeEnqueuedAt>, from(OptionStarOr<String>) = deserialize_date_before -> InvalidTaskDateError)]
pub before_enqueued_at: OptionStarOr<OffsetDateTime>,
#[deserr(default, error = DeserrQueryParamError<InvalidTaskAfterStartedAt>, from(OptionStarOr<String>) = deserialize_date_after -> InvalidTaskDateError)]
pub after_started_at: OptionStarOr<OffsetDateTime>,
#[deserr(default, error = DeserrQueryParamError<InvalidTaskBeforeStartedAt>, from(OptionStarOr<String>) = deserialize_date_before -> InvalidTaskDateError)]
pub before_started_at: OptionStarOr<OffsetDateTime>,
#[deserr(default, error = DeserrQueryParamError<InvalidTaskAfterFinishedAt>, from(OptionStarOr<String>) = deserialize_date_after -> InvalidTaskDateError)]
pub after_finished_at: OptionStarOr<OffsetDateTime>,
#[deserr(default, error = DeserrQueryParamError<InvalidTaskBeforeFinishedAt>, from(OptionStarOr<String>) = deserialize_date_before -> InvalidTaskDateError)]
pub before_finished_at: OptionStarOr<OffsetDateTime>,
} }
fn parse_option_cs_star_or<T: FromStr>( impl TasksFilterQuery {
s: Option<CS<StarOr<String>>>, fn into_query(self) -> Query {
) -> Result<Option<Vec<T>>, TakeErrorMessage<T::Err>> { Query {
if let Some(s) = s.and_then(fold_star_or) as Option<Vec<String>> { limit: Some(self.limit.0),
s.into_iter() from: self.from.as_deref().copied(),
.map(|s| T::from_str(&s)) statuses: self.statuses.merge_star_and_none(),
.collect::<Result<Vec<T>, T::Err>>() types: self.types.merge_star_and_none(),
.map_err(TakeErrorMessage) index_uids: self.index_uids.map(|x| x.to_string()).merge_star_and_none(),
.map(Some) uids: self.uids.merge_star_and_none(),
} else { canceled_by: self.canceled_by.merge_star_and_none(),
Ok(None) before_enqueued_at: self.before_enqueued_at.merge_star_and_none(),
} after_enqueued_at: self.after_enqueued_at.merge_star_and_none(),
} before_started_at: self.before_started_at.merge_star_and_none(),
fn parse_option_str<T: FromStr>(s: Option<String>) -> Result<Option<T>, TakeErrorMessage<T::Err>> { after_started_at: self.after_started_at.merge_star_and_none(),
if let Some(s) = s { before_finished_at: self.before_finished_at.merge_star_and_none(),
T::from_str(&s).map_err(TakeErrorMessage).map(Some) after_finished_at: self.after_finished_at.merge_star_and_none(),
} else { }
Ok(None)
} }
} }
fn parse_str<T: FromStr>(s: String) -> Result<T, TakeErrorMessage<T::Err>> { impl TaskDeletionOrCancelationQuery {
T::from_str(&s).map_err(TakeErrorMessage) fn is_empty(&self) -> bool {
matches!(
self,
TaskDeletionOrCancelationQuery {
uids: OptionStarOrList::None,
canceled_by: OptionStarOrList::None,
types: OptionStarOrList::None,
statuses: OptionStarOrList::None,
index_uids: OptionStarOrList::None,
after_enqueued_at: OptionStarOr::None,
before_enqueued_at: OptionStarOr::None,
after_started_at: OptionStarOr::None,
before_started_at: OptionStarOr::None,
after_finished_at: OptionStarOr::None,
before_finished_at: OptionStarOr::None
}
)
}
} }
#[derive(Debug, DeserializeFromValue)] #[derive(Debug, DeserializeFromValue)]
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
pub struct TasksFilterQuery {
#[deserr(error = DeserrError<InvalidTaskLimit>, default = DEFAULT_LIMIT(), from(String) = parse_str::<u32> -> TakeErrorMessage<ParseIntError>)]
pub limit: u32,
#[deserr(error = DeserrError<InvalidTaskFrom>, from(Option<String>) = parse_option_str::<TaskId> -> TakeErrorMessage<ParseIntError>)]
pub from: Option<TaskId>,
#[deserr(error = DeserrError<InvalidTaskUids>, from(Option<CS<String>>) = parse_option_cs::<u32> -> TakeErrorMessage<ParseIntError>)]
pub uids: Option<Vec<u32>>,
#[deserr(error = DeserrError<InvalidTaskCanceledBy>, from(Option<CS<String>>) = parse_option_cs::<u32> -> TakeErrorMessage<ParseIntError>)]
pub canceled_by: Option<Vec<u32>>,
#[deserr(error = DeserrError<InvalidTaskTypes>, default = None, from(Option<CS<StarOr<String>>>) = parse_option_cs_star_or::<Kind> -> TakeErrorMessage<ResponseError>)]
pub types: Option<Vec<Kind>>,
#[deserr(error = DeserrError<InvalidTaskStatuses>, default = None, from(Option<CS<StarOr<String>>>) = parse_option_cs_star_or::<Status> -> TakeErrorMessage<ResponseError>)]
pub statuses: Option<Vec<Status>>,
#[deserr(error = DeserrError<InvalidIndexUid>, default = None, from(Option<CS<StarOr<String>>>) = parse_option_cs_star_or::<IndexUid> -> TakeErrorMessage<ResponseError>)]
pub index_uids: Option<Vec<IndexUid>>,
#[deserr(error = DeserrError<InvalidTaskAfterEnqueuedAt>, default = None, from(Option<String>) = deserialize_date_after -> TakeErrorMessage<InvalidTaskDateError>)]
pub after_enqueued_at: Option<OffsetDateTime>,
#[deserr(error = DeserrError<InvalidTaskBeforeEnqueuedAt>, default = None, from(Option<String>) = deserialize_date_before -> TakeErrorMessage<InvalidTaskDateError>)]
pub before_enqueued_at: Option<OffsetDateTime>,
#[deserr(error = DeserrError<InvalidTaskAfterStartedAt>, default = None, from(Option<String>) = deserialize_date_after -> TakeErrorMessage<InvalidTaskDateError>)]
pub after_started_at: Option<OffsetDateTime>,
#[deserr(error = DeserrError<InvalidTaskBeforeStartedAt>, default = None, from(Option<String>) = deserialize_date_before -> TakeErrorMessage<InvalidTaskDateError>)]
pub before_started_at: Option<OffsetDateTime>,
#[deserr(error = DeserrError<InvalidTaskAfterFinishedAt>, default = None, from(Option<String>) = deserialize_date_after -> TakeErrorMessage<InvalidTaskDateError>)]
pub after_finished_at: Option<OffsetDateTime>,
#[deserr(error = DeserrError<InvalidTaskBeforeFinishedAt>, default = None, from(Option<String>) = deserialize_date_before -> TakeErrorMessage<InvalidTaskDateError>)]
pub before_finished_at: Option<OffsetDateTime>,
}
#[derive(Deserialize, Debug, DeserializeFromValue)]
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)]
pub struct TaskDeletionOrCancelationQuery { pub struct TaskDeletionOrCancelationQuery {
#[deserr(error = DeserrError<InvalidTaskUids>, from(Option<CS<String>>) = parse_option_cs::<u32> -> TakeErrorMessage<ParseIntError>)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskUids>)]
pub uids: Option<Vec<u32>>, pub uids: OptionStarOrList<u32>,
#[deserr(error = DeserrError<InvalidTaskCanceledBy>, from(Option<CS<String>>) = parse_option_cs::<u32> -> TakeErrorMessage<ParseIntError>)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskCanceledBy>)]
pub canceled_by: Option<Vec<u32>>, pub canceled_by: OptionStarOrList<u32>,
#[deserr(error = DeserrError<InvalidTaskTypes>, default = None, from(Option<CS<StarOr<String>>>) = parse_option_cs_star_or::<Kind> -> TakeErrorMessage<ResponseError>)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskTypes>)]
pub types: Option<Vec<Kind>>, pub types: OptionStarOrList<Kind>,
#[deserr(error = DeserrError<InvalidTaskStatuses>, default = None, from(Option<CS<StarOr<String>>>) = parse_option_cs_star_or::<Status> -> TakeErrorMessage<ResponseError>)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskStatuses>)]
pub statuses: Option<Vec<Status>>, pub statuses: OptionStarOrList<Status>,
#[deserr(error = DeserrError<InvalidIndexUid>, default = None, from(Option<CS<StarOr<String>>>) = parse_option_cs_star_or::<IndexUid> -> TakeErrorMessage<ResponseError>)] #[deserr(default, error = DeserrQueryParamError<InvalidIndexUid>)]
pub index_uids: Option<Vec<IndexUid>>, pub index_uids: OptionStarOrList<IndexUid>,
#[deserr(error = DeserrError<InvalidTaskAfterEnqueuedAt>, default = None, from(Option<String>) = deserialize_date_after -> TakeErrorMessage<InvalidTaskDateError>)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskAfterEnqueuedAt>, from(OptionStarOr<String>) = deserialize_date_after -> InvalidTaskDateError)]
pub after_enqueued_at: Option<OffsetDateTime>, pub after_enqueued_at: OptionStarOr<OffsetDateTime>,
#[deserr(error = DeserrError<InvalidTaskBeforeEnqueuedAt>, default = None, from(Option<String>) = deserialize_date_before -> TakeErrorMessage<InvalidTaskDateError>)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskBeforeEnqueuedAt>, from(OptionStarOr<String>) = deserialize_date_before -> InvalidTaskDateError)]
pub before_enqueued_at: Option<OffsetDateTime>, pub before_enqueued_at: OptionStarOr<OffsetDateTime>,
#[deserr(error = DeserrError<InvalidTaskAfterStartedAt>, default = None, from(Option<String>) = deserialize_date_after -> TakeErrorMessage<InvalidTaskDateError>)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskAfterStartedAt>, from(OptionStarOr<String>) = deserialize_date_after -> InvalidTaskDateError)]
pub after_started_at: Option<OffsetDateTime>, pub after_started_at: OptionStarOr<OffsetDateTime>,
#[deserr(error = DeserrError<InvalidTaskBeforeStartedAt>, default = None, from(Option<String>) = deserialize_date_before -> TakeErrorMessage<InvalidTaskDateError>)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskBeforeStartedAt>, from(OptionStarOr<String>) = deserialize_date_before -> InvalidTaskDateError)]
pub before_started_at: Option<OffsetDateTime>, pub before_started_at: OptionStarOr<OffsetDateTime>,
#[deserr(error = DeserrError<InvalidTaskAfterFinishedAt>, default = None, from(Option<String>) = deserialize_date_after -> TakeErrorMessage<InvalidTaskDateError>)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskAfterFinishedAt>, from(OptionStarOr<String>) = deserialize_date_after -> InvalidTaskDateError)]
pub after_finished_at: Option<OffsetDateTime>, pub after_finished_at: OptionStarOr<OffsetDateTime>,
#[deserr(error = DeserrError<InvalidTaskBeforeFinishedAt>, default = None, from(Option<String>) = deserialize_date_before -> TakeErrorMessage<InvalidTaskDateError>)] #[deserr(default, error = DeserrQueryParamError<InvalidTaskBeforeFinishedAt>, from(OptionStarOr<String>) = deserialize_date_before -> InvalidTaskDateError)]
pub before_finished_at: Option<OffsetDateTime>, pub before_finished_at: OptionStarOr<OffsetDateTime>,
}
impl TaskDeletionOrCancelationQuery {
fn into_query(self) -> Query {
Query {
limit: None,
from: None,
statuses: self.statuses.merge_star_and_none(),
types: self.types.merge_star_and_none(),
index_uids: self.index_uids.map(|x| x.to_string()).merge_star_and_none(),
uids: self.uids.merge_star_and_none(),
canceled_by: self.canceled_by.merge_star_and_none(),
before_enqueued_at: self.before_enqueued_at.merge_star_and_none(),
after_enqueued_at: self.after_enqueued_at.merge_star_and_none(),
before_started_at: self.before_started_at.merge_star_and_none(),
after_started_at: self.after_started_at.merge_star_and_none(),
before_finished_at: self.before_finished_at.merge_star_and_none(),
after_finished_at: self.after_finished_at.merge_star_and_none(),
}
}
} }
async fn cancel_tasks( async fn cancel_tasks(
index_scheduler: GuardedData<ActionPolicy<{ actions::TASKS_CANCEL }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::TASKS_CANCEL }>, Data<IndexScheduler>>,
params: QueryParameter<TaskDeletionOrCancelationQuery, DeserrError>, params: QueryParameter<TaskDeletionOrCancelationQuery, DeserrQueryParamError>,
req: HttpRequest, req: HttpRequest,
analytics: web::Data<dyn Analytics>, analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let TaskDeletionOrCancelationQuery { let params = params.into_inner();
types,
uids, if params.is_empty() {
canceled_by, return Err(index_scheduler::Error::TaskCancelationWithEmptyQuery.into());
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( analytics.publish(
"Tasks Canceled".to_string(), "Tasks Canceled".to_string(),
json!({ json!({
"filtered_by_uid": uids.is_some(), "filtered_by_uid": params.uids.is_some(),
"filtered_by_index_uid": index_uids.is_some(), "filtered_by_index_uid": params.index_uids.is_some(),
"filtered_by_type": types.is_some(), "filtered_by_type": params.types.is_some(),
"filtered_by_status": statuses.is_some(), "filtered_by_status": params.statuses.is_some(),
"filtered_by_canceled_by": canceled_by.is_some(), "filtered_by_canceled_by": params.canceled_by.is_some(),
"filtered_by_before_enqueued_at": before_enqueued_at.is_some(), "filtered_by_before_enqueued_at": params.before_enqueued_at.is_some(),
"filtered_by_after_enqueued_at": after_enqueued_at.is_some(), "filtered_by_after_enqueued_at": params.after_enqueued_at.is_some(),
"filtered_by_before_started_at": before_started_at.is_some(), "filtered_by_before_started_at": params.before_started_at.is_some(),
"filtered_by_after_started_at": after_started_at.is_some(), "filtered_by_after_started_at": params.after_started_at.is_some(),
"filtered_by_before_finished_at": before_finished_at.is_some(), "filtered_by_before_finished_at": params.before_finished_at.is_some(),
"filtered_by_after_finished_at": after_finished_at.is_some(), "filtered_by_after_finished_at": params.after_finished_at.is_some(),
}), }),
Some(&req), Some(&req),
); );
let query = Query { let query = params.into_query();
limit: None,
from: None,
statuses,
types,
index_uids: index_uids.map(|xs| xs.into_iter().map(|s| s.to_string()).collect()),
uids,
canceled_by,
before_enqueued_at,
after_enqueued_at,
before_started_at,
after_started_at,
before_finished_at,
after_finished_at,
};
if query.is_empty() {
return Err(index_scheduler::Error::TaskCancelationWithEmptyQuery.into());
}
let tasks = index_scheduler.get_task_ids_from_authorized_indexes( let tasks = index_scheduler.get_task_ids_from_authorized_indexes(
&index_scheduler.read_txn()?, &index_scheduler.read_txn()?,
@ -337,62 +330,34 @@ async fn cancel_tasks(
async fn delete_tasks( async fn delete_tasks(
index_scheduler: GuardedData<ActionPolicy<{ actions::TASKS_DELETE }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::TASKS_DELETE }>, Data<IndexScheduler>>,
params: QueryParameter<TaskDeletionOrCancelationQuery, DeserrError>, params: QueryParameter<TaskDeletionOrCancelationQuery, DeserrQueryParamError>,
req: HttpRequest, req: HttpRequest,
analytics: web::Data<dyn Analytics>, analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let TaskDeletionOrCancelationQuery { let params = params.into_inner();
types,
uids,
canceled_by,
statuses,
index_uids,
after_enqueued_at, if params.is_empty() {
before_enqueued_at, return Err(index_scheduler::Error::TaskDeletionWithEmptyQuery.into());
after_started_at, }
before_started_at,
after_finished_at,
before_finished_at,
} = params.into_inner();
analytics.publish( analytics.publish(
"Tasks Deleted".to_string(), "Tasks Deleted".to_string(),
json!({ json!({
"filtered_by_uid": uids.is_some(), "filtered_by_uid": params.uids.is_some(),
"filtered_by_index_uid": index_uids.is_some(), "filtered_by_index_uid": params.index_uids.is_some(),
"filtered_by_type": types.is_some(), "filtered_by_type": params.types.is_some(),
"filtered_by_status": statuses.is_some(), "filtered_by_status": params.statuses.is_some(),
"filtered_by_canceled_by": canceled_by.is_some(), "filtered_by_canceled_by": params.canceled_by.is_some(),
"filtered_by_before_enqueued_at": before_enqueued_at.is_some(), "filtered_by_before_enqueued_at": params.before_enqueued_at.is_some(),
"filtered_by_after_enqueued_at": after_enqueued_at.is_some(), "filtered_by_after_enqueued_at": params.after_enqueued_at.is_some(),
"filtered_by_before_started_at": before_started_at.is_some(), "filtered_by_before_started_at": params.before_started_at.is_some(),
"filtered_by_after_started_at": after_started_at.is_some(), "filtered_by_after_started_at": params.after_started_at.is_some(),
"filtered_by_before_finished_at": before_finished_at.is_some(), "filtered_by_before_finished_at": params.before_finished_at.is_some(),
"filtered_by_after_finished_at": after_finished_at.is_some(), "filtered_by_after_finished_at": params.after_finished_at.is_some(),
}), }),
Some(&req), Some(&req),
); );
let query = params.into_query();
let query = Query {
limit: None,
from: None,
statuses,
types,
index_uids: index_uids.map(|xs| xs.into_iter().map(|s| s.to_string()).collect()),
uids,
canceled_by,
after_enqueued_at,
before_enqueued_at,
after_started_at,
before_started_at,
after_finished_at,
before_finished_at,
};
if query.is_empty() {
return Err(index_scheduler::Error::TaskDeletionWithEmptyQuery.into());
}
let tasks = index_scheduler.get_task_ids_from_authorized_indexes( let tasks = index_scheduler.get_task_ids_from_authorized_indexes(
&index_scheduler.read_txn()?, &index_scheduler.read_txn()?,
@ -418,47 +383,17 @@ pub struct AllTasks {
async fn get_tasks( async fn get_tasks(
index_scheduler: GuardedData<ActionPolicy<{ actions::TASKS_GET }>, Data<IndexScheduler>>, index_scheduler: GuardedData<ActionPolicy<{ actions::TASKS_GET }>, Data<IndexScheduler>>,
params: QueryParameter<TasksFilterQuery, DeserrError>, params: QueryParameter<TasksFilterQuery, DeserrQueryParamError>,
req: HttpRequest, req: HttpRequest,
analytics: web::Data<dyn Analytics>, analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let params = params.into_inner(); let mut params = params.into_inner();
analytics.get_tasks(&params, &req); analytics.get_tasks(&params, &req);
let TasksFilterQuery {
types,
uids,
canceled_by,
statuses,
index_uids,
limit,
from,
after_enqueued_at,
before_enqueued_at,
after_started_at,
before_started_at,
after_finished_at,
before_finished_at,
} = params;
// We +1 just to know if there is more after this "page" or not. // We +1 just to know if there is more after this "page" or not.
let limit = limit.saturating_add(1); params.limit.0 = params.limit.0.saturating_add(1);
let limit = params.limit.0;
let query = index_scheduler::Query { let query = params.into_query();
limit: Some(limit),
from,
statuses,
types,
index_uids: index_uids.map(|xs| xs.into_iter().map(|s| s.to_string()).collect()),
uids,
canceled_by,
before_enqueued_at,
after_enqueued_at,
before_started_at,
after_started_at,
before_finished_at,
after_finished_at,
};
let mut tasks_results: Vec<TaskView> = index_scheduler let mut tasks_results: Vec<TaskView> = index_scheduler
.get_tasks_from_authorized_indexes( .get_tasks_from_authorized_indexes(
@ -524,7 +459,7 @@ pub enum DeserializeDateOption {
pub fn deserialize_date( pub fn deserialize_date(
value: &str, value: &str,
option: DeserializeDateOption, option: DeserializeDateOption,
) -> std::result::Result<OffsetDateTime, TakeErrorMessage<InvalidTaskDateError>> { ) -> std::result::Result<OffsetDateTime, InvalidTaskDateError> {
// We can't parse using time's rfc3339 format, since then we won't know what part of the // 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 // datetime was not explicitly specified, and thus we won't be able to increment it to the
// next step. // next step.
@ -546,54 +481,41 @@ pub fn deserialize_date(
} }
} }
} else { } else {
Err(TakeErrorMessage(InvalidTaskDateError(value.to_owned()))) Err(InvalidTaskDateError(value.to_owned()))
} }
} }
pub fn deserialize_date_before(
value: Option<String>,
) -> std::result::Result<Option<OffsetDateTime>, TakeErrorMessage<InvalidTaskDateError>> {
if let Some(value) = value {
let date = deserialize_date(&value, DeserializeDateOption::Before)?;
Ok(Some(date))
} else {
Ok(None)
}
}
pub fn deserialize_date_after( pub fn deserialize_date_after(
value: Option<String>, value: OptionStarOr<String>,
) -> std::result::Result<Option<OffsetDateTime>, TakeErrorMessage<InvalidTaskDateError>> { ) -> std::result::Result<OptionStarOr<OffsetDateTime>, InvalidTaskDateError> {
if let Some(value) = value { value.try_map(|x| deserialize_date(&x, DeserializeDateOption::After))
let date = deserialize_date(&value, DeserializeDateOption::After)?;
Ok(Some(date))
} else {
Ok(None)
}
} }
pub fn deserialize_date_before(
#[derive(Debug)] value: OptionStarOr<String>,
pub struct InvalidTaskDateError(String); ) -> std::result::Result<OptionStarOr<OffsetDateTime>, InvalidTaskDateError> {
impl std::fmt::Display for InvalidTaskDateError { value.try_map(|x| deserialize_date(&x, DeserializeDateOption::Before))
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)] #[cfg(test)]
mod tests { mod tests {
use deserr::DeserializeFromValue; use deserr::DeserializeFromValue;
use meili_snap::snapshot; use meili_snap::snapshot;
use meilisearch_types::error::DeserrError; use meilisearch_types::deserr::DeserrQueryParamError;
use meilisearch_types::error::{Code, ResponseError};
use crate::extractors::query_parameters::QueryParameter;
use crate::routes::tasks::{TaskDeletionOrCancelationQuery, TasksFilterQuery}; use crate::routes::tasks::{TaskDeletionOrCancelationQuery, TasksFilterQuery};
fn deserr_query_params<T>(j: &str) -> Result<T, actix_web::Error> fn deserr_query_params<T>(j: &str) -> Result<T, ResponseError>
where where
T: DeserializeFromValue<DeserrError>, T: DeserializeFromValue<DeserrQueryParamError>,
{ {
QueryParameter::<T, DeserrError>::from_query(j).map(|p| p.0) let value = serde_urlencoded::from_str::<serde_json::Value>(j)
.map_err(|e| ResponseError::from_msg(e.to_string(), Code::BadRequest))?;
match deserr::deserialize::<_, _, DeserrQueryParamError>(value) {
Ok(data) => Ok(data),
Err(e) => Err(ResponseError::from(e)),
}
} }
#[test] #[test]
@ -602,65 +524,113 @@ mod tests {
let params = "afterEnqueuedAt=2021-12-03&beforeEnqueuedAt=2021-12-03&afterStartedAt=2021-12-03&beforeStartedAt=2021-12-03&afterFinishedAt=2021-12-03&beforeFinishedAt=2021-12-03"; let 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::<TaskDeletionOrCancelationQuery>(params).unwrap(); let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap();
snapshot!(format!("{:?}", query.after_enqueued_at.unwrap()), @"2021-12-04 0:00:00.0 +00:00:00"); snapshot!(format!("{:?}", query.after_enqueued_at), @"Other(2021-12-04 0:00:00.0 +00:00:00)");
snapshot!(format!("{:?}", query.before_enqueued_at.unwrap()), @"2021-12-03 0:00:00.0 +00:00:00"); snapshot!(format!("{:?}", query.before_enqueued_at), @"Other(2021-12-03 0:00:00.0 +00:00:00)");
snapshot!(format!("{:?}", query.after_started_at.unwrap()), @"2021-12-04 0:00:00.0 +00:00:00"); snapshot!(format!("{:?}", query.after_started_at), @"Other(2021-12-04 0:00:00.0 +00:00:00)");
snapshot!(format!("{:?}", query.before_started_at.unwrap()), @"2021-12-03 0:00:00.0 +00:00:00"); snapshot!(format!("{:?}", query.before_started_at), @"Other(2021-12-03 0:00:00.0 +00:00:00)");
snapshot!(format!("{:?}", query.after_finished_at.unwrap()), @"2021-12-04 0:00:00.0 +00:00:00"); snapshot!(format!("{:?}", query.after_finished_at), @"Other(2021-12-04 0:00:00.0 +00:00:00)");
snapshot!(format!("{:?}", query.before_finished_at.unwrap()), @"2021-12-03 0:00:00.0 +00:00:00"); snapshot!(format!("{:?}", query.before_finished_at), @"Other(2021-12-03 0:00:00.0 +00:00:00)");
} }
{ {
let params = let params =
"afterEnqueuedAt=2021-12-03T23:45:23Z&beforeEnqueuedAt=2021-12-03T23:45:23Z"; "afterEnqueuedAt=2021-12-03T23:45:23Z&beforeEnqueuedAt=2021-12-03T23:45:23Z";
let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap(); let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap();
snapshot!(format!("{:?}", query.after_enqueued_at.unwrap()), @"2021-12-03 23:45:23.0 +00:00:00"); snapshot!(format!("{:?}", query.after_enqueued_at), @"Other(2021-12-03 23:45:23.0 +00:00:00)");
snapshot!(format!("{:?}", query.before_enqueued_at.unwrap()), @"2021-12-03 23:45:23.0 +00:00:00"); snapshot!(format!("{:?}", query.before_enqueued_at), @"Other(2021-12-03 23:45:23.0 +00:00:00)");
} }
{ {
let params = "afterEnqueuedAt=1997-11-12T09:55:06-06:20"; let params = "afterEnqueuedAt=1997-11-12T09:55:06-06:20";
let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap(); let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap();
snapshot!(format!("{:?}", query.after_enqueued_at.unwrap()), @"1997-11-12 9:55:06.0 -06:20:00"); snapshot!(format!("{:?}", query.after_enqueued_at), @"Other(1997-11-12 9:55:06.0 -06:20:00)");
} }
{ {
let params = "afterEnqueuedAt=1997-11-12T09:55:06%2B00:00"; let params = "afterEnqueuedAt=1997-11-12T09:55:06%2B00:00";
let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap(); let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap();
snapshot!(format!("{:?}", query.after_enqueued_at.unwrap()), @"1997-11-12 9:55:06.0 +00:00:00"); snapshot!(format!("{:?}", query.after_enqueued_at), @"Other(1997-11-12 9:55:06.0 +00:00:00)");
} }
{ {
let params = "afterEnqueuedAt=1997-11-12T09:55:06.200000300Z"; let params = "afterEnqueuedAt=1997-11-12T09:55:06.200000300Z";
let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap(); let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap();
snapshot!(format!("{:?}", query.after_enqueued_at.unwrap()), @"1997-11-12 9:55:06.2000003 +00:00:00"); snapshot!(format!("{:?}", query.after_enqueued_at), @"Other(1997-11-12 9:55:06.2000003 +00:00:00)");
}
{
// Stars are allowed in date fields as well
let params = "afterEnqueuedAt=*&beforeStartedAt=*&afterFinishedAt=*&beforeFinishedAt=*&afterStartedAt=*&beforeEnqueuedAt=*";
let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap();
snapshot!(format!("{:?}", query), @"TaskDeletionOrCancelationQuery { uids: None, canceled_by: None, types: None, statuses: None, index_uids: None, after_enqueued_at: Star, before_enqueued_at: Star, after_started_at: Star, before_started_at: Star, after_finished_at: Star, before_finished_at: Star }");
} }
{ {
let params = "afterFinishedAt=2021"; let params = "afterFinishedAt=2021";
let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err(); let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err();
snapshot!(format!("{err}"), @"`2021` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterFinishedAt`."); snapshot!(meili_snap::json_string!(err), @r###"
{
"message": "Invalid value in parameter `afterFinishedAt`: `2021` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_after_finished_at",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-after-finished-at"
}
"###);
} }
{ {
let params = "beforeFinishedAt=2021"; let params = "beforeFinishedAt=2021";
let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err(); let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err();
snapshot!(format!("{err}"), @"`2021` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeFinishedAt`."); snapshot!(meili_snap::json_string!(err), @r###"
{
"message": "Invalid value in parameter `beforeFinishedAt`: `2021` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_before_finished_at",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-before-finished-at"
}
"###);
} }
{ {
let params = "afterEnqueuedAt=2021-12"; let params = "afterEnqueuedAt=2021-12";
let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err(); let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err();
snapshot!(format!("{err}"), @"`2021-12` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterEnqueuedAt`."); snapshot!(meili_snap::json_string!(err), @r###"
{
"message": "Invalid value in parameter `afterEnqueuedAt`: `2021-12` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_after_enqueued_at",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-after-enqueued-at"
}
"###);
} }
{ {
let params = "beforeEnqueuedAt=2021-12-03T23"; let params = "beforeEnqueuedAt=2021-12-03T23";
let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err(); let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err();
snapshot!(format!("{err}"), @"`2021-12-03T23` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeEnqueuedAt`."); snapshot!(meili_snap::json_string!(err), @r###"
{
"message": "Invalid value in parameter `beforeEnqueuedAt`: `2021-12-03T23` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_before_enqueued_at",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-before-enqueued-at"
}
"###);
} }
{ {
let params = "afterStartedAt=2021-12-03T23:45"; let params = "afterStartedAt=2021-12-03T23:45";
let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err(); let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err();
snapshot!(format!("{err}"), @"`2021-12-03T23:45` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterStartedAt`."); snapshot!(meili_snap::json_string!(err), @r###"
{
"message": "Invalid value in parameter `afterStartedAt`: `2021-12-03T23:45` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_after_started_at",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-after-started-at"
}
"###);
} }
{ {
let params = "beforeStartedAt=2021-12-03T23:45"; let params = "beforeStartedAt=2021-12-03T23:45";
let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err(); let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err();
snapshot!(format!("{err}"), @"`2021-12-03T23:45` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeStartedAt`."); snapshot!(meili_snap::json_string!(err), @r###"
{
"message": "Invalid value in parameter `beforeStartedAt`: `2021-12-03T23:45` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_before_started_at",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-before-started-at"
}
"###);
} }
} }
@ -669,22 +639,48 @@ mod tests {
{ {
let params = "uids=78,1,12,73"; let params = "uids=78,1,12,73";
let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap(); let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap();
snapshot!(format!("{:?}", query.uids.unwrap()), @"[78, 1, 12, 73]"); snapshot!(format!("{:?}", query.uids), @"List([78, 1, 12, 73])");
} }
{ {
let params = "uids=1"; let params = "uids=1";
let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap(); let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap();
snapshot!(format!("{:?}", query.uids.unwrap()), @"[1]"); snapshot!(format!("{:?}", query.uids), @"List([1])");
}
{
let params = "uids=cat,*,dog";
let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err();
snapshot!(meili_snap::json_string!(err), @r###"
{
"message": "Invalid value in parameter `uids[0]`: could not parse `cat` as a positive integer",
"code": "invalid_task_uids",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-uids"
}
"###);
} }
{ {
let params = "uids=78,hello,world"; let params = "uids=78,hello,world";
let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err(); let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err();
snapshot!(format!("{err}"), @"invalid digit found in string at `.uids`."); snapshot!(meili_snap::json_string!(err), @r###"
{
"message": "Invalid value in parameter `uids[1]`: could not parse `hello` as a positive integer",
"code": "invalid_task_uids",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-uids"
}
"###);
} }
{ {
let params = "uids=cat"; let params = "uids=cat";
let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err(); let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err();
snapshot!(format!("{err}"), @"invalid digit found in string at `.uids`."); snapshot!(meili_snap::json_string!(err), @r###"
{
"message": "Invalid value in parameter `uids`: could not parse `cat` as a positive integer",
"code": "invalid_task_uids",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-uids"
}
"###);
} }
} }
@ -693,17 +689,24 @@ mod tests {
{ {
let params = "statuses=succeeded,failed,enqueued,processing,canceled"; let params = "statuses=succeeded,failed,enqueued,processing,canceled";
let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap(); let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap();
snapshot!(format!("{:?}", query.statuses.unwrap()), @"[Succeeded, Failed, Enqueued, Processing, Canceled]"); snapshot!(format!("{:?}", query.statuses), @"List([Succeeded, Failed, Enqueued, Processing, Canceled])");
} }
{ {
let params = "statuses=enqueued"; let params = "statuses=enqueued";
let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap(); let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap();
snapshot!(format!("{:?}", query.statuses.unwrap()), @"[Enqueued]"); snapshot!(format!("{:?}", query.statuses), @"List([Enqueued])");
} }
{ {
let params = "statuses=finished"; let params = "statuses=finished";
let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err(); let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err();
snapshot!(format!("{err}"), @"`finished` is not a status. Available status are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`. at `.statuses`."); snapshot!(meili_snap::json_string!(err), @r###"
{
"message": "Invalid value in parameter `statuses`: `finished` is not a valid task status. Available statuses are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`.",
"code": "invalid_task_statuses",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-statuses"
}
"###);
} }
} }
#[test] #[test]
@ -711,17 +714,24 @@ mod tests {
{ {
let params = "types=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::<TaskDeletionOrCancelationQuery>(params).unwrap(); let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap();
snapshot!(format!("{:?}", query.types.unwrap()), @"[DocumentAdditionOrUpdate, DocumentDeletion, SettingsUpdate, IndexCreation, IndexDeletion, IndexUpdate, IndexSwap, TaskCancelation, TaskDeletion, DumpCreation, SnapshotCreation]"); snapshot!(format!("{:?}", query.types), @"List([DocumentAdditionOrUpdate, DocumentDeletion, SettingsUpdate, IndexCreation, IndexDeletion, IndexUpdate, IndexSwap, TaskCancelation, TaskDeletion, DumpCreation, SnapshotCreation])");
} }
{ {
let params = "types=settingsUpdate"; let params = "types=settingsUpdate";
let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap(); let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap();
snapshot!(format!("{:?}", query.types.unwrap()), @"[SettingsUpdate]"); snapshot!(format!("{:?}", query.types), @"List([SettingsUpdate])");
} }
{ {
let params = "types=createIndex"; let params = "types=createIndex";
let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err(); let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err();
snapshot!(format!("{err}"), @"`createIndex` is not a type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`. at `.types`."); snapshot!(meili_snap::json_string!(err), @r###"
{
"message": "Invalid value in parameter `types`: `createIndex` is not a valid task type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`.",
"code": "invalid_task_types",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-types"
}
"###);
} }
} }
#[test] #[test]
@ -729,22 +739,36 @@ mod tests {
{ {
let params = "indexUids=toto,tata-78"; let params = "indexUids=toto,tata-78";
let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap(); let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap();
snapshot!(format!("{:?}", query.index_uids.unwrap()), @r###"[IndexUid("toto"), IndexUid("tata-78")]"###); snapshot!(format!("{:?}", query.index_uids), @r###"List([IndexUid("toto"), IndexUid("tata-78")])"###);
} }
{ {
let params = "indexUids=index_a"; let params = "indexUids=index_a";
let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap(); let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap();
snapshot!(format!("{:?}", query.index_uids.unwrap()), @r###"[IndexUid("index_a")]"###); snapshot!(format!("{:?}", query.index_uids), @r###"List([IndexUid("index_a")])"###);
} }
{ {
let params = "indexUids=1,hé"; let params = "indexUids=1,hé";
let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err(); let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err();
snapshot!(format!("{err}"), @"`hé` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_). at `.indexUids`."); snapshot!(meili_snap::json_string!(err), @r###"
{
"message": "Invalid value in parameter `indexUids[1]`: `hé` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).",
"code": "invalid_index_uid",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-index-uid"
}
"###);
} }
{ {
let params = "indexUids=hé"; let params = "indexUids=hé";
let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err(); let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err();
snapshot!(format!("{err}"), @"`hé` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_). at `.indexUids`."); snapshot!(meili_snap::json_string!(err), @r###"
{
"message": "Invalid value in parameter `indexUids`: `hé` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).",
"code": "invalid_index_uid",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-index-uid"
}
"###);
} }
} }
@ -753,38 +777,74 @@ mod tests {
{ {
let params = "from=12&limit=15&indexUids=toto,tata-78&statuses=succeeded,enqueued&afterEnqueuedAt=2012-04-23&uids=1,2,3"; 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::<TasksFilterQuery>(params).unwrap(); let query = deserr_query_params::<TasksFilterQuery>(params).unwrap();
snapshot!(format!("{:?}", query), @r###"TasksFilterQuery { limit: 15, from: Some(12), uids: Some([1, 2, 3]), canceled_by: None, types: None, statuses: Some([Succeeded, Enqueued]), index_uids: Some([IndexUid("toto"), IndexUid("tata-78")]), after_enqueued_at: Some(2012-04-24 0:00:00.0 +00:00:00), before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None }"###); snapshot!(format!("{:?}", query), @r###"TasksFilterQuery { limit: Param(15), from: Some(Param(12)), uids: List([1, 2, 3]), canceled_by: None, types: None, statuses: List([Succeeded, Enqueued]), index_uids: List([IndexUid("toto"), IndexUid("tata-78")]), after_enqueued_at: Other(2012-04-24 0:00:00.0 +00:00:00), before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None }"###);
} }
{ {
// Stars should translate to `None` in the query // Stars should translate to `None` in the query
// Verify value of the default limit // Verify value of the default limit
let params = "indexUids=*&statuses=succeeded,*&afterEnqueuedAt=2012-04-23&uids=1,2,3"; let params = "indexUids=*&statuses=succeeded,*&afterEnqueuedAt=2012-04-23&uids=1,2,3";
let query = deserr_query_params::<TasksFilterQuery>(params).unwrap(); let query = deserr_query_params::<TasksFilterQuery>(params).unwrap();
snapshot!(format!("{:?}", query), @"TasksFilterQuery { limit: 20, from: None, uids: Some([1, 2, 3]), canceled_by: None, types: None, statuses: None, index_uids: None, after_enqueued_at: Some(2012-04-24 0:00:00.0 +00:00:00), before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None }"); snapshot!(format!("{:?}", query), @"TasksFilterQuery { limit: Param(20), from: None, uids: List([1, 2, 3]), canceled_by: None, types: None, statuses: Star, index_uids: Star, after_enqueued_at: Other(2012-04-24 0:00:00.0 +00:00:00), before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None }");
} }
{ {
// Stars should also translate to `None` in task deletion/cancelation queries // Stars should also translate to `None` in task deletion/cancelation queries
let params = "indexUids=*&statuses=succeeded,*&afterEnqueuedAt=2012-04-23&uids=1,2,3"; let params = "indexUids=*&statuses=succeeded,*&afterEnqueuedAt=2012-04-23&uids=1,2,3";
let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap(); let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap();
snapshot!(format!("{:?}", query), @"TaskDeletionOrCancelationQuery { uids: Some([1, 2, 3]), canceled_by: None, types: None, statuses: None, index_uids: None, after_enqueued_at: Some(2012-04-24 0:00:00.0 +00:00:00), before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None }"); snapshot!(format!("{:?}", query), @"TaskDeletionOrCancelationQuery { uids: List([1, 2, 3]), canceled_by: None, types: None, statuses: Star, index_uids: Star, after_enqueued_at: Other(2012-04-24 0:00:00.0 +00:00:00), before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None }");
} }
{ {
// Stars in uids not allowed // Star in from not allowed
let params = "uids=*"; let params = "uids=*&from=*";
let err = deserr_query_params::<TasksFilterQuery>(params).unwrap_err(); let err = deserr_query_params::<TasksFilterQuery>(params).unwrap_err();
snapshot!(format!("{err}"), @"invalid digit found in string at `.uids`."); snapshot!(meili_snap::json_string!(err), @r###"
{
"message": "Invalid value in parameter `from`: could not parse `*` as a positive integer",
"code": "invalid_task_from",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-from"
}
"###);
} }
{ {
// From not allowed in task deletion/cancelation queries // From not allowed in task deletion/cancelation queries
let params = "from=12"; let params = "from=12";
let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err(); let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err();
snapshot!(format!("{err}"), @"Json deserialize error: unknown field `from`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``."); snapshot!(meili_snap::json_string!(err), @r###"
{
"message": "Unknown parameter `from`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`",
"code": "bad_request",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#bad-request"
}
"###);
} }
{ {
// Limit not allowed in task deletion/cancelation queries // Limit not allowed in task deletion/cancelation queries
let params = "limit=12"; let params = "limit=12";
let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err(); let err = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap_err();
snapshot!(format!("{err}"), @"Json deserialize error: unknown field `limit`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``."); snapshot!(meili_snap::json_string!(err), @r###"
{
"message": "Unknown parameter `limit`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`",
"code": "bad_request",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#bad-request"
}
"###);
}
}
#[test]
fn deserialize_task_delete_or_cancel_empty() {
{
let params = "";
let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap();
assert!(query.is_empty());
}
{
let params = "statuses=*";
let query = deserr_query_params::<TaskDeletionOrCancelationQuery>(params).unwrap();
assert!(!query.is_empty());
snapshot!(format!("{query:?}"), @"TaskDeletionOrCancelationQuery { uids: None, canceled_by: None, types: None, statuses: Star, index_uids: None, after_enqueued_at: None, before_enqueued_at: None, after_started_at: None, before_started_at: None, after_finished_at: None, before_finished_at: None }");
} }
} }
} }

View File

@ -5,8 +5,8 @@ use std::time::Instant;
use deserr::DeserializeFromValue; use deserr::DeserializeFromValue;
use either::Either; use either::Either;
use meilisearch_types::deserr::DeserrJsonError;
use meilisearch_types::error::deserr_codes::*; use meilisearch_types::error::deserr_codes::*;
use meilisearch_types::error::DeserrError;
use meilisearch_types::settings::DEFAULT_PAGINATION_MAX_TOTAL_HITS; use meilisearch_types::settings::DEFAULT_PAGINATION_MAX_TOTAL_HITS;
use meilisearch_types::{milli, Document}; use meilisearch_types::{milli, Document};
use milli::tokenizer::TokenizerBuilder; use milli::tokenizer::TokenizerBuilder;
@ -15,7 +15,7 @@ use milli::{
SortError, TermsMatchingStrategy, DEFAULT_VALUES_PER_FACET, SortError, TermsMatchingStrategy, DEFAULT_VALUES_PER_FACET,
}; };
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::Serialize;
use serde_json::{json, Value}; use serde_json::{json, Value};
use crate::error::MeilisearchHttpError; use crate::error::MeilisearchHttpError;
@ -30,41 +30,41 @@ pub const DEFAULT_HIGHLIGHT_PRE_TAG: fn() -> String = || "<em>".to_string();
pub const DEFAULT_HIGHLIGHT_POST_TAG: fn() -> String = || "</em>".to_string(); pub const DEFAULT_HIGHLIGHT_POST_TAG: fn() -> String = || "</em>".to_string();
#[derive(Debug, Clone, Default, PartialEq, DeserializeFromValue)] #[derive(Debug, Clone, Default, PartialEq, DeserializeFromValue)]
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
pub struct SearchQuery { pub struct SearchQuery {
#[deserr(error = DeserrError<InvalidSearchQ>)] #[deserr(default, error = DeserrJsonError<InvalidSearchQ>)]
pub q: Option<String>, pub q: Option<String>,
#[deserr(error = DeserrError<InvalidSearchOffset>, default = DEFAULT_SEARCH_OFFSET())] #[deserr(default = DEFAULT_SEARCH_OFFSET(), error = DeserrJsonError<InvalidSearchOffset>)]
pub offset: usize, pub offset: usize,
#[deserr(error = DeserrError<InvalidSearchLimit>, default = DEFAULT_SEARCH_LIMIT())] #[deserr(default = DEFAULT_SEARCH_LIMIT(), error = DeserrJsonError<InvalidSearchLimit>)]
pub limit: usize, pub limit: usize,
#[deserr(error = DeserrError<InvalidSearchPage>)] #[deserr(default, error = DeserrJsonError<InvalidSearchPage>)]
pub page: Option<usize>, pub page: Option<usize>,
#[deserr(error = DeserrError<InvalidSearchHitsPerPage>)] #[deserr(default, error = DeserrJsonError<InvalidSearchHitsPerPage>)]
pub hits_per_page: Option<usize>, pub hits_per_page: Option<usize>,
#[deserr(error = DeserrError<InvalidSearchAttributesToRetrieve>)] #[deserr(default, error = DeserrJsonError<InvalidSearchAttributesToRetrieve>)]
pub attributes_to_retrieve: Option<BTreeSet<String>>, pub attributes_to_retrieve: Option<BTreeSet<String>>,
#[deserr(error = DeserrError<InvalidSearchAttributesToCrop>)] #[deserr(default, error = DeserrJsonError<InvalidSearchAttributesToCrop>)]
pub attributes_to_crop: Option<Vec<String>>, pub attributes_to_crop: Option<Vec<String>>,
#[deserr(error = DeserrError<InvalidSearchCropLength>, default = DEFAULT_CROP_LENGTH())] #[deserr(default, error = DeserrJsonError<InvalidSearchCropLength>, default = DEFAULT_CROP_LENGTH())]
pub crop_length: usize, pub crop_length: usize,
#[deserr(error = DeserrError<InvalidSearchAttributesToHighlight>)] #[deserr(default, error = DeserrJsonError<InvalidSearchAttributesToHighlight>)]
pub attributes_to_highlight: Option<HashSet<String>>, pub attributes_to_highlight: Option<HashSet<String>>,
#[deserr(error = DeserrError<InvalidSearchShowMatchesPosition>, default)] #[deserr(default, error = DeserrJsonError<InvalidSearchShowMatchesPosition>, default)]
pub show_matches_position: bool, pub show_matches_position: bool,
#[deserr(error = DeserrError<InvalidSearchFilter>)] #[deserr(default, error = DeserrJsonError<InvalidSearchFilter>)]
pub filter: Option<Value>, pub filter: Option<Value>,
#[deserr(error = DeserrError<InvalidSearchSort>)] #[deserr(default, error = DeserrJsonError<InvalidSearchSort>)]
pub sort: Option<Vec<String>>, pub sort: Option<Vec<String>>,
#[deserr(error = DeserrError<InvalidSearchFacets>)] #[deserr(default, error = DeserrJsonError<InvalidSearchFacets>)]
pub facets: Option<Vec<String>>, pub facets: Option<Vec<String>>,
#[deserr(error = DeserrError<InvalidSearchHighlightPreTag>, default = DEFAULT_HIGHLIGHT_PRE_TAG())] #[deserr(default, error = DeserrJsonError<InvalidSearchHighlightPreTag>, default = DEFAULT_HIGHLIGHT_PRE_TAG())]
pub highlight_pre_tag: String, pub highlight_pre_tag: String,
#[deserr(error = DeserrError<InvalidSearchHighlightPostTag>, default = DEFAULT_HIGHLIGHT_POST_TAG())] #[deserr(default, error = DeserrJsonError<InvalidSearchHighlightPostTag>, default = DEFAULT_HIGHLIGHT_POST_TAG())]
pub highlight_post_tag: String, pub highlight_post_tag: String,
#[deserr(error = DeserrError<InvalidSearchCropMarker>, default = DEFAULT_CROP_MARKER())] #[deserr(default, error = DeserrJsonError<InvalidSearchCropMarker>, default = DEFAULT_CROP_MARKER())]
pub crop_marker: String, pub crop_marker: String,
#[deserr(error = DeserrError<InvalidSearchMatchingStrategy>, default)] #[deserr(default, error = DeserrJsonError<InvalidSearchMatchingStrategy>, default)]
pub matching_strategy: MatchingStrategy, pub matching_strategy: MatchingStrategy,
} }
@ -74,9 +74,8 @@ impl SearchQuery {
} }
} }
#[derive(Deserialize, Debug, Clone, PartialEq, Eq, DeserializeFromValue)] #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromValue)]
#[deserr(rename_all = camelCase)] #[deserr(rename_all = camelCase)]
#[serde(rename_all = "camelCase")]
pub enum MatchingStrategy { pub enum MatchingStrategy {
/// Remove query words from last to first /// Remove query words from last to first
Last, Last,

View File

@ -248,10 +248,10 @@ async fn error_add_api_key_missing_parameter() {
meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(code, @"400 Bad Request");
meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###"
{ {
"message": "Json deserialize error: missing field `indexes` at ``", "message": "Missing field `indexes`",
"code": "bad_request", "code": "missing_api_key_indexes",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#bad-request" "link": "https://docs.meilisearch.com/errors#missing-api-key-indexes"
} }
"###); "###);
@ -265,10 +265,10 @@ async fn error_add_api_key_missing_parameter() {
meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(code, @"400 Bad Request");
meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###"
{ {
"message": "Json deserialize error: missing field `actions` at ``", "message": "Missing field `actions`",
"code": "bad_request", "code": "missing_api_key_actions",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#bad-request" "link": "https://docs.meilisearch.com/errors#missing-api-key-actions"
} }
"###); "###);
@ -279,22 +279,13 @@ async fn error_add_api_key_missing_parameter() {
"actions": ["documents.add"], "actions": ["documents.add"],
}); });
let (response, code) = server.add_api_key(content).await; let (response, code) = server.add_api_key(content).await;
meili_snap::snapshot!(code, @"201 Created"); meili_snap::snapshot!(code, @"400 Bad Request");
meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }), @r###" meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]", ".uid" => "[ignored]", ".key" => "[ignored]" }), @r###"
{ {
"name": null, "message": "Missing field `expiresAt`",
"description": "Indexing API key", "code": "missing_api_key_expires_at",
"key": "[ignored]", "type": "invalid_request",
"uid": "[ignored]", "link": "https://docs.meilisearch.com/errors#missing-api-key-expires-at"
"actions": [
"documents.add"
],
"indexes": [
"products"
],
"expiresAt": null,
"createdAt": "[ignored]",
"updatedAt": "[ignored]"
} }
"###); "###);
} }
@ -314,7 +305,7 @@ async fn error_add_api_key_invalid_parameters_description() {
meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(code, @"400 Bad Request");
meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###"
{ {
"message": "invalid type: Map `{\"name\":\"products\"}`, expected a String at `.description`.", "message": "Invalid value type at `.description`: expected a string, but found an object: `{\"name\":\"products\"}`",
"code": "invalid_api_key_description", "code": "invalid_api_key_description",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-api-key-description" "link": "https://docs.meilisearch.com/errors#invalid-api-key-description"
@ -337,7 +328,7 @@ async fn error_add_api_key_invalid_parameters_name() {
meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(code, @"400 Bad Request");
meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###"
{ {
"message": "invalid type: Map `{\"name\":\"products\"}`, expected a String at `.name`.", "message": "Invalid value type at `.name`: expected a string, but found an object: `{\"name\":\"products\"}`",
"code": "invalid_api_key_name", "code": "invalid_api_key_name",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-api-key-name" "link": "https://docs.meilisearch.com/errors#invalid-api-key-name"
@ -360,7 +351,7 @@ async fn error_add_api_key_invalid_parameters_indexes() {
meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(code, @"400 Bad Request");
meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###"
{ {
"message": "invalid type: Map `{\"name\":\"products\"}`, expected a Sequence at `.indexes`.", "message": "Invalid value type at `.indexes`: expected an array, but found an object: `{\"name\":\"products\"}`",
"code": "invalid_api_key_indexes", "code": "invalid_api_key_indexes",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-api-key-indexes" "link": "https://docs.meilisearch.com/errors#invalid-api-key-indexes"
@ -386,7 +377,7 @@ async fn error_add_api_key_invalid_index_uids() {
meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(code, @"400 Bad Request");
meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###"
{ {
"message": "`invalid index # / \\name with spaces` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_). at `.indexes[0]`.", "message": "Invalid value at `.indexes[0]`: `invalid index # / \\name with spaces` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).",
"code": "invalid_api_key_indexes", "code": "invalid_api_key_indexes",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-api-key-indexes" "link": "https://docs.meilisearch.com/errors#invalid-api-key-indexes"
@ -411,7 +402,7 @@ async fn error_add_api_key_invalid_parameters_actions() {
meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(code, @"400 Bad Request");
meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###"
{ {
"message": "invalid type: Map `{\"name\":\"products\"}`, expected a Sequence at `.actions`.", "message": "Invalid value type at `.actions`: expected an array, but found an object: `{\"name\":\"products\"}`",
"code": "invalid_api_key_actions", "code": "invalid_api_key_actions",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-api-key-actions" "link": "https://docs.meilisearch.com/errors#invalid-api-key-actions"
@ -431,7 +422,7 @@ async fn error_add_api_key_invalid_parameters_actions() {
meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(code, @"400 Bad Request");
meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###"
{ {
"message": "Json deserialize error: unknown value `doc.add`, expected one of `*`, `search`, `documents.*`, `documents.add`, `documents.get`, `documents.delete`, `indexes.*`, `indexes.create`, `indexes.get`, `indexes.update`, `indexes.delete`, `indexes.swap`, `tasks.*`, `tasks.cancel`, `tasks.delete`, `tasks.get`, `settings.*`, `settings.get`, `settings.update`, `stats.*`, `stats.get`, `metrics.*`, `metrics.get`, `dumps.*`, `dumps.create`, `version`, `keys.create`, `keys.get`, `keys.update`, `keys.delete` at `.actions[0]`.", "message": "Unknown value `doc.add` at `.actions[0]`: expected one of `*`, `search`, `documents.*`, `documents.add`, `documents.get`, `documents.delete`, `indexes.*`, `indexes.create`, `indexes.get`, `indexes.update`, `indexes.delete`, `indexes.swap`, `tasks.*`, `tasks.cancel`, `tasks.delete`, `tasks.get`, `settings.*`, `settings.get`, `settings.update`, `stats.*`, `stats.get`, `metrics.*`, `metrics.get`, `dumps.*`, `dumps.create`, `version`, `keys.create`, `keys.get`, `keys.update`, `keys.delete`",
"code": "invalid_api_key_actions", "code": "invalid_api_key_actions",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-api-key-actions" "link": "https://docs.meilisearch.com/errors#invalid-api-key-actions"
@ -455,7 +446,7 @@ async fn error_add_api_key_invalid_parameters_expires_at() {
meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(code, @"400 Bad Request");
meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###"
{ {
"message": "invalid type: Map `{\"name\":\"products\"}`, expected a String at `.expiresAt`.", "message": "Invalid value type at `.expiresAt`: expected a string, but found an object: `{\"name\":\"products\"}`",
"code": "invalid_api_key_expires_at", "code": "invalid_api_key_expires_at",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-api-key-expires-at" "link": "https://docs.meilisearch.com/errors#invalid-api-key-expires-at"
@ -478,7 +469,7 @@ async fn error_add_api_key_invalid_parameters_expires_at_in_the_past() {
meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###"
{ {
"message": "`2010-11-13T00:00:00Z` is not a valid date. It should follow the RFC 3339 format to represents a date or datetime in the future or specified as a null value. e.g. 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'.\n at `.expiresAt`.", "message": "Invalid value at `.expiresAt`: `2010-11-13T00:00:00Z` is not a valid date. It should follow the RFC 3339 format to represents a date or datetime in the future or specified as a null value. e.g. 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'.\n",
"code": "invalid_api_key_expires_at", "code": "invalid_api_key_expires_at",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-api-key-expires-at" "link": "https://docs.meilisearch.com/errors#invalid-api-key-expires-at"
@ -503,7 +494,7 @@ async fn error_add_api_key_invalid_parameters_uid() {
meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###"
{ {
"message": "invalid length: expected length 32 for simple format, found 13 at `.uid`.", "message": "Invalid value at `.uid`: invalid length: expected length 32 for simple format, found 13",
"code": "invalid_api_key_uid", "code": "invalid_api_key_uid",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-api-key-uid" "link": "https://docs.meilisearch.com/errors#invalid-api-key-uid"
@ -1403,7 +1394,7 @@ async fn error_patch_api_key_indexes() {
let (response, code) = server.patch_api_key(&uid, content).await; let (response, code) = server.patch_api_key(&uid, content).await;
meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###"
{ {
"message": "Json deserialize error: unknown field `indexes`, expected one of `description`, `name` at ``.", "message": "Unknown field `indexes`: expected one of `description`, `name`",
"code": "immutable_api_key_indexes", "code": "immutable_api_key_indexes",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#immutable-api-key-indexes" "link": "https://docs.meilisearch.com/errors#immutable-api-key-indexes"
@ -1480,7 +1471,7 @@ async fn error_patch_api_key_actions() {
let (response, code) = server.patch_api_key(&uid, content).await; let (response, code) = server.patch_api_key(&uid, content).await;
meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###"
{ {
"message": "Json deserialize error: unknown field `actions`, expected one of `description`, `name` at ``.", "message": "Unknown field `actions`: expected one of `description`, `name`",
"code": "immutable_api_key_actions", "code": "immutable_api_key_actions",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#immutable-api-key-actions" "link": "https://docs.meilisearch.com/errors#immutable-api-key-actions"
@ -1549,7 +1540,7 @@ async fn error_patch_api_key_expiration_date() {
let (response, code) = server.patch_api_key(&uid, content).await; let (response, code) = server.patch_api_key(&uid, content).await;
meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###"
{ {
"message": "Json deserialize error: unknown field `expiresAt`, expected one of `description`, `name` at ``.", "message": "Unknown field `expiresAt`: expected one of `description`, `name`",
"code": "immutable_api_key_expires_at", "code": "immutable_api_key_expires_at",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#immutable-api-key-expires-at" "link": "https://docs.meilisearch.com/errors#immutable-api-key-expires-at"
@ -1670,7 +1661,7 @@ async fn error_patch_api_key_indexes_invalid_parameters() {
let (response, code) = server.patch_api_key(&uid, content).await; let (response, code) = server.patch_api_key(&uid, content).await;
meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###"
{ {
"message": "invalid type: Integer `13`, expected a String at `.description`.", "message": "Invalid value type at `.description`: expected a string, but found a positive integer: `13`",
"code": "invalid_api_key_description", "code": "invalid_api_key_description",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-api-key-description" "link": "https://docs.meilisearch.com/errors#invalid-api-key-description"
@ -1686,7 +1677,7 @@ async fn error_patch_api_key_indexes_invalid_parameters() {
let (response, code) = server.patch_api_key(&uid, content).await; let (response, code) = server.patch_api_key(&uid, content).await;
meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###"
{ {
"message": "invalid type: Integer `13`, expected a String at `.name`.", "message": "Invalid value type at `.name`: expected a string, but found a positive integer: `13`",
"code": "invalid_api_key_name", "code": "invalid_api_key_name",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-api-key-name" "link": "https://docs.meilisearch.com/errors#invalid-api-key-name"

View File

@ -289,8 +289,8 @@ impl Index<'_> {
eprintln!("Error with post search"); eprintln!("Error with post search");
resume_unwind(e); resume_unwind(e);
} }
let query = yaup::to_string(&query).unwrap();
let (response, code) = self.search_get(query).await; let (response, code) = self.search_get(&query).await;
if let Err(e) = catch_unwind(move || test(response, code)) { if let Err(e) = catch_unwind(move || test(response, code)) {
eprintln!("Error with get search"); eprintln!("Error with get search");
resume_unwind(e); resume_unwind(e);
@ -302,9 +302,8 @@ impl Index<'_> {
self.service.post_encoded(url, query, self.encoder).await self.service.post_encoded(url, query, self.encoder).await
} }
pub async fn search_get(&self, query: Value) -> (Value, StatusCode) { pub async fn search_get(&self, query: &str) -> (Value, StatusCode) {
let params = yaup::to_string(&query).unwrap(); let url = format!("/indexes/{}/search?{}", urlencode(self.uid.as_ref()), query);
let url = format!("/indexes/{}/search?{}", urlencode(self.uid.as_ref()), params);
self.service.get(url).await self.service.get(url).await
} }

View File

@ -132,8 +132,8 @@ impl Server {
self.service.get("/tasks").await self.service.get("/tasks").await
} }
pub async fn tasks_filter(&self, filter: Value) -> (Value, StatusCode) { pub async fn tasks_filter(&self, filter: &str) -> (Value, StatusCode) {
self.service.get(format!("/tasks?{}", yaup::to_string(&filter).unwrap())).await self.service.get(format!("/tasks?{}", filter)).await
} }
pub async fn get_dump_status(&self, uid: &str) -> (Value, StatusCode) { pub async fn get_dump_status(&self, uid: &str) -> (Value, StatusCode) {
@ -148,14 +148,12 @@ impl Server {
self.service.post("/swap-indexes", value).await self.service.post("/swap-indexes", value).await
} }
pub async fn cancel_tasks(&self, value: Value) -> (Value, StatusCode) { pub async fn cancel_tasks(&self, value: &str) -> (Value, StatusCode) {
self.service self.service.post(format!("/tasks/cancel?{}", value), json!(null)).await
.post(format!("/tasks/cancel?{}", yaup::to_string(&value).unwrap()), json!(null))
.await
} }
pub async fn delete_tasks(&self, value: Value) -> (Value, StatusCode) { pub async fn delete_tasks(&self, value: &str) -> (Value, StatusCode) {
self.service.delete(format!("/tasks?{}", yaup::to_string(&value).unwrap())).await self.service.delete(format!("/tasks?{}", value)).await
} }
pub async fn wait_task(&self, update_id: u64) -> Value { pub async fn wait_task(&self, update_id: u64) -> Value {

View File

@ -1,6 +1,7 @@
use actix_web::http::header::ContentType; use actix_web::http::header::ContentType;
use actix_web::test; use actix_web::test;
use http::header::ACCEPT_ENCODING; use http::header::ACCEPT_ENCODING;
use meili_snap::{json_string, snapshot};
use serde_json::{json, Value}; use serde_json::{json, Value};
use crate::common::encoder::Encoder; use crate::common::encoder::Encoder;
@ -188,13 +189,13 @@ async fn error_create_with_invalid_index_uid() {
let index = server.index("test test#!"); let index = server.index("test test#!");
let (response, code) = index.create(None).await; let (response, code) = index.create(None).await;
let expected_response = json!({ snapshot!(code, @"400 Bad Request");
"message": "`test test#!` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", snapshot!(json_string!(response), @r###"
"code": "invalid_index_uid", {
"type": "invalid_request", "message": "Invalid value at `.uid`: `test test#!` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).",
"link": "https://docs.meilisearch.com/errors#invalid-index-uid" "code": "invalid_index_uid",
}); "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-index-uid"
assert_eq!(response, expected_response); }
assert_eq!(code, 400); "###);
} }

View File

@ -1,3 +1,4 @@
use meili_snap::{json_string, snapshot};
use serde_json::{json, Value}; use serde_json::{json, Value};
use crate::common::Server; use crate::common::Server;
@ -182,15 +183,13 @@ async fn get_invalid_index_uid() {
let index = server.index("this is not a valid index name"); let index = server.index("this is not a valid index name");
let (response, code) = index.get().await; let (response, code) = index.get().await;
assert_eq!(code, 404); snapshot!(code, @"400 Bad Request");
assert_eq!( snapshot!(json_string!(response), @r###"
response, {
json!( "message": "`this is not a valid index name` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).",
{ "code": "invalid_index_uid",
"message": "Index `this is not a valid index name` not found.", "type": "invalid_request",
"code": "index_not_found", "link": "https://docs.meilisearch.com/errors#invalid-index-uid"
"type": "invalid_request", }
"link": "https://docs.meilisearch.com/errors#index-not-found" "###);
})
);
} }

View File

@ -46,7 +46,7 @@ async fn search_bad_q() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: Sequence `[\"doggo\"]`, expected a String at `.q`.", "message": "Invalid value type at `.q`: expected a string, but found an array: `[\"doggo\"]`",
"code": "invalid_search_q", "code": "invalid_search_q",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-search-q" "link": "https://docs.meilisearch.com/errors#invalid-search-q"
@ -64,18 +64,18 @@ async fn search_bad_offset() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Integer at `.offset`.", "message": "Invalid value type at `.offset`: expected a positive integer, but found a string: `\"doggo\"`",
"code": "invalid_search_offset", "code": "invalid_search_offset",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-search-offset" "link": "https://docs.meilisearch.com/errors#invalid-search-offset"
} }
"###); "###);
let (response, code) = index.search_get(json!({"offset": "doggo"})).await; let (response, code) = index.search_get("offset=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid digit found in string at `.offset`.", "message": "Invalid value in parameter `offset`: could not parse `doggo` as a positive integer",
"code": "invalid_search_offset", "code": "invalid_search_offset",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-search-offset" "link": "https://docs.meilisearch.com/errors#invalid-search-offset"
@ -92,18 +92,18 @@ async fn search_bad_limit() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Integer at `.limit`.", "message": "Invalid value type at `.limit`: expected a positive integer, but found a string: `\"doggo\"`",
"code": "invalid_search_limit", "code": "invalid_search_limit",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-search-limit" "link": "https://docs.meilisearch.com/errors#invalid-search-limit"
} }
"###); "###);
let (response, code) = index.search_get(json!({"limit": "doggo"})).await; let (response, code) = index.search_get("limit=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid digit found in string at `.limit`.", "message": "Invalid value in parameter `limit`: could not parse `doggo` as a positive integer",
"code": "invalid_search_limit", "code": "invalid_search_limit",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-search-limit" "link": "https://docs.meilisearch.com/errors#invalid-search-limit"
@ -120,18 +120,18 @@ async fn search_bad_page() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Integer at `.page`.", "message": "Invalid value type at `.page`: expected a positive integer, but found a string: `\"doggo\"`",
"code": "invalid_search_page", "code": "invalid_search_page",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-search-page" "link": "https://docs.meilisearch.com/errors#invalid-search-page"
} }
"###); "###);
let (response, code) = index.search_get(json!({"page": "doggo"})).await; let (response, code) = index.search_get("page=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid digit found in string at `.page`.", "message": "Invalid value in parameter `page`: could not parse `doggo` as a positive integer",
"code": "invalid_search_page", "code": "invalid_search_page",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-search-page" "link": "https://docs.meilisearch.com/errors#invalid-search-page"
@ -148,18 +148,18 @@ async fn search_bad_hits_per_page() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Integer at `.hitsPerPage`.", "message": "Invalid value type at `.hitsPerPage`: expected a positive integer, but found a string: `\"doggo\"`",
"code": "invalid_search_hits_per_page", "code": "invalid_search_hits_per_page",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-search-hits-per-page" "link": "https://docs.meilisearch.com/errors#invalid-search-hits-per-page"
} }
"###); "###);
let (response, code) = index.search_get(json!({"hitsPerPage": "doggo"})).await; let (response, code) = index.search_get("hitsPerPage=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid digit found in string at `.hitsPerPage`.", "message": "Invalid value in parameter `hitsPerPage`: could not parse `doggo` as a positive integer",
"code": "invalid_search_hits_per_page", "code": "invalid_search_hits_per_page",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-search-hits-per-page" "link": "https://docs.meilisearch.com/errors#invalid-search-hits-per-page"
@ -176,7 +176,7 @@ async fn search_bad_attributes_to_crop() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Sequence at `.attributesToCrop`.", "message": "Invalid value type at `.attributesToCrop`: expected an array, but found a string: `\"doggo\"`",
"code": "invalid_search_attributes_to_crop", "code": "invalid_search_attributes_to_crop",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-search-attributes-to-crop" "link": "https://docs.meilisearch.com/errors#invalid-search-attributes-to-crop"
@ -194,18 +194,18 @@ async fn search_bad_crop_length() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Integer at `.cropLength`.", "message": "Invalid value type at `.cropLength`: expected a positive integer, but found a string: `\"doggo\"`",
"code": "invalid_search_crop_length", "code": "invalid_search_crop_length",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-search-crop-length" "link": "https://docs.meilisearch.com/errors#invalid-search-crop-length"
} }
"###); "###);
let (response, code) = index.search_get(json!({"cropLength": "doggo"})).await; let (response, code) = index.search_get("cropLength=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid digit found in string at `.cropLength`.", "message": "Invalid value in parameter `cropLength`: could not parse `doggo` as a positive integer",
"code": "invalid_search_crop_length", "code": "invalid_search_crop_length",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-search-crop-length" "link": "https://docs.meilisearch.com/errors#invalid-search-crop-length"
@ -222,7 +222,7 @@ async fn search_bad_attributes_to_highlight() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Sequence at `.attributesToHighlight`.", "message": "Invalid value type at `.attributesToHighlight`: expected an array, but found a string: `\"doggo\"`",
"code": "invalid_search_attributes_to_highlight", "code": "invalid_search_attributes_to_highlight",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-search-attributes-to-highlight" "link": "https://docs.meilisearch.com/errors#invalid-search-attributes-to-highlight"
@ -266,7 +266,7 @@ async fn search_bad_sort() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Sequence at `.sort`.", "message": "Invalid value type at `.sort`: expected an array, but found a string: `\"doggo\"`",
"code": "invalid_search_sort", "code": "invalid_search_sort",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-search-sort" "link": "https://docs.meilisearch.com/errors#invalid-search-sort"
@ -284,18 +284,18 @@ async fn search_bad_show_matches_position() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Boolean at `.showMatchesPosition`.", "message": "Invalid value type at `.showMatchesPosition`: expected a boolean, but found a string: `\"doggo\"`",
"code": "invalid_search_show_matches_position", "code": "invalid_search_show_matches_position",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-search-show-matches-position" "link": "https://docs.meilisearch.com/errors#invalid-search-show-matches-position"
} }
"###); "###);
let (response, code) = index.search_get(json!({"showMatchesPosition": "doggo"})).await; let (response, code) = index.search_get("showMatchesPosition=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "provided string was not `true` or `false` at `.showMatchesPosition`.", "message": "Invalid value in parameter `showMatchesPosition`: could not parse `doggo` as a boolean, expected either `true` or `false`",
"code": "invalid_search_show_matches_position", "code": "invalid_search_show_matches_position",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-search-show-matches-position" "link": "https://docs.meilisearch.com/errors#invalid-search-show-matches-position"
@ -312,7 +312,7 @@ async fn search_bad_facets() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Sequence at `.facets`.", "message": "Invalid value type at `.facets`: expected an array, but found a string: `\"doggo\"`",
"code": "invalid_search_facets", "code": "invalid_search_facets",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-search-facets" "link": "https://docs.meilisearch.com/errors#invalid-search-facets"
@ -330,7 +330,7 @@ async fn search_bad_highlight_pre_tag() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: Sequence `[\"doggo\"]`, expected a String at `.highlightPreTag`.", "message": "Invalid value type at `.highlightPreTag`: expected a string, but found an array: `[\"doggo\"]`",
"code": "invalid_search_highlight_pre_tag", "code": "invalid_search_highlight_pre_tag",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-search-highlight-pre-tag" "link": "https://docs.meilisearch.com/errors#invalid-search-highlight-pre-tag"
@ -348,7 +348,7 @@ async fn search_bad_highlight_post_tag() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: Sequence `[\"doggo\"]`, expected a String at `.highlightPostTag`.", "message": "Invalid value type at `.highlightPostTag`: expected a string, but found an array: `[\"doggo\"]`",
"code": "invalid_search_highlight_post_tag", "code": "invalid_search_highlight_post_tag",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-search-highlight-post-tag" "link": "https://docs.meilisearch.com/errors#invalid-search-highlight-post-tag"
@ -366,7 +366,7 @@ async fn search_bad_crop_marker() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: Sequence `[\"doggo\"]`, expected a String at `.cropMarker`.", "message": "Invalid value type at `.cropMarker`: expected a string, but found an array: `[\"doggo\"]`",
"code": "invalid_search_crop_marker", "code": "invalid_search_crop_marker",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-search-crop-marker" "link": "https://docs.meilisearch.com/errors#invalid-search-crop-marker"
@ -384,18 +384,18 @@ async fn search_bad_matching_strategy() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "Json deserialize error: unknown value `doggo`, expected one of `last`, `all` at `.matchingStrategy`.", "message": "Unknown value `doggo` at `.matchingStrategy`: expected one of `last`, `all`",
"code": "invalid_search_matching_strategy", "code": "invalid_search_matching_strategy",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-search-matching-strategy" "link": "https://docs.meilisearch.com/errors#invalid-search-matching-strategy"
} }
"###); "###);
let (response, code) = index.search_get(json!({"matchingStrategy": "doggo"})).await; let (response, code) = index.search_get("matchingStrategy=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "Json deserialize error: unknown value `doggo`, expected one of `last`, `all` at `.matchingStrategy`.", "message": "Unknown value `doggo` for parameter `matchingStrategy`: expected one of `last`, `all`",
"code": "invalid_search_matching_strategy", "code": "invalid_search_matching_strategy",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-search-matching-strategy" "link": "https://docs.meilisearch.com/errors#invalid-search-matching-strategy"

View File

@ -12,7 +12,7 @@ async fn settings_bad_displayed_attributes() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Sequence at `.displayedAttributes`.", "message": "Invalid value type at `.displayedAttributes`: expected an array, but found a string: `\"doggo\"`",
"code": "invalid_settings_displayed_attributes", "code": "invalid_settings_displayed_attributes",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-settings-displayed-attributes" "link": "https://docs.meilisearch.com/errors#invalid-settings-displayed-attributes"
@ -23,7 +23,7 @@ async fn settings_bad_displayed_attributes() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Sequence at ``.", "message": "Invalid value type: expected an array, but found a string: `\"doggo\"`",
"code": "invalid_settings_displayed_attributes", "code": "invalid_settings_displayed_attributes",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-settings-displayed-attributes" "link": "https://docs.meilisearch.com/errors#invalid-settings-displayed-attributes"
@ -40,7 +40,7 @@ async fn settings_bad_searchable_attributes() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Sequence at `.searchableAttributes`.", "message": "Invalid value type at `.searchableAttributes`: expected an array, but found a string: `\"doggo\"`",
"code": "invalid_settings_searchable_attributes", "code": "invalid_settings_searchable_attributes",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-settings-searchable-attributes" "link": "https://docs.meilisearch.com/errors#invalid-settings-searchable-attributes"
@ -51,7 +51,7 @@ async fn settings_bad_searchable_attributes() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Sequence at ``.", "message": "Invalid value type: expected an array, but found a string: `\"doggo\"`",
"code": "invalid_settings_searchable_attributes", "code": "invalid_settings_searchable_attributes",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-settings-searchable-attributes" "link": "https://docs.meilisearch.com/errors#invalid-settings-searchable-attributes"
@ -68,7 +68,7 @@ async fn settings_bad_filterable_attributes() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Sequence at `.filterableAttributes`.", "message": "Invalid value type at `.filterableAttributes`: expected an array, but found a string: `\"doggo\"`",
"code": "invalid_settings_filterable_attributes", "code": "invalid_settings_filterable_attributes",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-settings-filterable-attributes" "link": "https://docs.meilisearch.com/errors#invalid-settings-filterable-attributes"
@ -79,7 +79,7 @@ async fn settings_bad_filterable_attributes() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Sequence at ``.", "message": "Invalid value type: expected an array, but found a string: `\"doggo\"`",
"code": "invalid_settings_filterable_attributes", "code": "invalid_settings_filterable_attributes",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-settings-filterable-attributes" "link": "https://docs.meilisearch.com/errors#invalid-settings-filterable-attributes"
@ -96,7 +96,7 @@ async fn settings_bad_sortable_attributes() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Sequence at `.sortableAttributes`.", "message": "Invalid value type at `.sortableAttributes`: expected an array, but found a string: `\"doggo\"`",
"code": "invalid_settings_sortable_attributes", "code": "invalid_settings_sortable_attributes",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-settings-sortable-attributes" "link": "https://docs.meilisearch.com/errors#invalid-settings-sortable-attributes"
@ -107,7 +107,7 @@ async fn settings_bad_sortable_attributes() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Sequence at ``.", "message": "Invalid value type: expected an array, but found a string: `\"doggo\"`",
"code": "invalid_settings_sortable_attributes", "code": "invalid_settings_sortable_attributes",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-settings-sortable-attributes" "link": "https://docs.meilisearch.com/errors#invalid-settings-sortable-attributes"
@ -124,7 +124,7 @@ async fn settings_bad_ranking_rules() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Sequence at `.rankingRules`.", "message": "Invalid value type at `.rankingRules`: expected an array, but found a string: `\"doggo\"`",
"code": "invalid_settings_ranking_rules", "code": "invalid_settings_ranking_rules",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-settings-ranking-rules" "link": "https://docs.meilisearch.com/errors#invalid-settings-ranking-rules"
@ -135,7 +135,7 @@ async fn settings_bad_ranking_rules() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Sequence at ``.", "message": "Invalid value type: expected an array, but found a string: `\"doggo\"`",
"code": "invalid_settings_ranking_rules", "code": "invalid_settings_ranking_rules",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-settings-ranking-rules" "link": "https://docs.meilisearch.com/errors#invalid-settings-ranking-rules"
@ -152,7 +152,7 @@ async fn settings_bad_stop_words() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Sequence at `.stopWords`.", "message": "Invalid value type at `.stopWords`: expected an array, but found a string: `\"doggo\"`",
"code": "invalid_settings_stop_words", "code": "invalid_settings_stop_words",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-settings-stop-words" "link": "https://docs.meilisearch.com/errors#invalid-settings-stop-words"
@ -163,7 +163,7 @@ async fn settings_bad_stop_words() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Sequence at ``.", "message": "Invalid value type: expected an array, but found a string: `\"doggo\"`",
"code": "invalid_settings_stop_words", "code": "invalid_settings_stop_words",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-settings-stop-words" "link": "https://docs.meilisearch.com/errors#invalid-settings-stop-words"
@ -180,7 +180,7 @@ async fn settings_bad_synonyms() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Map at `.synonyms`.", "message": "Invalid value type at `.synonyms`: expected an object, but found a string: `\"doggo\"`",
"code": "invalid_settings_synonyms", "code": "invalid_settings_synonyms",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-settings-synonyms" "link": "https://docs.meilisearch.com/errors#invalid-settings-synonyms"
@ -191,7 +191,7 @@ async fn settings_bad_synonyms() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Map at ``.", "message": "Invalid value type: expected an object, but found a string: `\"doggo\"`",
"code": "invalid_settings_synonyms", "code": "invalid_settings_synonyms",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-settings-synonyms" "link": "https://docs.meilisearch.com/errors#invalid-settings-synonyms"
@ -208,7 +208,7 @@ async fn settings_bad_distinct_attribute() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: Sequence `[\"doggo\"]`, expected a String at `.distinctAttribute`.", "message": "Invalid value type at `.distinctAttribute`: expected a string, but found an array: `[\"doggo\"]`",
"code": "invalid_settings_distinct_attribute", "code": "invalid_settings_distinct_attribute",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-settings-distinct-attribute" "link": "https://docs.meilisearch.com/errors#invalid-settings-distinct-attribute"
@ -219,7 +219,7 @@ async fn settings_bad_distinct_attribute() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: Sequence `[\"doggo\"]`, expected a String at ``.", "message": "Invalid value type: expected a string, but found an array: `[\"doggo\"]`",
"code": "invalid_settings_distinct_attribute", "code": "invalid_settings_distinct_attribute",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-settings-distinct-attribute" "link": "https://docs.meilisearch.com/errors#invalid-settings-distinct-attribute"
@ -236,7 +236,7 @@ async fn settings_bad_typo_tolerance() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Map at `.typoTolerance`.", "message": "Invalid value type at `.typoTolerance`: expected an object, but found a string: `\"doggo\"`",
"code": "invalid_settings_typo_tolerance", "code": "invalid_settings_typo_tolerance",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-settings-typo-tolerance" "link": "https://docs.meilisearch.com/errors#invalid-settings-typo-tolerance"
@ -247,7 +247,7 @@ async fn settings_bad_typo_tolerance() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Map at ``.", "message": "Invalid value type: expected an object, but found a string: `\"doggo\"`",
"code": "invalid_settings_typo_tolerance", "code": "invalid_settings_typo_tolerance",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-settings-typo-tolerance" "link": "https://docs.meilisearch.com/errors#invalid-settings-typo-tolerance"
@ -264,7 +264,7 @@ async fn settings_bad_faceting() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Map at `.faceting`.", "message": "Invalid value type at `.faceting`: expected an object, but found a string: `\"doggo\"`",
"code": "invalid_settings_faceting", "code": "invalid_settings_faceting",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-settings-faceting" "link": "https://docs.meilisearch.com/errors#invalid-settings-faceting"
@ -275,7 +275,7 @@ async fn settings_bad_faceting() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Map at ``.", "message": "Invalid value type: expected an object, but found a string: `\"doggo\"`",
"code": "invalid_settings_faceting", "code": "invalid_settings_faceting",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-settings-faceting" "link": "https://docs.meilisearch.com/errors#invalid-settings-faceting"
@ -292,7 +292,7 @@ async fn settings_bad_pagination() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Map at `.pagination`.", "message": "Invalid value type at `.pagination`: expected an object, but found a string: `\"doggo\"`",
"code": "invalid_settings_pagination", "code": "invalid_settings_pagination",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-settings-pagination" "link": "https://docs.meilisearch.com/errors#invalid-settings-pagination"
@ -303,7 +303,7 @@ async fn settings_bad_pagination() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid type: String `\"doggo\"`, expected a Map at ``.", "message": "Invalid value type: expected an object, but found a string: `\"doggo\"`",
"code": "invalid_settings_pagination", "code": "invalid_settings_pagination",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-settings-pagination" "link": "https://docs.meilisearch.com/errors#invalid-settings-pagination"

View File

@ -282,7 +282,7 @@ async fn error_set_invalid_ranking_rules() {
meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(code, @"400 Bad Request");
meili_snap::snapshot!(meili_snap::json_string!(response), @r###" meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
{ {
"message": "`manyTheFish` ranking rule is invalid. Valid ranking rules are words, typo, sort, proximity, attribute, exactness and custom ranking rules. at `.rankingRules[0]`.", "message": "Invalid value at `.rankingRules[0]`: `manyTheFish` ranking rule is invalid. Valid ranking rules are words, typo, sort, proximity, attribute, exactness and custom ranking rules.",
"code": "invalid_settings_ranking_rules", "code": "invalid_settings_ranking_rules",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-settings-ranking-rules" "link": "https://docs.meilisearch.com/errors#invalid-settings-ranking-rules"

View File

@ -1,5 +1,4 @@
use meili_snap::*; use meili_snap::*;
use serde_json::json;
use crate::common::Server; use crate::common::Server;
@ -7,33 +6,44 @@ use crate::common::Server;
async fn task_bad_uids() { async fn task_bad_uids() {
let server = Server::new().await; let server = Server::new().await;
let (response, code) = server.tasks_filter(json!({"uids": "doggo"})).await; let (response, code) = server.tasks_filter("uids=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid digit found in string at `.uids`.", "message": "Invalid value in parameter `uids`: could not parse `doggo` as a positive integer",
"code": "invalid_task_uids", "code": "invalid_task_uids",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-uids" "link": "https://docs.meilisearch.com/errors#invalid-task-uids"
} }
"###); "###);
let (response, code) = server.cancel_tasks(json!({"uids": "doggo"})).await; let (response, code) = server.cancel_tasks("uids=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid digit found in string at `.uids`.", "message": "Invalid value in parameter `uids`: could not parse `doggo` as a positive integer",
"code": "invalid_task_uids", "code": "invalid_task_uids",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-uids" "link": "https://docs.meilisearch.com/errors#invalid-task-uids"
} }
"###); "###);
let (response, code) = server.delete_tasks(json!({"uids": "doggo"})).await; let (response, code) = server.delete_tasks("uids=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid digit found in string at `.uids`.", "message": "Invalid value in parameter `uids`: could not parse `doggo` as a positive integer",
"code": "invalid_task_uids",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-uids"
}
"###);
let (response, code) = server.delete_tasks("uids=1,dogo").await;
snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###"
{
"message": "Invalid value in parameter `uids[1]`: could not parse `dogo` as a positive integer",
"code": "invalid_task_uids", "code": "invalid_task_uids",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-uids" "link": "https://docs.meilisearch.com/errors#invalid-task-uids"
@ -45,33 +55,33 @@ async fn task_bad_uids() {
async fn task_bad_canceled_by() { async fn task_bad_canceled_by() {
let server = Server::new().await; let server = Server::new().await;
let (response, code) = server.tasks_filter(json!({"canceledBy": "doggo"})).await; let (response, code) = server.tasks_filter("canceledBy=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid digit found in string at `.canceledBy`.", "message": "Invalid value in parameter `canceledBy`: could not parse `doggo` as a positive integer",
"code": "invalid_task_canceled_by", "code": "invalid_task_canceled_by",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-canceled-by" "link": "https://docs.meilisearch.com/errors#invalid-task-canceled-by"
} }
"###); "###);
let (response, code) = server.cancel_tasks(json!({"canceledBy": "doggo"})).await; let (response, code) = server.cancel_tasks("canceledBy=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid digit found in string at `.canceledBy`.", "message": "Invalid value in parameter `canceledBy`: could not parse `doggo` as a positive integer",
"code": "invalid_task_canceled_by", "code": "invalid_task_canceled_by",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-canceled-by" "link": "https://docs.meilisearch.com/errors#invalid-task-canceled-by"
} }
"###); "###);
let (response, code) = server.delete_tasks(json!({"canceledBy": "doggo"})).await; let (response, code) = server.delete_tasks("canceledBy=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid digit found in string at `.canceledBy`.", "message": "Invalid value in parameter `canceledBy`: could not parse `doggo` as a positive integer",
"code": "invalid_task_canceled_by", "code": "invalid_task_canceled_by",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-canceled-by" "link": "https://docs.meilisearch.com/errors#invalid-task-canceled-by"
@ -83,33 +93,33 @@ async fn task_bad_canceled_by() {
async fn task_bad_types() { async fn task_bad_types() {
let server = Server::new().await; let server = Server::new().await;
let (response, code) = server.tasks_filter(json!({"types": "doggo"})).await; let (response, code) = server.tasks_filter("types=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`doggo` is not a type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`. at `.types`.", "message": "Invalid value in parameter `types`: `doggo` is not a valid task type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`.",
"code": "invalid_task_types", "code": "invalid_task_types",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-types" "link": "https://docs.meilisearch.com/errors#invalid-task-types"
} }
"###); "###);
let (response, code) = server.cancel_tasks(json!({"types": "doggo"})).await; let (response, code) = server.cancel_tasks("types=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`doggo` is not a type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`. at `.types`.", "message": "Invalid value in parameter `types`: `doggo` is not a valid task type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`.",
"code": "invalid_task_types", "code": "invalid_task_types",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-types" "link": "https://docs.meilisearch.com/errors#invalid-task-types"
} }
"###); "###);
let (response, code) = server.delete_tasks(json!({"types": "doggo"})).await; let (response, code) = server.delete_tasks("types=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`doggo` is not a type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`. at `.types`.", "message": "Invalid value in parameter `types`: `doggo` is not a valid task type. Available types are `documentAdditionOrUpdate`, `documentDeletion`, `settingsUpdate`, `indexCreation`, `indexDeletion`, `indexUpdate`, `indexSwap`, `taskCancelation`, `taskDeletion`, `dumpCreation`, `snapshotCreation`.",
"code": "invalid_task_types", "code": "invalid_task_types",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-types" "link": "https://docs.meilisearch.com/errors#invalid-task-types"
@ -121,33 +131,33 @@ async fn task_bad_types() {
async fn task_bad_statuses() { async fn task_bad_statuses() {
let server = Server::new().await; let server = Server::new().await;
let (response, code) = server.tasks_filter(json!({"statuses": "doggo"})).await; let (response, code) = server.tasks_filter("statuses=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`doggo` is not a status. Available status are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`. at `.statuses`.", "message": "Invalid value in parameter `statuses`: `doggo` is not a valid task status. Available statuses are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`.",
"code": "invalid_task_statuses", "code": "invalid_task_statuses",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-statuses" "link": "https://docs.meilisearch.com/errors#invalid-task-statuses"
} }
"###); "###);
let (response, code) = server.cancel_tasks(json!({"statuses": "doggo"})).await; let (response, code) = server.cancel_tasks("statuses=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`doggo` is not a status. Available status are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`. at `.statuses`.", "message": "Invalid value in parameter `statuses`: `doggo` is not a valid task status. Available statuses are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`.",
"code": "invalid_task_statuses", "code": "invalid_task_statuses",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-statuses" "link": "https://docs.meilisearch.com/errors#invalid-task-statuses"
} }
"###); "###);
let (response, code) = server.delete_tasks(json!({"statuses": "doggo"})).await; let (response, code) = server.delete_tasks("statuses=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`doggo` is not a status. Available status are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`. at `.statuses`.", "message": "Invalid value in parameter `statuses`: `doggo` is not a valid task status. Available statuses are `enqueued`, `processing`, `succeeded`, `failed`, `canceled`.",
"code": "invalid_task_statuses", "code": "invalid_task_statuses",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-statuses" "link": "https://docs.meilisearch.com/errors#invalid-task-statuses"
@ -159,33 +169,33 @@ async fn task_bad_statuses() {
async fn task_bad_index_uids() { async fn task_bad_index_uids() {
let server = Server::new().await; let server = Server::new().await;
let (response, code) = server.tasks_filter(json!({"indexUids": "the good doggo"})).await; let (response, code) = server.tasks_filter("indexUids=the%20good%20doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`the good doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_). at `.indexUids`.", "message": "Invalid value in parameter `indexUids`: `the good doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).",
"code": "invalid_index_uid", "code": "invalid_index_uid",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-index-uid" "link": "https://docs.meilisearch.com/errors#invalid-index-uid"
} }
"###); "###);
let (response, code) = server.cancel_tasks(json!({"indexUids": "the good doggo"})).await; let (response, code) = server.cancel_tasks("indexUids=the%20good%20doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`the good doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_). at `.indexUids`.", "message": "Invalid value in parameter `indexUids`: `the good doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).",
"code": "invalid_index_uid", "code": "invalid_index_uid",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-index-uid" "link": "https://docs.meilisearch.com/errors#invalid-index-uid"
} }
"###); "###);
let (response, code) = server.delete_tasks(json!({"indexUids": "the good doggo"})).await; let (response, code) = server.delete_tasks("indexUids=the%20good%20doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`the good doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_). at `.indexUids`.", "message": "Invalid value in parameter `indexUids`: `the good doggo` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).",
"code": "invalid_index_uid", "code": "invalid_index_uid",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-index-uid" "link": "https://docs.meilisearch.com/errors#invalid-index-uid"
@ -197,33 +207,33 @@ async fn task_bad_index_uids() {
async fn task_bad_limit() { async fn task_bad_limit() {
let server = Server::new().await; let server = Server::new().await;
let (response, code) = server.tasks_filter(json!({"limit": "doggo"})).await; let (response, code) = server.tasks_filter("limit=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid digit found in string at `.limit`.", "message": "Invalid value in parameter `limit`: could not parse `doggo` as a positive integer",
"code": "invalid_task_limit", "code": "invalid_task_limit",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-limit" "link": "https://docs.meilisearch.com/errors#invalid-task-limit"
} }
"###); "###);
let (response, code) = server.cancel_tasks(json!({"limit": "doggo"})).await; let (response, code) = server.cancel_tasks("limit=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "Json deserialize error: unknown field `limit`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", "message": "Unknown parameter `limit`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`",
"code": "bad_request", "code": "bad_request",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#bad-request" "link": "https://docs.meilisearch.com/errors#bad-request"
} }
"###); "###);
let (response, code) = server.delete_tasks(json!({"limit": "doggo"})).await; let (response, code) = server.delete_tasks("limit=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "Json deserialize error: unknown field `limit`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", "message": "Unknown parameter `limit`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`",
"code": "bad_request", "code": "bad_request",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#bad-request" "link": "https://docs.meilisearch.com/errors#bad-request"
@ -235,33 +245,33 @@ async fn task_bad_limit() {
async fn task_bad_from() { async fn task_bad_from() {
let server = Server::new().await; let server = Server::new().await;
let (response, code) = server.tasks_filter(json!({"from": "doggo"})).await; let (response, code) = server.tasks_filter("from=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "invalid digit found in string at `.from`.", "message": "Invalid value in parameter `from`: could not parse `doggo` as a positive integer",
"code": "invalid_task_from", "code": "invalid_task_from",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-from" "link": "https://docs.meilisearch.com/errors#invalid-task-from"
} }
"###); "###);
let (response, code) = server.cancel_tasks(json!({"from": "doggo"})).await; let (response, code) = server.cancel_tasks("from=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "Json deserialize error: unknown field `from`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", "message": "Unknown parameter `from`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`",
"code": "bad_request", "code": "bad_request",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#bad-request" "link": "https://docs.meilisearch.com/errors#bad-request"
} }
"###); "###);
let (response, code) = server.delete_tasks(json!({"from": "doggo"})).await; let (response, code) = server.delete_tasks("from=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "Json deserialize error: unknown field `from`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", "message": "Unknown parameter `from`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`",
"code": "bad_request", "code": "bad_request",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#bad-request" "link": "https://docs.meilisearch.com/errors#bad-request"
@ -273,33 +283,33 @@ async fn task_bad_from() {
async fn task_bad_after_enqueued_at() { async fn task_bad_after_enqueued_at() {
let server = Server::new().await; let server = Server::new().await;
let (response, code) = server.tasks_filter(json!({"afterEnqueuedAt": "doggo"})).await; let (response, code) = server.tasks_filter("afterEnqueuedAt=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterEnqueuedAt`.", "message": "Invalid value in parameter `afterEnqueuedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_after_enqueued_at", "code": "invalid_task_after_enqueued_at",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-after-enqueued-at" "link": "https://docs.meilisearch.com/errors#invalid-task-after-enqueued-at"
} }
"###); "###);
let (response, code) = server.cancel_tasks(json!({"afterEnqueuedAt": "doggo"})).await; let (response, code) = server.cancel_tasks("afterEnqueuedAt=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterEnqueuedAt`.", "message": "Invalid value in parameter `afterEnqueuedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_after_enqueued_at", "code": "invalid_task_after_enqueued_at",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-after-enqueued-at" "link": "https://docs.meilisearch.com/errors#invalid-task-after-enqueued-at"
} }
"###); "###);
let (response, code) = server.delete_tasks(json!({"afterEnqueuedAt": "doggo"})).await; let (response, code) = server.delete_tasks("afterEnqueuedAt=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterEnqueuedAt`.", "message": "Invalid value in parameter `afterEnqueuedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_after_enqueued_at", "code": "invalid_task_after_enqueued_at",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-after-enqueued-at" "link": "https://docs.meilisearch.com/errors#invalid-task-after-enqueued-at"
@ -311,33 +321,33 @@ async fn task_bad_after_enqueued_at() {
async fn task_bad_before_enqueued_at() { async fn task_bad_before_enqueued_at() {
let server = Server::new().await; let server = Server::new().await;
let (response, code) = server.tasks_filter(json!({"beforeEnqueuedAt": "doggo"})).await; let (response, code) = server.tasks_filter("beforeEnqueuedAt=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeEnqueuedAt`.", "message": "Invalid value in parameter `beforeEnqueuedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_before_enqueued_at", "code": "invalid_task_before_enqueued_at",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-before-enqueued-at" "link": "https://docs.meilisearch.com/errors#invalid-task-before-enqueued-at"
} }
"###); "###);
let (response, code) = server.cancel_tasks(json!({"beforeEnqueuedAt": "doggo"})).await; let (response, code) = server.cancel_tasks("beforeEnqueuedAt=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeEnqueuedAt`.", "message": "Invalid value in parameter `beforeEnqueuedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_before_enqueued_at", "code": "invalid_task_before_enqueued_at",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-before-enqueued-at" "link": "https://docs.meilisearch.com/errors#invalid-task-before-enqueued-at"
} }
"###); "###);
let (response, code) = server.delete_tasks(json!({"beforeEnqueuedAt": "doggo"})).await; let (response, code) = server.delete_tasks("beforeEnqueuedAt=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeEnqueuedAt`.", "message": "Invalid value in parameter `beforeEnqueuedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_before_enqueued_at", "code": "invalid_task_before_enqueued_at",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-before-enqueued-at" "link": "https://docs.meilisearch.com/errors#invalid-task-before-enqueued-at"
@ -349,33 +359,33 @@ async fn task_bad_before_enqueued_at() {
async fn task_bad_after_started_at() { async fn task_bad_after_started_at() {
let server = Server::new().await; let server = Server::new().await;
let (response, code) = server.tasks_filter(json!({"afterStartedAt": "doggo"})).await; let (response, code) = server.tasks_filter("afterStartedAt=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterStartedAt`.", "message": "Invalid value in parameter `afterStartedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_after_started_at", "code": "invalid_task_after_started_at",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-after-started-at" "link": "https://docs.meilisearch.com/errors#invalid-task-after-started-at"
} }
"###); "###);
let (response, code) = server.cancel_tasks(json!({"afterStartedAt": "doggo"})).await; let (response, code) = server.cancel_tasks("afterStartedAt=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterStartedAt`.", "message": "Invalid value in parameter `afterStartedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_after_started_at", "code": "invalid_task_after_started_at",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-after-started-at" "link": "https://docs.meilisearch.com/errors#invalid-task-after-started-at"
} }
"###); "###);
let (response, code) = server.delete_tasks(json!({"afterStartedAt": "doggo"})).await; let (response, code) = server.delete_tasks("afterStartedAt=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterStartedAt`.", "message": "Invalid value in parameter `afterStartedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_after_started_at", "code": "invalid_task_after_started_at",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-after-started-at" "link": "https://docs.meilisearch.com/errors#invalid-task-after-started-at"
@ -387,33 +397,33 @@ async fn task_bad_after_started_at() {
async fn task_bad_before_started_at() { async fn task_bad_before_started_at() {
let server = Server::new().await; let server = Server::new().await;
let (response, code) = server.tasks_filter(json!({"beforeStartedAt": "doggo"})).await; let (response, code) = server.tasks_filter("beforeStartedAt=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeStartedAt`.", "message": "Invalid value in parameter `beforeStartedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_before_started_at", "code": "invalid_task_before_started_at",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-before-started-at" "link": "https://docs.meilisearch.com/errors#invalid-task-before-started-at"
} }
"###); "###);
let (response, code) = server.cancel_tasks(json!({"beforeStartedAt": "doggo"})).await; let (response, code) = server.cancel_tasks("beforeStartedAt=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeStartedAt`.", "message": "Invalid value in parameter `beforeStartedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_before_started_at", "code": "invalid_task_before_started_at",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-before-started-at" "link": "https://docs.meilisearch.com/errors#invalid-task-before-started-at"
} }
"###); "###);
let (response, code) = server.delete_tasks(json!({"beforeStartedAt": "doggo"})).await; let (response, code) = server.delete_tasks("beforeStartedAt=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeStartedAt`.", "message": "Invalid value in parameter `beforeStartedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_before_started_at", "code": "invalid_task_before_started_at",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-before-started-at" "link": "https://docs.meilisearch.com/errors#invalid-task-before-started-at"
@ -425,33 +435,33 @@ async fn task_bad_before_started_at() {
async fn task_bad_after_finished_at() { async fn task_bad_after_finished_at() {
let server = Server::new().await; let server = Server::new().await;
let (response, code) = server.tasks_filter(json!({"afterFinishedAt": "doggo"})).await; let (response, code) = server.tasks_filter("afterFinishedAt=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterFinishedAt`.", "message": "Invalid value in parameter `afterFinishedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_after_finished_at", "code": "invalid_task_after_finished_at",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-after-finished-at" "link": "https://docs.meilisearch.com/errors#invalid-task-after-finished-at"
} }
"###); "###);
let (response, code) = server.cancel_tasks(json!({"afterFinishedAt": "doggo"})).await; let (response, code) = server.cancel_tasks("afterFinishedAt=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterFinishedAt`.", "message": "Invalid value in parameter `afterFinishedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_after_finished_at", "code": "invalid_task_after_finished_at",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-after-finished-at" "link": "https://docs.meilisearch.com/errors#invalid-task-after-finished-at"
} }
"###); "###);
let (response, code) = server.delete_tasks(json!({"afterFinishedAt": "doggo"})).await; let (response, code) = server.delete_tasks("afterFinishedAt=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.afterFinishedAt`.", "message": "Invalid value in parameter `afterFinishedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_after_finished_at", "code": "invalid_task_after_finished_at",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-after-finished-at" "link": "https://docs.meilisearch.com/errors#invalid-task-after-finished-at"
@ -463,33 +473,33 @@ async fn task_bad_after_finished_at() {
async fn task_bad_before_finished_at() { async fn task_bad_before_finished_at() {
let server = Server::new().await; let server = Server::new().await;
let (response, code) = server.tasks_filter(json!({"beforeFinishedAt": "doggo"})).await; let (response, code) = server.tasks_filter("beforeFinishedAt=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeFinishedAt`.", "message": "Invalid value in parameter `beforeFinishedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_before_finished_at", "code": "invalid_task_before_finished_at",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-before-finished-at" "link": "https://docs.meilisearch.com/errors#invalid-task-before-finished-at"
} }
"###); "###);
let (response, code) = server.cancel_tasks(json!({"beforeFinishedAt": "doggo"})).await; let (response, code) = server.cancel_tasks("beforeFinishedAt=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeFinishedAt`.", "message": "Invalid value in parameter `beforeFinishedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_before_finished_at", "code": "invalid_task_before_finished_at",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-before-finished-at" "link": "https://docs.meilisearch.com/errors#invalid-task-before-finished-at"
} }
"###); "###);
let (response, code) = server.delete_tasks(json!({"beforeFinishedAt": "doggo"})).await; let (response, code) = server.delete_tasks("beforeFinishedAt=doggo").await;
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" snapshot!(json_string!(response), @r###"
{ {
"message": "`doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeFinishedAt`.", "message": "Invalid value in parameter `beforeFinishedAt`: `doggo` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_before_finished_at", "code": "invalid_task_before_finished_at",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-before-finished-at" "link": "https://docs.meilisearch.com/errors#invalid-task-before-finished-at"

View File

@ -179,44 +179,44 @@ async fn list_tasks_status_and_type_filtered() {
async fn get_task_filter_error() { async fn get_task_filter_error() {
let server = Server::new().await; let server = Server::new().await;
let (response, code) = server.tasks_filter(json!( { "lol": "pied" })).await; let (response, code) = server.tasks_filter("lol=pied").await;
assert_eq!(code, 400, "{}", response); assert_eq!(code, 400, "{}", response);
meili_snap::snapshot!(meili_snap::json_string!(response), @r###" meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
{ {
"message": "Json deserialize error: unknown field `lol`, expected one of `limit`, `from`, `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", "message": "Unknown parameter `lol`: expected one of `limit`, `from`, `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`",
"code": "bad_request", "code": "bad_request",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#bad-request" "link": "https://docs.meilisearch.com/errors#bad-request"
} }
"###); "###);
let (response, code) = server.tasks_filter(json!( { "uids": "pied" })).await; let (response, code) = server.tasks_filter("uids=pied").await;
assert_eq!(code, 400, "{}", response); assert_eq!(code, 400, "{}", response);
meili_snap::snapshot!(meili_snap::json_string!(response), @r###" meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
{ {
"message": "invalid digit found in string at `.uids`.", "message": "Invalid value in parameter `uids`: could not parse `pied` as a positive integer",
"code": "invalid_task_uids", "code": "invalid_task_uids",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-uids" "link": "https://docs.meilisearch.com/errors#invalid-task-uids"
} }
"###); "###);
let (response, code) = server.tasks_filter(json!( { "from": "pied" })).await; let (response, code) = server.tasks_filter("from=pied").await;
assert_eq!(code, 400, "{}", response); assert_eq!(code, 400, "{}", response);
meili_snap::snapshot!(meili_snap::json_string!(response), @r###" meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
{ {
"message": "invalid digit found in string at `.from`.", "message": "Invalid value in parameter `from`: could not parse `pied` as a positive integer",
"code": "invalid_task_from", "code": "invalid_task_from",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-from" "link": "https://docs.meilisearch.com/errors#invalid-task-from"
} }
"###); "###);
let (response, code) = server.tasks_filter(json!( { "beforeStartedAt": "pied" })).await; let (response, code) = server.tasks_filter("beforeStartedAt=pied").await;
assert_eq!(code, 400, "{}", response); assert_eq!(code, 400, "{}", response);
meili_snap::snapshot!(meili_snap::json_string!(response), @r###" meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
{ {
"message": "`pied` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format. at `.beforeStartedAt`.", "message": "Invalid value in parameter `beforeStartedAt`: `pied` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_before_started_at", "code": "invalid_task_before_started_at",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-before-started-at" "link": "https://docs.meilisearch.com/errors#invalid-task-before-started-at"
@ -228,7 +228,7 @@ async fn get_task_filter_error() {
async fn delete_task_filter_error() { async fn delete_task_filter_error() {
let server = Server::new().await; let server = Server::new().await;
let (response, code) = server.delete_tasks(json!(null)).await; let (response, code) = server.delete_tasks("").await;
assert_eq!(code, 400, "{}", response); assert_eq!(code, 400, "{}", response);
meili_snap::snapshot!(meili_snap::json_string!(response), @r###" meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
{ {
@ -239,22 +239,22 @@ async fn delete_task_filter_error() {
} }
"###); "###);
let (response, code) = server.delete_tasks(json!({ "lol": "pied" })).await; let (response, code) = server.delete_tasks("lol=pied").await;
assert_eq!(code, 400, "{}", response); assert_eq!(code, 400, "{}", response);
meili_snap::snapshot!(meili_snap::json_string!(response), @r###" meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
{ {
"message": "Json deserialize error: unknown field `lol`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", "message": "Unknown parameter `lol`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`",
"code": "bad_request", "code": "bad_request",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#bad-request" "link": "https://docs.meilisearch.com/errors#bad-request"
} }
"###); "###);
let (response, code) = server.delete_tasks(json!({ "uids": "pied" })).await; let (response, code) = server.delete_tasks("uids=pied").await;
assert_eq!(code, 400, "{}", response); assert_eq!(code, 400, "{}", response);
meili_snap::snapshot!(meili_snap::json_string!(response), @r###" meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
{ {
"message": "invalid digit found in string at `.uids`.", "message": "Invalid value in parameter `uids`: could not parse `pied` as a positive integer",
"code": "invalid_task_uids", "code": "invalid_task_uids",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-uids" "link": "https://docs.meilisearch.com/errors#invalid-task-uids"
@ -266,7 +266,7 @@ async fn delete_task_filter_error() {
async fn cancel_task_filter_error() { async fn cancel_task_filter_error() {
let server = Server::new().await; let server = Server::new().await;
let (response, code) = server.cancel_tasks(json!(null)).await; let (response, code) = server.cancel_tasks("").await;
assert_eq!(code, 400, "{}", response); assert_eq!(code, 400, "{}", response);
meili_snap::snapshot!(meili_snap::json_string!(response), @r###" meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
{ {
@ -277,22 +277,22 @@ async fn cancel_task_filter_error() {
} }
"###); "###);
let (response, code) = server.cancel_tasks(json!({ "lol": "pied" })).await; let (response, code) = server.cancel_tasks("lol=pied").await;
assert_eq!(code, 400, "{}", response); assert_eq!(code, 400, "{}", response);
meili_snap::snapshot!(meili_snap::json_string!(response), @r###" meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
{ {
"message": "Json deserialize error: unknown field `lol`, expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt` at ``.", "message": "Unknown parameter `lol`: expected one of `uids`, `canceledBy`, `types`, `statuses`, `indexUids`, `afterEnqueuedAt`, `beforeEnqueuedAt`, `afterStartedAt`, `beforeStartedAt`, `afterFinishedAt`, `beforeFinishedAt`",
"code": "bad_request", "code": "bad_request",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#bad-request" "link": "https://docs.meilisearch.com/errors#bad-request"
} }
"###); "###);
let (response, code) = server.cancel_tasks(json!({ "uids": "pied" })).await; let (response, code) = server.cancel_tasks("uids=pied").await;
assert_eq!(code, 400, "{}", response); assert_eq!(code, 400, "{}", response);
meili_snap::snapshot!(meili_snap::json_string!(response), @r###" meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
{ {
"message": "invalid digit found in string at `.uids`.", "message": "Invalid value in parameter `uids`: could not parse `pied` as a positive integer",
"code": "invalid_task_uids", "code": "invalid_task_uids",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-uids" "link": "https://docs.meilisearch.com/errors#invalid-task-uids"
@ -523,7 +523,7 @@ async fn test_summarized_settings_update() {
meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(code, @"400 Bad Request");
meili_snap::snapshot!(meili_snap::json_string!(response), @r###" meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
{ {
"message": "`custom` ranking rule is invalid. Valid ranking rules are words, typo, sort, proximity, attribute, exactness and custom ranking rules. at `.rankingRules[0]`.", "message": "Invalid value at `.rankingRules[0]`: `custom` ranking rule is invalid. Valid ranking rules are words, typo, sort, proximity, attribute, exactness and custom ranking rules.",
"code": "invalid_settings_ranking_rules", "code": "invalid_settings_ranking_rules",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-settings-ranking-rules" "link": "https://docs.meilisearch.com/errors#invalid-settings-ranking-rules"
@ -899,7 +899,7 @@ async fn test_summarized_task_cancelation() {
// to avoid being flaky we're only going to cancel an already finished task :( // to avoid being flaky we're only going to cancel an already finished task :(
index.create(None).await; index.create(None).await;
index.wait_task(0).await; index.wait_task(0).await;
server.cancel_tasks(json!({ "uids": [0] })).await; server.cancel_tasks("uids=0").await;
index.wait_task(1).await; index.wait_task(1).await;
let (task, _) = index.get_task(1).await; let (task, _) = index.get_task(1).await;
assert_json_snapshot!(task, assert_json_snapshot!(task,
@ -932,7 +932,7 @@ async fn test_summarized_task_deletion() {
// to avoid being flaky we're only going to delete an already finished task :( // to avoid being flaky we're only going to delete an already finished task :(
index.create(None).await; index.create(None).await;
index.wait_task(0).await; index.wait_task(0).await;
server.delete_tasks(json!({ "uids": [0] })).await; server.delete_tasks("uids=0").await;
index.wait_task(1).await; index.wait_task(1).await;
let (task, _) = index.get_task(1).await; let (task, _) = index.get_task(1).await;
assert_json_snapshot!(task, assert_json_snapshot!(task,