3246: Implement most of the error handling enhancement planned for v1.0 r=irevoire a=irevoire

Fix #3095 and #2325
Close https://github.com/meilisearch/meilisearch/pull/2540

Implements most of https://github.com/meilisearch/specifications/pull/212

## Generic error message we re-implements (in deserr):

- [x] Json
  - [x] Incorrect value kind
  - [x] Missing field
  - [x] Unknown key
  - [x] Unexpected
  - [x] Reimplement the way we show the location

- [x] Query parameter
  - [x] Incorrect value kind
  - [x] Missing field
  - [x] Unknown key
  - [x] Unexpected

## Routes to implements:
- [x] Get search
- [x] Post search
- [x] Settings
- [x] Swap indexes
- [x] Task API
- [x] Documents ressource

Error codes to implements;
## Swap API

- [x] `duplicate_index_found` → `invalid_swap_duplicate_index_found`

## Search API

- [x] `invalid_search_q`
- [x] `invalid_search_offset`
- [x] `invalid_search_limit`
- [x] `invalid_search_page`
- [x] `invalid_search_hits_per_page`
- [x] `invalid_search_attributes_to_retrieve`
- [x] `invalid_search_attributes_to_crop`
- [x] `invalid_search_crop_length`
- [x] `invalid_search_attributes_to_highlight`
- [x] `invalid_search_show_matches_position`
- [x] `invalid_search_filter`
- [x] `invalid_search_sort`
- [x] `invalid_search_facets`
- [x] `invalid_search_highlight_pre_tag`
- [x] `invalid_search_highlight_post_tag`
- [x] `invalid_search_crop_marker`
- [x] `invalid_search_matching_strategy`

## Settings API

- [x] invalid_settings_displayed_attributes
- [x] invalid_settings_searchable_attributes
- [x] invalid_settings_filterable_attributes
- [x] invalid_settings_sortable_attributes
- [x] invalid_settings_ranking_rules
- [x] invalid_settings_stop_words
- [x] invalid_settings_synonyms
- [x] invalid_settings_distinct_attribute
- [x] Add invalid_settings_typo_tolerance
    - [x] ~~invalid_settings_typo_tolerance_min_word_size_for_typos~~ (Merge in **invalid_settings_typo_tolerance**)
- [x] invalid_settings_faceting
- [x] invalid_settings_pagination

## Task API

- [x] invalid_task_date_filer → invalid_task_before_enqueued_at_filter (for all date filter) ?

## Document Resource

- [x] ~~`primary_key_inference_failed` → `index_primary_key_`~~ This doesn't exists anymore after `@dureuill` PR's on the primary key inference

------------------

# Changes

# `code` property

## Swap API

- [x] `invalid_swap_duplicate_index_found`  [RENAME]
- [x] `invalid_swap_indexes`  [NEW]

## Index API

### POST

- [x] `missing_index_uid`  [NEW]

### POST/PATCH

- [x] `invalid_index_primary_key`  [NEW]

### GET

- [x] `invalid_index_limit`  [NEW]
- [x] `invalid_index_offset`  [NEW]

## Documents API

### GET

- [x] `fields` parameter error `bad_request` → `invalid_document_fields`  [NEW]
- [x] `limit` parameter error `bad_request` → `invalid_document_limit`  [NEW]
- [x] `offset` parameter error `bad_request` → `invalid_document_offset`  [NEW]

### POST/PUT

- [x] `?primaryKey` parameter error `bad_request` →  `invalid_index_primary_key`  [NEW]

## Keys API

### POST

- ~~`missing_parameter`~~
    - [x] `missing_api_key_actions`  [NEW]
    - [x] `missing_api_key_indexes`  [NEW]
    - [x] `missing_api_key_expires_at`  [NEW]

### GET

- [x] `limit` parameter `bad_request` → `invalid_api_key_limit`  [NEW]
- [x] `offset` parameter `bad_request` → `invalid_api_key_offset`  [NEW]

## Misc
- [x] ~~`invalid_geo_field`~~ → `invalid_document_geo_field`  [RENAME]

# `type` property

## `system`   [NEW]

- [x] `no_space_left_on_device` error code
- [x] `io_error` error code (**does not exist in the current spec, need a catch-up**)
- [x] `too_many_open_files` error code (**does not exist in the current spec, need a catch-up**)

Co-authored-by: Tamo <tamo@meilisearch.com>
Co-authored-by: Loïc Lecrenier <loic.lecrenier@me.com>
This commit is contained in:
bors[bot] 2023-01-09 16:25:48 +00:00 committed by GitHub
commit e27bb8ab3e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1785 additions and 709 deletions

892
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -249,17 +249,17 @@ pub(crate) mod test {
pub fn create_test_settings() -> Settings<Checked> { pub fn create_test_settings() -> Settings<Checked> {
let settings = Settings { let settings = Settings {
displayed_attributes: Setting::Set(vec![S("race"), S("name")]), displayed_attributes: Setting::Set(vec![S("race"), S("name")]).into(),
searchable_attributes: Setting::Set(vec![S("name"), S("race")]), searchable_attributes: Setting::Set(vec![S("name"), S("race")]).into(),
filterable_attributes: Setting::Set(btreeset! { S("race"), S("age") }), filterable_attributes: Setting::Set(btreeset! { S("race"), S("age") }).into(),
sortable_attributes: Setting::Set(btreeset! { S("age") }), sortable_attributes: Setting::Set(btreeset! { S("age") }).into(),
ranking_rules: Setting::NotSet, ranking_rules: Setting::NotSet.into(),
stop_words: Setting::NotSet, stop_words: Setting::NotSet.into(),
synonyms: Setting::NotSet, synonyms: Setting::NotSet.into(),
distinct_attribute: Setting::NotSet, distinct_attribute: Setting::NotSet.into(),
typo_tolerance: Setting::NotSet, typo_tolerance: Setting::NotSet.into(),
faceting: Setting::NotSet, faceting: Setting::NotSet.into(),
pagination: Setting::NotSet, pagination: Setting::NotSet.into(),
_kind: std::marker::PhantomData, _kind: std::marker::PhantomData,
}; };
settings.check() settings.check()

View File

@ -272,7 +272,7 @@ impl From<v5::ResponseError> for v6::ResponseError {
"database_size_limit_reached" => v6::Code::DatabaseSizeLimitReached, "database_size_limit_reached" => v6::Code::DatabaseSizeLimitReached,
"document_not_found" => v6::Code::DocumentNotFound, "document_not_found" => v6::Code::DocumentNotFound,
"internal" => v6::Code::Internal, "internal" => v6::Code::Internal,
"invalid_geo_field" => v6::Code::InvalidGeoField, "invalid_geo_field" => v6::Code::InvalidDocumentGeoField,
"invalid_ranking_rule" => v6::Code::InvalidRankingRule, "invalid_ranking_rule" => v6::Code::InvalidRankingRule,
"invalid_store_file" => v6::Code::InvalidStore, "invalid_store_file" => v6::Code::InvalidStore,
"invalid_api_key" => v6::Code::InvalidToken, "invalid_api_key" => v6::Code::InvalidToken,
@ -291,7 +291,7 @@ impl From<v5::ResponseError> for v6::ResponseError {
"malformed_payload" => v6::Code::MalformedPayload, "malformed_payload" => v6::Code::MalformedPayload,
"missing_payload" => v6::Code::MissingPayload, "missing_payload" => v6::Code::MissingPayload,
"api_key_not_found" => v6::Code::ApiKeyNotFound, "api_key_not_found" => v6::Code::ApiKeyNotFound,
"missing_parameter" => v6::Code::MissingParameter, "missing_parameter" => v6::Code::UnretrievableErrorCode,
"invalid_api_key_actions" => v6::Code::InvalidApiKeyActions, "invalid_api_key_actions" => v6::Code::InvalidApiKeyActions,
"invalid_api_key_indexes" => v6::Code::InvalidApiKeyIndexes, "invalid_api_key_indexes" => v6::Code::InvalidApiKeyIndexes,
"invalid_api_key_expires_at" => v6::Code::InvalidApiKeyExpiresAt, "invalid_api_key_expires_at" => v6::Code::InvalidApiKeyExpiresAt,

View File

@ -26,7 +26,7 @@ pub type Kind = crate::KindDump;
pub type Details = meilisearch_types::tasks::Details; pub type Details = meilisearch_types::tasks::Details;
// everything related to the settings // everything related to the settings
pub type Setting<T> = meilisearch_types::milli::update::Setting<T>; pub type Setting<T> = meilisearch_types::settings::Setting<T>;
pub type TypoTolerance = meilisearch_types::settings::TypoSettings; pub type TypoTolerance = meilisearch_types::settings::TypoSettings;
pub type MinWordSizeForTypos = meilisearch_types::settings::MinWordSizeTyposSetting; pub type MinWordSizeForTypos = meilisearch_types::settings::MinWordSizeTyposSetting;
pub type FacetingSettings = meilisearch_types::settings::FacetingSettings; pub type FacetingSettings = meilisearch_types::settings::FacetingSettings;

View File

@ -882,11 +882,11 @@ impl IndexScheduler {
} }
if !not_found_indexes.is_empty() { if !not_found_indexes.is_empty() {
if not_found_indexes.len() == 1 { if not_found_indexes.len() == 1 {
return Err(Error::IndexNotFound( return Err(Error::SwapIndexNotFound(
not_found_indexes.into_iter().next().unwrap().clone(), not_found_indexes.into_iter().next().unwrap().clone(),
)); ));
} else { } else {
return Err(Error::IndexesNotFound( return Err(Error::SwapIndexesNotFound(
not_found_indexes.into_iter().cloned().collect(), not_found_indexes.into_iter().cloned().collect(),
)); ));
} }

View File

@ -1,3 +1,5 @@
use std::fmt::Display;
use meilisearch_types::error::{Code, ErrorCode}; use meilisearch_types::error::{Code, ErrorCode};
use meilisearch_types::tasks::{Kind, Status}; use meilisearch_types::tasks::{Kind, Status};
use meilisearch_types::{heed, milli}; use meilisearch_types::{heed, milli};
@ -5,16 +7,47 @@ use thiserror::Error;
use crate::TaskId; use crate::TaskId;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum DateField {
BeforeEnqueuedAt,
AfterEnqueuedAt,
BeforeStartedAt,
AfterStartedAt,
BeforeFinishedAt,
AfterFinishedAt,
}
impl Display for DateField {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DateField::BeforeEnqueuedAt => write!(f, "beforeEnqueuedAt"),
DateField::AfterEnqueuedAt => write!(f, "afterEnqueuedAt"),
DateField::BeforeStartedAt => write!(f, "beforeStartedAt"),
DateField::AfterStartedAt => write!(f, "afterStartedAt"),
DateField::BeforeFinishedAt => write!(f, "beforeFinishedAt"),
DateField::AfterFinishedAt => write!(f, "afterFinishedAt"),
}
}
}
impl From<DateField> for Code {
fn from(date: DateField) -> Self {
match date {
DateField::BeforeEnqueuedAt => Code::InvalidTaskBeforeEnqueuedAt,
DateField::AfterEnqueuedAt => Code::InvalidTaskAfterEnqueuedAt,
DateField::BeforeStartedAt => Code::InvalidTaskBeforeStartedAt,
DateField::AfterStartedAt => Code::InvalidTaskAfterStartedAt,
DateField::BeforeFinishedAt => Code::InvalidTaskBeforeFinishedAt,
DateField::AfterFinishedAt => Code::InvalidTaskAfterFinishedAt,
}
}
}
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
#[error("Index `{0}` not found.")] #[error("Index `{0}` not found.")]
IndexNotFound(String), IndexNotFound(String),
#[error(
"Indexes {} not found.",
.0.iter().map(|s| format!("`{}`", s)).collect::<Vec<_>>().join(", ")
)]
IndexesNotFound(Vec<String>),
#[error("Index `{0}` already exists.")] #[error("Index `{0}` already exists.")]
IndexAlreadyExists(String), IndexAlreadyExists(String),
#[error( #[error(
@ -26,12 +59,19 @@ pub enum Error {
.0.iter().map(|s| format!("`{}`", s)).collect::<Vec<_>>().join(", ") .0.iter().map(|s| format!("`{}`", s)).collect::<Vec<_>>().join(", ")
)] )]
SwapDuplicateIndexesFound(Vec<String>), SwapDuplicateIndexesFound(Vec<String>),
#[error("Index `{0}` not found.")]
SwapIndexNotFound(String),
#[error(
"Indexes {} not found.",
.0.iter().map(|s| format!("`{}`", s)).collect::<Vec<_>>().join(", ")
)]
SwapIndexesNotFound(Vec<String>),
#[error("Corrupted dump.")] #[error("Corrupted dump.")]
CorruptedDump, CorruptedDump,
#[error( #[error(
"Task `{field}` `{date}` is invalid. It should follow the YYYY-MM-DD or RFC 3339 date-time format." "Task `{field}` `{date}` is invalid. It should follow the YYYY-MM-DD or RFC 3339 date-time format."
)] )]
InvalidTaskDate { field: String, date: String }, InvalidTaskDate { field: DateField, date: String },
#[error("Task uid `{task_uid}` is invalid. It should only contain numeric characters.")] #[error("Task uid `{task_uid}` is invalid. It should only contain numeric characters.")]
InvalidTaskUids { task_uid: String }, InvalidTaskUids { task_uid: String },
#[error( #[error(
@ -98,15 +138,16 @@ impl ErrorCode for Error {
fn error_code(&self) -> Code { fn error_code(&self) -> Code {
match self { match self {
Error::IndexNotFound(_) => Code::IndexNotFound, Error::IndexNotFound(_) => Code::IndexNotFound,
Error::IndexesNotFound(_) => Code::IndexNotFound,
Error::IndexAlreadyExists(_) => Code::IndexAlreadyExists, Error::IndexAlreadyExists(_) => Code::IndexAlreadyExists,
Error::SwapDuplicateIndexesFound(_) => Code::DuplicateIndexFound, Error::SwapDuplicateIndexesFound(_) => Code::InvalidDuplicateIndexesFound,
Error::SwapDuplicateIndexFound(_) => Code::DuplicateIndexFound, Error::SwapDuplicateIndexFound(_) => Code::InvalidDuplicateIndexesFound,
Error::InvalidTaskDate { .. } => Code::InvalidTaskDateFilter, Error::SwapIndexNotFound(_) => Code::InvalidSwapIndexes,
Error::InvalidTaskUids { .. } => Code::InvalidTaskUidsFilter, Error::SwapIndexesNotFound(_) => Code::InvalidSwapIndexes,
Error::InvalidTaskStatuses { .. } => Code::InvalidTaskStatusesFilter, Error::InvalidTaskDate { field, .. } => (*field).into(),
Error::InvalidTaskTypes { .. } => Code::InvalidTaskTypesFilter, Error::InvalidTaskUids { .. } => Code::InvalidTaskUids,
Error::InvalidTaskCanceledBy { .. } => Code::InvalidTaskCanceledByFilter, Error::InvalidTaskStatuses { .. } => Code::InvalidTaskStatuses,
Error::InvalidTaskTypes { .. } => Code::InvalidTaskTypes,
Error::InvalidTaskCanceledBy { .. } => Code::InvalidTaskCanceledBy,
Error::InvalidIndexUid { .. } => Code::InvalidIndexUid, Error::InvalidIndexUid { .. } => Code::InvalidIndexUid,
Error::TaskNotFound(_) => Code::TaskNotFound, Error::TaskNotFound(_) => Code::TaskNotFound,
Error::TaskDeletionWithEmptyQuery => Code::TaskDeletionWithEmptyQuery, Error::TaskDeletionWithEmptyQuery => Code::TaskDeletionWithEmptyQuery,
@ -119,6 +160,7 @@ impl ErrorCode for Error {
Error::FileStore(e) => e.error_code(), Error::FileStore(e) => e.error_code(),
Error::IoError(e) => e.error_code(), Error::IoError(e) => e.error_code(),
Error::Persist(e) => e.error_code(), Error::Persist(e) => e.error_code(),
// Irrecoverable errors // Irrecoverable errors
Error::Anyhow(_) => Code::Internal, Error::Anyhow(_) => Code::Internal,
Error::CorruptedTaskQueue => Code::Internal, Error::CorruptedTaskQueue => Code::Internal,

View File

@ -10,7 +10,7 @@ source: index-scheduler/src/lib.rs
1 {uid: 1, status: succeeded, details: { primary_key: Some("id") }, kind: IndexCreation { index_uid: "b", primary_key: Some("id") }} 1 {uid: 1, status: succeeded, details: { primary_key: Some("id") }, kind: IndexCreation { index_uid: "b", primary_key: Some("id") }}
2 {uid: 2, status: succeeded, details: { primary_key: Some("id") }, kind: IndexCreation { index_uid: "c", primary_key: Some("id") }} 2 {uid: 2, status: succeeded, details: { primary_key: Some("id") }, kind: IndexCreation { index_uid: "c", primary_key: Some("id") }}
3 {uid: 3, status: succeeded, details: { primary_key: Some("id") }, kind: IndexCreation { index_uid: "d", primary_key: Some("id") }} 3 {uid: 3, status: succeeded, details: { primary_key: Some("id") }, kind: IndexCreation { index_uid: "d", primary_key: Some("id") }}
4 {uid: 4, status: failed, error: ResponseError { code: 200, message: "Indexes `e`, `f` not found.", error_code: "index_not_found", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#index-not-found" }, details: { swaps: [IndexSwap { indexes: ("a", "b") }, IndexSwap { indexes: ("c", "e") }, IndexSwap { indexes: ("d", "f") }] }, kind: IndexSwap { swaps: [IndexSwap { indexes: ("a", "b") }, IndexSwap { indexes: ("c", "e") }, IndexSwap { indexes: ("d", "f") }] }} 4 {uid: 4, status: failed, error: ResponseError { code: 200, message: "Indexes `e`, `f` not found.", error_code: "invalid_swap_indexes", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#invalid-swap-indexes" }, details: { swaps: [IndexSwap { indexes: ("a", "b") }, IndexSwap { indexes: ("c", "e") }, IndexSwap { indexes: ("d", "f") }] }, kind: IndexSwap { swaps: [IndexSwap { indexes: ("a", "b") }, IndexSwap { indexes: ("c", "e") }, IndexSwap { indexes: ("d", "f") }] }}
---------------------------------------------------------------------- ----------------------------------------------------------------------
### Status: ### Status:
enqueued [] enqueued []

View File

@ -9,6 +9,7 @@ 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 = { version = "0.1.2", features = ["serde-json"] }
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" }

View File

@ -95,6 +95,7 @@ enum ErrorType {
InternalError, InternalError,
InvalidRequestError, InvalidRequestError,
AuthenticationError, AuthenticationError,
System,
} }
impl fmt::Display for ErrorType { impl fmt::Display for ErrorType {
@ -105,6 +106,7 @@ impl fmt::Display for ErrorType {
InternalError => write!(f, "internal"), InternalError => write!(f, "internal"),
InvalidRequestError => write!(f, "invalid_request"), InvalidRequestError => write!(f, "invalid_request"),
AuthenticationError => write!(f, "auth"), AuthenticationError => write!(f, "auth"),
System => write!(f, "system"),
} }
} }
} }
@ -119,9 +121,13 @@ pub enum Code {
// index related error // index related error
CreateIndex, CreateIndex,
IndexAlreadyExists, IndexAlreadyExists,
InvalidIndexPrimaryKey,
IndexNotFound, IndexNotFound,
InvalidIndexUid, InvalidIndexUid,
MissingIndexUid,
InvalidMinWordLengthForTypo, InvalidMinWordLengthForTypo,
InvalidIndexLimit,
InvalidIndexOffset,
DuplicateIndexFound, DuplicateIndexFound,
@ -138,23 +144,73 @@ pub enum Code {
Filter, Filter,
Sort, Sort,
// Invalid swap-indexes
InvalidSwapIndexes,
InvalidDuplicateIndexesFound,
// Invalid settings update request
InvalidSettingsDisplayedAttributes,
InvalidSettingsSearchableAttributes,
InvalidSettingsFilterableAttributes,
InvalidSettingsSortableAttributes,
InvalidSettingsRankingRules,
InvalidSettingsStopWords,
InvalidSettingsSynonyms,
InvalidSettingsDistinctAttribute,
InvalidSettingsTypoTolerance,
InvalidSettingsFaceting,
InvalidSettingsPagination,
// Invalid search request
InvalidSearchQ,
InvalidSearchOffset,
InvalidSearchLimit,
InvalidSearchPage,
InvalidSearchHitsPerPage,
InvalidSearchAttributesToRetrieve,
InvalidSearchAttributesToCrop,
InvalidSearchCropLength,
InvalidSearchAttributesToHighlight,
InvalidSearchShowMatchesPosition,
InvalidSearchFilter,
InvalidSearchSort,
InvalidSearchFacets,
InvalidSearchHighlightPreTag,
InvalidSearchHighlightPostTag,
InvalidSearchCropMarker,
InvalidSearchMatchingStrategy,
// Related to the tasks
InvalidTaskUids,
InvalidTaskTypes,
InvalidTaskStatuses,
InvalidTaskCanceledBy,
InvalidTaskLimit,
InvalidTaskFrom,
InvalidTaskBeforeEnqueuedAt,
InvalidTaskAfterEnqueuedAt,
InvalidTaskBeforeStartedAt,
InvalidTaskAfterStartedAt,
InvalidTaskBeforeFinishedAt,
InvalidTaskAfterFinishedAt,
// Documents API
InvalidDocumentFields,
InvalidDocumentLimit,
InvalidDocumentOffset,
BadParameter, BadParameter,
BadRequest, BadRequest,
DatabaseSizeLimitReached, DatabaseSizeLimitReached,
DocumentNotFound, DocumentNotFound,
Internal, Internal,
InvalidGeoField, InvalidDocumentGeoField,
InvalidRankingRule, InvalidRankingRule,
InvalidStore, InvalidStore,
InvalidToken, InvalidToken,
MissingAuthorizationHeader, MissingAuthorizationHeader,
MissingMasterKey, MissingMasterKey,
DumpNotFound, DumpNotFound,
InvalidTaskDateFilter,
InvalidTaskStatusesFilter,
InvalidTaskTypesFilter,
InvalidTaskCanceledByFilter,
InvalidTaskUidsFilter,
TaskNotFound, TaskNotFound,
TaskDeletionWithEmptyQuery, TaskDeletionWithEmptyQuery,
TaskCancelationWithEmptyQuery, TaskCancelationWithEmptyQuery,
@ -174,7 +230,13 @@ pub enum Code {
MissingPayload, MissingPayload,
ApiKeyNotFound, ApiKeyNotFound,
MissingParameter,
MissingApiKeyActions,
MissingApiKeyExpiresAt,
MissingApiKeyIndexes,
InvalidApiKeyOffset,
InvalidApiKeyLimit,
InvalidApiKeyActions, InvalidApiKeyActions,
InvalidApiKeyIndexes, InvalidApiKeyIndexes,
InvalidApiKeyExpiresAt, InvalidApiKeyExpiresAt,
@ -192,12 +254,12 @@ impl Code {
match self { match self {
// related to the setup // related to the setup
IoError => ErrCode::invalid("io_error", StatusCode::UNPROCESSABLE_ENTITY), IoError => ErrCode::system("io_error", StatusCode::UNPROCESSABLE_ENTITY),
TooManyOpenFiles => { TooManyOpenFiles => {
ErrCode::invalid("too_many_open_files", StatusCode::UNPROCESSABLE_ENTITY) ErrCode::system("too_many_open_files", StatusCode::UNPROCESSABLE_ENTITY)
} }
NoSpaceLeftOnDevice => { NoSpaceLeftOnDevice => {
ErrCode::invalid("no_space_left_on_device", StatusCode::UNPROCESSABLE_ENTITY) ErrCode::system("no_space_left_on_device", StatusCode::UNPROCESSABLE_ENTITY)
} }
// index related errors // index related errors
@ -209,6 +271,12 @@ impl Code {
// thrown when requesting an unexisting index // thrown when requesting an unexisting index
IndexNotFound => ErrCode::invalid("index_not_found", StatusCode::NOT_FOUND), IndexNotFound => ErrCode::invalid("index_not_found", StatusCode::NOT_FOUND),
InvalidIndexUid => ErrCode::invalid("invalid_index_uid", StatusCode::BAD_REQUEST), InvalidIndexUid => ErrCode::invalid("invalid_index_uid", StatusCode::BAD_REQUEST),
MissingIndexUid => ErrCode::invalid("missing_index_uid", StatusCode::BAD_REQUEST),
InvalidIndexPrimaryKey => {
ErrCode::invalid("invalid_index_primary_key", StatusCode::BAD_REQUEST)
}
InvalidIndexLimit => ErrCode::invalid("invalid_index_limit", StatusCode::BAD_REQUEST),
InvalidIndexOffset => ErrCode::invalid("invalid_index_offset", StatusCode::BAD_REQUEST),
// invalid state error // invalid state error
InvalidState => ErrCode::internal("invalid_state", StatusCode::INTERNAL_SERVER_ERROR), InvalidState => ErrCode::internal("invalid_state", StatusCode::INTERNAL_SERVER_ERROR),
@ -251,7 +319,9 @@ impl Code {
} }
DocumentNotFound => ErrCode::invalid("document_not_found", StatusCode::NOT_FOUND), DocumentNotFound => ErrCode::invalid("document_not_found", StatusCode::NOT_FOUND),
Internal => ErrCode::internal("internal", StatusCode::INTERNAL_SERVER_ERROR), Internal => ErrCode::internal("internal", StatusCode::INTERNAL_SERVER_ERROR),
InvalidGeoField => ErrCode::invalid("invalid_geo_field", StatusCode::BAD_REQUEST), InvalidDocumentGeoField => {
ErrCode::invalid("invalid_document_geo_field", StatusCode::BAD_REQUEST)
}
InvalidToken => ErrCode::authentication("invalid_api_key", StatusCode::FORBIDDEN), InvalidToken => ErrCode::authentication("invalid_api_key", StatusCode::FORBIDDEN),
MissingAuthorizationHeader => { MissingAuthorizationHeader => {
ErrCode::authentication("missing_authorization_header", StatusCode::UNAUTHORIZED) ErrCode::authentication("missing_authorization_header", StatusCode::UNAUTHORIZED)
@ -259,21 +329,6 @@ impl Code {
MissingMasterKey => { MissingMasterKey => {
ErrCode::authentication("missing_master_key", StatusCode::UNAUTHORIZED) ErrCode::authentication("missing_master_key", StatusCode::UNAUTHORIZED)
} }
InvalidTaskDateFilter => {
ErrCode::invalid("invalid_task_date_filter", StatusCode::BAD_REQUEST)
}
InvalidTaskUidsFilter => {
ErrCode::invalid("invalid_task_uids_filter", StatusCode::BAD_REQUEST)
}
InvalidTaskStatusesFilter => {
ErrCode::invalid("invalid_task_statuses_filter", StatusCode::BAD_REQUEST)
}
InvalidTaskTypesFilter => {
ErrCode::invalid("invalid_task_types_filter", StatusCode::BAD_REQUEST)
}
InvalidTaskCanceledByFilter => {
ErrCode::invalid("invalid_task_canceled_by_filter", StatusCode::BAD_REQUEST)
}
TaskNotFound => ErrCode::invalid("task_not_found", StatusCode::NOT_FOUND), TaskNotFound => ErrCode::invalid("task_not_found", StatusCode::NOT_FOUND),
TaskDeletionWithEmptyQuery => { TaskDeletionWithEmptyQuery => {
ErrCode::invalid("missing_task_filters", StatusCode::BAD_REQUEST) ErrCode::invalid("missing_task_filters", StatusCode::BAD_REQUEST)
@ -313,7 +368,25 @@ impl Code {
// error related to keys // error related to keys
ApiKeyNotFound => ErrCode::invalid("api_key_not_found", StatusCode::NOT_FOUND), ApiKeyNotFound => ErrCode::invalid("api_key_not_found", StatusCode::NOT_FOUND),
MissingParameter => ErrCode::invalid("missing_parameter", StatusCode::BAD_REQUEST),
MissingApiKeyExpiresAt => {
ErrCode::invalid("missing_api_key_expires_at", StatusCode::BAD_REQUEST)
}
MissingApiKeyActions => {
ErrCode::invalid("missing_api_key_actions", StatusCode::BAD_REQUEST)
}
MissingApiKeyIndexes => {
ErrCode::invalid("missing_api_key_indexes", StatusCode::BAD_REQUEST)
}
InvalidApiKeyOffset => {
ErrCode::invalid("invalid_api_key_offset", StatusCode::BAD_REQUEST)
}
InvalidApiKeyLimit => {
ErrCode::invalid("invalid_api_key_limit", StatusCode::BAD_REQUEST)
}
InvalidApiKeyActions => { InvalidApiKeyActions => {
ErrCode::invalid("invalid_api_key_actions", StatusCode::BAD_REQUEST) ErrCode::invalid("invalid_api_key_actions", StatusCode::BAD_REQUEST)
} }
@ -336,6 +409,132 @@ impl Code {
DuplicateIndexFound => { DuplicateIndexFound => {
ErrCode::invalid("duplicate_index_found", StatusCode::BAD_REQUEST) ErrCode::invalid("duplicate_index_found", StatusCode::BAD_REQUEST)
} }
// Swap indexes error
InvalidSwapIndexes => ErrCode::invalid("invalid_swap_indexes", StatusCode::BAD_REQUEST),
InvalidDuplicateIndexesFound => {
ErrCode::invalid("invalid_swap_duplicate_index_found", StatusCode::BAD_REQUEST)
}
// Invalid settings
InvalidSettingsDisplayedAttributes => {
ErrCode::invalid("invalid_settings_displayed_attributes", StatusCode::BAD_REQUEST)
}
InvalidSettingsSearchableAttributes => {
ErrCode::invalid("invalid_settings_searchable_attributes", StatusCode::BAD_REQUEST)
}
InvalidSettingsFilterableAttributes => {
ErrCode::invalid("invalid_settings_filterable_attributes", StatusCode::BAD_REQUEST)
}
InvalidSettingsSortableAttributes => {
ErrCode::invalid("invalid_settings_sortable_attributes", StatusCode::BAD_REQUEST)
}
InvalidSettingsRankingRules => {
ErrCode::invalid("invalid_settings_ranking_rules", StatusCode::BAD_REQUEST)
}
InvalidSettingsStopWords => {
ErrCode::invalid("invalid_settings_stop_words", StatusCode::BAD_REQUEST)
}
InvalidSettingsSynonyms => {
ErrCode::invalid("invalid_settings_synonyms", StatusCode::BAD_REQUEST)
}
InvalidSettingsDistinctAttribute => {
ErrCode::invalid("invalid_settings_distinct_attribute", StatusCode::BAD_REQUEST)
}
InvalidSettingsTypoTolerance => {
ErrCode::invalid("invalid_settings_typo_tolerance", StatusCode::BAD_REQUEST)
}
InvalidSettingsFaceting => {
ErrCode::invalid("invalid_settings_faceting", StatusCode::BAD_REQUEST)
}
InvalidSettingsPagination => {
ErrCode::invalid("invalid_settings_pagination", StatusCode::BAD_REQUEST)
}
// Invalid search
InvalidSearchQ => ErrCode::invalid("invalid_search_q", StatusCode::BAD_REQUEST),
InvalidSearchOffset => {
ErrCode::invalid("invalid_search_offset", StatusCode::BAD_REQUEST)
}
InvalidSearchLimit => ErrCode::invalid("invalid_search_limit", StatusCode::BAD_REQUEST),
InvalidSearchPage => ErrCode::invalid("invalid_search_page", StatusCode::BAD_REQUEST),
InvalidSearchHitsPerPage => {
ErrCode::invalid("invalid_search_hits_per_page", StatusCode::BAD_REQUEST)
}
InvalidSearchAttributesToRetrieve => {
ErrCode::invalid("invalid_search_attributes_to_retrieve", StatusCode::BAD_REQUEST)
}
InvalidSearchAttributesToCrop => {
ErrCode::invalid("invalid_search_attributes_to_crop", StatusCode::BAD_REQUEST)
}
InvalidSearchCropLength => {
ErrCode::invalid("invalid_search_crop_length", StatusCode::BAD_REQUEST)
}
InvalidSearchAttributesToHighlight => {
ErrCode::invalid("invalid_search_attributes_to_highlight", StatusCode::BAD_REQUEST)
}
InvalidSearchShowMatchesPosition => {
ErrCode::invalid("invalid_search_show_matches_position", StatusCode::BAD_REQUEST)
}
InvalidSearchFilter => {
ErrCode::invalid("invalid_search_filter", StatusCode::BAD_REQUEST)
}
InvalidSearchSort => ErrCode::invalid("invalid_search_sort", StatusCode::BAD_REQUEST),
InvalidSearchFacets => {
ErrCode::invalid("invalid_search_facets", StatusCode::BAD_REQUEST)
}
InvalidSearchHighlightPreTag => {
ErrCode::invalid("invalid_search_highlight_pre_tag", StatusCode::BAD_REQUEST)
}
InvalidSearchHighlightPostTag => {
ErrCode::invalid("invalid_search_highlight_post_tag", StatusCode::BAD_REQUEST)
}
InvalidSearchCropMarker => {
ErrCode::invalid("invalid_search_crop_marker", StatusCode::BAD_REQUEST)
}
InvalidSearchMatchingStrategy => {
ErrCode::invalid("invalid_search_matching_strategy", StatusCode::BAD_REQUEST)
}
// Related to the tasks
InvalidTaskUids => ErrCode::invalid("invalid_task_uids", StatusCode::BAD_REQUEST),
InvalidTaskTypes => ErrCode::invalid("invalid_task_types", StatusCode::BAD_REQUEST),
InvalidTaskStatuses => {
ErrCode::invalid("invalid_task_statuses", StatusCode::BAD_REQUEST)
}
InvalidTaskCanceledBy => {
ErrCode::invalid("invalid_task_canceled_by", StatusCode::BAD_REQUEST)
}
InvalidTaskLimit => ErrCode::invalid("invalid_task_limit", StatusCode::BAD_REQUEST),
InvalidTaskFrom => ErrCode::invalid("invalid_task_from", StatusCode::BAD_REQUEST),
InvalidTaskBeforeEnqueuedAt => {
ErrCode::invalid("invalid_task_before_enqueued_at", StatusCode::BAD_REQUEST)
}
InvalidTaskAfterEnqueuedAt => {
ErrCode::invalid("invalid_task_after_enqueued_at", StatusCode::BAD_REQUEST)
}
InvalidTaskBeforeStartedAt => {
ErrCode::invalid("invalid_task_before_started_at", StatusCode::BAD_REQUEST)
}
InvalidTaskAfterStartedAt => {
ErrCode::invalid("invalid_task_after_started_at", StatusCode::BAD_REQUEST)
}
InvalidTaskBeforeFinishedAt => {
ErrCode::invalid("invalid_task_before_finished_at", StatusCode::BAD_REQUEST)
}
InvalidTaskAfterFinishedAt => {
ErrCode::invalid("invalid_task_after_finished_at", StatusCode::BAD_REQUEST)
}
InvalidDocumentFields => {
ErrCode::invalid("invalid_document_fields", StatusCode::BAD_REQUEST)
}
InvalidDocumentLimit => {
ErrCode::invalid("invalid_document_limit", StatusCode::BAD_REQUEST)
}
InvalidDocumentOffset => {
ErrCode::invalid("invalid_document_offset", StatusCode::BAD_REQUEST)
}
} }
} }
@ -382,6 +581,10 @@ impl ErrCode {
fn invalid(error_name: &'static str, status_code: StatusCode) -> ErrCode { fn invalid(error_name: &'static str, status_code: StatusCode) -> ErrCode {
ErrCode { status_code, error_name, error_type: ErrorType::InvalidRequestError } ErrCode { status_code, error_name, error_type: ErrorType::InvalidRequestError }
} }
fn system(error_name: &'static str, status_code: StatusCode) -> ErrCode {
ErrCode { status_code, error_name, error_type: ErrorType::System }
}
} }
impl ErrorCode for JoinError { impl ErrorCode for JoinError {
@ -423,7 +626,7 @@ impl ErrorCode for milli::Error {
UserError::InvalidFacetsDistribution { .. } => Code::BadRequest, UserError::InvalidFacetsDistribution { .. } => Code::BadRequest,
UserError::InvalidSortableAttribute { .. } => Code::Sort, UserError::InvalidSortableAttribute { .. } => Code::Sort,
UserError::CriterionError(_) => Code::InvalidRankingRule, UserError::CriterionError(_) => Code::InvalidRankingRule,
UserError::InvalidGeoField { .. } => Code::InvalidGeoField, UserError::InvalidGeoField { .. } => Code::InvalidDocumentGeoField,
UserError::SortError(_) => Code::Sort, UserError::SortError(_) => Code::Sort,
UserError::InvalidMinTypoWordLenSetting(_, _) => { UserError::InvalidMinTypoWordLenSetting(_, _) => {
Code::InvalidMinWordLengthForTypo Code::InvalidMinWordLengthForTypo
@ -476,6 +679,13 @@ impl ErrorCode for io::Error {
} }
} }
pub fn unwrap_any<T>(any: Result<T, T>) -> T {
match any {
Ok(any) => any,
Err(any) => any,
}
}
#[cfg(feature = "test-traits")] #[cfg(feature = "test-traits")]
mod strategy { mod strategy {
use proptest::strategy::Strategy; use proptest::strategy::Strategy;

View File

@ -60,7 +60,7 @@ impl Key {
.map(|act| { .map(|act| {
from_value(act.clone()).map_err(|_| Error::InvalidApiKeyActions(act.clone())) from_value(act.clone()).map_err(|_| Error::InvalidApiKeyActions(act.clone()))
}) })
.ok_or(Error::MissingParameter("actions"))??; .ok_or(Error::MissingApiKeyActions)??;
let indexes = value let indexes = value
.get("indexes") .get("indexes")
@ -75,12 +75,12 @@ impl Key {
.collect() .collect()
}) })
}) })
.ok_or(Error::MissingParameter("indexes"))??; .ok_or(Error::MissingApiKeyIndexes)??;
let expires_at = value let expires_at = value
.get("expiresAt") .get("expiresAt")
.map(parse_expiration_date) .map(parse_expiration_date)
.ok_or(Error::MissingParameter("expiresAt"))??; .ok_or(Error::MissingApiKeyExpiresAt)??;
let created_at = OffsetDateTime::now_utc(); let created_at = OffsetDateTime::now_utc();
let updated_at = created_at; let updated_at = created_at;
@ -344,8 +344,12 @@ pub mod actions {
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
#[error("`{0}` field is mandatory.")] #[error("`expiresAt` field is mandatory.")]
MissingParameter(&'static str), MissingApiKeyExpiresAt,
#[error("`indexes` field is mandatory.")]
MissingApiKeyIndexes,
#[error("`actions` field is mandatory.")]
MissingApiKeyActions,
#[error("`actions` field value `{0}` is invalid. It should be an array of string representing action names.")] #[error("`actions` field value `{0}` is invalid. It should be an array of string representing action names.")]
InvalidApiKeyActions(Value), InvalidApiKeyActions(Value),
#[error("`indexes` field value `{0}` is invalid. It should be an array of string representing index names.")] #[error("`indexes` field value `{0}` is invalid. It should be an array of string representing index names.")]
@ -375,7 +379,9 @@ impl From<IndexUidFormatError> for Error {
impl ErrorCode for Error { impl ErrorCode for Error {
fn error_code(&self) -> Code { fn error_code(&self) -> Code {
match self { match self {
Self::MissingParameter(_) => Code::MissingParameter, Self::MissingApiKeyExpiresAt => Code::MissingApiKeyExpiresAt,
Self::MissingApiKeyIndexes => Code::MissingApiKeyIndexes,
Self::MissingApiKeyActions => Code::MissingApiKeyActions,
Self::InvalidApiKeyActions(_) => Code::InvalidApiKeyActions, Self::InvalidApiKeyActions(_) => Code::InvalidApiKeyActions,
Self::InvalidApiKeyIndexes(_) | Self::InvalidApiKeyIndexUid(_) => { Self::InvalidApiKeyIndexes(_) | Self::InvalidApiKeyIndexUid(_) => {
Code::InvalidApiKeyIndexes Code::InvalidApiKeyIndexes

View File

@ -2,10 +2,10 @@ use std::collections::{BTreeMap, BTreeSet};
use std::marker::PhantomData; use std::marker::PhantomData;
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use deserr::{DeserializeError, DeserializeFromValue};
use fst::IntoStreamer; use fst::IntoStreamer;
use milli::update::Setting;
use milli::{Index, DEFAULT_VALUES_PER_FACET}; use milli::{Index, DEFAULT_VALUES_PER_FACET};
use serde::{Deserialize, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
/// 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.
@ -27,16 +27,135 @@ where
.serialize(s) .serialize(s)
} }
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
pub enum Setting<T> {
Set(T),
Reset,
NotSet,
}
impl<T> Default for Setting<T> {
fn default() -> Self {
Self::NotSet
}
}
impl<T> From<Setting<T>> for milli::update::Setting<T> {
fn from(value: Setting<T>) -> Self {
match value {
Setting::Set(x) => milli::update::Setting::Set(x),
Setting::Reset => milli::update::Setting::Reset,
Setting::NotSet => milli::update::Setting::NotSet,
}
}
}
impl<T> From<milli::update::Setting<T>> for Setting<T> {
fn from(value: milli::update::Setting<T>) -> Self {
match value {
milli::update::Setting::Set(x) => Setting::Set(x),
milli::update::Setting::Reset => Setting::Reset,
milli::update::Setting::NotSet => Setting::NotSet,
}
}
}
impl<T> Setting<T> {
pub fn set(self) -> Option<T> {
match self {
Self::Set(value) => Some(value),
_ => None,
}
}
pub const fn as_ref(&self) -> Setting<&T> {
match *self {
Self::Set(ref value) => Setting::Set(value),
Self::Reset => Setting::Reset,
Self::NotSet => Setting::NotSet,
}
}
pub const fn is_not_set(&self) -> bool {
matches!(self, Self::NotSet)
}
/// If `Self` is `Reset`, then map self to `Set` with the provided `val`.
pub fn or_reset(self, val: T) -> Self {
match self {
Self::Reset => Self::Set(val),
otherwise => otherwise,
}
}
}
impl<T: Serialize> Serialize for Setting<T> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::Set(value) => Some(value),
// Usually not_set isn't serialized by setting skip_serializing_if field attribute
Self::NotSet | Self::Reset => None,
}
.serialize(serializer)
}
}
impl<'de, T: Deserialize<'de>> Deserialize<'de> for Setting<T> {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Deserialize::deserialize(deserializer).map(|x| match x {
Some(x) => Self::Set(x),
None => Self::Reset, // Reset is forced by sending null value
})
}
}
impl<T, E> DeserializeFromValue<E> for Setting<T>
where
T: DeserializeFromValue<E>,
E: DeserializeError,
{
fn deserialize_from_value<V: deserr::IntoValue>(
value: deserr::Value<V>,
location: deserr::ValuePointerRef,
) -> Result<Self, E> {
match value {
deserr::Value::Null => Ok(Setting::Reset),
_ => T::deserialize_from_value(value, location).map(Setting::Set),
}
}
fn default() -> Option<Self> {
Some(Self::NotSet)
}
}
#[derive(Clone, Default, Debug, Serialize, PartialEq, Eq)] #[derive(Clone, Default, Debug, Serialize, PartialEq, Eq)]
pub struct Checked; pub struct Checked;
#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq)] #[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Unchecked; pub struct Unchecked;
impl<E> DeserializeFromValue<E> for Unchecked
where
E: DeserializeError,
{
fn deserialize_from_value<V: deserr::IntoValue>(
_value: deserr::Value<V>,
_location: deserr::ValuePointerRef,
) -> Result<Self, E> {
unreachable!()
}
}
#[cfg_attr(test, derive(proptest_derive::Arbitrary))] #[cfg_attr(test, derive(proptest_derive::Arbitrary))]
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[deserr(rename_all = camelCase, deny_unknown_fields)]
pub struct MinWordSizeTyposSetting { pub struct MinWordSizeTyposSetting {
#[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))] #[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))]
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
@ -47,9 +166,10 @@ pub struct MinWordSizeTyposSetting {
} }
#[cfg_attr(test, derive(proptest_derive::Arbitrary))] #[cfg_attr(test, derive(proptest_derive::Arbitrary))]
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[deserr(rename_all = camelCase, deny_unknown_fields)]
pub struct TypoSettings { pub struct TypoSettings {
#[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))] #[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))]
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
@ -66,9 +186,10 @@ pub struct TypoSettings {
} }
#[cfg_attr(test, derive(proptest_derive::Arbitrary))] #[cfg_attr(test, derive(proptest_derive::Arbitrary))]
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[deserr(rename_all = camelCase, deny_unknown_fields)]
pub struct FacetingSettings { pub struct FacetingSettings {
#[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))] #[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))]
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
@ -76,9 +197,10 @@ pub struct FacetingSettings {
} }
#[cfg_attr(test, derive(proptest_derive::Arbitrary))] #[cfg_attr(test, derive(proptest_derive::Arbitrary))]
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[deserr(rename_all = camelCase, deny_unknown_fields)]
pub struct PaginationSettings { pub struct PaginationSettings {
#[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))] #[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))]
#[serde(default, skip_serializing_if = "Setting::is_not_set")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
@ -88,10 +210,11 @@ pub struct PaginationSettings {
/// Holds all the settings for an index. `T` can either be `Checked` if they represents settings /// Holds all the settings for an index. `T` can either be `Checked` if they represents settings
/// whose validity is guaranteed, or `Unchecked` if they need to be validated. In the later case, a /// whose validity is guaranteed, or `Unchecked` if they need to be validated. In the later case, a
/// call to `check` will return a `Settings<Checked>` from a `Settings<Unchecked>`. /// call to `check` will return a `Settings<Checked>` from a `Settings<Unchecked>`.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[serde(bound(serialize = "T: Serialize", deserialize = "T: Deserialize<'static>"))] #[serde(bound(serialize = "T: Serialize", deserialize = "T: Deserialize<'static>"))]
#[deserr(rename_all = camelCase, deny_unknown_fields)]
#[cfg_attr(test, derive(proptest_derive::Arbitrary))] #[cfg_attr(test, derive(proptest_derive::Arbitrary))]
pub struct Settings<T> { pub struct Settings<T> {
#[serde( #[serde(

View File

@ -19,6 +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 = { version = "0.1.2", features = ["serde-json"] }
dump = { path = "../dump" } dump = { path = "../dump" }
either = "1.8.0" either = "1.8.0"
env_logger = "0.9.1" env_logger = "0.9.1"
@ -71,6 +72,8 @@ toml = "0.5.9"
uuid = { version = "1.1.2", features = ["serde", "v4"] } uuid = { version = "1.1.2", features = ["serde", "v4"] }
walkdir = "2.3.2" walkdir = "2.3.2"
yaup = "0.2.0" yaup = "0.2.0"
serde_urlencoded = "0.7.1"
actix-utils = "3.0.1"
[dev-dependencies] [dev-dependencies]
actix-rt = "2.7.0" actix-rt = "2.7.0"
@ -99,17 +102,7 @@ zip = { version = "0.6.2", optional = true }
default = ["analytics", "meilisearch-types/default", "mini-dashboard"] default = ["analytics", "meilisearch-types/default", "mini-dashboard"]
metrics = ["prometheus"] metrics = ["prometheus"]
analytics = ["segment"] analytics = ["segment"]
mini-dashboard = [ mini-dashboard = ["actix-web-static-files", "static-files", "anyhow", "cargo_toml", "hex", "reqwest", "sha-1", "tempfile", "zip"]
"actix-web-static-files",
"static-files",
"anyhow",
"cargo_toml",
"hex",
"reqwest",
"sha-1",
"tempfile",
"zip",
]
chinese = ["meilisearch-types/chinese"] chinese = ["meilisearch-types/chinese"]
hebrew = ["meilisearch-types/hebrew"] hebrew = ["meilisearch-types/hebrew"]
japanese = ["meilisearch-types/japanese"] japanese = ["meilisearch-types/japanese"]

View File

@ -57,7 +57,7 @@ impl ErrorCode for MeilisearchHttpError {
MeilisearchHttpError::DocumentNotFound(_) => Code::DocumentNotFound, MeilisearchHttpError::DocumentNotFound(_) => Code::DocumentNotFound,
MeilisearchHttpError::InvalidExpression(_, _) => Code::Filter, MeilisearchHttpError::InvalidExpression(_, _) => Code::Filter,
MeilisearchHttpError::PayloadTooLarge => Code::PayloadTooLarge, MeilisearchHttpError::PayloadTooLarge => Code::PayloadTooLarge,
MeilisearchHttpError::SwapIndexPayloadWrongLength(_) => Code::BadRequest, MeilisearchHttpError::SwapIndexPayloadWrongLength(_) => Code::InvalidSwapIndexes,
MeilisearchHttpError::IndexUid(e) => e.error_code(), MeilisearchHttpError::IndexUid(e) => e.error_code(),
MeilisearchHttpError::SerdeJson(_) => Code::Internal, MeilisearchHttpError::SerdeJson(_) => Code::Internal,
MeilisearchHttpError::HeedError(_) => Code::Internal, MeilisearchHttpError::HeedError(_) => Code::Internal,

View File

@ -0,0 +1,78 @@
use std::fmt::Debug;
use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::task::{Context, Poll};
use actix_web::dev::Payload;
use actix_web::web::Json;
use actix_web::{FromRequest, HttpRequest};
use deserr::{DeserializeError, DeserializeFromValue};
use futures::ready;
use meilisearch_types::error::{ErrorCode, ResponseError};
/// Extractor for typed data from Json request payloads
/// deserialised by deserr.
///
/// # Extractor
/// To extract typed data from a request body, the inner type `T` must implement the
/// [`deserr::DeserializeFromError<E>`] trait. The inner type `E` must implement the
/// [`ErrorCode`](meilisearch_error::ErrorCode) trait.
#[derive(Debug)]
pub struct ValidatedJson<T, E>(pub T, PhantomData<*const E>);
impl<T, E> ValidatedJson<T, E> {
pub fn new(data: T) -> Self {
ValidatedJson(data, PhantomData)
}
pub fn into_inner(self) -> T {
self.0
}
}
impl<T, E> FromRequest for ValidatedJson<T, E>
where
E: DeserializeError + ErrorCode + 'static,
T: DeserializeFromValue<E>,
{
type Error = actix_web::Error;
type Future = ValidatedJsonExtractFut<T, E>;
#[inline]
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
ValidatedJsonExtractFut {
fut: Json::<serde_json::Value>::from_request(req, payload),
_phantom: PhantomData,
}
}
}
pub struct ValidatedJsonExtractFut<T, E> {
fut: <Json<serde_json::Value> as FromRequest>::Future,
_phantom: PhantomData<*const (T, E)>,
}
impl<T, E> Future for ValidatedJsonExtractFut<T, E>
where
T: DeserializeFromValue<E>,
E: DeserializeError + ErrorCode + 'static,
{
type Output = Result<ValidatedJson<T, E>, actix_web::Error>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let ValidatedJsonExtractFut { fut, .. } = self.get_mut();
let fut = Pin::new(fut);
let res = ready!(fut.poll(cx));
let res = match res {
Err(err) => Err(err),
Ok(data) => match deserr::deserialize::<_, _, E>(data.into_inner()) {
Ok(data) => Ok(ValidatedJson::new(data)),
Err(e) => Err(ResponseError::from(e).into()),
},
};
Poll::Ready(res)
}
}

View File

@ -1,4 +1,6 @@
pub mod payload; pub mod payload;
#[macro_use] #[macro_use]
pub mod authentication; pub mod authentication;
pub mod json;
pub mod query_parameters;
pub mod sequential_extractor; pub mod sequential_extractor;

View File

@ -0,0 +1,70 @@
//! A module to parse query parameter with deserr
use std::marker::PhantomData;
use std::{fmt, ops};
use actix_http::Payload;
use actix_utils::future::{err, ok, Ready};
use actix_web::{FromRequest, HttpRequest};
use deserr::{DeserializeError, DeserializeFromValue};
use meilisearch_types::error::{Code, ErrorCode, ResponseError};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct QueryParameter<T, E>(pub T, PhantomData<*const E>);
impl<T, E> QueryParameter<T, E> {
/// Unwrap into inner `T` value.
pub fn into_inner(self) -> T {
self.0
}
}
impl<T, E> QueryParameter<T, E>
where
T: DeserializeFromValue<E>,
E: DeserializeError + ErrorCode + 'static,
{
pub fn from_query(query_str: &str) -> Result<Self, actix_web::Error> {
let value = serde_urlencoded::from_str::<serde_json::Value>(query_str)
.map_err(|e| ResponseError::from_msg(e.to_string(), Code::BadRequest))?;
match deserr::deserialize::<_, _, E>(value) {
Ok(data) => Ok(QueryParameter(data, PhantomData)),
Err(e) => Err(ResponseError::from(e).into()),
}
}
}
impl<T, E> ops::Deref for QueryParameter<T, E> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
impl<T, E> ops::DerefMut for QueryParameter<T, E> {
fn deref_mut(&mut self) -> &mut T {
&mut self.0
}
}
impl<T: fmt::Display, E> fmt::Display for QueryParameter<T, E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl<T, E> FromRequest for QueryParameter<T, E>
where
T: DeserializeFromValue<E>,
E: DeserializeError + ErrorCode + 'static,
{
type Error = actix_web::Error;
type Future = Ready<Result<Self, actix_web::Error>>;
#[inline]
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
QueryParameter::from_query(req.query_string()).map(ok).unwrap_or_else(err)
}
}

View File

@ -1,9 +1,12 @@
use std::str; use std::convert::Infallible;
use std::num::ParseIntError;
use std::{fmt, str};
use actix_web::{web, HttpRequest, HttpResponse}; use actix_web::{web, HttpRequest, HttpResponse};
use deserr::{DeserializeError, IntoValue, MergeWithError, ValuePointerRef};
use meilisearch_auth::error::AuthControllerError; use meilisearch_auth::error::AuthControllerError;
use meilisearch_auth::AuthController; use meilisearch_auth::AuthController;
use meilisearch_types::error::{Code, ResponseError}; use meilisearch_types::error::{unwrap_any, Code, ErrorCode, ResponseError};
use meilisearch_types::keys::{Action, Key}; use meilisearch_types::keys::{Action, Key};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
@ -12,6 +15,7 @@ use uuid::Uuid;
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::sequential_extractor::SeqHandler; use crate::extractors::sequential_extractor::SeqHandler;
use crate::routes::Pagination; use crate::routes::Pagination;
@ -45,10 +49,72 @@ pub async fn create_api_key(
Ok(HttpResponse::Created().json(res)) Ok(HttpResponse::Created().json(res))
} }
#[derive(Debug)]
pub struct PaginationDeserrError {
error: String,
code: Code,
}
impl std::fmt::Display for PaginationDeserrError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.error)
}
}
impl std::error::Error for PaginationDeserrError {}
impl ErrorCode for PaginationDeserrError {
fn error_code(&self) -> Code {
self.code
}
}
impl MergeWithError<PaginationDeserrError> for PaginationDeserrError {
fn merge(
_self_: Option<Self>,
other: PaginationDeserrError,
_merge_location: ValuePointerRef,
) -> Result<Self, Self> {
Err(other)
}
}
impl DeserializeError for PaginationDeserrError {
fn error<V: IntoValue>(
_self_: Option<Self>,
error: deserr::ErrorKind<V>,
location: ValuePointerRef,
) -> Result<Self, Self> {
let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0;
let code = match location.last_field() {
Some("offset") => Code::InvalidApiKeyLimit,
Some("limit") => Code::InvalidApiKeyOffset,
_ => Code::BadRequest,
};
Err(PaginationDeserrError { error, code })
}
}
impl MergeWithError<ParseIntError> for PaginationDeserrError {
fn merge(
_self_: Option<Self>,
other: ParseIntError,
merge_location: ValuePointerRef,
) -> Result<Self, Self> {
PaginationDeserrError::error::<Infallible>(
None,
deserr::ErrorKind::Unexpected { msg: other.to_string() },
merge_location,
)
}
}
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>,
paginate: web::Query<Pagination>, paginate: QueryParameter<Pagination, PaginationDeserrError>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let paginate = paginate.into_inner();
let page_view = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> { let page_view = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> {
let keys = auth_controller.list_keys()?; let keys = auth_controller.list_keys()?;
let page_view = paginate let page_view = paginate

View File

@ -1,14 +1,19 @@
use std::convert::Infallible;
use std::fmt;
use std::io::ErrorKind; use std::io::ErrorKind;
use std::num::ParseIntError;
use std::str::FromStr;
use actix_web::http::header::CONTENT_TYPE; use actix_web::http::header::CONTENT_TYPE;
use actix_web::web::Data; use actix_web::web::Data;
use actix_web::{web, HttpMessage, HttpRequest, HttpResponse}; use actix_web::{web, HttpMessage, HttpRequest, HttpResponse};
use bstr::ByteSlice; use bstr::ByteSlice;
use deserr::{DeserializeError, DeserializeFromValue, IntoValue, MergeWithError, ValuePointerRef};
use futures::StreamExt; use futures::StreamExt;
use index_scheduler::IndexScheduler; use index_scheduler::IndexScheduler;
use log::debug; use log::debug;
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::ResponseError; use meilisearch_types::error::{unwrap_any, Code, ErrorCode, 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;
@ -30,6 +35,7 @@ use crate::error::PayloadError::ReceivePayload;
use crate::extractors::authentication::policies::*; use crate::extractors::authentication::policies::*;
use crate::extractors::authentication::GuardedData; use crate::extractors::authentication::GuardedData;
use crate::extractors::payload::Payload; use crate::extractors::payload::Payload;
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::{fold_star_or, PaginationView, SummarizedTaskView};
@ -76,16 +82,62 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
); );
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug, DeserializeFromValue)]
#[serde(rename_all = "camelCase", deny_unknown_fields)] #[deserr(rename_all = camelCase, deny_unknown_fields)]
pub struct GetDocument { pub struct GetDocument {
fields: Option<CS<StarOr<String>>>, fields: Option<CS<StarOr<String>>>,
} }
#[derive(Debug)]
pub struct GetDocumentDeserrError {
error: String,
code: Code,
}
impl std::fmt::Display for GetDocumentDeserrError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.error)
}
}
impl std::error::Error for GetDocumentDeserrError {}
impl ErrorCode for GetDocumentDeserrError {
fn error_code(&self) -> Code {
self.code
}
}
impl MergeWithError<GetDocumentDeserrError> for GetDocumentDeserrError {
fn merge(
_self_: Option<Self>,
other: GetDocumentDeserrError,
_merge_location: ValuePointerRef,
) -> Result<Self, Self> {
Err(other)
}
}
impl DeserializeError for GetDocumentDeserrError {
fn error<V: IntoValue>(
_self_: Option<Self>,
error: deserr::ErrorKind<V>,
location: ValuePointerRef,
) -> Result<Self, Self> {
let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0;
let code = match location.last_field() {
Some("fields") => Code::InvalidDocumentFields,
_ => Code::BadRequest,
};
Err(GetDocumentDeserrError { error, code })
}
}
pub async fn get_document( 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>, path: web::Path<DocumentParam>,
params: web::Query<GetDocument>, params: QueryParameter<GetDocument, GetDocumentDeserrError>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let GetDocument { fields } = params.into_inner(); let GetDocument { fields } = params.into_inner();
let attributes_to_retrieve = fields.and_then(fold_star_or); let attributes_to_retrieve = fields.and_then(fold_star_or);
@ -112,20 +164,82 @@ pub async fn delete_document(
Ok(HttpResponse::Accepted().json(task)) Ok(HttpResponse::Accepted().json(task))
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug, DeserializeFromValue)]
#[serde(rename_all = "camelCase", deny_unknown_fields)] #[deserr(rename_all = camelCase, deny_unknown_fields)]
pub struct BrowseQuery { pub struct BrowseQuery {
#[serde(default)] #[deserr(default, from(&String) = FromStr::from_str -> ParseIntError)]
offset: usize, offset: usize,
#[serde(default = "crate::routes::PAGINATION_DEFAULT_LIMIT")] #[deserr(default = crate::routes::PAGINATION_DEFAULT_LIMIT(), from(&String) = FromStr::from_str -> ParseIntError)]
limit: usize, limit: usize,
fields: Option<CS<StarOr<String>>>, fields: Option<CS<StarOr<String>>>,
} }
#[derive(Debug)]
pub struct BrowseQueryDeserrError {
error: String,
code: Code,
}
impl std::fmt::Display for BrowseQueryDeserrError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.error)
}
}
impl std::error::Error for BrowseQueryDeserrError {}
impl ErrorCode for BrowseQueryDeserrError {
fn error_code(&self) -> Code {
self.code
}
}
impl MergeWithError<BrowseQueryDeserrError> for BrowseQueryDeserrError {
fn merge(
_self_: Option<Self>,
other: BrowseQueryDeserrError,
_merge_location: ValuePointerRef,
) -> Result<Self, Self> {
Err(other)
}
}
impl DeserializeError for BrowseQueryDeserrError {
fn error<V: IntoValue>(
_self_: Option<Self>,
error: deserr::ErrorKind<V>,
location: ValuePointerRef,
) -> Result<Self, Self> {
let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0;
let code = match location.last_field() {
Some("fields") => Code::InvalidDocumentFields,
Some("offset") => Code::InvalidDocumentOffset,
Some("limit") => Code::InvalidDocumentLimit,
_ => Code::BadRequest,
};
Err(BrowseQueryDeserrError { error, code })
}
}
impl MergeWithError<ParseIntError> for BrowseQueryDeserrError {
fn merge(
_self_: Option<Self>,
other: ParseIntError,
merge_location: ValuePointerRef,
) -> Result<Self, Self> {
BrowseQueryDeserrError::error::<Infallible>(
None,
deserr::ErrorKind::Unexpected { msg: other.to_string() },
merge_location,
)
}
}
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: web::Query<BrowseQuery>, params: QueryParameter<BrowseQuery, BrowseQueryDeserrError>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
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();
@ -140,16 +254,62 @@ pub async fn get_all_documents(
Ok(HttpResponse::Ok().json(ret)) Ok(HttpResponse::Ok().json(ret))
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug, DeserializeFromValue)]
#[serde(rename_all = "camelCase", deny_unknown_fields)] #[deserr(rename_all = camelCase, deny_unknown_fields)]
pub struct UpdateDocumentsQuery { pub struct UpdateDocumentsQuery {
pub primary_key: Option<String>, pub primary_key: Option<String>,
} }
#[derive(Debug)]
pub struct UpdateDocumentsQueryDeserrError {
error: String,
code: Code,
}
impl std::fmt::Display for UpdateDocumentsQueryDeserrError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.error)
}
}
impl std::error::Error for UpdateDocumentsQueryDeserrError {}
impl ErrorCode for UpdateDocumentsQueryDeserrError {
fn error_code(&self) -> Code {
self.code
}
}
impl MergeWithError<UpdateDocumentsQueryDeserrError> for UpdateDocumentsQueryDeserrError {
fn merge(
_self_: Option<Self>,
other: UpdateDocumentsQueryDeserrError,
_merge_location: ValuePointerRef,
) -> Result<Self, Self> {
Err(other)
}
}
impl DeserializeError for UpdateDocumentsQueryDeserrError {
fn error<V: IntoValue>(
_self_: Option<Self>,
error: deserr::ErrorKind<V>,
location: ValuePointerRef,
) -> Result<Self, Self> {
let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0;
let code = match location.last_field() {
Some("primaryKey") => Code::InvalidIndexPrimaryKey,
_ => Code::BadRequest,
};
Err(UpdateDocumentsQueryDeserrError { error, code })
}
}
pub async fn add_documents( 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: web::Query<UpdateDocumentsQuery>, params: QueryParameter<UpdateDocumentsQuery, UpdateDocumentsQueryDeserrError>,
body: Payload, body: Payload,
req: HttpRequest, req: HttpRequest,
analytics: web::Data<dyn Analytics>, analytics: web::Data<dyn Analytics>,
@ -177,7 +337,7 @@ 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>, path: web::Path<String>,
params: web::Query<UpdateDocumentsQuery>, params: QueryParameter<UpdateDocumentsQuery, UpdateDocumentsQueryDeserrError>,
body: Payload, body: Payload,
req: HttpRequest, req: HttpRequest,
analytics: web::Data<dyn Analytics>, analytics: web::Data<dyn Analytics>,

View File

@ -1,8 +1,14 @@
use std::convert::Infallible;
use std::num::ParseIntError;
use actix_web::web::Data; use actix_web::web::Data;
use actix_web::{web, HttpRequest, HttpResponse}; use actix_web::{web, HttpRequest, HttpResponse};
use deserr::{
DeserializeError, DeserializeFromValue, ErrorKind, IntoValue, MergeWithError, ValuePointerRef,
};
use index_scheduler::IndexScheduler; use index_scheduler::IndexScheduler;
use log::debug; use log::debug;
use meilisearch_types::error::ResponseError; use meilisearch_types::error::{unwrap_any, Code, ErrorCode, 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;
@ -14,6 +20,8 @@ use super::{Pagination, SummarizedTaskView};
use crate::analytics::Analytics; use crate::analytics::Analytics;
use crate::extractors::authentication::policies::*; use crate::extractors::authentication::policies::*;
use crate::extractors::authentication::{AuthenticationError, GuardedData}; use crate::extractors::authentication::{AuthenticationError, GuardedData};
use crate::extractors::json::ValidatedJson;
use crate::extractors::query_parameters::QueryParameter;
use crate::extractors::sequential_extractor::SeqHandler; use crate::extractors::sequential_extractor::SeqHandler;
pub mod documents; pub mod documents;
@ -66,7 +74,7 @@ impl IndexView {
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: web::Query<Pagination>, paginate: QueryParameter<Pagination, ListIndexesDeserrError>,
) -> 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()?;
@ -82,8 +90,68 @@ pub async fn list_indexes(
Ok(HttpResponse::Ok().json(ret)) Ok(HttpResponse::Ok().json(ret))
} }
#[derive(Debug, Deserialize)] #[derive(Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct ListIndexesDeserrError {
error: String,
code: Code,
}
impl std::fmt::Display for ListIndexesDeserrError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.error)
}
}
impl std::error::Error for ListIndexesDeserrError {}
impl ErrorCode for ListIndexesDeserrError {
fn error_code(&self) -> Code {
self.code
}
}
impl MergeWithError<ListIndexesDeserrError> for ListIndexesDeserrError {
fn merge(
_self_: Option<Self>,
other: ListIndexesDeserrError,
_merge_location: ValuePointerRef,
) -> Result<Self, Self> {
Err(other)
}
}
impl deserr::DeserializeError for ListIndexesDeserrError {
fn error<V: IntoValue>(
_self_: Option<Self>,
error: ErrorKind<V>,
location: ValuePointerRef,
) -> Result<Self, Self> {
let code = match location.last_field() {
Some("offset") => Code::InvalidIndexLimit,
Some("limit") => Code::InvalidIndexOffset,
_ => Code::BadRequest,
};
let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0;
Err(ListIndexesDeserrError { error, code })
}
}
impl MergeWithError<ParseIntError> for ListIndexesDeserrError {
fn merge(
_self_: Option<Self>,
other: ParseIntError,
merge_location: ValuePointerRef,
) -> Result<Self, Self> {
ListIndexesDeserrError::error::<Infallible>(
None,
ErrorKind::Unexpected { msg: other.to_string() },
merge_location,
)
}
}
#[derive(DeserializeFromValue, Debug)]
#[deserr(rename_all = camelCase, deny_unknown_fields)]
pub struct IndexCreateRequest { pub struct IndexCreateRequest {
uid: String, uid: String,
primary_key: Option<String>, primary_key: Option<String>,
@ -91,7 +159,7 @@ pub struct IndexCreateRequest {
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: web::Json<IndexCreateRequest>, body: ValidatedJson<IndexCreateRequest, CreateIndexesDeserrError>,
req: HttpRequest, req: HttpRequest,
analytics: web::Data<dyn Analytics>, analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
@ -116,11 +184,58 @@ pub async fn create_index(
} }
} }
#[derive(Debug, Deserialize)] #[derive(Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct CreateIndexesDeserrError {
#[allow(dead_code)] error: String,
code: Code,
}
impl std::fmt::Display for CreateIndexesDeserrError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.error)
}
}
impl std::error::Error for CreateIndexesDeserrError {}
impl ErrorCode for CreateIndexesDeserrError {
fn error_code(&self) -> Code {
self.code
}
}
impl MergeWithError<CreateIndexesDeserrError> for CreateIndexesDeserrError {
fn merge(
_self_: Option<Self>,
other: CreateIndexesDeserrError,
_merge_location: ValuePointerRef,
) -> Result<Self, Self> {
Err(other)
}
}
impl deserr::DeserializeError for CreateIndexesDeserrError {
fn error<V: IntoValue>(
_self_: Option<Self>,
error: ErrorKind<V>,
location: ValuePointerRef,
) -> Result<Self, Self> {
let code = match location.last_field() {
Some("uid") => Code::InvalidIndexUid,
Some("primaryKey") => Code::InvalidIndexPrimaryKey,
None if matches!(error, ErrorKind::MissingField { field } if field == "uid") => {
Code::MissingIndexUid
}
_ => Code::BadRequest,
};
let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0;
Err(CreateIndexesDeserrError { error, code })
}
}
#[derive(DeserializeFromValue, Debug)]
#[deserr(rename_all = camelCase, deny_unknown_fields)]
pub struct UpdateIndexRequest { pub struct UpdateIndexRequest {
uid: Option<String>,
primary_key: Option<String>, primary_key: Option<String>,
} }
@ -139,7 +254,7 @@ 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>, path: web::Path<String>,
body: web::Json<UpdateIndexRequest>, body: ValidatedJson<UpdateIndexRequest, UpdateIndexesDeserrError>,
req: HttpRequest, req: HttpRequest,
analytics: web::Data<dyn Analytics>, analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
@ -147,7 +262,7 @@ pub async fn update_index(
let body = body.into_inner(); let body = body.into_inner();
analytics.publish( analytics.publish(
"Index Updated".to_string(), "Index Updated".to_string(),
json!({ "primary_key": body.primary_key}), json!({ "primary_key": body.primary_key }),
Some(&req), Some(&req),
); );
@ -163,6 +278,51 @@ pub async fn update_index(
Ok(HttpResponse::Accepted().json(task)) Ok(HttpResponse::Accepted().json(task))
} }
#[derive(Debug)]
pub struct UpdateIndexesDeserrError {
error: String,
code: Code,
}
impl std::fmt::Display for UpdateIndexesDeserrError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.error)
}
}
impl std::error::Error for UpdateIndexesDeserrError {}
impl ErrorCode for UpdateIndexesDeserrError {
fn error_code(&self) -> Code {
self.code
}
}
impl MergeWithError<UpdateIndexesDeserrError> for UpdateIndexesDeserrError {
fn merge(
_self_: Option<Self>,
other: UpdateIndexesDeserrError,
_merge_location: ValuePointerRef,
) -> Result<Self, Self> {
Err(other)
}
}
impl deserr::DeserializeError for UpdateIndexesDeserrError {
fn error<V: IntoValue>(
_self_: Option<Self>,
error: ErrorKind<V>,
location: ValuePointerRef,
) -> Result<Self, Self> {
let code = match location.last_field() {
Some("primaryKey") => Code::InvalidIndexPrimaryKey,
_ => Code::BadRequest,
};
let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0;
Err(UpdateIndexesDeserrError { error, code })
}
}
pub async fn delete_index( 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>,

View File

@ -1,21 +1,25 @@
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::error::ResponseError; use meilisearch_types::error::ResponseError;
use serde::Deserialize;
use serde_cs::vec::CS; use serde_cs::vec::CS;
use serde_json::Value; use serde_json::Value;
use crate::analytics::{Analytics, SearchAggregator}; use crate::analytics::{Analytics, SearchAggregator};
use crate::extractors::authentication::policies::*; use crate::extractors::authentication::policies::*;
use crate::extractors::authentication::GuardedData; use crate::extractors::authentication::GuardedData;
use crate::extractors::json::ValidatedJson;
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;
use crate::search::{ use crate::search::{
perform_search, MatchingStrategy, SearchQuery, DEFAULT_CROP_LENGTH, DEFAULT_CROP_MARKER, perform_search, MatchingStrategy, SearchDeserError, SearchQuery, DEFAULT_CROP_LENGTH,
DEFAULT_HIGHLIGHT_POST_TAG, DEFAULT_HIGHLIGHT_PRE_TAG, DEFAULT_SEARCH_LIMIT, DEFAULT_CROP_MARKER, DEFAULT_HIGHLIGHT_POST_TAG, DEFAULT_HIGHLIGHT_PRE_TAG,
DEFAULT_SEARCH_OFFSET, DEFAULT_SEARCH_LIMIT, DEFAULT_SEARCH_OFFSET,
}; };
pub fn configure(cfg: &mut web::ServiceConfig) { pub fn configure(cfg: &mut web::ServiceConfig) {
@ -26,33 +30,35 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
); );
} }
#[derive(Deserialize, Debug)] #[derive(Debug, deserr::DeserializeFromValue)]
#[serde(rename_all = "camelCase", deny_unknown_fields)] #[deserr(rename_all = camelCase, deny_unknown_fields)]
pub struct SearchQueryGet { pub struct SearchQueryGet {
q: Option<String>, q: Option<String>,
#[serde(default = "DEFAULT_SEARCH_OFFSET")] #[deserr(default = DEFAULT_SEARCH_OFFSET(), from(&String) = FromStr::from_str -> std::num::ParseIntError)]
offset: usize, offset: usize,
#[serde(default = "DEFAULT_SEARCH_LIMIT")] #[deserr(default = DEFAULT_SEARCH_LIMIT(), from(&String) = FromStr::from_str -> std::num::ParseIntError)]
limit: usize, limit: usize,
#[deserr(from(&String) = from_string_to_option -> std::num::ParseIntError)]
page: Option<usize>, page: Option<usize>,
#[deserr(from(&String) = from_string_to_option -> std::num::ParseIntError)]
hits_per_page: Option<usize>, hits_per_page: Option<usize>,
attributes_to_retrieve: Option<CS<String>>, attributes_to_retrieve: Option<CS<String>>,
attributes_to_crop: Option<CS<String>>, attributes_to_crop: Option<CS<String>>,
#[serde(default = "DEFAULT_CROP_LENGTH")] #[deserr(default = DEFAULT_CROP_LENGTH(), from(&String) = FromStr::from_str -> std::num::ParseIntError)]
crop_length: usize, crop_length: usize,
attributes_to_highlight: Option<CS<String>>, attributes_to_highlight: Option<CS<String>>,
filter: Option<String>, filter: Option<String>,
sort: Option<String>, sort: Option<String>,
#[serde(default = "Default::default")] #[deserr(default, from(&String) = FromStr::from_str -> std::str::ParseBoolError)]
show_matches_position: bool, show_matches_position: bool,
facets: Option<CS<String>>, facets: Option<CS<String>>,
#[serde(default = "DEFAULT_HIGHLIGHT_PRE_TAG")] #[deserr(default = DEFAULT_HIGHLIGHT_PRE_TAG())]
highlight_pre_tag: String, highlight_pre_tag: String,
#[serde(default = "DEFAULT_HIGHLIGHT_POST_TAG")] #[deserr(default = DEFAULT_HIGHLIGHT_POST_TAG())]
highlight_post_tag: String, highlight_post_tag: String,
#[serde(default = "DEFAULT_CROP_MARKER")] #[deserr(default = DEFAULT_CROP_MARKER())]
crop_marker: String, crop_marker: String,
#[serde(default)] #[deserr(default)]
matching_strategy: MatchingStrategy, matching_strategy: MatchingStrategy,
} }
@ -136,7 +142,7 @@ 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: web::Query<SearchQueryGet>, params: QueryParameter<SearchQueryGet, SearchDeserError>,
req: HttpRequest, req: HttpRequest,
analytics: web::Data<dyn Analytics>, analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
@ -168,7 +174,7 @@ 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: web::Json<SearchQuery>, params: ValidatedJson<SearchQuery, SearchDeserError>,
req: HttpRequest, req: HttpRequest,
analytics: web::Data<dyn Analytics>, analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {

View File

@ -1,8 +1,11 @@
use std::fmt;
use actix_web::web::Data; use actix_web::web::Data;
use actix_web::{web, HttpRequest, HttpResponse}; use actix_web::{web, HttpRequest, HttpResponse};
use deserr::{IntoValue, ValuePointerRef};
use index_scheduler::IndexScheduler; use index_scheduler::IndexScheduler;
use log::debug; use log::debug;
use meilisearch_types::error::ResponseError; use meilisearch_types::error::{unwrap_any, Code, ErrorCode, ResponseError};
use meilisearch_types::index_uid::IndexUid; use meilisearch_types::index_uid::IndexUid;
use meilisearch_types::settings::{settings, Settings, Unchecked}; use meilisearch_types::settings::{settings, Settings, Unchecked};
use meilisearch_types::tasks::KindWithContent; use meilisearch_types::tasks::KindWithContent;
@ -11,6 +14,7 @@ use serde_json::json;
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::json::ValidatedJson;
use crate::routes::SummarizedTaskView; use crate::routes::SummarizedTaskView;
#[macro_export] #[macro_export]
@ -39,7 +43,7 @@ macro_rules! make_setting_route {
>, >,
index_uid: web::Path<String>, index_uid: web::Path<String>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {
let new_settings = Settings { $attr: Setting::Reset, ..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 index_uid = IndexUid::try_from(index_uid.into_inner())?.into_inner();
@ -74,8 +78,8 @@ macro_rules! make_setting_route {
let new_settings = Settings { let new_settings = Settings {
$attr: match body { $attr: match body {
Some(inner_body) => Setting::Set(inner_body), Some(inner_body) => Setting::Set(inner_body).into(),
None => Setting::Reset, None => Setting::Reset.into(),
}, },
..Default::default() ..Default::default()
}; };
@ -208,7 +212,7 @@ make_setting_route!(
"TypoTolerance Updated".to_string(), "TypoTolerance Updated".to_string(),
json!({ json!({
"typo_tolerance": { "typo_tolerance": {
"enabled": setting.as_ref().map(|s| !matches!(s.enabled, Setting::Set(false))), "enabled": setting.as_ref().map(|s| !matches!(s.enabled.into(), Setting::Set(false))),
"disable_on_attributes": setting "disable_on_attributes": setting
.as_ref() .as_ref()
.and_then(|s| s.disable_on_attributes.as_ref().set().map(|m| !m.is_empty())), .and_then(|s| s.disable_on_attributes.as_ref().set().map(|m| !m.is_empty())),
@ -424,10 +428,66 @@ generate_configure!(
faceting faceting
); );
#[derive(Debug)]
pub struct SettingsDeserrError {
error: String,
code: Code,
}
impl std::fmt::Display for SettingsDeserrError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.error)
}
}
impl std::error::Error for SettingsDeserrError {}
impl ErrorCode for SettingsDeserrError {
fn error_code(&self) -> Code {
self.code
}
}
impl deserr::MergeWithError<SettingsDeserrError> for SettingsDeserrError {
fn merge(
_self_: Option<Self>,
other: SettingsDeserrError,
_merge_location: ValuePointerRef,
) -> Result<Self, Self> {
Err(other)
}
}
impl deserr::DeserializeError for SettingsDeserrError {
fn error<V: IntoValue>(
_self_: Option<Self>,
error: deserr::ErrorKind<V>,
location: ValuePointerRef,
) -> Result<Self, Self> {
let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0;
let code = match location.first_field() {
Some("displayedAttributes") => Code::InvalidSettingsDisplayedAttributes,
Some("searchableAttributes") => Code::InvalidSettingsSearchableAttributes,
Some("filterableAttributes") => Code::InvalidSettingsFilterableAttributes,
Some("sortableAttributes") => Code::InvalidSettingsSortableAttributes,
Some("rankingRules") => Code::InvalidSettingsRankingRules,
Some("stopWords") => Code::InvalidSettingsStopWords,
Some("synonyms") => Code::InvalidSettingsSynonyms,
Some("distinctAttribute") => Code::InvalidSettingsDistinctAttribute,
Some("typoTolerance") => Code::InvalidSettingsTypoTolerance,
Some("faceting") => Code::InvalidSettingsFaceting,
Some("pagination") => Code::InvalidSettingsPagination,
_ => Code::BadRequest,
};
Err(SettingsDeserrError { error, code })
}
}
pub async fn update_all( 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: web::Json<Settings<Unchecked>>, body: ValidatedJson<Settings<Unchecked>, SettingsDeserrError>,
req: HttpRequest, req: HttpRequest,
analytics: web::Data<dyn Analytics>, analytics: web::Data<dyn Analytics>,
) -> Result<HttpResponse, ResponseError> { ) -> Result<HttpResponse, ResponseError> {

View File

@ -1,7 +1,9 @@
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 deserr::DeserializeFromValue;
use index_scheduler::{IndexScheduler, Query}; use index_scheduler::{IndexScheduler, Query};
use log::debug; use log::debug;
use meilisearch_types::error::ResponseError; use meilisearch_types::error::ResponseError;
@ -49,6 +51,13 @@ where
.collect() .collect()
} }
pub fn from_string_to_option<T, E>(input: &str) -> Result<Option<T>, E>
where
T: FromStr<Err = E>,
{
Ok(Some(input.parse()?))
}
const PAGINATION_DEFAULT_LIMIT: fn() -> usize = || 20; const PAGINATION_DEFAULT_LIMIT: fn() -> usize = || 20;
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
@ -75,12 +84,15 @@ impl From<Task> for SummarizedTaskView {
} }
} }
#[derive(Debug, Clone, Copy, Deserialize)] #[derive(DeserializeFromValue, Deserialize, Debug, Clone, Copy)]
#[deserr(rename_all = camelCase, deny_unknown_fields)]
#[serde(rename_all = "camelCase", deny_unknown_fields)] #[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Pagination { pub struct Pagination {
#[serde(default)] #[serde(default)]
#[deserr(default, from(&String) = FromStr::from_str -> std::num::ParseIntError)]
pub offset: usize, pub offset: usize,
#[serde(default = "PAGINATION_DEFAULT_LIMIT")] #[serde(default = "PAGINATION_DEFAULT_LIMIT")]
#[deserr(default = PAGINATION_DEFAULT_LIMIT(), from(&String) = FromStr::from_str -> std::num::ParseIntError)]
pub limit: usize, pub limit: usize,
} }

View File

@ -1,9 +1,11 @@
use std::fmt;
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, IntoValue, ValuePointerRef};
use index_scheduler::IndexScheduler; use index_scheduler::IndexScheduler;
use meilisearch_types::error::ResponseError; use meilisearch_types::error::{unwrap_any, Code, ErrorCode, ResponseError};
use meilisearch_types::tasks::{IndexSwap, KindWithContent}; use meilisearch_types::tasks::{IndexSwap, KindWithContent};
use serde::Deserialize;
use serde_json::json; use serde_json::json;
use super::SummarizedTaskView; use super::SummarizedTaskView;
@ -11,23 +13,26 @@ use crate::analytics::Analytics;
use crate::error::MeilisearchHttpError; use crate::error::MeilisearchHttpError;
use crate::extractors::authentication::policies::*; use crate::extractors::authentication::policies::*;
use crate::extractors::authentication::{AuthenticationError, GuardedData}; use crate::extractors::authentication::{AuthenticationError, GuardedData};
use crate::extractors::json::ValidatedJson;
use crate::extractors::sequential_extractor::SeqHandler; use crate::extractors::sequential_extractor::SeqHandler;
pub fn configure(cfg: &mut web::ServiceConfig) { pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(web::resource("").route(web::post().to(SeqHandler(swap_indexes)))); cfg.service(web::resource("").route(web::post().to(SeqHandler(swap_indexes))));
} }
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)] #[derive(DeserializeFromValue, Debug, Clone, PartialEq, Eq)]
#[deserr(rename_all = camelCase, deny_unknown_fields)]
pub struct SwapIndexesPayload { pub struct SwapIndexesPayload {
indexes: Vec<String>, indexes: Vec<String>,
} }
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: web::Json<Vec<SwapIndexesPayload>>, params: ValidatedJson<Vec<SwapIndexesPayload>, SwapIndexesDeserrError>,
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();
analytics.publish( analytics.publish(
"Indexes Swapped".to_string(), "Indexes Swapped".to_string(),
json!({ json!({
@ -38,7 +43,7 @@ pub async fn swap_indexes(
let search_rules = &index_scheduler.filters().search_rules; let search_rules = &index_scheduler.filters().search_rules;
let mut swaps = vec![]; let mut swaps = vec![];
for SwapIndexesPayload { indexes } in params.into_inner().into_iter() { for SwapIndexesPayload { indexes } in params.into_iter() {
let (lhs, rhs) = match indexes.as_slice() { let (lhs, rhs) = match indexes.as_slice() {
[lhs, rhs] => (lhs, rhs), [lhs, rhs] => (lhs, rhs),
_ => { _ => {
@ -57,3 +62,49 @@ pub async fn swap_indexes(
let task: SummarizedTaskView = task.into(); let task: SummarizedTaskView = task.into();
Ok(HttpResponse::Accepted().json(task)) Ok(HttpResponse::Accepted().json(task))
} }
#[derive(Debug)]
pub struct SwapIndexesDeserrError {
error: String,
code: Code,
}
impl std::fmt::Display for SwapIndexesDeserrError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.error)
}
}
impl std::error::Error for SwapIndexesDeserrError {}
impl ErrorCode for SwapIndexesDeserrError {
fn error_code(&self) -> Code {
self.code
}
}
impl deserr::MergeWithError<SwapIndexesDeserrError> for SwapIndexesDeserrError {
fn merge(
_self_: Option<Self>,
other: SwapIndexesDeserrError,
_merge_location: ValuePointerRef,
) -> Result<Self, Self> {
Err(other)
}
}
impl deserr::DeserializeError for SwapIndexesDeserrError {
fn error<V: IntoValue>(
_self_: Option<Self>,
error: deserr::ErrorKind<V>,
location: ValuePointerRef,
) -> Result<Self, Self> {
let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0;
let code = match location.last_field() {
Some("indexes") => Code::InvalidSwapIndexes,
_ => Code::BadRequest,
};
Err(SwapIndexesDeserrError { error, code })
}
}

View File

@ -2,6 +2,7 @@ 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::error::DateField;
use index_scheduler::{IndexScheduler, Query, TaskId}; use index_scheduler::{IndexScheduler, Query, TaskId};
use meilisearch_types::error::ResponseError; use meilisearch_types::error::ResponseError;
use meilisearch_types::index_uid::IndexUid; use meilisearch_types::index_uid::IndexUid;
@ -168,6 +169,7 @@ pub struct TaskCommonQueryRaw {
pub statuses: Option<CS<StarOr<String>>>, pub statuses: Option<CS<StarOr<String>>>,
pub index_uids: Option<CS<StarOr<String>>>, pub index_uids: Option<CS<StarOr<String>>>,
} }
impl TaskCommonQueryRaw { impl TaskCommonQueryRaw {
fn validate(self) -> Result<TaskCommonQuery, ResponseError> { fn validate(self) -> Result<TaskCommonQuery, ResponseError> {
let Self { uids, canceled_by, types, statuses, index_uids } = self; let Self { uids, canceled_by, types, statuses, index_uids } = self;
@ -290,37 +292,37 @@ impl TaskDateQueryRaw {
for (field_name, string_value, before_or_after, dest) in [ for (field_name, string_value, before_or_after, dest) in [
( (
"afterEnqueuedAt", DateField::AfterEnqueuedAt,
after_enqueued_at, after_enqueued_at,
DeserializeDateOption::After, DeserializeDateOption::After,
&mut query.after_enqueued_at, &mut query.after_enqueued_at,
), ),
( (
"beforeEnqueuedAt", DateField::BeforeEnqueuedAt,
before_enqueued_at, before_enqueued_at,
DeserializeDateOption::Before, DeserializeDateOption::Before,
&mut query.before_enqueued_at, &mut query.before_enqueued_at,
), ),
( (
"afterStartedAt", DateField::AfterStartedAt,
after_started_at, after_started_at,
DeserializeDateOption::After, DeserializeDateOption::After,
&mut query.after_started_at, &mut query.after_started_at,
), ),
( (
"beforeStartedAt", DateField::BeforeStartedAt,
before_started_at, before_started_at,
DeserializeDateOption::Before, DeserializeDateOption::Before,
&mut query.before_started_at, &mut query.before_started_at,
), ),
( (
"afterFinishedAt", DateField::AfterFinishedAt,
after_finished_at, after_finished_at,
DeserializeDateOption::After, DeserializeDateOption::After,
&mut query.after_finished_at, &mut query.after_finished_at,
), ),
( (
"beforeFinishedAt", DateField::BeforeFinishedAt,
before_finished_at, before_finished_at,
DeserializeDateOption::Before, DeserializeDateOption::Before,
&mut query.before_finished_at, &mut query.before_finished_at,
@ -690,6 +692,7 @@ async fn get_task(
} }
pub(crate) mod date_deserializer { pub(crate) mod date_deserializer {
use index_scheduler::error::DateField;
use meilisearch_types::error::ResponseError; use meilisearch_types::error::ResponseError;
use time::format_description::well_known::Rfc3339; use time::format_description::well_known::Rfc3339;
use time::macros::format_description; use time::macros::format_description;
@ -701,7 +704,7 @@ pub(crate) mod date_deserializer {
} }
pub fn deserialize_date( pub fn deserialize_date(
field_name: &str, field_name: DateField,
value: &str, value: &str,
option: DeserializeDateOption, option: DeserializeDateOption,
) -> std::result::Result<OffsetDateTime, ResponseError> { ) -> std::result::Result<OffsetDateTime, ResponseError> {
@ -727,7 +730,7 @@ pub(crate) mod date_deserializer {
} }
} else { } else {
Err(index_scheduler::Error::InvalidTaskDate { Err(index_scheduler::Error::InvalidTaskDate {
field: field_name.to_string(), field: field_name,
date: value.to_string(), date: value.to_string(),
} }
.into()) .into())

View File

@ -1,9 +1,16 @@
use std::cmp::min; use std::cmp::min;
use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::str::FromStr; use std::convert::Infallible;
use std::fmt;
use std::num::ParseIntError;
use std::str::{FromStr, ParseBoolError};
use std::time::Instant; use std::time::Instant;
use deserr::{
DeserializeError, DeserializeFromValue, ErrorKind, IntoValue, MergeWithError, ValuePointerRef,
};
use either::Either; use either::Either;
use meilisearch_types::error::{unwrap_any, Code, ErrorCode};
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;
@ -26,34 +33,33 @@ pub const DEFAULT_CROP_MARKER: fn() -> String = || "…".to_string();
pub const DEFAULT_HIGHLIGHT_PRE_TAG: fn() -> String = || "<em>".to_string(); 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(Deserialize, Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, Default, PartialEq, DeserializeFromValue)]
#[serde(rename_all = "camelCase", deny_unknown_fields)] #[deserr(rename_all = camelCase, deny_unknown_fields)]
pub struct SearchQuery { pub struct SearchQuery {
pub q: Option<String>, pub q: Option<String>,
#[serde(default = "DEFAULT_SEARCH_OFFSET")] #[deserr(default = DEFAULT_SEARCH_OFFSET())]
pub offset: usize, pub offset: usize,
#[serde(default = "DEFAULT_SEARCH_LIMIT")] #[deserr(default = DEFAULT_SEARCH_LIMIT())]
pub limit: usize, pub limit: usize,
pub page: Option<usize>, pub page: Option<usize>,
pub hits_per_page: Option<usize>, pub hits_per_page: Option<usize>,
pub attributes_to_retrieve: Option<BTreeSet<String>>, pub attributes_to_retrieve: Option<BTreeSet<String>>,
pub attributes_to_crop: Option<Vec<String>>, pub attributes_to_crop: Option<Vec<String>>,
#[serde(default = "DEFAULT_CROP_LENGTH")] #[deserr(default = DEFAULT_CROP_LENGTH())]
pub crop_length: usize, pub crop_length: usize,
pub attributes_to_highlight: Option<HashSet<String>>, pub attributes_to_highlight: Option<HashSet<String>>,
// Default to false #[deserr(default)]
#[serde(default = "Default::default")]
pub show_matches_position: bool, pub show_matches_position: bool,
pub filter: Option<Value>, pub filter: Option<Value>,
pub sort: Option<Vec<String>>, pub sort: Option<Vec<String>>,
pub facets: Option<Vec<String>>, pub facets: Option<Vec<String>>,
#[serde(default = "DEFAULT_HIGHLIGHT_PRE_TAG")] #[deserr(default = DEFAULT_HIGHLIGHT_PRE_TAG())]
pub highlight_pre_tag: String, pub highlight_pre_tag: String,
#[serde(default = "DEFAULT_HIGHLIGHT_POST_TAG")] #[deserr(default = DEFAULT_HIGHLIGHT_POST_TAG())]
pub highlight_post_tag: String, pub highlight_post_tag: String,
#[serde(default = "DEFAULT_CROP_MARKER")] #[deserr(default = DEFAULT_CROP_MARKER())]
pub crop_marker: String, pub crop_marker: String,
#[serde(default)] #[deserr(default)]
pub matching_strategy: MatchingStrategy, pub matching_strategy: MatchingStrategy,
} }
@ -63,7 +69,8 @@ impl SearchQuery {
} }
} }
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)] #[derive(Deserialize, Debug, Clone, PartialEq, Eq, DeserializeFromValue)]
#[deserr(rename_all = camelCase)]
#[serde(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
@ -87,6 +94,96 @@ impl From<MatchingStrategy> for TermsMatchingStrategy {
} }
} }
#[derive(Debug)]
pub struct SearchDeserError {
error: String,
code: Code,
}
impl std::fmt::Display for SearchDeserError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.error)
}
}
impl std::error::Error for SearchDeserError {}
impl ErrorCode for SearchDeserError {
fn error_code(&self) -> Code {
self.code
}
}
impl MergeWithError<SearchDeserError> for SearchDeserError {
fn merge(
_self_: Option<Self>,
other: SearchDeserError,
_merge_location: ValuePointerRef,
) -> Result<Self, Self> {
Err(other)
}
}
impl DeserializeError for SearchDeserError {
fn error<V: IntoValue>(
_self_: Option<Self>,
error: ErrorKind<V>,
location: ValuePointerRef,
) -> Result<Self, Self> {
let error = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0;
let code = match location.last_field() {
Some("q") => Code::InvalidSearchQ,
Some("offset") => Code::InvalidSearchOffset,
Some("limit") => Code::InvalidSearchLimit,
Some("page") => Code::InvalidSearchPage,
Some("hitsPerPage") => Code::InvalidSearchHitsPerPage,
Some("attributesToRetrieve") => Code::InvalidSearchAttributesToRetrieve,
Some("attributesToCrop") => Code::InvalidSearchAttributesToCrop,
Some("cropLength") => Code::InvalidSearchCropLength,
Some("attributesToHighlight") => Code::InvalidSearchAttributesToHighlight,
Some("showMatchesPosition") => Code::InvalidSearchShowMatchesPosition,
Some("filter") => Code::InvalidSearchFilter,
Some("sort") => Code::InvalidSearchSort,
Some("facets") => Code::InvalidSearchFacets,
Some("highlightPreTag") => Code::InvalidSearchHighlightPreTag,
Some("highlightPostTag") => Code::InvalidSearchHighlightPostTag,
Some("cropMarker") => Code::InvalidSearchCropMarker,
Some("matchingStrategy") => Code::InvalidSearchMatchingStrategy,
_ => Code::BadRequest,
};
Err(SearchDeserError { error, code })
}
}
impl MergeWithError<ParseBoolError> for SearchDeserError {
fn merge(
_self_: Option<Self>,
other: ParseBoolError,
merge_location: ValuePointerRef,
) -> Result<Self, Self> {
SearchDeserError::error::<Infallible>(
None,
ErrorKind::Unexpected { msg: other.to_string() },
merge_location,
)
}
}
impl MergeWithError<ParseIntError> for SearchDeserError {
fn merge(
_self_: Option<Self>,
other: ParseIntError,
merge_location: ValuePointerRef,
) -> Result<Self, Self> {
SearchDeserError::error::<Infallible>(
None,
ErrorKind::Unexpected { msg: other.to_string() },
merge_location,
)
}
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct SearchHit { pub struct SearchHit {
#[serde(flatten)] #[serde(flatten)]

View File

@ -245,9 +245,9 @@ async fn error_add_api_key_missing_parameter() {
let expected_response = json!({ let expected_response = json!({
"message": "`indexes` field is mandatory.", "message": "`indexes` field is mandatory.",
"code": "missing_parameter", "code": "missing_api_key_indexes",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#missing-parameter" "link": "https://docs.meilisearch.com/errors#missing-api-key-indexes"
}); });
assert_eq!(response, expected_response); assert_eq!(response, expected_response);
@ -263,9 +263,9 @@ async fn error_add_api_key_missing_parameter() {
let expected_response = json!({ let expected_response = json!({
"message": "`actions` field is mandatory.", "message": "`actions` field is mandatory.",
"code": "missing_parameter", "code": "missing_api_key_actions",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#missing-parameter" "link": "https://docs.meilisearch.com/errors#missing-api-key-actions"
}); });
assert_eq!(response, expected_response); assert_eq!(response, expected_response);
@ -281,9 +281,9 @@ async fn error_add_api_key_missing_parameter() {
let expected_response = json!({ let expected_response = json!({
"message": "`expiresAt` field is mandatory.", "message": "`expiresAt` field is mandatory.",
"code": "missing_parameter", "code": "missing_api_key_expires_at",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#missing-parameter" "link": "https://docs.meilisearch.com/errors#missing-api-key-expires-at"
}); });
assert_eq!(response, expected_response); assert_eq!(response, expected_response);

View File

@ -37,25 +37,105 @@ async fn search_unexisting_parameter() {
} }
#[actix_rt::test] #[actix_rt::test]
async fn search_invalid_highlight_and_crop_tags() { async fn search_invalid_crop_marker() {
let server = Server::new().await; let server = Server::new().await;
let index = server.index("test"); let index = server.index("test");
let fields = &["cropMarker", "highlightPreTag", "highlightPostTag"]; // object
let response = index.search_post(json!({"cropMarker": { "marker": "<crop>" }})).await;
meili_snap::snapshot!(format!("{:#?}", response), @r###"
(
Object {
"message": String("invalid type: Map `{\"marker\":\"<crop>\"}`, expected a String at `.cropMarker`."),
"code": String("invalid_search_crop_marker"),
"type": String("invalid_request"),
"link": String("https://docs.meilisearch.com/errors#invalid-search-crop-marker"),
},
400,
)
"###);
for field in fields { // array
// object let response = index.search_post(json!({"cropMarker": ["marker", "<crop>"]})).await;
let (response, code) = meili_snap::snapshot!(format!("{:#?}", response), @r###"
index.search_post(json!({field.to_string(): {"marker": "<crop>"}})).await; (
assert_eq!(code, 400, "field {} passing object: {}", &field, response); Object {
assert_eq!(response["code"], "bad_request"); "message": String("invalid type: Sequence `[\"marker\",\"<crop>\"]`, expected a String at `.cropMarker`."),
"code": String("invalid_search_crop_marker"),
"type": String("invalid_request"),
"link": String("https://docs.meilisearch.com/errors#invalid-search-crop-marker"),
},
400,
)
"###);
}
// array #[actix_rt::test]
let (response, code) = async fn search_invalid_highlight_pre_tag() {
index.search_post(json!({field.to_string(): ["marker", "<crop>"]})).await; let server = Server::new().await;
assert_eq!(code, 400, "field {} passing array: {}", &field, response); let index = server.index("test");
assert_eq!(response["code"], "bad_request");
} // object
let response = index.search_post(json!({"highlightPreTag": { "marker": "<em>" }})).await;
meili_snap::snapshot!(format!("{:#?}", response), @r###"
(
Object {
"message": String("invalid type: Map `{\"marker\":\"<em>\"}`, expected a String at `.highlightPreTag`."),
"code": String("invalid_search_highlight_pre_tag"),
"type": String("invalid_request"),
"link": String("https://docs.meilisearch.com/errors#invalid-search-highlight-pre-tag"),
},
400,
)
"###);
// array
let response = index.search_post(json!({"highlightPreTag": ["marker", "<em>"]})).await;
meili_snap::snapshot!(format!("{:#?}", response), @r###"
(
Object {
"message": String("invalid type: Sequence `[\"marker\",\"<em>\"]`, expected a String at `.highlightPreTag`."),
"code": String("invalid_search_highlight_pre_tag"),
"type": String("invalid_request"),
"link": String("https://docs.meilisearch.com/errors#invalid-search-highlight-pre-tag"),
},
400,
)
"###);
}
#[actix_rt::test]
async fn search_invalid_highlight_post_tag() {
let server = Server::new().await;
let index = server.index("test");
// object
let response = index.search_post(json!({"highlightPostTag": { "marker": "</em>" }})).await;
meili_snap::snapshot!(format!("{:#?}", response), @r###"
(
Object {
"message": String("invalid type: Map `{\"marker\":\"</em>\"}`, expected a String at `.highlightPostTag`."),
"code": String("invalid_search_highlight_post_tag"),
"type": String("invalid_request"),
"link": String("https://docs.meilisearch.com/errors#invalid-search-highlight-post-tag"),
},
400,
)
"###);
// array
let response = index.search_post(json!({"highlightPostTag": ["marker", "</em>"]})).await;
meili_snap::snapshot!(format!("{:#?}", response), @r###"
(
Object {
"message": String("invalid type: Sequence `[\"marker\",\"</em>\"]`, expected a String at `.highlightPostTag`."),
"code": String("invalid_search_highlight_post_tag"),
"type": String("invalid_request"),
"link": String("https://docs.meilisearch.com/errors#invalid-search-highlight-post-tag"),
},
400,
)
"###);
} }
#[actix_rt::test] #[actix_rt::test]

View File

@ -193,9 +193,9 @@ async fn get_task_filter_error() {
insta::assert_json_snapshot!(response, @r###" insta::assert_json_snapshot!(response, @r###"
{ {
"message": "Task uid `pied` is invalid. It should only contain numeric characters.", "message": "Task uid `pied` is invalid. It should only contain numeric characters.",
"code": "invalid_task_uids_filter", "code": "invalid_task_uids",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-uids-filter" "link": "https://docs.meilisearch.com/errors#invalid-task-uids"
} }
"###); "###);
@ -215,9 +215,9 @@ async fn get_task_filter_error() {
insta::assert_json_snapshot!(response, @r###" insta::assert_json_snapshot!(response, @r###"
{ {
"message": "Task `beforeStartedAt` `pied` is invalid. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", "message": "Task `beforeStartedAt` `pied` is invalid. It should follow the YYYY-MM-DD or RFC 3339 date-time format.",
"code": "invalid_task_date_filter", "code": "invalid_task_before_started_at",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-date-filter" "link": "https://docs.meilisearch.com/errors#invalid-task-before-started-at"
} }
"###); "###);
} }
@ -253,9 +253,9 @@ async fn delete_task_filter_error() {
insta::assert_json_snapshot!(response, @r###" insta::assert_json_snapshot!(response, @r###"
{ {
"message": "Task uid `pied` is invalid. It should only contain numeric characters.", "message": "Task uid `pied` is invalid. It should only contain numeric characters.",
"code": "invalid_task_uids_filter", "code": "invalid_task_uids",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-uids-filter" "link": "https://docs.meilisearch.com/errors#invalid-task-uids"
} }
"###); "###);
} }
@ -291,9 +291,9 @@ async fn cancel_task_filter_error() {
insta::assert_json_snapshot!(response, @r###" insta::assert_json_snapshot!(response, @r###"
{ {
"message": "Task uid `pied` is invalid. It should only contain numeric characters.", "message": "Task uid `pied` is invalid. It should only contain numeric characters.",
"code": "invalid_task_uids_filter", "code": "invalid_task_uids",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid-task-uids-filter" "link": "https://docs.meilisearch.com/errors#invalid-task-uids"
} }
"###); "###);
} }
@ -862,9 +862,9 @@ async fn test_summarized_index_swap() {
}, },
"error": { "error": {
"message": "Indexes `cattos`, `doggos` not found.", "message": "Indexes `cattos`, `doggos` not found.",
"code": "index_not_found", "code": "invalid_swap_indexes",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#index-not-found" "link": "https://docs.meilisearch.com/errors#invalid-swap-indexes"
}, },
"duration": "[duration]", "duration": "[duration]",
"enqueuedAt": "[date]", "enqueuedAt": "[date]",