mirror of
https://github.com/meilisearch/MeiliSearch
synced 2024-11-29 16:24:26 +01:00
Merge #3246
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:
commit
e27bb8ab3e
892
Cargo.lock
generated
892
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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()
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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 []
|
||||||
|
@ -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" }
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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"]
|
||||||
|
@ -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,
|
||||||
|
78
meilisearch/src/extractors/json.rs
Normal file
78
meilisearch/src/extractors/json.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
70
meilisearch/src/extractors/query_parameters.rs
Normal file
70
meilisearch/src/extractors/query_parameters.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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>,
|
||||||
|
@ -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>,
|
||||||
|
@ -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> {
|
||||||
|
@ -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> {
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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())
|
||||||
|
@ -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)]
|
||||||
|
@ -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);
|
||||||
|
@ -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]
|
||||||
|
@ -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]",
|
||||||
|
Loading…
Reference in New Issue
Block a user