mirror of
https://github.com/meilisearch/MeiliSearch
synced 2024-11-29 16:24:26 +01:00
Refactor deserr integration
This commit is contained in:
parent
2bc2e99ff3
commit
1fc11264e8
5
Cargo.lock
generated
5
Cargo.lock
generated
@ -1310,7 +1310,6 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "filter-parser"
|
name = "filter-parser"
|
||||||
version = "0.38.0"
|
version = "0.38.0"
|
||||||
source = "git+https://github.com/meilisearch/milli.git?tag=v0.38.0#c3f4835e8e102586bd6d5eb1e55c4bba5e92f994"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nom",
|
"nom",
|
||||||
"nom_locate",
|
"nom_locate",
|
||||||
@ -1329,7 +1328,6 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "flatten-serde-json"
|
name = "flatten-serde-json"
|
||||||
version = "0.38.0"
|
version = "0.38.0"
|
||||||
source = "git+https://github.com/meilisearch/milli.git?tag=v0.38.0#c3f4835e8e102586bd6d5eb1e55c4bba5e92f994"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
@ -1894,7 +1892,6 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "json-depth-checker"
|
name = "json-depth-checker"
|
||||||
version = "0.38.0"
|
version = "0.38.0"
|
||||||
source = "git+https://github.com/meilisearch/milli.git?tag=v0.38.0#c3f4835e8e102586bd6d5eb1e55c4bba5e92f994"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
@ -2443,7 +2440,6 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "milli"
|
name = "milli"
|
||||||
version = "0.38.0"
|
version = "0.38.0"
|
||||||
source = "git+https://github.com/meilisearch/milli.git?tag=v0.38.0#c3f4835e8e102586bd6d5eb1e55c4bba5e92f994"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bimap",
|
"bimap",
|
||||||
"bincode",
|
"bincode",
|
||||||
@ -2453,6 +2449,7 @@ dependencies = [
|
|||||||
"concat-arrays",
|
"concat-arrays",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"csv",
|
"csv",
|
||||||
|
"deserr",
|
||||||
"either",
|
"either",
|
||||||
"filter-parser",
|
"filter-parser",
|
||||||
"flatten-serde-json",
|
"flatten-serde-json",
|
||||||
|
@ -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")]).into(),
|
displayed_attributes: Setting::Set(vec![S("race"), S("name")]),
|
||||||
searchable_attributes: Setting::Set(vec![S("name"), S("race")]).into(),
|
searchable_attributes: Setting::Set(vec![S("name"), S("race")]),
|
||||||
filterable_attributes: Setting::Set(btreeset! { S("race"), S("age") }).into(),
|
filterable_attributes: Setting::Set(btreeset! { S("race"), S("age") }),
|
||||||
sortable_attributes: Setting::Set(btreeset! { S("age") }).into(),
|
sortable_attributes: Setting::Set(btreeset! { S("age") }),
|
||||||
ranking_rules: Setting::NotSet.into(),
|
ranking_rules: Setting::NotSet,
|
||||||
stop_words: Setting::NotSet.into(),
|
stop_words: Setting::NotSet,
|
||||||
synonyms: Setting::NotSet.into(),
|
synonyms: Setting::NotSet,
|
||||||
distinct_attribute: Setting::NotSet.into(),
|
distinct_attribute: Setting::NotSet,
|
||||||
typo_tolerance: Setting::NotSet.into(),
|
typo_tolerance: Setting::NotSet,
|
||||||
faceting: Setting::NotSet.into(),
|
faceting: Setting::NotSet,
|
||||||
pagination: Setting::NotSet.into(),
|
pagination: Setting::NotSet,
|
||||||
_kind: std::marker::PhantomData,
|
_kind: std::marker::PhantomData,
|
||||||
};
|
};
|
||||||
settings.check()
|
settings.check()
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use super::v4_to_v5::{CompatIndexV4ToV5, CompatV4ToV5};
|
use super::v4_to_v5::{CompatIndexV4ToV5, CompatV4ToV5};
|
||||||
use crate::reader::{v5, v6, Document, UpdateFile};
|
use crate::reader::{v5, v6, Document, UpdateFile};
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
@ -315,7 +317,26 @@ impl<T> From<v5::Settings<T>> for v6::Settings<v6::Unchecked> {
|
|||||||
searchable_attributes: settings.searchable_attributes.into(),
|
searchable_attributes: settings.searchable_attributes.into(),
|
||||||
filterable_attributes: settings.filterable_attributes.into(),
|
filterable_attributes: settings.filterable_attributes.into(),
|
||||||
sortable_attributes: settings.sortable_attributes.into(),
|
sortable_attributes: settings.sortable_attributes.into(),
|
||||||
ranking_rules: settings.ranking_rules.into(),
|
ranking_rules: {
|
||||||
|
match settings.ranking_rules {
|
||||||
|
v5::settings::Setting::Set(ranking_rules) => {
|
||||||
|
let mut new_ranking_rules = vec![];
|
||||||
|
for rule in ranking_rules {
|
||||||
|
match v6::RankingRuleView::from_str(&rule) {
|
||||||
|
Ok(new_rule) => {
|
||||||
|
new_ranking_rules.push(new_rule);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
log::warn!("Error while importing settings. The ranking rule `{rule}` does not exist anymore.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v6::Setting::Set(new_ranking_rules)
|
||||||
|
}
|
||||||
|
v5::settings::Setting::Reset => v6::Setting::Reset,
|
||||||
|
v5::settings::Setting::NotSet => v6::Setting::NotSet,
|
||||||
|
}
|
||||||
|
},
|
||||||
stop_words: settings.stop_words.into(),
|
stop_words: settings.stop_words.into(),
|
||||||
synonyms: settings.synonyms.into(),
|
synonyms: settings.synonyms.into(),
|
||||||
distinct_attribute: settings.distinct_attribute.into(),
|
distinct_attribute: settings.distinct_attribute.into(),
|
||||||
|
@ -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::settings::Setting<T>;
|
pub type Setting<T> = meilisearch_types::milli::update::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;
|
||||||
@ -40,6 +40,7 @@ pub type IndexUid = meilisearch_types::index_uid::IndexUid;
|
|||||||
// everything related to the errors
|
// everything related to the errors
|
||||||
pub type ResponseError = meilisearch_types::error::ResponseError;
|
pub type ResponseError = meilisearch_types::error::ResponseError;
|
||||||
pub type Code = meilisearch_types::error::Code;
|
pub type Code = meilisearch_types::error::Code;
|
||||||
|
pub type RankingRuleView = meilisearch_types::settings::RankingRuleView;
|
||||||
|
|
||||||
pub struct V6Reader {
|
pub struct V6Reader {
|
||||||
dump: TempDir,
|
dump: TempDir,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
use meilisearch_types::error::{Code, ErrorCode};
|
use meilisearch_types::error::{Code, ErrorCode};
|
||||||
use meilisearch_types::{internal_error, keys};
|
use meilisearch_types::internal_error;
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, AuthControllerError>;
|
pub type Result<T> = std::result::Result<T, AuthControllerError>;
|
||||||
|
|
||||||
@ -11,8 +11,6 @@ pub enum AuthControllerError {
|
|||||||
ApiKeyNotFound(String),
|
ApiKeyNotFound(String),
|
||||||
#[error("`uid` field value `{0}` is already an existing API key.")]
|
#[error("`uid` field value `{0}` is already an existing API key.")]
|
||||||
ApiKeyAlreadyExists(String),
|
ApiKeyAlreadyExists(String),
|
||||||
#[error(transparent)]
|
|
||||||
ApiKey(#[from] keys::Error),
|
|
||||||
#[error("Internal error: {0}")]
|
#[error("Internal error: {0}")]
|
||||||
Internal(Box<dyn Error + Send + Sync + 'static>),
|
Internal(Box<dyn Error + Send + Sync + 'static>),
|
||||||
}
|
}
|
||||||
@ -27,7 +25,6 @@ internal_error!(
|
|||||||
impl ErrorCode for AuthControllerError {
|
impl ErrorCode for AuthControllerError {
|
||||||
fn error_code(&self) -> Code {
|
fn error_code(&self) -> Code {
|
||||||
match self {
|
match self {
|
||||||
Self::ApiKey(e) => e.error_code(),
|
|
||||||
Self::ApiKeyNotFound(_) => Code::ApiKeyNotFound,
|
Self::ApiKeyNotFound(_) => Code::ApiKeyNotFound,
|
||||||
Self::ApiKeyAlreadyExists(_) => Code::ApiKeyAlreadyExists,
|
Self::ApiKeyAlreadyExists(_) => Code::ApiKeyAlreadyExists,
|
||||||
Self::Internal(_) => Code::Internal,
|
Self::Internal(_) => Code::Internal,
|
||||||
|
@ -8,10 +8,9 @@ use std::path::Path;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use error::{AuthControllerError, Result};
|
use error::{AuthControllerError, Result};
|
||||||
use meilisearch_types::keys::{Action, Key};
|
use meilisearch_types::keys::{Action, CreateApiKey, Key, PatchApiKey};
|
||||||
use meilisearch_types::star_or::StarOr;
|
use meilisearch_types::star_or::StarOr;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
|
||||||
pub use store::open_auth_store_env;
|
pub use store::open_auth_store_env;
|
||||||
use store::{generate_key_as_hexa, HeedAuthStore};
|
use store::{generate_key_as_hexa, HeedAuthStore};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
@ -34,17 +33,19 @@ impl AuthController {
|
|||||||
Ok(Self { store: Arc::new(store), master_key: master_key.clone() })
|
Ok(Self { store: Arc::new(store), master_key: master_key.clone() })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_key(&self, value: Value) -> Result<Key> {
|
pub fn create_key(&self, value: CreateApiKey) -> Result<Key> {
|
||||||
let key = Key::create_from_value(value)?;
|
let create_key = value;
|
||||||
match self.store.get_api_key(key.uid)? {
|
match self.store.get_api_key(create_key.uid)? {
|
||||||
Some(_) => Err(AuthControllerError::ApiKeyAlreadyExists(key.uid.to_string())),
|
Some(_) => Err(AuthControllerError::ApiKeyAlreadyExists(create_key.uid.to_string())),
|
||||||
None => self.store.put_api_key(key),
|
None => self.store.put_api_key(create_key.to_key()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_key(&self, uid: Uuid, value: Value) -> Result<Key> {
|
pub fn update_key(&self, uid: Uuid, patch: PatchApiKey) -> Result<Key> {
|
||||||
let mut key = self.get_key(uid)?;
|
let mut key = self.get_key(uid)?;
|
||||||
key.update_from_value(value)?;
|
key.description = patch.description;
|
||||||
|
key.name = patch.name;
|
||||||
|
key.updated_at = OffsetDateTime::now_utc();
|
||||||
self.store.put_api_key(key)
|
self.store.put_api_key(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ file-store = { path = "../file-store" }
|
|||||||
flate2 = "1.0.24"
|
flate2 = "1.0.24"
|
||||||
fst = "0.4.7"
|
fst = "0.4.7"
|
||||||
memmap2 = "0.5.7"
|
memmap2 = "0.5.7"
|
||||||
milli = { git = "https://github.com/meilisearch/milli.git", tag = "v0.38.0", default-features = false }
|
milli = { path = "/Users/meilisearch/Documents/milli2/milli" } # git = "https://github.com/meilisearch/milli.git", tag = "v0.38.0", default-features = false }
|
||||||
proptest = { version = "1.0.0", optional = true }
|
proptest = { version = "1.0.0", optional = true }
|
||||||
proptest-derive = { version = "0.3.0", optional = true }
|
proptest-derive = { version = "0.3.0", optional = true }
|
||||||
roaring = { version = "0.10.0", features = ["serde"] }
|
roaring = { version = "0.10.0", features = ["serde"] }
|
||||||
|
@ -10,6 +10,8 @@ use deserr::{DeserializeError, IntoValue, MergeWithError, ValuePointerRef};
|
|||||||
use milli::heed::{Error as HeedError, MdbError};
|
use milli::heed::{Error as HeedError, MdbError};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use self::deserr_codes::MissingIndexUid;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[cfg_attr(feature = "test-traits", derive(proptest_derive::Arbitrary))]
|
#[cfg_attr(feature = "test-traits", derive(proptest_derive::Arbitrary))]
|
||||||
@ -410,6 +412,82 @@ mod strategy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct DeserrError<C: ErrorCode = deserr_codes::BadRequest> {
|
||||||
|
pub msg: String,
|
||||||
|
pub code: Code,
|
||||||
|
_phantom: PhantomData<C>,
|
||||||
|
}
|
||||||
|
impl<C: ErrorCode> std::fmt::Debug for DeserrError<C> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("DeserrError").field("msg", &self.msg).field("code", &self.code).finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: ErrorCode> std::fmt::Display for DeserrError<C> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: ErrorCode> std::error::Error for DeserrError<C> {}
|
||||||
|
impl<C: ErrorCode> ErrorCode for DeserrError<C> {
|
||||||
|
fn error_code(&self) -> Code {
|
||||||
|
self.code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C1: ErrorCode, C2: ErrorCode> MergeWithError<DeserrError<C2>> for DeserrError<C1> {
|
||||||
|
fn merge(
|
||||||
|
_self_: Option<Self>,
|
||||||
|
other: DeserrError<C2>,
|
||||||
|
_merge_location: ValuePointerRef,
|
||||||
|
) -> Result<Self, Self> {
|
||||||
|
Err(DeserrError { msg: other.msg, code: other.code, _phantom: PhantomData })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeserrError<MissingIndexUid> {
|
||||||
|
pub fn missing_index_uid(field: &str, location: ValuePointerRef) -> Self {
|
||||||
|
let x = unwrap_any(Self::error::<Infallible>(
|
||||||
|
None,
|
||||||
|
deserr::ErrorKind::MissingField { field },
|
||||||
|
location,
|
||||||
|
));
|
||||||
|
Self { msg: x.msg, code: MissingIndexUid.error_code(), _phantom: PhantomData }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Default + ErrorCode> deserr::DeserializeError for DeserrError<C> {
|
||||||
|
fn error<V: IntoValue>(
|
||||||
|
_self_: Option<Self>,
|
||||||
|
error: deserr::ErrorKind<V>,
|
||||||
|
location: ValuePointerRef,
|
||||||
|
) -> Result<Self, Self> {
|
||||||
|
let msg = unwrap_any(deserr::serde_json::JsonError::error(None, error, location)).0;
|
||||||
|
|
||||||
|
Err(DeserrError { msg, code: C::default().error_code(), _phantom: PhantomData })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TakeErrorMessage<T>(pub T);
|
||||||
|
|
||||||
|
impl<C: Default + ErrorCode, T> MergeWithError<TakeErrorMessage<T>> for DeserrError<C>
|
||||||
|
where
|
||||||
|
T: std::error::Error,
|
||||||
|
{
|
||||||
|
fn merge(
|
||||||
|
_self_: Option<Self>,
|
||||||
|
other: TakeErrorMessage<T>,
|
||||||
|
merge_location: ValuePointerRef,
|
||||||
|
) -> Result<Self, Self> {
|
||||||
|
DeserrError::error::<Infallible>(
|
||||||
|
None,
|
||||||
|
deserr::ErrorKind::Unexpected { msg: other.0.to_string() },
|
||||||
|
merge_location,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! internal_error {
|
macro_rules! internal_error {
|
||||||
($target:ty : $($other:path), *) => {
|
($target:ty : $($other:path), *) => {
|
||||||
|
@ -1,22 +1,129 @@
|
|||||||
|
use std::convert::Infallible;
|
||||||
|
use std::fmt::Display;
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
|
use deserr::{DeserializeError, DeserializeFromValue, MergeWithError, ValueKind, ValuePointerRef};
|
||||||
use enum_iterator::Sequence;
|
use enum_iterator::Sequence;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{from_value, Value};
|
|
||||||
use time::format_description::well_known::Rfc3339;
|
use time::format_description::well_known::Rfc3339;
|
||||||
use time::macros::{format_description, time};
|
use time::macros::{format_description, time};
|
||||||
use time::{Date, OffsetDateTime, PrimitiveDateTime};
|
use time::{Date, OffsetDateTime, PrimitiveDateTime};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::error::{Code, ErrorCode};
|
use crate::error::deserr_codes::*;
|
||||||
|
use crate::error::{unwrap_any, Code, DeserrError, ErrorCode, TakeErrorMessage};
|
||||||
use crate::index_uid::{IndexUid, IndexUidFormatError};
|
use crate::index_uid::{IndexUid, IndexUidFormatError};
|
||||||
use crate::star_or::StarOr;
|
use crate::star_or::StarOr;
|
||||||
|
|
||||||
type Result<T> = std::result::Result<T, Error>;
|
|
||||||
|
|
||||||
pub type KeyId = Uuid;
|
pub type KeyId = Uuid;
|
||||||
|
|
||||||
|
impl<C: Default + ErrorCode> DeserializeFromValue<DeserrError<C>> for Uuid {
|
||||||
|
fn deserialize_from_value<V: deserr::IntoValue>(
|
||||||
|
value: deserr::Value<V>,
|
||||||
|
location: deserr::ValuePointerRef,
|
||||||
|
) -> std::result::Result<Self, DeserrError<C>> {
|
||||||
|
match value {
|
||||||
|
deserr::Value::String(s) => match Uuid::parse_str(&s) {
|
||||||
|
Ok(x) => Ok(x),
|
||||||
|
Err(e) => Err(unwrap_any(DeserrError::<C>::error::<V>(
|
||||||
|
None,
|
||||||
|
deserr::ErrorKind::Unexpected { msg: e.to_string() },
|
||||||
|
location,
|
||||||
|
))),
|
||||||
|
},
|
||||||
|
_ => Err(unwrap_any(DeserrError::<C>::error(
|
||||||
|
None,
|
||||||
|
deserr::ErrorKind::IncorrectValueKind {
|
||||||
|
actual: value,
|
||||||
|
accepted: &[ValueKind::String],
|
||||||
|
},
|
||||||
|
location,
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<C: Default + ErrorCode> MergeWithError<IndexUidFormatError> for DeserrError<C> {
|
||||||
|
fn merge(
|
||||||
|
_self_: Option<Self>,
|
||||||
|
other: IndexUidFormatError,
|
||||||
|
merge_location: deserr::ValuePointerRef,
|
||||||
|
) -> std::result::Result<Self, Self> {
|
||||||
|
DeserrError::error::<Infallible>(
|
||||||
|
None,
|
||||||
|
deserr::ErrorKind::Unexpected { msg: other.to_string() },
|
||||||
|
merge_location,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, DeserializeFromValue)]
|
||||||
|
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)]
|
||||||
|
pub struct CreateApiKey {
|
||||||
|
#[deserr(error = DeserrError<InvalidApiKeyDescription>)]
|
||||||
|
pub description: Option<String>,
|
||||||
|
#[deserr(error = DeserrError<InvalidApiKeyName>)]
|
||||||
|
pub name: Option<String>,
|
||||||
|
#[deserr(default = Uuid::new_v4(), error = DeserrError<InvalidApiKeyUid>)]
|
||||||
|
pub uid: KeyId,
|
||||||
|
// Value at `.name` is invalid. It is a dictionary, but is expected to be an array of strings containing action names.
|
||||||
|
// Value `indeex.create` at `.name[1]` is invalid. It is expected to be one of: ....
|
||||||
|
#[deserr(error = DeserrError<InvalidApiKeyActions>)]
|
||||||
|
//, expected = "an array of string containing action names.")]
|
||||||
|
pub actions: Vec<Action>,
|
||||||
|
#[deserr(error = DeserrError<InvalidApiKeyIndexes>)]
|
||||||
|
pub indexes: Vec<StarOr<IndexUid>>,
|
||||||
|
#[deserr(error = DeserrError<InvalidApiKeyExpiresAt>, default = None, from(&String) = parse_expiration_date -> TakeErrorMessage<ParseOffsetDateTimeError>)]
|
||||||
|
pub expires_at: Option<OffsetDateTime>,
|
||||||
|
}
|
||||||
|
impl CreateApiKey {
|
||||||
|
pub fn to_key(self) -> Key {
|
||||||
|
let CreateApiKey { description, name, uid, actions, indexes, expires_at } = self;
|
||||||
|
let now = OffsetDateTime::now_utc();
|
||||||
|
Key {
|
||||||
|
description,
|
||||||
|
name,
|
||||||
|
uid,
|
||||||
|
actions,
|
||||||
|
indexes,
|
||||||
|
expires_at,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deny_immutable_fields_api_key(
|
||||||
|
field: &str,
|
||||||
|
accepted: &[&str],
|
||||||
|
location: ValuePointerRef,
|
||||||
|
) -> DeserrError {
|
||||||
|
let mut error = unwrap_any(DeserrError::<BadRequest>::error::<Infallible>(
|
||||||
|
None,
|
||||||
|
deserr::ErrorKind::UnknownKey { key: field, accepted },
|
||||||
|
location,
|
||||||
|
));
|
||||||
|
|
||||||
|
error.code = match field {
|
||||||
|
"uid" => Code::ImmutableField,
|
||||||
|
"actions" => Code::ImmutableField,
|
||||||
|
"indexes" => Code::ImmutableField,
|
||||||
|
"expiresAt" => Code::ImmutableField,
|
||||||
|
"createdAt" => Code::ImmutableField,
|
||||||
|
"updatedAt" => Code::ImmutableField,
|
||||||
|
_ => Code::BadRequest,
|
||||||
|
};
|
||||||
|
error
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, DeserializeFromValue)]
|
||||||
|
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_api_key)]
|
||||||
|
pub struct PatchApiKey {
|
||||||
|
#[deserr(error = DeserrError<InvalidApiKeyDescription>)]
|
||||||
|
pub description: Option<String>,
|
||||||
|
#[deserr(error = DeserrError<InvalidApiKeyName>)]
|
||||||
|
pub name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
pub struct Key {
|
pub struct Key {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@ -35,100 +142,6 @@ pub struct Key {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Key {
|
impl Key {
|
||||||
pub fn create_from_value(value: Value) -> Result<Self> {
|
|
||||||
let name = match value.get("name") {
|
|
||||||
None | Some(Value::Null) => None,
|
|
||||||
Some(des) => from_value(des.clone())
|
|
||||||
.map(Some)
|
|
||||||
.map_err(|_| Error::InvalidApiKeyName(des.clone()))?,
|
|
||||||
};
|
|
||||||
|
|
||||||
let description = match value.get("description") {
|
|
||||||
None | Some(Value::Null) => None,
|
|
||||||
Some(des) => from_value(des.clone())
|
|
||||||
.map(Some)
|
|
||||||
.map_err(|_| Error::InvalidApiKeyDescription(des.clone()))?,
|
|
||||||
};
|
|
||||||
|
|
||||||
let uid = value.get("uid").map_or_else(
|
|
||||||
|| Ok(Uuid::new_v4()),
|
|
||||||
|uid| from_value(uid.clone()).map_err(|_| Error::InvalidApiKeyUid(uid.clone())),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let actions = value
|
|
||||||
.get("actions")
|
|
||||||
.map(|act| {
|
|
||||||
from_value(act.clone()).map_err(|_| Error::InvalidApiKeyActions(act.clone()))
|
|
||||||
})
|
|
||||||
.ok_or(Error::MissingApiKeyActions)??;
|
|
||||||
|
|
||||||
let indexes = value
|
|
||||||
.get("indexes")
|
|
||||||
.map(|ind| {
|
|
||||||
from_value::<Vec<String>>(ind.clone())
|
|
||||||
// If it's not a vec of string, return an API key parsing error.
|
|
||||||
.map_err(|_| Error::InvalidApiKeyIndexes(ind.clone()))
|
|
||||||
.and_then(|ind| {
|
|
||||||
ind.into_iter()
|
|
||||||
// If it's not a valid Index uid, return an Index Uid parsing error.
|
|
||||||
.map(|i| StarOr::<IndexUid>::from_str(&i).map_err(Error::from))
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.ok_or(Error::MissingApiKeyIndexes)??;
|
|
||||||
|
|
||||||
let expires_at = value
|
|
||||||
.get("expiresAt")
|
|
||||||
.map(parse_expiration_date)
|
|
||||||
.ok_or(Error::MissingApiKeyExpiresAt)??;
|
|
||||||
|
|
||||||
let created_at = OffsetDateTime::now_utc();
|
|
||||||
let updated_at = created_at;
|
|
||||||
|
|
||||||
Ok(Self { name, description, uid, actions, indexes, expires_at, created_at, updated_at })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_from_value(&mut self, value: Value) -> Result<()> {
|
|
||||||
if let Some(des) = value.get("description") {
|
|
||||||
let des =
|
|
||||||
from_value(des.clone()).map_err(|_| Error::InvalidApiKeyDescription(des.clone()));
|
|
||||||
self.description = des?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(des) = value.get("name") {
|
|
||||||
let des = from_value(des.clone()).map_err(|_| Error::InvalidApiKeyName(des.clone()));
|
|
||||||
self.name = des?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if value.get("uid").is_some() {
|
|
||||||
return Err(Error::ImmutableField("uid".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if value.get("actions").is_some() {
|
|
||||||
return Err(Error::ImmutableField("actions".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if value.get("indexes").is_some() {
|
|
||||||
return Err(Error::ImmutableField("indexes".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if value.get("expiresAt").is_some() {
|
|
||||||
return Err(Error::ImmutableField("expiresAt".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if value.get("createdAt").is_some() {
|
|
||||||
return Err(Error::ImmutableField("createdAt".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if value.get("updatedAt").is_some() {
|
|
||||||
return Err(Error::ImmutableField("updatedAt".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.updated_at = OffsetDateTime::now_utc();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn default_admin() -> Self {
|
pub fn default_admin() -> Self {
|
||||||
let now = OffsetDateTime::now_utc();
|
let now = OffsetDateTime::now_utc();
|
||||||
let uid = Uuid::new_v4();
|
let uid = Uuid::new_v4();
|
||||||
@ -160,107 +173,143 @@ impl Key {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_expiration_date(value: &Value) -> Result<Option<OffsetDateTime>> {
|
#[derive(Debug)]
|
||||||
match value {
|
pub struct ParseOffsetDateTimeError(String);
|
||||||
Value::String(string) => OffsetDateTime::parse(string, &Rfc3339)
|
impl Display for ParseOffsetDateTimeError {
|
||||||
.or_else(|_| {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
PrimitiveDateTime::parse(
|
writeln!(f, "`{original}` is not a valid date. It should follow the RFC 3339 format to represents a date or datetime in the future or specified as a null value. e.g. 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'.", original = self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::error::Error for ParseOffsetDateTimeError {}
|
||||||
|
|
||||||
|
fn parse_expiration_date(
|
||||||
|
string: &str,
|
||||||
|
) -> std::result::Result<Option<OffsetDateTime>, TakeErrorMessage<ParseOffsetDateTimeError>> {
|
||||||
|
let datetime = if let Ok(datetime) = OffsetDateTime::parse(string, &Rfc3339) {
|
||||||
|
datetime
|
||||||
|
} else if let Ok(primitive_datetime) = PrimitiveDateTime::parse(
|
||||||
string,
|
string,
|
||||||
format_description!(
|
format_description!(
|
||||||
"[year repr:full base:calendar]-[month repr:numerical]-[day]T[hour]:[minute]:[second]"
|
"[year repr:full base:calendar]-[month repr:numerical]-[day]T[hour]:[minute]:[second]"
|
||||||
),
|
),
|
||||||
).map(|datetime| datetime.assume_utc())
|
) {
|
||||||
})
|
primitive_datetime.assume_utc()
|
||||||
.or_else(|_| {
|
} else if let Ok(primitive_datetime) = PrimitiveDateTime::parse(
|
||||||
PrimitiveDateTime::parse(
|
|
||||||
string,
|
string,
|
||||||
format_description!(
|
format_description!(
|
||||||
"[year repr:full base:calendar]-[month repr:numerical]-[day] [hour]:[minute]:[second]"
|
"[year repr:full base:calendar]-[month repr:numerical]-[day] [hour]:[minute]:[second]"
|
||||||
),
|
),
|
||||||
).map(|datetime| datetime.assume_utc())
|
) {
|
||||||
})
|
primitive_datetime.assume_utc()
|
||||||
.or_else(|_| {
|
} else if let Ok(date) = Date::parse(
|
||||||
Date::parse(string, format_description!(
|
string,
|
||||||
"[year repr:full base:calendar]-[month repr:numerical]-[day]"
|
format_description!("[year repr:full base:calendar]-[month repr:numerical]-[day]"),
|
||||||
)).map(|date| PrimitiveDateTime::new(date, time!(00:00)).assume_utc())
|
) {
|
||||||
})
|
PrimitiveDateTime::new(date, time!(00:00)).assume_utc()
|
||||||
.map_err(|_| Error::InvalidApiKeyExpiresAt(value.clone()))
|
|
||||||
// check if the key is already expired.
|
|
||||||
.and_then(|d| {
|
|
||||||
if d > OffsetDateTime::now_utc() {
|
|
||||||
Ok(d)
|
|
||||||
} else {
|
} else {
|
||||||
Err(Error::InvalidApiKeyExpiresAt(value.clone()))
|
return Err(TakeErrorMessage(ParseOffsetDateTimeError(string.to_owned())));
|
||||||
}
|
};
|
||||||
})
|
if datetime > OffsetDateTime::now_utc() {
|
||||||
.map(Option::Some),
|
Ok(Some(datetime))
|
||||||
Value::Null => Ok(None),
|
} else {
|
||||||
_otherwise => Err(Error::InvalidApiKeyExpiresAt(value.clone())),
|
Err(TakeErrorMessage(ParseOffsetDateTimeError(string.to_owned())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Sequence)]
|
#[derive(
|
||||||
|
Copy, Clone, Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Sequence, DeserializeFromValue,
|
||||||
|
)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
#[serde(rename = "*")]
|
#[serde(rename = "*")]
|
||||||
|
#[deserr(rename = "*")]
|
||||||
All = 0,
|
All = 0,
|
||||||
#[serde(rename = "search")]
|
#[serde(rename = "search")]
|
||||||
|
#[deserr(rename = "search")]
|
||||||
Search,
|
Search,
|
||||||
#[serde(rename = "documents.*")]
|
#[serde(rename = "documents.*")]
|
||||||
|
#[deserr(rename = "documents.*")]
|
||||||
DocumentsAll,
|
DocumentsAll,
|
||||||
#[serde(rename = "documents.add")]
|
#[serde(rename = "documents.add")]
|
||||||
|
#[deserr(rename = "documents.add")]
|
||||||
DocumentsAdd,
|
DocumentsAdd,
|
||||||
#[serde(rename = "documents.get")]
|
#[serde(rename = "documents.get")]
|
||||||
|
#[deserr(rename = "documents.get")]
|
||||||
DocumentsGet,
|
DocumentsGet,
|
||||||
#[serde(rename = "documents.delete")]
|
#[serde(rename = "documents.delete")]
|
||||||
|
#[deserr(rename = "documents.delete")]
|
||||||
DocumentsDelete,
|
DocumentsDelete,
|
||||||
#[serde(rename = "indexes.*")]
|
#[serde(rename = "indexes.*")]
|
||||||
|
#[deserr(rename = "indexes.*")]
|
||||||
IndexesAll,
|
IndexesAll,
|
||||||
#[serde(rename = "indexes.create")]
|
#[serde(rename = "indexes.create")]
|
||||||
|
#[deserr(rename = "indexes.create")]
|
||||||
IndexesAdd,
|
IndexesAdd,
|
||||||
#[serde(rename = "indexes.get")]
|
#[serde(rename = "indexes.get")]
|
||||||
|
#[deserr(rename = "indexes.get")]
|
||||||
IndexesGet,
|
IndexesGet,
|
||||||
#[serde(rename = "indexes.update")]
|
#[serde(rename = "indexes.update")]
|
||||||
|
#[deserr(rename = "indexes.update")]
|
||||||
IndexesUpdate,
|
IndexesUpdate,
|
||||||
#[serde(rename = "indexes.delete")]
|
#[serde(rename = "indexes.delete")]
|
||||||
|
#[deserr(rename = "indexes.delete")]
|
||||||
IndexesDelete,
|
IndexesDelete,
|
||||||
#[serde(rename = "indexes.swap")]
|
#[serde(rename = "indexes.swap")]
|
||||||
|
#[deserr(rename = "indexes.swap")]
|
||||||
IndexesSwap,
|
IndexesSwap,
|
||||||
#[serde(rename = "tasks.*")]
|
#[serde(rename = "tasks.*")]
|
||||||
|
#[deserr(rename = "tasks.*")]
|
||||||
TasksAll,
|
TasksAll,
|
||||||
#[serde(rename = "tasks.cancel")]
|
#[serde(rename = "tasks.cancel")]
|
||||||
|
#[deserr(rename = "tasks.cancel")]
|
||||||
TasksCancel,
|
TasksCancel,
|
||||||
#[serde(rename = "tasks.delete")]
|
#[serde(rename = "tasks.delete")]
|
||||||
|
#[deserr(rename = "tasks.delete")]
|
||||||
TasksDelete,
|
TasksDelete,
|
||||||
#[serde(rename = "tasks.get")]
|
#[serde(rename = "tasks.get")]
|
||||||
|
#[deserr(rename = "tasks.get")]
|
||||||
TasksGet,
|
TasksGet,
|
||||||
#[serde(rename = "settings.*")]
|
#[serde(rename = "settings.*")]
|
||||||
|
#[deserr(rename = "settings.*")]
|
||||||
SettingsAll,
|
SettingsAll,
|
||||||
#[serde(rename = "settings.get")]
|
#[serde(rename = "settings.get")]
|
||||||
|
#[deserr(rename = "settings.get")]
|
||||||
SettingsGet,
|
SettingsGet,
|
||||||
#[serde(rename = "settings.update")]
|
#[serde(rename = "settings.update")]
|
||||||
|
#[deserr(rename = "settings.update")]
|
||||||
SettingsUpdate,
|
SettingsUpdate,
|
||||||
#[serde(rename = "stats.*")]
|
#[serde(rename = "stats.*")]
|
||||||
|
#[deserr(rename = "stats.*")]
|
||||||
StatsAll,
|
StatsAll,
|
||||||
#[serde(rename = "stats.get")]
|
#[serde(rename = "stats.get")]
|
||||||
|
#[deserr(rename = "stats.get")]
|
||||||
StatsGet,
|
StatsGet,
|
||||||
#[serde(rename = "metrics.*")]
|
#[serde(rename = "metrics.*")]
|
||||||
|
#[deserr(rename = "metrics.*")]
|
||||||
MetricsAll,
|
MetricsAll,
|
||||||
#[serde(rename = "metrics.get")]
|
#[serde(rename = "metrics.get")]
|
||||||
|
#[deserr(rename = "metrics.get")]
|
||||||
MetricsGet,
|
MetricsGet,
|
||||||
#[serde(rename = "dumps.*")]
|
#[serde(rename = "dumps.*")]
|
||||||
|
#[deserr(rename = "dumps.*")]
|
||||||
DumpsAll,
|
DumpsAll,
|
||||||
#[serde(rename = "dumps.create")]
|
#[serde(rename = "dumps.create")]
|
||||||
|
#[deserr(rename = "dumps.create")]
|
||||||
DumpsCreate,
|
DumpsCreate,
|
||||||
#[serde(rename = "version")]
|
#[serde(rename = "version")]
|
||||||
|
#[deserr(rename = "version")]
|
||||||
Version,
|
Version,
|
||||||
#[serde(rename = "keys.create")]
|
#[serde(rename = "keys.create")]
|
||||||
|
#[deserr(rename = "keys.create")]
|
||||||
KeysAdd,
|
KeysAdd,
|
||||||
#[serde(rename = "keys.get")]
|
#[serde(rename = "keys.get")]
|
||||||
|
#[deserr(rename = "keys.get")]
|
||||||
KeysGet,
|
KeysGet,
|
||||||
#[serde(rename = "keys.update")]
|
#[serde(rename = "keys.update")]
|
||||||
|
#[deserr(rename = "keys.update")]
|
||||||
KeysUpdate,
|
KeysUpdate,
|
||||||
#[serde(rename = "keys.delete")]
|
#[serde(rename = "keys.delete")]
|
||||||
|
#[deserr(rename = "keys.delete")]
|
||||||
KeysDelete,
|
KeysDelete,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -341,56 +390,3 @@ pub mod actions {
|
|||||||
pub const KEYS_UPDATE: u8 = KeysUpdate.repr();
|
pub const KEYS_UPDATE: u8 = KeysUpdate.repr();
|
||||||
pub const KEYS_DELETE: u8 = KeysDelete.repr();
|
pub const KEYS_DELETE: u8 = KeysDelete.repr();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("`expiresAt` field is mandatory.")]
|
|
||||||
MissingApiKeyExpiresAt,
|
|
||||||
#[error("`indexes` field is mandatory.")]
|
|
||||||
MissingApiKeyIndexes,
|
|
||||||
#[error("`actions` field is mandatory.")]
|
|
||||||
MissingApiKeyActions,
|
|
||||||
#[error("`actions` field value `{0}` is invalid. It should be an array of string representing action names.")]
|
|
||||||
InvalidApiKeyActions(Value),
|
|
||||||
#[error("`indexes` field value `{0}` is invalid. It should be an array of string representing index names.")]
|
|
||||||
InvalidApiKeyIndexes(Value),
|
|
||||||
#[error("{0}")]
|
|
||||||
InvalidApiKeyIndexUid(IndexUidFormatError),
|
|
||||||
#[error("`expiresAt` field value `{0}` is invalid. It should follow the RFC 3339 format to represents a date or datetime in the future or specified as a null value. e.g. 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'.")]
|
|
||||||
InvalidApiKeyExpiresAt(Value),
|
|
||||||
#[error("`description` field value `{0}` is invalid. It should be a string or specified as a null value.")]
|
|
||||||
InvalidApiKeyDescription(Value),
|
|
||||||
#[error(
|
|
||||||
"`name` field value `{0}` is invalid. It should be a string or specified as a null value."
|
|
||||||
)]
|
|
||||||
InvalidApiKeyName(Value),
|
|
||||||
#[error("`uid` field value `{0}` is invalid. It should be a valid UUID v4 string or omitted.")]
|
|
||||||
InvalidApiKeyUid(Value),
|
|
||||||
#[error("The `{0}` field cannot be modified for the given resource.")]
|
|
||||||
ImmutableField(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<IndexUidFormatError> for Error {
|
|
||||||
fn from(e: IndexUidFormatError) -> Self {
|
|
||||||
Self::InvalidApiKeyIndexUid(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ErrorCode for Error {
|
|
||||||
fn error_code(&self) -> Code {
|
|
||||||
match self {
|
|
||||||
Self::MissingApiKeyExpiresAt => Code::MissingApiKeyExpiresAt,
|
|
||||||
Self::MissingApiKeyIndexes => Code::MissingApiKeyIndexes,
|
|
||||||
Self::MissingApiKeyActions => Code::MissingApiKeyActions,
|
|
||||||
Self::InvalidApiKeyActions(_) => Code::InvalidApiKeyActions,
|
|
||||||
Self::InvalidApiKeyIndexes(_) | Self::InvalidApiKeyIndexUid(_) => {
|
|
||||||
Code::InvalidApiKeyIndexes
|
|
||||||
}
|
|
||||||
Self::InvalidApiKeyExpiresAt(_) => Code::InvalidApiKeyExpiresAt,
|
|
||||||
Self::InvalidApiKeyDescription(_) => Code::InvalidApiKeyDescription,
|
|
||||||
Self::InvalidApiKeyName(_) => Code::InvalidApiKeyName,
|
|
||||||
Self::InvalidApiKeyUid(_) => Code::InvalidApiKeyUid,
|
|
||||||
Self::ImmutableField(_) => Code::ImmutableField,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,11 +1,18 @@
|
|||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
use std::convert::Infallible;
|
||||||
|
use std::fmt;
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use deserr::{DeserializeError, DeserializeFromValue};
|
use deserr::{DeserializeError, DeserializeFromValue, ErrorKind, MergeWithError, ValuePointerRef};
|
||||||
use fst::IntoStreamer;
|
use fst::IntoStreamer;
|
||||||
use milli::{Index, DEFAULT_VALUES_PER_FACET};
|
use milli::update::Setting;
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
use milli::{Criterion, CriterionError, Index, DEFAULT_VALUES_PER_FACET};
|
||||||
|
use serde::{Deserialize, Serialize, Serializer};
|
||||||
|
|
||||||
|
use crate::error::DeserrError;
|
||||||
|
use crate::error::{deserr_codes::*, unwrap_any};
|
||||||
|
|
||||||
/// The maximimum number of results that the engine
|
/// The maximimum number of results that the engine
|
||||||
/// will be able to return in one search call.
|
/// will be able to return in one search call.
|
||||||
@ -27,112 +34,6 @@ 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;
|
||||||
|
|
||||||
@ -151,78 +52,90 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
|
fn validate_min_word_size_for_typo_setting<E: DeserializeError>(
|
||||||
|
s: MinWordSizeTyposSetting,
|
||||||
|
location: ValuePointerRef,
|
||||||
|
) -> Result<MinWordSizeTyposSetting, E> {
|
||||||
|
if let (Setting::Set(one), Setting::Set(two)) = (s.one_typo, s.two_typos) {
|
||||||
|
if one > two {
|
||||||
|
return Err(unwrap_any(E::error::<Infallible>(None, ErrorKind::Unexpected { msg: format!("`minWordSizeForTypos` setting is invalid. `oneTypo` and `twoTypos` fields should be between `0` and `255`, and `twoTypos` should be greater or equals to `oneTypo` but found `oneTypo: {one}` and twoTypos: {two}`.") }, location)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(s)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[deserr(deny_unknown_fields, rename_all = camelCase, validate = validate_min_word_size_for_typo_setting -> DeserrError<InvalidMinWordLengthForTypo>)]
|
||||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
|
||||||
pub struct MinWordSizeTyposSetting {
|
pub struct MinWordSizeTyposSetting {
|
||||||
#[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")]
|
||||||
pub one_typo: Setting<u8>,
|
pub one_typo: Setting<u8>,
|
||||||
#[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")]
|
||||||
pub two_typos: Setting<u8>,
|
pub two_typos: Setting<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[deserr(deny_unknown_fields, rename_all = camelCase, where_predicate = __Deserr_E: deserr::MergeWithError<DeserrError<InvalidMinWordLengthForTypo>>)]
|
||||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
|
||||||
pub struct TypoSettings {
|
pub struct TypoSettings {
|
||||||
#[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")]
|
||||||
pub enabled: Setting<bool>,
|
pub enabled: Setting<bool>,
|
||||||
#[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")]
|
||||||
|
#[deserr(error = DeserrError<InvalidMinWordLengthForTypo>)]
|
||||||
pub min_word_size_for_typos: Setting<MinWordSizeTyposSetting>,
|
pub min_word_size_for_typos: Setting<MinWordSizeTyposSetting>,
|
||||||
#[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")]
|
||||||
pub disable_on_words: Setting<BTreeSet<String>>,
|
pub disable_on_words: Setting<BTreeSet<String>>,
|
||||||
#[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")]
|
||||||
pub disable_on_attributes: Setting<BTreeSet<String>>,
|
pub disable_on_attributes: Setting<BTreeSet<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
||||||
pub struct FacetingSettings {
|
pub struct FacetingSettings {
|
||||||
#[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")]
|
||||||
pub max_values_per_facet: Setting<usize>,
|
pub max_values_per_facet: Setting<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
||||||
pub struct PaginationSettings {
|
pub struct PaginationSettings {
|
||||||
#[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")]
|
||||||
pub max_total_hits: Setting<usize>,
|
pub max_total_hits: Setting<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MergeWithError<milli::CriterionError> for DeserrError<InvalidSettingsRankingRules> {
|
||||||
|
fn merge(
|
||||||
|
_self_: Option<Self>,
|
||||||
|
other: milli::CriterionError,
|
||||||
|
merge_location: ValuePointerRef,
|
||||||
|
) -> Result<Self, Self> {
|
||||||
|
Self::error::<Infallible>(
|
||||||
|
None,
|
||||||
|
ErrorKind::Unexpected { msg: other.to_string() },
|
||||||
|
merge_location,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Holds all the settings for an index. `T` can either be `Checked` if they represents settings
|
/// 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, DeserializeFromValue)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(
|
||||||
#[serde(rename_all = "camelCase")]
|
deny_unknown_fields,
|
||||||
#[serde(bound(serialize = "T: Serialize", deserialize = "T: Deserialize<'static>"))]
|
rename_all = "camelCase",
|
||||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
bound(serialize = "T: Serialize", deserialize = "T: Deserialize<'static>")
|
||||||
#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
|
)]
|
||||||
|
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)]
|
||||||
pub struct Settings<T> {
|
pub struct Settings<T> {
|
||||||
#[serde(
|
#[serde(
|
||||||
default,
|
default,
|
||||||
serialize_with = "serialize_with_wildcard",
|
serialize_with = "serialize_with_wildcard",
|
||||||
skip_serializing_if = "Setting::is_not_set"
|
skip_serializing_if = "Setting::is_not_set"
|
||||||
)]
|
)]
|
||||||
#[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))]
|
#[deserr(error = DeserrError<InvalidSettingsDisplayedAttributes>)]
|
||||||
pub displayed_attributes: Setting<Vec<String>>,
|
pub displayed_attributes: Setting<Vec<String>>,
|
||||||
|
|
||||||
#[serde(
|
#[serde(
|
||||||
@ -230,38 +143,39 @@ pub struct Settings<T> {
|
|||||||
serialize_with = "serialize_with_wildcard",
|
serialize_with = "serialize_with_wildcard",
|
||||||
skip_serializing_if = "Setting::is_not_set"
|
skip_serializing_if = "Setting::is_not_set"
|
||||||
)]
|
)]
|
||||||
#[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))]
|
#[deserr(error = DeserrError<InvalidSettingsSearchableAttributes>)]
|
||||||
pub searchable_attributes: Setting<Vec<String>>,
|
pub searchable_attributes: Setting<Vec<String>>,
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))]
|
#[deserr(error = DeserrError<InvalidSettingsFilterableAttributes>)]
|
||||||
pub filterable_attributes: Setting<BTreeSet<String>>,
|
pub filterable_attributes: Setting<BTreeSet<String>>,
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))]
|
#[deserr(error = DeserrError<InvalidSettingsSortableAttributes>)]
|
||||||
pub sortable_attributes: Setting<BTreeSet<String>>,
|
pub sortable_attributes: Setting<BTreeSet<String>>,
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))]
|
#[deserr(error = DeserrError<InvalidSettingsRankingRules>)]
|
||||||
pub ranking_rules: Setting<Vec<String>>,
|
pub ranking_rules: Setting<Vec<RankingRuleView>>,
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))]
|
#[deserr(error = DeserrError<InvalidSettingsStopWords>)]
|
||||||
pub stop_words: Setting<BTreeSet<String>>,
|
pub stop_words: Setting<BTreeSet<String>>,
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))]
|
#[deserr(error = DeserrError<InvalidSettingsSynonyms>)]
|
||||||
pub synonyms: Setting<BTreeMap<String, Vec<String>>>,
|
pub synonyms: Setting<BTreeMap<String, Vec<String>>>,
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))]
|
#[deserr(error = DeserrError<InvalidSettingsDistinctAttribute>)]
|
||||||
pub distinct_attribute: Setting<String>,
|
pub distinct_attribute: Setting<String>,
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))]
|
#[deserr(error = DeserrError<InvalidSettingsTypoTolerance>)]
|
||||||
pub typo_tolerance: Setting<TypoSettings>,
|
pub typo_tolerance: Setting<TypoSettings>,
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))]
|
#[deserr(error = DeserrError<InvalidSettingsFaceting>)]
|
||||||
pub faceting: Setting<FacetingSettings>,
|
pub faceting: Setting<FacetingSettings>,
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[cfg_attr(test, proptest(strategy = "test::setting_strategy()"))]
|
#[deserr(error = DeserrError<InvalidSettingsPagination>)]
|
||||||
pub pagination: Setting<PaginationSettings>,
|
pub pagination: Setting<PaginationSettings>,
|
||||||
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
|
#[deserr(skip)]
|
||||||
pub _kind: PhantomData<T>,
|
pub _kind: PhantomData<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -396,7 +310,9 @@ pub fn apply_settings_to_builder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
match settings.ranking_rules {
|
match settings.ranking_rules {
|
||||||
Setting::Set(ref criteria) => builder.set_criteria(criteria.clone()),
|
Setting::Set(ref criteria) => {
|
||||||
|
builder.set_criteria(criteria.iter().map(|c| c.clone().into()).collect())
|
||||||
|
}
|
||||||
Setting::Reset => builder.reset_criteria(),
|
Setting::Reset => builder.reset_criteria(),
|
||||||
Setting::NotSet => (),
|
Setting::NotSet => (),
|
||||||
}
|
}
|
||||||
@ -510,7 +426,7 @@ pub fn settings(
|
|||||||
|
|
||||||
let sortable_attributes = index.sortable_fields(rtxn)?.into_iter().collect();
|
let sortable_attributes = index.sortable_fields(rtxn)?.into_iter().collect();
|
||||||
|
|
||||||
let criteria = index.criteria(rtxn)?.into_iter().map(|c| c.to_string()).collect();
|
let criteria = index.criteria(rtxn)?;
|
||||||
|
|
||||||
let stop_words = index
|
let stop_words = index
|
||||||
.stop_words(rtxn)?
|
.stop_words(rtxn)?
|
||||||
@ -571,7 +487,7 @@ pub fn settings(
|
|||||||
},
|
},
|
||||||
filterable_attributes: Setting::Set(filterable_attributes),
|
filterable_attributes: Setting::Set(filterable_attributes),
|
||||||
sortable_attributes: Setting::Set(sortable_attributes),
|
sortable_attributes: Setting::Set(sortable_attributes),
|
||||||
ranking_rules: Setting::Set(criteria),
|
ranking_rules: Setting::Set(criteria.iter().map(|c| c.clone().into()).collect()),
|
||||||
stop_words: Setting::Set(stop_words),
|
stop_words: Setting::Set(stop_words),
|
||||||
distinct_attribute: match distinct_field {
|
distinct_attribute: match distinct_field {
|
||||||
Some(field) => Setting::Set(field),
|
Some(field) => Setting::Set(field),
|
||||||
@ -585,16 +501,106 @@ pub fn settings(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, DeserializeFromValue)]
|
||||||
|
#[deserr(from(&String) = FromStr::from_str -> CriterionError)]
|
||||||
|
pub enum RankingRuleView {
|
||||||
|
/// Sorted by decreasing number of matched query terms.
|
||||||
|
/// Query words at the front of an attribute is considered better than if it was at the back.
|
||||||
|
Words,
|
||||||
|
/// Sorted by increasing number of typos.
|
||||||
|
Typo,
|
||||||
|
/// Sorted by increasing distance between matched query terms.
|
||||||
|
Proximity,
|
||||||
|
/// Documents with quey words contained in more important
|
||||||
|
/// attributes are considered better.
|
||||||
|
Attribute,
|
||||||
|
/// Dynamically sort at query time the documents. None, one or multiple Asc/Desc sortable
|
||||||
|
/// attributes can be used in place of this criterion at query time.
|
||||||
|
Sort,
|
||||||
|
/// Sorted by the similarity of the matched words with the query words.
|
||||||
|
Exactness,
|
||||||
|
/// Sorted by the increasing value of the field specified.
|
||||||
|
Asc(String),
|
||||||
|
/// Sorted by the decreasing value of the field specified.
|
||||||
|
Desc(String),
|
||||||
|
}
|
||||||
|
impl Serialize for RankingRuleView {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&format!("{}", Criterion::from(self.clone())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'de> Deserialize<'de> for RankingRuleView {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
struct Visitor;
|
||||||
|
impl<'de> serde::de::Visitor<'de> for Visitor {
|
||||||
|
type Value = RankingRuleView;
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(formatter, "the name of a valid ranking rule (string)")
|
||||||
|
}
|
||||||
|
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
let criterion = Criterion::from_str(v).map_err(|_| {
|
||||||
|
E::invalid_value(serde::de::Unexpected::Str(v), &"a valid ranking rule")
|
||||||
|
})?;
|
||||||
|
Ok(RankingRuleView::from(criterion))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deserializer.deserialize_str(Visitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for RankingRuleView {
|
||||||
|
type Err = <Criterion as FromStr>::Err;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Ok(RankingRuleView::from(Criterion::from_str(s)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl fmt::Display for RankingRuleView {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
fmt::Display::fmt(&Criterion::from(self.clone()), f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<Criterion> for RankingRuleView {
|
||||||
|
fn from(value: Criterion) -> Self {
|
||||||
|
match value {
|
||||||
|
Criterion::Words => RankingRuleView::Words,
|
||||||
|
Criterion::Typo => RankingRuleView::Typo,
|
||||||
|
Criterion::Proximity => RankingRuleView::Proximity,
|
||||||
|
Criterion::Attribute => RankingRuleView::Attribute,
|
||||||
|
Criterion::Sort => RankingRuleView::Sort,
|
||||||
|
Criterion::Exactness => RankingRuleView::Exactness,
|
||||||
|
Criterion::Asc(x) => RankingRuleView::Asc(x),
|
||||||
|
Criterion::Desc(x) => RankingRuleView::Desc(x),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<RankingRuleView> for Criterion {
|
||||||
|
fn from(value: RankingRuleView) -> Self {
|
||||||
|
match value {
|
||||||
|
RankingRuleView::Words => Criterion::Words,
|
||||||
|
RankingRuleView::Typo => Criterion::Typo,
|
||||||
|
RankingRuleView::Proximity => Criterion::Proximity,
|
||||||
|
RankingRuleView::Attribute => Criterion::Attribute,
|
||||||
|
RankingRuleView::Sort => Criterion::Sort,
|
||||||
|
RankingRuleView::Exactness => Criterion::Exactness,
|
||||||
|
RankingRuleView::Asc(x) => Criterion::Asc(x),
|
||||||
|
RankingRuleView::Desc(x) => Criterion::Desc(x),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod test {
|
pub(crate) mod test {
|
||||||
use proptest::prelude::*;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub(super) fn setting_strategy<T: Arbitrary + Clone>() -> impl Strategy<Value = Setting<T>> {
|
|
||||||
prop_oneof![Just(Setting::NotSet), Just(Setting::Reset), any::<T>().prop_map(Setting::Set)]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_setting_check() {
|
fn test_setting_check() {
|
||||||
// test no changes
|
// test no changes
|
||||||
|
@ -3,9 +3,12 @@ use std::marker::PhantomData;
|
|||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use deserr::{DeserializeError, DeserializeFromValue, MergeWithError, ValueKind};
|
||||||
use serde::de::Visitor;
|
use serde::de::Visitor;
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
|
||||||
|
use crate::error::unwrap_any;
|
||||||
|
|
||||||
/// A type that tries to match either a star (*) or
|
/// A type that tries to match either a star (*) or
|
||||||
/// any other thing that implements `FromStr`.
|
/// any other thing that implements `FromStr`.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -14,6 +17,35 @@ pub enum StarOr<T> {
|
|||||||
Other(T),
|
Other(T),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<E: DeserializeError, T> DeserializeFromValue<E> for StarOr<T>
|
||||||
|
where
|
||||||
|
T: FromStr,
|
||||||
|
E: MergeWithError<T::Err>,
|
||||||
|
{
|
||||||
|
fn deserialize_from_value<V: deserr::IntoValue>(
|
||||||
|
value: deserr::Value<V>,
|
||||||
|
location: deserr::ValuePointerRef,
|
||||||
|
) -> Result<Self, E> {
|
||||||
|
match value {
|
||||||
|
deserr::Value::String(v) => match v.as_str() {
|
||||||
|
"*" => Ok(StarOr::Star),
|
||||||
|
v => match FromStr::from_str(v) {
|
||||||
|
Ok(x) => Ok(StarOr::Other(x)),
|
||||||
|
Err(e) => Err(unwrap_any(E::merge(None, e, location))),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_ => Err(unwrap_any(E::error::<V>(
|
||||||
|
None,
|
||||||
|
deserr::ErrorKind::IncorrectValueKind {
|
||||||
|
actual: value,
|
||||||
|
accepted: &[ValueKind::String],
|
||||||
|
},
|
||||||
|
location,
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T: FromStr> FromStr for StarOr<T> {
|
impl<T: FromStr> FromStr for StarOr<T> {
|
||||||
type Err = T::Err;
|
type Err = T::Err;
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ impl ErrorCode for AuthenticationError {
|
|||||||
fn error_code(&self) -> Code {
|
fn error_code(&self) -> Code {
|
||||||
match self {
|
match self {
|
||||||
AuthenticationError::MissingAuthorizationHeader => Code::MissingAuthorizationHeader,
|
AuthenticationError::MissingAuthorizationHeader => Code::MissingAuthorizationHeader,
|
||||||
AuthenticationError::InvalidToken => Code::InvalidToken,
|
AuthenticationError::InvalidToken => Code::InvalidApiKey,
|
||||||
AuthenticationError::IrretrievableState => Code::Internal,
|
AuthenticationError::IrretrievableState => Code::Internal,
|
||||||
AuthenticationError::MissingMasterKey => Code::MissingMasterKey,
|
AuthenticationError::MissingMasterKey => Code::MissingMasterKey,
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ impl<T, E> ValidatedJson<T, E> {
|
|||||||
|
|
||||||
impl<T, E> FromRequest for ValidatedJson<T, E>
|
impl<T, E> FromRequest for ValidatedJson<T, E>
|
||||||
where
|
where
|
||||||
E: DeserializeError + ErrorCode + 'static,
|
E: DeserializeError + ErrorCode + std::error::Error + 'static,
|
||||||
T: DeserializeFromValue<E>,
|
T: DeserializeFromValue<E>,
|
||||||
{
|
{
|
||||||
type Error = actix_web::Error;
|
type Error = actix_web::Error;
|
||||||
@ -55,7 +55,7 @@ pub struct ValidatedJsonExtractFut<T, E> {
|
|||||||
impl<T, E> Future for ValidatedJsonExtractFut<T, E>
|
impl<T, E> Future for ValidatedJsonExtractFut<T, E>
|
||||||
where
|
where
|
||||||
T: DeserializeFromValue<E>,
|
T: DeserializeFromValue<E>,
|
||||||
E: DeserializeError + ErrorCode + 'static,
|
E: DeserializeError + ErrorCode + std::error::Error + 'static,
|
||||||
{
|
{
|
||||||
type Output = Result<ValidatedJson<T, E>, actix_web::Error>;
|
type Output = Result<ValidatedJson<T, E>, actix_web::Error>;
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ impl<T, E> QueryParameter<T, E> {
|
|||||||
impl<T, E> QueryParameter<T, E>
|
impl<T, E> QueryParameter<T, E>
|
||||||
where
|
where
|
||||||
T: DeserializeFromValue<E>,
|
T: DeserializeFromValue<E>,
|
||||||
E: DeserializeError + ErrorCode + 'static,
|
E: DeserializeError + ErrorCode + std::error::Error + 'static,
|
||||||
{
|
{
|
||||||
pub fn from_query(query_str: &str) -> Result<Self, actix_web::Error> {
|
pub fn from_query(query_str: &str) -> Result<Self, actix_web::Error> {
|
||||||
let value = serde_urlencoded::from_str::<serde_json::Value>(query_str)
|
let value = serde_urlencoded::from_str::<serde_json::Value>(query_str)
|
||||||
@ -58,7 +58,7 @@ impl<T: fmt::Display, E> fmt::Display for QueryParameter<T, E> {
|
|||||||
impl<T, E> FromRequest for QueryParameter<T, E>
|
impl<T, E> FromRequest for QueryParameter<T, E>
|
||||||
where
|
where
|
||||||
T: DeserializeFromValue<E>,
|
T: DeserializeFromValue<E>,
|
||||||
E: DeserializeError + ErrorCode + 'static,
|
E: DeserializeError + ErrorCode + std::error::Error + 'static,
|
||||||
{
|
{
|
||||||
type Error = actix_web::Error;
|
type Error = actix_web::Error;
|
||||||
type Future = Ready<Result<Self, actix_web::Error>>;
|
type Future = Ready<Result<Self, actix_web::Error>>;
|
||||||
|
@ -1,24 +1,26 @@
|
|||||||
use std::convert::Infallible;
|
use std::str;
|
||||||
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 deserr::DeserializeFromValue;
|
||||||
use meilisearch_auth::error::AuthControllerError;
|
use meilisearch_auth::error::AuthControllerError;
|
||||||
use meilisearch_auth::AuthController;
|
use meilisearch_auth::AuthController;
|
||||||
use meilisearch_types::error::{unwrap_any, Code, ErrorCode, ResponseError};
|
use meilisearch_types::error::{deserr_codes::*, TakeErrorMessage};
|
||||||
use meilisearch_types::keys::{Action, Key};
|
use meilisearch_types::error::{Code, DeserrError, ResponseError};
|
||||||
|
use meilisearch_types::keys::{Action, CreateApiKey, Key, PatchApiKey};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use uuid::Uuid;
|
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::json::ValidatedJson;
|
||||||
use crate::extractors::query_parameters::QueryParameter;
|
use crate::extractors::query_parameters::QueryParameter;
|
||||||
use crate::extractors::sequential_extractor::SeqHandler;
|
use crate::extractors::sequential_extractor::SeqHandler;
|
||||||
use crate::routes::Pagination;
|
use crate::routes::Pagination;
|
||||||
|
|
||||||
|
use super::indexes::search::parse_usize_take_error_message;
|
||||||
|
use super::PAGINATION_DEFAULT_LIMIT;
|
||||||
|
|
||||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(
|
cfg.service(
|
||||||
web::resource("")
|
web::resource("")
|
||||||
@ -35,7 +37,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
|||||||
|
|
||||||
pub async fn create_api_key(
|
pub async fn create_api_key(
|
||||||
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_CREATE }>, AuthController>,
|
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_CREATE }>, AuthController>,
|
||||||
body: web::Json<Value>,
|
body: ValidatedJson<CreateApiKey, DeserrError>,
|
||||||
_req: HttpRequest,
|
_req: HttpRequest,
|
||||||
) -> Result<HttpResponse, ResponseError> {
|
) -> Result<HttpResponse, ResponseError> {
|
||||||
let v = body.into_inner();
|
let v = body.into_inner();
|
||||||
@ -49,72 +51,28 @@ pub async fn create_api_key(
|
|||||||
Ok(HttpResponse::Created().json(res))
|
Ok(HttpResponse::Created().json(res))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(DeserializeFromValue, Deserialize, Debug, Clone, Copy)]
|
||||||
pub struct PaginationDeserrError {
|
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)]
|
||||||
error: String,
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
code: Code,
|
pub struct ListApiKeys {
|
||||||
|
#[serde(default)]
|
||||||
|
#[deserr(error = DeserrError<InvalidApiKeyOffset>, default, from(&String) = parse_usize_take_error_message -> TakeErrorMessage<std::num::ParseIntError>)]
|
||||||
|
pub offset: usize,
|
||||||
|
#[serde(default = "PAGINATION_DEFAULT_LIMIT")]
|
||||||
|
#[deserr(error = DeserrError<InvalidApiKeyLimit>, default = PAGINATION_DEFAULT_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage<std::num::ParseIntError>)]
|
||||||
|
pub limit: usize,
|
||||||
}
|
}
|
||||||
|
impl ListApiKeys {
|
||||||
impl std::fmt::Display for PaginationDeserrError {
|
fn as_pagination(self) -> Pagination {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
Pagination { offset: self.offset, limit: self.limit }
|
||||||
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: QueryParameter<Pagination, PaginationDeserrError>,
|
list_api_keys: QueryParameter<ListApiKeys, DeserrError>,
|
||||||
) -> Result<HttpResponse, ResponseError> {
|
) -> Result<HttpResponse, ResponseError> {
|
||||||
let paginate = paginate.into_inner();
|
let paginate = list_api_keys.into_inner().as_pagination();
|
||||||
let page_view = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> {
|
let 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
|
||||||
@ -149,15 +107,15 @@ pub async fn get_api_key(
|
|||||||
|
|
||||||
pub async fn patch_api_key(
|
pub async fn patch_api_key(
|
||||||
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_UPDATE }>, AuthController>,
|
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_UPDATE }>, AuthController>,
|
||||||
body: web::Json<Value>,
|
body: ValidatedJson<PatchApiKey, DeserrError>,
|
||||||
path: web::Path<AuthParam>,
|
path: web::Path<AuthParam>,
|
||||||
) -> Result<HttpResponse, ResponseError> {
|
) -> Result<HttpResponse, ResponseError> {
|
||||||
let key = path.into_inner().key;
|
let key = path.into_inner().key;
|
||||||
let body = body.into_inner();
|
let patch_api_key = body.into_inner();
|
||||||
let res = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> {
|
let res = tokio::task::spawn_blocking(move || -> Result<_, AuthControllerError> {
|
||||||
let uid =
|
let uid =
|
||||||
Uuid::parse_str(&key).or_else(|_| auth_controller.get_uid_from_encoded_key(&key))?;
|
Uuid::parse_str(&key).or_else(|_| auth_controller.get_uid_from_encoded_key(&key))?;
|
||||||
let key = auth_controller.update_key(uid, body)?;
|
let key = auth_controller.update_key(uid, patch_api_key)?;
|
||||||
|
|
||||||
Ok(KeyView::from_key(key, &auth_controller))
|
Ok(KeyView::from_key(key, &auth_controller))
|
||||||
})
|
})
|
||||||
|
@ -1,19 +1,23 @@
|
|||||||
use std::convert::Infallible;
|
use crate::analytics::{Analytics, DocumentDeletionKind};
|
||||||
use std::fmt;
|
use crate::error::MeilisearchHttpError;
|
||||||
use std::io::ErrorKind;
|
use crate::error::PayloadError::ReceivePayload;
|
||||||
use std::num::ParseIntError;
|
use crate::extractors::authentication::policies::*;
|
||||||
use std::str::FromStr;
|
use crate::extractors::authentication::GuardedData;
|
||||||
|
use crate::extractors::payload::Payload;
|
||||||
|
use crate::extractors::query_parameters::QueryParameter;
|
||||||
|
use crate::extractors::sequential_extractor::SeqHandler;
|
||||||
|
use crate::routes::{fold_star_or, PaginationView, SummarizedTaskView};
|
||||||
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 deserr::DeserializeFromValue;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use index_scheduler::IndexScheduler;
|
use index_scheduler::IndexScheduler;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use meilisearch_types::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::{unwrap_any, Code, ErrorCode, ResponseError};
|
use meilisearch_types::error::{deserr_codes::*, TakeErrorMessage};
|
||||||
|
use meilisearch_types::error::{DeserrError, 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;
|
||||||
@ -25,19 +29,13 @@ use once_cell::sync::Lazy;
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_cs::vec::CS;
|
use serde_cs::vec::CS;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use std::io::ErrorKind;
|
||||||
|
use std::num::ParseIntError;
|
||||||
use tempfile::tempfile;
|
use tempfile::tempfile;
|
||||||
use tokio::fs::File;
|
use tokio::fs::File;
|
||||||
use tokio::io::{AsyncSeekExt, AsyncWriteExt, BufWriter};
|
use tokio::io::{AsyncSeekExt, AsyncWriteExt, BufWriter};
|
||||||
|
|
||||||
use crate::analytics::{Analytics, DocumentDeletionKind};
|
use super::search::parse_usize_take_error_message;
|
||||||
use crate::error::MeilisearchHttpError;
|
|
||||||
use crate::error::PayloadError::ReceivePayload;
|
|
||||||
use crate::extractors::authentication::policies::*;
|
|
||||||
use crate::extractors::authentication::GuardedData;
|
|
||||||
use crate::extractors::payload::Payload;
|
|
||||||
use crate::extractors::query_parameters::QueryParameter;
|
|
||||||
use crate::extractors::sequential_extractor::SeqHandler;
|
|
||||||
use crate::routes::{fold_star_or, PaginationView, SummarizedTaskView};
|
|
||||||
|
|
||||||
static ACCEPTED_CONTENT_TYPE: Lazy<Vec<String>> = Lazy::new(|| {
|
static ACCEPTED_CONTENT_TYPE: Lazy<Vec<String>> = Lazy::new(|| {
|
||||||
vec!["application/json".to_string(), "application/x-ndjson".to_string(), "text/csv".to_string()]
|
vec!["application/json".to_string(), "application/x-ndjson".to_string(), "text/csv".to_string()]
|
||||||
@ -83,61 +81,16 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, DeserializeFromValue)]
|
#[derive(Deserialize, Debug, DeserializeFromValue)]
|
||||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)]
|
||||||
pub struct GetDocument {
|
pub struct GetDocument {
|
||||||
|
#[deserr(error = DeserrError<InvalidDocumentFields>)]
|
||||||
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: QueryParameter<GetDocument, GetDocumentDeserrError>,
|
params: QueryParameter<GetDocument, DeserrError>,
|
||||||
) -> 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);
|
||||||
@ -165,81 +118,20 @@ pub async fn delete_document(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, DeserializeFromValue)]
|
#[derive(Deserialize, Debug, DeserializeFromValue)]
|
||||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)]
|
||||||
pub struct BrowseQuery {
|
pub struct BrowseQuery {
|
||||||
#[deserr(default, from(&String) = FromStr::from_str -> ParseIntError)]
|
#[deserr(error = DeserrError<InvalidDocumentFields>, default, from(&String) = parse_usize_take_error_message -> TakeErrorMessage<ParseIntError>)]
|
||||||
offset: usize,
|
offset: usize,
|
||||||
#[deserr(default = crate::routes::PAGINATION_DEFAULT_LIMIT(), from(&String) = FromStr::from_str -> ParseIntError)]
|
#[deserr(error = DeserrError<InvalidDocumentLimit>, default = crate::routes::PAGINATION_DEFAULT_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage<ParseIntError>)]
|
||||||
limit: usize,
|
limit: usize,
|
||||||
|
#[deserr(error = DeserrError<InvalidDocumentLimit>)]
|
||||||
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: QueryParameter<BrowseQuery, BrowseQueryDeserrError>,
|
params: QueryParameter<BrowseQuery, DeserrError>,
|
||||||
) -> 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();
|
||||||
@ -255,61 +147,16 @@ pub async fn get_all_documents(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, DeserializeFromValue)]
|
#[derive(Deserialize, Debug, DeserializeFromValue)]
|
||||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)]
|
||||||
pub struct UpdateDocumentsQuery {
|
pub struct UpdateDocumentsQuery {
|
||||||
|
#[deserr(error = DeserrError<InvalidIndexPrimaryKey>)]
|
||||||
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: QueryParameter<UpdateDocumentsQuery, UpdateDocumentsQueryDeserrError>,
|
params: QueryParameter<UpdateDocumentsQuery, DeserrError>,
|
||||||
body: Payload,
|
body: Payload,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
analytics: web::Data<dyn Analytics>,
|
analytics: web::Data<dyn Analytics>,
|
||||||
@ -337,7 +184,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: QueryParameter<UpdateDocumentsQuery, UpdateDocumentsQueryDeserrError>,
|
params: QueryParameter<UpdateDocumentsQuery, DeserrError>,
|
||||||
body: Payload,
|
body: Payload,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
analytics: web::Data<dyn Analytics>,
|
analytics: web::Data<dyn Analytics>,
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
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::{
|
use deserr::DeserializeFromValue;
|
||||||
DeserializeError, DeserializeFromValue, ErrorKind, IntoValue, MergeWithError, ValuePointerRef,
|
|
||||||
};
|
|
||||||
use index_scheduler::IndexScheduler;
|
use index_scheduler::IndexScheduler;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use meilisearch_types::error::{unwrap_any, Code, ErrorCode, ResponseError};
|
use meilisearch_types::error::deserr_codes::*;
|
||||||
|
use meilisearch_types::error::{DeserrError, 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;
|
||||||
@ -16,7 +12,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
use super::{Pagination, SummarizedTaskView};
|
use super::{ListIndexes, 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};
|
||||||
@ -74,7 +70,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: QueryParameter<Pagination, ListIndexesDeserrError>,
|
paginate: QueryParameter<ListIndexes, DeserrError>,
|
||||||
) -> 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()?;
|
||||||
@ -84,82 +80,24 @@ pub async fn list_indexes(
|
|||||||
.map(|(name, index)| IndexView::new(name, &index))
|
.map(|(name, index)| IndexView::new(name, &index))
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
let ret = paginate.auto_paginate_sized(indexes.into_iter());
|
let ret = paginate.as_pagination().auto_paginate_sized(indexes.into_iter());
|
||||||
|
|
||||||
debug!("returns: {:?}", ret);
|
debug!("returns: {:?}", ret);
|
||||||
Ok(HttpResponse::Ok().json(ret))
|
Ok(HttpResponse::Ok().json(ret))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ListIndexesDeserrError {
|
|
||||||
error: String,
|
|
||||||
code: Code,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for ListIndexesDeserrError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "{}", self.error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for ListIndexesDeserrError {}
|
|
||||||
impl ErrorCode for ListIndexesDeserrError {
|
|
||||||
fn error_code(&self) -> Code {
|
|
||||||
self.code
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MergeWithError<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)]
|
#[derive(DeserializeFromValue, Debug)]
|
||||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)]
|
||||||
pub struct IndexCreateRequest {
|
pub struct IndexCreateRequest {
|
||||||
|
#[deserr(error = DeserrError<InvalidIndexUid>, missing_field_error = DeserrError::missing_index_uid)]
|
||||||
uid: String,
|
uid: String,
|
||||||
|
#[deserr(error = DeserrError<InvalidIndexPrimaryKey>)]
|
||||||
primary_key: Option<String>,
|
primary_key: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_index(
|
pub async fn create_index(
|
||||||
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_CREATE }>, Data<IndexScheduler>>,
|
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_CREATE }>, Data<IndexScheduler>>,
|
||||||
body: ValidatedJson<IndexCreateRequest, CreateIndexesDeserrError>,
|
body: ValidatedJson<IndexCreateRequest, DeserrError>,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
analytics: web::Data<dyn Analytics>,
|
analytics: web::Data<dyn Analytics>,
|
||||||
) -> Result<HttpResponse, ResponseError> {
|
) -> Result<HttpResponse, ResponseError> {
|
||||||
@ -184,58 +122,10 @@ pub async fn create_index(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct CreateIndexesDeserrError {
|
|
||||||
error: String,
|
|
||||||
code: Code,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for CreateIndexesDeserrError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "{}", self.error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for CreateIndexesDeserrError {}
|
|
||||||
impl ErrorCode for CreateIndexesDeserrError {
|
|
||||||
fn error_code(&self) -> Code {
|
|
||||||
self.code
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MergeWithError<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)]
|
#[derive(DeserializeFromValue, Debug)]
|
||||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)]
|
||||||
pub struct UpdateIndexRequest {
|
pub struct UpdateIndexRequest {
|
||||||
|
#[deserr(error = DeserrError<InvalidIndexPrimaryKey>)]
|
||||||
primary_key: Option<String>,
|
primary_key: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,7 +144,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: ValidatedJson<UpdateIndexRequest, UpdateIndexesDeserrError>,
|
body: ValidatedJson<UpdateIndexRequest, DeserrError>,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
analytics: web::Data<dyn Analytics>,
|
analytics: web::Data<dyn Analytics>,
|
||||||
) -> Result<HttpResponse, ResponseError> {
|
) -> Result<HttpResponse, ResponseError> {
|
||||||
@ -278,51 +168,6 @@ 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>,
|
||||||
|
@ -5,7 +5,8 @@ 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::deserr_codes::*;
|
||||||
|
use meilisearch_types::error::{DeserrError, ResponseError, TakeErrorMessage};
|
||||||
use serde_cs::vec::CS;
|
use serde_cs::vec::CS;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
@ -15,11 +16,11 @@ use crate::extractors::authentication::GuardedData;
|
|||||||
use crate::extractors::json::ValidatedJson;
|
use crate::extractors::json::ValidatedJson;
|
||||||
use crate::extractors::query_parameters::QueryParameter;
|
use crate::extractors::query_parameters::QueryParameter;
|
||||||
use crate::extractors::sequential_extractor::SeqHandler;
|
use crate::extractors::sequential_extractor::SeqHandler;
|
||||||
use crate::routes::from_string_to_option;
|
use crate::routes::from_string_to_option_take_error_message;
|
||||||
use crate::search::{
|
use crate::search::{
|
||||||
perform_search, MatchingStrategy, SearchDeserError, SearchQuery, DEFAULT_CROP_LENGTH,
|
perform_search, MatchingStrategy, SearchQuery, DEFAULT_CROP_LENGTH, DEFAULT_CROP_MARKER,
|
||||||
DEFAULT_CROP_MARKER, DEFAULT_HIGHLIGHT_POST_TAG, DEFAULT_HIGHLIGHT_PRE_TAG,
|
DEFAULT_HIGHLIGHT_POST_TAG, DEFAULT_HIGHLIGHT_PRE_TAG, DEFAULT_SEARCH_LIMIT,
|
||||||
DEFAULT_SEARCH_LIMIT, DEFAULT_SEARCH_OFFSET,
|
DEFAULT_SEARCH_OFFSET,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||||
@ -30,35 +31,54 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_usize_take_error_message(
|
||||||
|
s: &str,
|
||||||
|
) -> Result<usize, TakeErrorMessage<std::num::ParseIntError>> {
|
||||||
|
usize::from_str(s).map_err(TakeErrorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_bool_take_error_message(
|
||||||
|
s: &str,
|
||||||
|
) -> Result<bool, TakeErrorMessage<std::str::ParseBoolError>> {
|
||||||
|
s.parse().map_err(TakeErrorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, deserr::DeserializeFromValue)]
|
#[derive(Debug, deserr::DeserializeFromValue)]
|
||||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)]
|
||||||
pub struct SearchQueryGet {
|
pub struct SearchQueryGet {
|
||||||
|
#[deserr(error = DeserrError<InvalidSearchQ>)]
|
||||||
q: Option<String>,
|
q: Option<String>,
|
||||||
#[deserr(default = DEFAULT_SEARCH_OFFSET(), from(&String) = FromStr::from_str -> std::num::ParseIntError)]
|
#[deserr(error = DeserrError<InvalidSearchOffset>, default = DEFAULT_SEARCH_OFFSET(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage<std::num::ParseIntError>)]
|
||||||
offset: usize,
|
offset: usize,
|
||||||
#[deserr(default = DEFAULT_SEARCH_LIMIT(), from(&String) = FromStr::from_str -> std::num::ParseIntError)]
|
#[deserr(error = DeserrError<InvalidSearchLimit>, default = DEFAULT_SEARCH_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage<std::num::ParseIntError>)]
|
||||||
limit: usize,
|
limit: usize,
|
||||||
#[deserr(from(&String) = from_string_to_option -> std::num::ParseIntError)]
|
#[deserr(error = DeserrError<InvalidSearchPage>, from(&String) = from_string_to_option_take_error_message -> TakeErrorMessage<std::num::ParseIntError>)]
|
||||||
page: Option<usize>,
|
page: Option<usize>,
|
||||||
#[deserr(from(&String) = from_string_to_option -> std::num::ParseIntError)]
|
#[deserr(error = DeserrError<InvalidSearchHitsPerPage>, from(&String) = from_string_to_option_take_error_message -> TakeErrorMessage<std::num::ParseIntError>)]
|
||||||
hits_per_page: Option<usize>,
|
hits_per_page: Option<usize>,
|
||||||
|
#[deserr(error = DeserrError<InvalidSearchAttributesToRetrieve>)]
|
||||||
attributes_to_retrieve: Option<CS<String>>,
|
attributes_to_retrieve: Option<CS<String>>,
|
||||||
|
#[deserr(error = DeserrError<InvalidSearchAttributesToCrop>)]
|
||||||
attributes_to_crop: Option<CS<String>>,
|
attributes_to_crop: Option<CS<String>>,
|
||||||
#[deserr(default = DEFAULT_CROP_LENGTH(), from(&String) = FromStr::from_str -> std::num::ParseIntError)]
|
#[deserr(error = DeserrError<InvalidSearchCropLength>, default = DEFAULT_CROP_LENGTH(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage<std::num::ParseIntError>)]
|
||||||
crop_length: usize,
|
crop_length: usize,
|
||||||
|
#[deserr(error = DeserrError<InvalidSearchAttributesToHighlight>)]
|
||||||
attributes_to_highlight: Option<CS<String>>,
|
attributes_to_highlight: Option<CS<String>>,
|
||||||
|
#[deserr(error = DeserrError<InvalidSearchFilter>)]
|
||||||
filter: Option<String>,
|
filter: Option<String>,
|
||||||
|
#[deserr(error = DeserrError<InvalidSearchSort>)]
|
||||||
sort: Option<String>,
|
sort: Option<String>,
|
||||||
#[deserr(default, from(&String) = FromStr::from_str -> std::str::ParseBoolError)]
|
#[deserr(error = DeserrError<InvalidSearchShowMatchesPosition>, default, from(&String) = parse_bool_take_error_message -> TakeErrorMessage<std::str::ParseBoolError>)]
|
||||||
show_matches_position: bool,
|
show_matches_position: bool,
|
||||||
|
#[deserr(error = DeserrError<InvalidSearchFacets>)]
|
||||||
facets: Option<CS<String>>,
|
facets: Option<CS<String>>,
|
||||||
#[deserr(default = DEFAULT_HIGHLIGHT_PRE_TAG())]
|
#[deserr(error = DeserrError<InvalidSearchHighlightPreTag>, default = DEFAULT_HIGHLIGHT_PRE_TAG())]
|
||||||
highlight_pre_tag: String,
|
highlight_pre_tag: String,
|
||||||
#[deserr(default = DEFAULT_HIGHLIGHT_POST_TAG())]
|
#[deserr(error = DeserrError<InvalidSearchHighlightPostTag>, default = DEFAULT_HIGHLIGHT_POST_TAG())]
|
||||||
highlight_post_tag: String,
|
highlight_post_tag: String,
|
||||||
#[deserr(default = DEFAULT_CROP_MARKER())]
|
#[deserr(error = DeserrError<InvalidSearchCropMarker>, default = DEFAULT_CROP_MARKER())]
|
||||||
crop_marker: String,
|
crop_marker: String,
|
||||||
#[deserr(default)]
|
#[deserr(error = DeserrError<InvalidSearchMatchingStrategy>, default)]
|
||||||
matching_strategy: MatchingStrategy,
|
matching_strategy: MatchingStrategy,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,7 +162,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: QueryParameter<SearchQueryGet, SearchDeserError>,
|
params: QueryParameter<SearchQueryGet, DeserrError>,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
analytics: web::Data<dyn Analytics>,
|
analytics: web::Data<dyn Analytics>,
|
||||||
) -> Result<HttpResponse, ResponseError> {
|
) -> Result<HttpResponse, ResponseError> {
|
||||||
@ -174,7 +194,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: ValidatedJson<SearchQuery, SearchDeserError>,
|
params: ValidatedJson<SearchQuery, DeserrError>,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
analytics: web::Data<dyn Analytics>,
|
analytics: web::Data<dyn Analytics>,
|
||||||
) -> Result<HttpResponse, ResponseError> {
|
) -> Result<HttpResponse, ResponseError> {
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
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::{unwrap_any, Code, ErrorCode, ResponseError};
|
use meilisearch_types::error::{DeserrError, 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, RankingRuleView, Settings, Unchecked};
|
||||||
use meilisearch_types::tasks::KindWithContent;
|
use meilisearch_types::tasks::KindWithContent;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
@ -212,7 +209,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.into(), Setting::Set(false))),
|
"enabled": setting.as_ref().map(|s| !matches!(s.enabled, 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())),
|
||||||
@ -331,24 +328,24 @@ make_setting_route!(
|
|||||||
make_setting_route!(
|
make_setting_route!(
|
||||||
"/ranking-rules",
|
"/ranking-rules",
|
||||||
put,
|
put,
|
||||||
Vec<String>,
|
Vec<meilisearch_types::settings::RankingRuleView>,
|
||||||
ranking_rules,
|
ranking_rules,
|
||||||
"rankingRules",
|
"rankingRules",
|
||||||
analytics,
|
analytics,
|
||||||
|setting: &Option<Vec<String>>, req: &HttpRequest| {
|
|setting: &Option<Vec<meilisearch_types::settings::RankingRuleView>>, req: &HttpRequest| {
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
analytics.publish(
|
analytics.publish(
|
||||||
"RankingRules Updated".to_string(),
|
"RankingRules Updated".to_string(),
|
||||||
json!({
|
json!({
|
||||||
"ranking_rules": {
|
"ranking_rules": {
|
||||||
"words_position": setting.as_ref().map(|rr| rr.iter().position(|s| s == "words")),
|
"words_position": setting.as_ref().map(|rr| rr.iter().position(|s| matches!(s, meilisearch_types::settings::RankingRuleView::Words))),
|
||||||
"typo_position": setting.as_ref().map(|rr| rr.iter().position(|s| s == "typo")),
|
"typo_position": setting.as_ref().map(|rr| rr.iter().position(|s| matches!(s, meilisearch_types::settings::RankingRuleView::Typo))),
|
||||||
"proximity_position": setting.as_ref().map(|rr| rr.iter().position(|s| s == "proximity")),
|
"proximity_position": setting.as_ref().map(|rr| rr.iter().position(|s| matches!(s, meilisearch_types::settings::RankingRuleView::Proximity))),
|
||||||
"attribute_position": setting.as_ref().map(|rr| rr.iter().position(|s| s == "attribute")),
|
"attribute_position": setting.as_ref().map(|rr| rr.iter().position(|s| matches!(s, meilisearch_types::settings::RankingRuleView::Attribute))),
|
||||||
"sort_position": setting.as_ref().map(|rr| rr.iter().position(|s| s == "sort")),
|
"sort_position": setting.as_ref().map(|rr| rr.iter().position(|s| matches!(s, meilisearch_types::settings::RankingRuleView::Sort))),
|
||||||
"exactness_position": setting.as_ref().map(|rr| rr.iter().position(|s| s == "exactness")),
|
"exactness_position": setting.as_ref().map(|rr| rr.iter().position(|s| matches!(s, meilisearch_types::settings::RankingRuleView::Exactness))),
|
||||||
"values": setting.as_ref().map(|rr| rr.iter().filter(|s| !s.contains(':')).cloned().collect::<Vec<_>>().join(", ")),
|
"values": setting.as_ref().map(|rr| rr.iter().filter(|s| matches!(s, meilisearch_types::settings::RankingRuleView::Asc(_) | meilisearch_types::settings::RankingRuleView::Desc(_)) ).map(|x| x.to_string()).collect::<Vec<_>>().join(", ")),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
Some(req),
|
Some(req),
|
||||||
@ -428,66 +425,10 @@ 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: ValidatedJson<Settings<Unchecked>, SettingsDeserrError>,
|
body: ValidatedJson<Settings<Unchecked>, DeserrError>,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
analytics: web::Data<dyn Analytics>,
|
analytics: web::Data<dyn Analytics>,
|
||||||
) -> Result<HttpResponse, ResponseError> {
|
) -> Result<HttpResponse, ResponseError> {
|
||||||
@ -497,13 +438,13 @@ pub async fn update_all(
|
|||||||
"Settings Updated".to_string(),
|
"Settings Updated".to_string(),
|
||||||
json!({
|
json!({
|
||||||
"ranking_rules": {
|
"ranking_rules": {
|
||||||
"words_position": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().position(|s| s == "words")),
|
"words_position": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().position(|s| matches!(s, RankingRuleView::Words))),
|
||||||
"typo_position": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().position(|s| s == "typo")),
|
"typo_position": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().position(|s| matches!(s, RankingRuleView::Typo))),
|
||||||
"proximity_position": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().position(|s| s == "proximity")),
|
"proximity_position": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().position(|s| matches!(s, RankingRuleView::Proximity))),
|
||||||
"attribute_position": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().position(|s| s == "attribute")),
|
"attribute_position": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().position(|s| matches!(s, RankingRuleView::Attribute))),
|
||||||
"sort_position": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().position(|s| s == "sort")),
|
"sort_position": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().position(|s| matches!(s, RankingRuleView::Sort))),
|
||||||
"exactness_position": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().position(|s| s == "exactness")),
|
"exactness_position": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().position(|s| matches!(s, RankingRuleView::Exactness))),
|
||||||
"values": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().filter(|s| !s.contains(':')).cloned().collect::<Vec<_>>().join(", ")),
|
"values": new_settings.ranking_rules.as_ref().set().map(|rr| rr.iter().filter(|s| !matches!(s, RankingRuleView::Asc(_) | RankingRuleView::Desc(_)) ).map(|x| x.to_string()).collect::<Vec<_>>().join(", ")),
|
||||||
},
|
},
|
||||||
"searchable_attributes": {
|
"searchable_attributes": {
|
||||||
"total": new_settings.searchable_attributes.as_ref().set().map(|searchable| searchable.len()),
|
"total": new_settings.searchable_attributes.as_ref().set().map(|searchable| searchable.len()),
|
||||||
|
@ -6,7 +6,8 @@ use actix_web::{web, HttpRequest, HttpResponse};
|
|||||||
use deserr::DeserializeFromValue;
|
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::deserr_codes::*;
|
||||||
|
use meilisearch_types::error::{DeserrError, ResponseError, TakeErrorMessage};
|
||||||
use meilisearch_types::settings::{Settings, Unchecked};
|
use meilisearch_types::settings::{Settings, Unchecked};
|
||||||
use meilisearch_types::star_or::StarOr;
|
use meilisearch_types::star_or::StarOr;
|
||||||
use meilisearch_types::tasks::{Kind, Status, Task, TaskId};
|
use meilisearch_types::tasks::{Kind, Status, Task, TaskId};
|
||||||
@ -14,6 +15,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
use self::indexes::search::parse_usize_take_error_message;
|
||||||
use self::indexes::IndexStats;
|
use self::indexes::IndexStats;
|
||||||
use crate::analytics::Analytics;
|
use crate::analytics::Analytics;
|
||||||
use crate::extractors::authentication::policies::*;
|
use crate::extractors::authentication::policies::*;
|
||||||
@ -57,6 +59,14 @@ where
|
|||||||
{
|
{
|
||||||
Ok(Some(input.parse()?))
|
Ok(Some(input.parse()?))
|
||||||
}
|
}
|
||||||
|
pub fn from_string_to_option_take_error_message<T, E>(
|
||||||
|
input: &str,
|
||||||
|
) -> Result<Option<T>, TakeErrorMessage<E>>
|
||||||
|
where
|
||||||
|
T: FromStr<Err = E>,
|
||||||
|
{
|
||||||
|
Ok(Some(input.parse().map_err(TakeErrorMessage)?))
|
||||||
|
}
|
||||||
|
|
||||||
const PAGINATION_DEFAULT_LIMIT: fn() -> usize = || 20;
|
const PAGINATION_DEFAULT_LIMIT: fn() -> usize = || 20;
|
||||||
|
|
||||||
@ -83,18 +93,27 @@ impl From<Task> for SummarizedTaskView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub struct Pagination {
|
||||||
|
pub offset: usize,
|
||||||
|
pub limit: usize,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(DeserializeFromValue, Deserialize, Debug, Clone, Copy)]
|
#[derive(DeserializeFromValue, Deserialize, Debug, Clone, Copy)]
|
||||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
#[deserr(error = DeserrError, 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 ListIndexes {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[deserr(default, from(&String) = FromStr::from_str -> std::num::ParseIntError)]
|
#[deserr(error = DeserrError<InvalidIndexOffset>, default, from(&String) = parse_usize_take_error_message -> TakeErrorMessage<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)]
|
#[deserr(error = DeserrError<InvalidIndexLimit>, default = PAGINATION_DEFAULT_LIMIT(), from(&String) = parse_usize_take_error_message -> TakeErrorMessage<std::num::ParseIntError>)]
|
||||||
pub limit: usize,
|
pub limit: usize,
|
||||||
}
|
}
|
||||||
|
impl ListIndexes {
|
||||||
|
fn as_pagination(self) -> Pagination {
|
||||||
|
Pagination { offset: self.offset, limit: self.limit }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct PaginationView<T> {
|
pub struct PaginationView<T> {
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
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 deserr::DeserializeFromValue;
|
||||||
use index_scheduler::IndexScheduler;
|
use index_scheduler::IndexScheduler;
|
||||||
use meilisearch_types::error::{unwrap_any, Code, ErrorCode, ResponseError};
|
use meilisearch_types::error::deserr_codes::InvalidSwapIndexes;
|
||||||
|
use meilisearch_types::error::{DeserrError, ResponseError};
|
||||||
use meilisearch_types::tasks::{IndexSwap, KindWithContent};
|
use meilisearch_types::tasks::{IndexSwap, KindWithContent};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
@ -21,14 +20,15 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(DeserializeFromValue, Debug, Clone, PartialEq, Eq)]
|
#[derive(DeserializeFromValue, Debug, Clone, PartialEq, Eq)]
|
||||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)]
|
||||||
pub struct SwapIndexesPayload {
|
pub struct SwapIndexesPayload {
|
||||||
|
#[deserr(error = DeserrError<InvalidSwapIndexes>)]
|
||||||
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: ValidatedJson<Vec<SwapIndexesPayload>, SwapIndexesDeserrError>,
|
params: ValidatedJson<Vec<SwapIndexesPayload>, DeserrError>,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
analytics: web::Data<dyn Analytics>,
|
analytics: web::Data<dyn Analytics>,
|
||||||
) -> Result<HttpResponse, ResponseError> {
|
) -> Result<HttpResponse, ResponseError> {
|
||||||
@ -62,49 +62,3 @@ 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 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,16 +1,11 @@
|
|||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||||
use std::convert::Infallible;
|
use std::str::FromStr;
|
||||||
use std::fmt;
|
|
||||||
use std::num::ParseIntError;
|
|
||||||
use std::str::{FromStr, ParseBoolError};
|
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use deserr::{
|
use deserr::DeserializeFromValue;
|
||||||
DeserializeError, DeserializeFromValue, ErrorKind, IntoValue, MergeWithError, ValuePointerRef,
|
|
||||||
};
|
|
||||||
use either::Either;
|
use either::Either;
|
||||||
use meilisearch_types::error::{unwrap_any, Code, ErrorCode};
|
use meilisearch_types::error::{deserr_codes::*, DeserrError};
|
||||||
use meilisearch_types::settings::DEFAULT_PAGINATION_MAX_TOTAL_HITS;
|
use meilisearch_types::settings::DEFAULT_PAGINATION_MAX_TOTAL_HITS;
|
||||||
use meilisearch_types::{milli, Document};
|
use meilisearch_types::{milli, Document};
|
||||||
use milli::tokenizer::TokenizerBuilder;
|
use milli::tokenizer::TokenizerBuilder;
|
||||||
@ -34,32 +29,41 @@ pub const DEFAULT_HIGHLIGHT_PRE_TAG: fn() -> String = || "<em>".to_string();
|
|||||||
pub const DEFAULT_HIGHLIGHT_POST_TAG: fn() -> String = || "</em>".to_string();
|
pub const DEFAULT_HIGHLIGHT_POST_TAG: fn() -> String = || "</em>".to_string();
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, PartialEq, DeserializeFromValue)]
|
#[derive(Debug, Clone, Default, PartialEq, DeserializeFromValue)]
|
||||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)]
|
||||||
pub struct SearchQuery {
|
pub struct SearchQuery {
|
||||||
|
#[deserr(error = DeserrError<InvalidSearchQ>)]
|
||||||
pub q: Option<String>,
|
pub q: Option<String>,
|
||||||
#[deserr(default = DEFAULT_SEARCH_OFFSET())]
|
#[deserr(error = DeserrError<InvalidSearchOffset>, default = DEFAULT_SEARCH_OFFSET())]
|
||||||
pub offset: usize,
|
pub offset: usize,
|
||||||
#[deserr(default = DEFAULT_SEARCH_LIMIT())]
|
#[deserr(error = DeserrError<InvalidSearchLimit>, default = DEFAULT_SEARCH_LIMIT())]
|
||||||
pub limit: usize,
|
pub limit: usize,
|
||||||
|
#[deserr(error = DeserrError<InvalidSearchPage>)]
|
||||||
pub page: Option<usize>,
|
pub page: Option<usize>,
|
||||||
|
#[deserr(error = DeserrError<InvalidSearchHitsPerPage>)]
|
||||||
pub hits_per_page: Option<usize>,
|
pub hits_per_page: Option<usize>,
|
||||||
|
#[deserr(error = DeserrError<InvalidSearchAttributesToRetrieve>)]
|
||||||
pub attributes_to_retrieve: Option<BTreeSet<String>>,
|
pub attributes_to_retrieve: Option<BTreeSet<String>>,
|
||||||
|
#[deserr(error = DeserrError<InvalidSearchAttributesToRetrieve>)]
|
||||||
pub attributes_to_crop: Option<Vec<String>>,
|
pub attributes_to_crop: Option<Vec<String>>,
|
||||||
#[deserr(default = DEFAULT_CROP_LENGTH())]
|
#[deserr(error = DeserrError<InvalidSearchCropLength>, default = DEFAULT_CROP_LENGTH())]
|
||||||
pub crop_length: usize,
|
pub crop_length: usize,
|
||||||
|
#[deserr(error = DeserrError<InvalidSearchAttributesToHighlight>)]
|
||||||
pub attributes_to_highlight: Option<HashSet<String>>,
|
pub attributes_to_highlight: Option<HashSet<String>>,
|
||||||
#[deserr(default)]
|
#[deserr(error = DeserrError<InvalidSearchShowMatchesPosition>, default)]
|
||||||
pub show_matches_position: bool,
|
pub show_matches_position: bool,
|
||||||
|
#[deserr(error = DeserrError<InvalidSearchFilter>)]
|
||||||
pub filter: Option<Value>,
|
pub filter: Option<Value>,
|
||||||
|
#[deserr(error = DeserrError<InvalidSearchSort>)]
|
||||||
pub sort: Option<Vec<String>>,
|
pub sort: Option<Vec<String>>,
|
||||||
|
#[deserr(error = DeserrError<InvalidSearchFacets>)]
|
||||||
pub facets: Option<Vec<String>>,
|
pub facets: Option<Vec<String>>,
|
||||||
#[deserr(default = DEFAULT_HIGHLIGHT_PRE_TAG())]
|
#[deserr(error = DeserrError<InvalidSearchHighlightPreTag>, default = DEFAULT_HIGHLIGHT_PRE_TAG())]
|
||||||
pub highlight_pre_tag: String,
|
pub highlight_pre_tag: String,
|
||||||
#[deserr(default = DEFAULT_HIGHLIGHT_POST_TAG())]
|
#[deserr(error = DeserrError<InvalidSearchHighlightPostTag>, default = DEFAULT_HIGHLIGHT_POST_TAG())]
|
||||||
pub highlight_post_tag: String,
|
pub highlight_post_tag: String,
|
||||||
#[deserr(default = DEFAULT_CROP_MARKER())]
|
#[deserr(error = DeserrError<InvalidSearchCropMarker>, default = DEFAULT_CROP_MARKER())]
|
||||||
pub crop_marker: String,
|
pub crop_marker: String,
|
||||||
#[deserr(default)]
|
#[deserr(error = DeserrError<InvalidSearchMatchingStrategy>, default)]
|
||||||
pub matching_strategy: MatchingStrategy,
|
pub matching_strategy: MatchingStrategy,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,96 +98,6 @@ 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)]
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -278,22 +278,16 @@ async fn error_set_invalid_ranking_rules() {
|
|||||||
let index = server.index("test");
|
let index = server.index("test");
|
||||||
index.create(None).await;
|
index.create(None).await;
|
||||||
|
|
||||||
let (_response, _code) =
|
let (response, code) = index.update_settings(json!({ "rankingRules": [ "manyTheFish"]})).await;
|
||||||
index.update_settings(json!({ "rankingRules": [ "manyTheFish"]})).await;
|
meili_snap::insta::assert_debug_snapshot!(code, @"400");
|
||||||
index.wait_task(1).await;
|
meili_snap::insta::assert_json_snapshot!(response, @r###"
|
||||||
let (response, code) = index.get_task(1).await;
|
{
|
||||||
|
"message": "`manyTheFish` ranking rule is invalid. Valid ranking rules are words, typo, sort, proximity, attribute, exactness and custom ranking rules. at `.rankingRules[0]`.",
|
||||||
assert_eq!(code, 200);
|
|
||||||
assert_eq!(response["status"], "failed");
|
|
||||||
|
|
||||||
let expected_error = json!({
|
|
||||||
"message": r#"`manyTheFish` ranking rule is invalid. Valid ranking rules are words, typo, sort, proximity, attribute, exactness and custom ranking rules."#,
|
|
||||||
"code": "invalid_settings_ranking_rules",
|
"code": "invalid_settings_ranking_rules",
|
||||||
"type": "invalid_request",
|
"type": "invalid_request",
|
||||||
"link": "https://docs.meilisearch.com/errors#invalid-settings-ranking-rules"
|
"link": "https://docs.meilisearch.com/errors#invalid-settings-ranking-rules"
|
||||||
});
|
}
|
||||||
|
"###);
|
||||||
assert_eq!(response["error"], expected_error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use meili_snap::insta::{self, assert_json_snapshot};
|
use meili_snap::insta::{self, assert_debug_snapshot, assert_json_snapshot};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use time::format_description::well_known::Rfc3339;
|
use time::format_description::well_known::Rfc3339;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
@ -517,46 +517,26 @@ async fn test_summarized_settings_update() {
|
|||||||
let server = Server::new().await;
|
let server = Server::new().await;
|
||||||
let index = server.index("test");
|
let index = server.index("test");
|
||||||
// here we should find my payload even in the failed task.
|
// here we should find my payload even in the failed task.
|
||||||
index.update_settings(json!({ "rankingRules": ["custom"] })).await;
|
let (response, code) = index.update_settings(json!({ "rankingRules": ["custom"] })).await;
|
||||||
|
assert_debug_snapshot!(code, @"400");
|
||||||
|
assert_json_snapshot!(response, @r###"
|
||||||
|
{
|
||||||
|
"message": "`custom` ranking rule is invalid. Valid ranking rules are words, typo, sort, proximity, attribute, exactness and custom ranking rules. at `.rankingRules[0]`.",
|
||||||
|
"code": "invalid_settings_ranking_rules",
|
||||||
|
"type": "invalid_request",
|
||||||
|
"link": "https://docs.meilisearch.com/errors#invalid-settings-ranking-rules"
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
|
||||||
|
index.update_settings(json!({ "displayedAttributes": ["doggos", "name"], "filterableAttributes": ["age", "nb_paw_pads"], "sortableAttributes": ["iq"] })).await;
|
||||||
index.wait_task(0).await;
|
index.wait_task(0).await;
|
||||||
let (task, _) = index.get_task(0).await;
|
let (task, _) = index.get_task(0).await;
|
||||||
dbg!(&task);
|
|
||||||
assert_json_snapshot!(task,
|
assert_json_snapshot!(task,
|
||||||
{ ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" },
|
{ ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" },
|
||||||
@r###"
|
@r###"
|
||||||
{
|
{
|
||||||
"uid": 0,
|
"uid": 0,
|
||||||
"indexUid": "test",
|
"indexUid": "test",
|
||||||
"status": "failed",
|
|
||||||
"type": "settingsUpdate",
|
|
||||||
"canceledBy": null,
|
|
||||||
"details": {
|
|
||||||
"rankingRules": [
|
|
||||||
"custom"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"message": "`custom` ranking rule is invalid. Valid ranking rules are words, typo, sort, proximity, attribute, exactness and custom ranking rules.",
|
|
||||||
"code": "invalid_settings_ranking_rules",
|
|
||||||
"type": "invalid_request",
|
|
||||||
"link": "https://docs.meilisearch.com/errors#invalid-settings-ranking-rules"
|
|
||||||
},
|
|
||||||
"duration": "[duration]",
|
|
||||||
"enqueuedAt": "[date]",
|
|
||||||
"startedAt": "[date]",
|
|
||||||
"finishedAt": "[date]"
|
|
||||||
}
|
|
||||||
"###);
|
|
||||||
|
|
||||||
index.update_settings(json!({ "displayedAttributes": ["doggos", "name"], "filterableAttributes": ["age", "nb_paw_pads"], "sortableAttributes": ["iq"] })).await;
|
|
||||||
index.wait_task(1).await;
|
|
||||||
let (task, _) = index.get_task(1).await;
|
|
||||||
assert_json_snapshot!(task,
|
|
||||||
{ ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" },
|
|
||||||
@r###"
|
|
||||||
{
|
|
||||||
"uid": 1,
|
|
||||||
"indexUid": "test",
|
|
||||||
"status": "succeeded",
|
"status": "succeeded",
|
||||||
"type": "settingsUpdate",
|
"type": "settingsUpdate",
|
||||||
"canceledBy": null,
|
"canceledBy": null,
|
||||||
|
Loading…
Reference in New Issue
Block a user