mirror of
https://github.com/meilisearch/MeiliSearch
synced 2025-07-03 11:57:07 +02:00
Merge remote-tracking branch 'origin/main' into temp-wildcard
This commit is contained in:
commit
4570d5bf3a
164 changed files with 8353 additions and 2473 deletions
328
meilisearch-types/src/deserr/error_messages.rs
Normal file
328
meilisearch-types/src/deserr/error_messages.rs
Normal file
|
@ -0,0 +1,328 @@
|
|||
/*!
|
||||
This module implements the error messages of deserialization errors.
|
||||
|
||||
We try to:
|
||||
1. Give a human-readable description of where the error originated.
|
||||
2. Use the correct terms depending on the format of the request (json/query param)
|
||||
3. Categorise the type of the error (e.g. missing field, wrong value type, unexpected error, etc.)
|
||||
*/
|
||||
use deserr::{ErrorKind, IntoValue, ValueKind, ValuePointerRef};
|
||||
|
||||
use super::{DeserrJsonError, DeserrQueryParamError};
|
||||
use crate::error::{Code, ErrorCode};
|
||||
|
||||
/// Return a description of the given location in a Json, preceded by the given article.
|
||||
/// e.g. `at .key1[8].key2`. If the location is the origin, the given article will not be
|
||||
/// included in the description.
|
||||
pub fn location_json_description(location: ValuePointerRef, article: &str) -> String {
|
||||
fn rec(location: ValuePointerRef) -> String {
|
||||
match location {
|
||||
ValuePointerRef::Origin => String::new(),
|
||||
ValuePointerRef::Key { key, prev } => rec(*prev) + "." + key,
|
||||
ValuePointerRef::Index { index, prev } => format!("{}[{index}]", rec(*prev)),
|
||||
}
|
||||
}
|
||||
match location {
|
||||
ValuePointerRef::Origin => String::new(),
|
||||
_ => {
|
||||
format!("{article} `{}`", rec(location))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a description of the list of value kinds for a Json payload.
|
||||
fn value_kinds_description_json(kinds: &[ValueKind]) -> String {
|
||||
// Rank each value kind so that they can be sorted (and deduplicated)
|
||||
// Having a predictable order helps with pattern matching
|
||||
fn order(kind: &ValueKind) -> u8 {
|
||||
match kind {
|
||||
ValueKind::Null => 0,
|
||||
ValueKind::Boolean => 1,
|
||||
ValueKind::Integer => 2,
|
||||
ValueKind::NegativeInteger => 3,
|
||||
ValueKind::Float => 4,
|
||||
ValueKind::String => 5,
|
||||
ValueKind::Sequence => 6,
|
||||
ValueKind::Map => 7,
|
||||
}
|
||||
}
|
||||
// Return a description of a single value kind, preceded by an article
|
||||
fn single_description(kind: &ValueKind) -> &'static str {
|
||||
match kind {
|
||||
ValueKind::Null => "null",
|
||||
ValueKind::Boolean => "a boolean",
|
||||
ValueKind::Integer => "a positive integer",
|
||||
ValueKind::NegativeInteger => "a negative integer",
|
||||
ValueKind::Float => "a number",
|
||||
ValueKind::String => "a string",
|
||||
ValueKind::Sequence => "an array",
|
||||
ValueKind::Map => "an object",
|
||||
}
|
||||
}
|
||||
|
||||
fn description_rec(kinds: &[ValueKind], count_items: &mut usize, message: &mut String) {
|
||||
let (msg_part, rest): (_, &[ValueKind]) = match kinds {
|
||||
[] => (String::new(), &[]),
|
||||
[ValueKind::Integer | ValueKind::NegativeInteger, ValueKind::Float, rest @ ..] => {
|
||||
("a number".to_owned(), rest)
|
||||
}
|
||||
[ValueKind::Integer, ValueKind::NegativeInteger, ValueKind::Float, rest @ ..] => {
|
||||
("a number".to_owned(), rest)
|
||||
}
|
||||
[ValueKind::Integer, ValueKind::NegativeInteger, rest @ ..] => {
|
||||
("an integer".to_owned(), rest)
|
||||
}
|
||||
[a] => (single_description(a).to_owned(), &[]),
|
||||
[a, rest @ ..] => (single_description(a).to_owned(), rest),
|
||||
};
|
||||
|
||||
if rest.is_empty() {
|
||||
if *count_items == 0 {
|
||||
message.push_str(&msg_part);
|
||||
} else if *count_items == 1 {
|
||||
message.push_str(&format!(" or {msg_part}"));
|
||||
} else {
|
||||
message.push_str(&format!(", or {msg_part}"));
|
||||
}
|
||||
} else {
|
||||
if *count_items == 0 {
|
||||
message.push_str(&msg_part);
|
||||
} else {
|
||||
message.push_str(&format!(", {msg_part}"));
|
||||
}
|
||||
|
||||
*count_items += 1;
|
||||
description_rec(rest, count_items, message);
|
||||
}
|
||||
}
|
||||
|
||||
let mut kinds = kinds.to_owned();
|
||||
kinds.sort_by_key(order);
|
||||
kinds.dedup();
|
||||
|
||||
if kinds.is_empty() {
|
||||
// Should not happen ideally
|
||||
"a different value".to_owned()
|
||||
} else {
|
||||
let mut message = String::new();
|
||||
description_rec(kinds.as_slice(), &mut 0, &mut message);
|
||||
message
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the JSON string of the value preceded by a description of its kind
|
||||
fn value_description_with_kind_json(v: &serde_json::Value) -> String {
|
||||
match v.kind() {
|
||||
ValueKind::Null => "null".to_owned(),
|
||||
kind => {
|
||||
format!(
|
||||
"{}: `{}`",
|
||||
value_kinds_description_json(&[kind]),
|
||||
serde_json::to_string(v).unwrap()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Default + ErrorCode> deserr::DeserializeError for DeserrJsonError<C> {
|
||||
fn error<V: IntoValue>(
|
||||
_self_: Option<Self>,
|
||||
error: deserr::ErrorKind<V>,
|
||||
location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
let mut message = String::new();
|
||||
|
||||
message.push_str(&match error {
|
||||
ErrorKind::IncorrectValueKind { actual, accepted } => {
|
||||
let expected = value_kinds_description_json(accepted);
|
||||
let received = value_description_with_kind_json(&serde_json::Value::from(actual));
|
||||
|
||||
let location = location_json_description(location, " at");
|
||||
|
||||
format!("Invalid value type{location}: expected {expected}, but found {received}")
|
||||
}
|
||||
ErrorKind::MissingField { field } => {
|
||||
let location = location_json_description(location, " inside");
|
||||
format!("Missing field `{field}`{location}")
|
||||
}
|
||||
ErrorKind::UnknownKey { key, accepted } => {
|
||||
let location = location_json_description(location, " inside");
|
||||
format!(
|
||||
"Unknown field `{}`{location}: expected one of {}",
|
||||
key,
|
||||
accepted
|
||||
.iter()
|
||||
.map(|accepted| format!("`{}`", accepted))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
ErrorKind::UnknownValue { value, accepted } => {
|
||||
let location = location_json_description(location, " at");
|
||||
format!(
|
||||
"Unknown value `{}`{location}: expected one of {}",
|
||||
value,
|
||||
accepted
|
||||
.iter()
|
||||
.map(|accepted| format!("`{}`", accepted))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", "),
|
||||
)
|
||||
}
|
||||
ErrorKind::Unexpected { msg } => {
|
||||
let location = location_json_description(location, " at");
|
||||
format!("Invalid value{location}: {msg}")
|
||||
}
|
||||
});
|
||||
|
||||
Err(DeserrJsonError::new(message, C::default().error_code()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn immutable_field_error(field: &str, accepted: &[&str], code: Code) -> DeserrJsonError {
|
||||
let msg = format!(
|
||||
"Immutable field `{field}`: expected one of {}",
|
||||
accepted
|
||||
.iter()
|
||||
.map(|accepted| format!("`{}`", accepted))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
);
|
||||
|
||||
DeserrJsonError::new(msg, code)
|
||||
}
|
||||
|
||||
/// Return a description of the given location in query parameters, preceded by the
|
||||
/// given article. e.g. `at key5[2]`. If the location is the origin, the given article
|
||||
/// will not be included in the description.
|
||||
pub fn location_query_param_description(location: ValuePointerRef, article: &str) -> String {
|
||||
fn rec(location: ValuePointerRef) -> String {
|
||||
match location {
|
||||
ValuePointerRef::Origin => String::new(),
|
||||
ValuePointerRef::Key { key, prev } => {
|
||||
if matches!(prev, ValuePointerRef::Origin) {
|
||||
key.to_owned()
|
||||
} else {
|
||||
rec(*prev) + "." + key
|
||||
}
|
||||
}
|
||||
ValuePointerRef::Index { index, prev } => format!("{}[{index}]", rec(*prev)),
|
||||
}
|
||||
}
|
||||
match location {
|
||||
ValuePointerRef::Origin => String::new(),
|
||||
_ => {
|
||||
format!("{article} `{}`", rec(location))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Default + ErrorCode> deserr::DeserializeError for DeserrQueryParamError<C> {
|
||||
fn error<V: IntoValue>(
|
||||
_self_: Option<Self>,
|
||||
error: deserr::ErrorKind<V>,
|
||||
location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
let mut message = String::new();
|
||||
|
||||
message.push_str(&match error {
|
||||
ErrorKind::IncorrectValueKind { actual, accepted } => {
|
||||
let expected = value_kinds_description_query_param(accepted);
|
||||
let received = value_description_with_kind_query_param(actual);
|
||||
|
||||
let location = location_query_param_description(location, " for parameter");
|
||||
|
||||
format!("Invalid value type{location}: expected {expected}, but found {received}")
|
||||
}
|
||||
ErrorKind::MissingField { field } => {
|
||||
let location = location_query_param_description(location, " inside");
|
||||
format!("Missing parameter `{field}`{location}")
|
||||
}
|
||||
ErrorKind::UnknownKey { key, accepted } => {
|
||||
let location = location_query_param_description(location, " inside");
|
||||
format!(
|
||||
"Unknown parameter `{}`{location}: expected one of {}",
|
||||
key,
|
||||
accepted
|
||||
.iter()
|
||||
.map(|accepted| format!("`{}`", accepted))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
ErrorKind::UnknownValue { value, accepted } => {
|
||||
let location = location_query_param_description(location, " for parameter");
|
||||
format!(
|
||||
"Unknown value `{}`{location}: expected one of {}",
|
||||
value,
|
||||
accepted
|
||||
.iter()
|
||||
.map(|accepted| format!("`{}`", accepted))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", "),
|
||||
)
|
||||
}
|
||||
ErrorKind::Unexpected { msg } => {
|
||||
let location = location_query_param_description(location, " in parameter");
|
||||
format!("Invalid value{location}: {msg}")
|
||||
}
|
||||
});
|
||||
|
||||
Err(DeserrQueryParamError::new(message, C::default().error_code()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a description of the list of value kinds for query parameters
|
||||
/// Since query parameters are always treated as strings, we always return
|
||||
/// "a string" for now.
|
||||
fn value_kinds_description_query_param(_accepted: &[ValueKind]) -> String {
|
||||
"a string".to_owned()
|
||||
}
|
||||
|
||||
fn value_description_with_kind_query_param<V: IntoValue>(actual: deserr::Value<V>) -> String {
|
||||
match actual {
|
||||
deserr::Value::Null => "null".to_owned(),
|
||||
deserr::Value::Boolean(x) => format!("a boolean: `{x}`"),
|
||||
deserr::Value::Integer(x) => format!("an integer: `{x}`"),
|
||||
deserr::Value::NegativeInteger(x) => {
|
||||
format!("an integer: `{x}`")
|
||||
}
|
||||
deserr::Value::Float(x) => {
|
||||
format!("a number: `{x}`")
|
||||
}
|
||||
deserr::Value::String(x) => {
|
||||
format!("a string: `{x}`")
|
||||
}
|
||||
deserr::Value::Sequence(_) => "multiple values".to_owned(),
|
||||
deserr::Value::Map(_) => "multiple parameters".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use deserr::ValueKind;
|
||||
|
||||
use crate::deserr::error_messages::value_kinds_description_json;
|
||||
|
||||
#[test]
|
||||
fn test_value_kinds_description_json() {
|
||||
insta::assert_display_snapshot!(value_kinds_description_json(&[]), @"a different value");
|
||||
|
||||
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Boolean]), @"a boolean");
|
||||
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer]), @"a positive integer");
|
||||
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::NegativeInteger]), @"a negative integer");
|
||||
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer]), @"a positive integer");
|
||||
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::String]), @"a string");
|
||||
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Sequence]), @"an array");
|
||||
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Map]), @"an object");
|
||||
|
||||
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer, ValueKind::Boolean]), @"a boolean or a positive integer");
|
||||
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Null, ValueKind::Integer]), @"null or a positive integer");
|
||||
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Sequence, ValueKind::NegativeInteger]), @"a negative integer or an array");
|
||||
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer, ValueKind::Float]), @"a number");
|
||||
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer, ValueKind::Float, ValueKind::NegativeInteger]), @"a number");
|
||||
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Integer, ValueKind::Float, ValueKind::NegativeInteger, ValueKind::Null]), @"null or a number");
|
||||
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Boolean, ValueKind::Integer, ValueKind::Float, ValueKind::NegativeInteger, ValueKind::Null]), @"null, a boolean, or a number");
|
||||
insta::assert_display_snapshot!(value_kinds_description_json(&[ValueKind::Null, ValueKind::Boolean, ValueKind::Integer, ValueKind::Float, ValueKind::NegativeInteger, ValueKind::Null]), @"null, a boolean, or a number");
|
||||
}
|
||||
}
|
134
meilisearch-types/src/deserr/mod.rs
Normal file
134
meilisearch-types/src/deserr/mod.rs
Normal file
|
@ -0,0 +1,134 @@
|
|||
use std::convert::Infallible;
|
||||
use std::fmt;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use deserr::{DeserializeError, MergeWithError, ValuePointerRef};
|
||||
|
||||
use crate::error::deserr_codes::{self, *};
|
||||
use crate::error::{
|
||||
unwrap_any, Code, DeserrParseBoolError, DeserrParseIntError, ErrorCode, InvalidTaskDateError,
|
||||
ParseOffsetDateTimeError,
|
||||
};
|
||||
use crate::index_uid::IndexUidFormatError;
|
||||
use crate::tasks::{ParseTaskKindError, ParseTaskStatusError};
|
||||
|
||||
pub mod error_messages;
|
||||
pub mod query_params;
|
||||
|
||||
/// Marker type for the Json format
|
||||
pub struct DeserrJson;
|
||||
/// Marker type for the Query Parameter format
|
||||
pub struct DeserrQueryParam;
|
||||
|
||||
pub type DeserrJsonError<C = deserr_codes::BadRequest> = DeserrError<DeserrJson, C>;
|
||||
pub type DeserrQueryParamError<C = deserr_codes::BadRequest> = DeserrError<DeserrQueryParam, C>;
|
||||
|
||||
/// A request deserialization error.
|
||||
///
|
||||
/// The first generic paramater is a marker type describing the format of the request: either json (e.g. [`DeserrJson`] or [`DeserrQueryParam`]).
|
||||
/// The second generic parameter is the default error code for the deserialization error, in case it is not given.
|
||||
pub struct DeserrError<Format, C: Default + ErrorCode> {
|
||||
pub msg: String,
|
||||
pub code: Code,
|
||||
_phantom: PhantomData<(Format, C)>,
|
||||
}
|
||||
impl<Format, C: Default + ErrorCode> DeserrError<Format, C> {
|
||||
pub fn new(msg: String, code: Code) -> Self {
|
||||
Self { msg, code, _phantom: PhantomData }
|
||||
}
|
||||
}
|
||||
impl<Format, C: Default + ErrorCode> std::fmt::Debug for DeserrError<Format, C> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("DeserrError").field("msg", &self.msg).field("code", &self.code).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Format, C: Default + ErrorCode> std::fmt::Display for DeserrError<Format, C> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Format, C: Default + ErrorCode> std::error::Error for DeserrError<Format, C> {}
|
||||
impl<Format, C: Default + ErrorCode> ErrorCode for DeserrError<Format, C> {
|
||||
fn error_code(&self) -> Code {
|
||||
self.code
|
||||
}
|
||||
}
|
||||
|
||||
// For now, we don't accumulate errors. Only one deserialisation error is ever returned at a time.
|
||||
impl<Format, C1: Default + ErrorCode, C2: Default + ErrorCode>
|
||||
MergeWithError<DeserrError<Format, C2>> for DeserrError<Format, C1>
|
||||
{
|
||||
fn merge(
|
||||
_self_: Option<Self>,
|
||||
other: DeserrError<Format, C2>,
|
||||
_merge_location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
Err(DeserrError { msg: other.msg, code: other.code, _phantom: PhantomData })
|
||||
}
|
||||
}
|
||||
|
||||
impl<Format, C: Default + ErrorCode> MergeWithError<Infallible> for DeserrError<Format, C> {
|
||||
fn merge(
|
||||
_self_: Option<Self>,
|
||||
_other: Infallible,
|
||||
_merge_location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
// Implement a convenience function to build a `missing_field` error
|
||||
macro_rules! make_missing_field_convenience_builder {
|
||||
($err_code:ident, $fn_name:ident) => {
|
||||
impl DeserrJsonError<$err_code> {
|
||||
pub fn $fn_name(field: &str, location: ValuePointerRef) -> Self {
|
||||
let x = unwrap_any(Self::error::<Infallible>(
|
||||
None,
|
||||
deserr::ErrorKind::MissingField { field },
|
||||
location,
|
||||
));
|
||||
Self { msg: x.msg, code: $err_code.error_code(), _phantom: PhantomData }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
make_missing_field_convenience_builder!(MissingIndexUid, missing_index_uid);
|
||||
make_missing_field_convenience_builder!(MissingApiKeyActions, missing_api_key_actions);
|
||||
make_missing_field_convenience_builder!(MissingApiKeyExpiresAt, missing_api_key_expires_at);
|
||||
make_missing_field_convenience_builder!(MissingApiKeyIndexes, missing_api_key_indexes);
|
||||
make_missing_field_convenience_builder!(MissingSwapIndexes, missing_swap_indexes);
|
||||
|
||||
// Integrate a sub-error into a [`DeserrError`] by taking its error message but using
|
||||
// the default error code (C) from `Self`
|
||||
macro_rules! merge_with_error_impl_take_error_message {
|
||||
($err_type:ty) => {
|
||||
impl<Format, C: Default + ErrorCode> MergeWithError<$err_type> for DeserrError<Format, C>
|
||||
where
|
||||
DeserrError<Format, C>: deserr::DeserializeError,
|
||||
{
|
||||
fn merge(
|
||||
_self_: Option<Self>,
|
||||
other: $err_type,
|
||||
merge_location: ValuePointerRef,
|
||||
) -> Result<Self, Self> {
|
||||
DeserrError::<Format, C>::error::<Infallible>(
|
||||
None,
|
||||
deserr::ErrorKind::Unexpected { msg: other.to_string() },
|
||||
merge_location,
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// All these errors can be merged into a `DeserrError`
|
||||
merge_with_error_impl_take_error_message!(DeserrParseIntError);
|
||||
merge_with_error_impl_take_error_message!(DeserrParseBoolError);
|
||||
merge_with_error_impl_take_error_message!(uuid::Error);
|
||||
merge_with_error_impl_take_error_message!(InvalidTaskDateError);
|
||||
merge_with_error_impl_take_error_message!(ParseOffsetDateTimeError);
|
||||
merge_with_error_impl_take_error_message!(ParseTaskKindError);
|
||||
merge_with_error_impl_take_error_message!(ParseTaskStatusError);
|
||||
merge_with_error_impl_take_error_message!(IndexUidFormatError);
|
115
meilisearch-types/src/deserr/query_params.rs
Normal file
115
meilisearch-types/src/deserr/query_params.rs
Normal file
|
@ -0,0 +1,115 @@
|
|||
/*!
|
||||
This module provides helper traits, types, and functions to deserialize query parameters.
|
||||
|
||||
The source of the problem is that query parameters only give us a string to work with.
|
||||
This means `deserr` is never given a sequence or numbers, and thus the default deserialization
|
||||
code for common types such as `usize` or `Vec<T>` does not work. To work around it, we create a
|
||||
wrapper type called `Param<T>`, which is deserialised using the `from_query_param` method of the trait
|
||||
`FromQueryParameter`.
|
||||
|
||||
We also use other helper types such as `CS` (i.e. comma-separated) from `serde_cs` as well as
|
||||
`StarOr`, `OptionStarOr`, and `OptionStarOrList`.
|
||||
*/
|
||||
|
||||
use std::convert::Infallible;
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
|
||||
use deserr::{DeserializeError, DeserializeFromValue, MergeWithError, ValueKind};
|
||||
|
||||
use super::{DeserrParseBoolError, DeserrParseIntError};
|
||||
use crate::error::unwrap_any;
|
||||
use crate::index_uid::IndexUid;
|
||||
use crate::tasks::{Kind, Status};
|
||||
|
||||
/// A wrapper type indicating that the inner value should be
|
||||
/// deserialised from a query parameter string.
|
||||
///
|
||||
/// Note that if the field is optional, it is better to use
|
||||
/// `Option<Param<T>>` instead of `Param<Option<T>>`.
|
||||
#[derive(Default, Debug, Clone, Copy)]
|
||||
pub struct Param<T>(pub T);
|
||||
|
||||
impl<T> Deref for Param<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> DeserializeFromValue<E> for Param<T>
|
||||
where
|
||||
E: DeserializeError + MergeWithError<T::Err>,
|
||||
T: FromQueryParameter,
|
||||
{
|
||||
fn deserialize_from_value<V: deserr::IntoValue>(
|
||||
value: deserr::Value<V>,
|
||||
location: deserr::ValuePointerRef,
|
||||
) -> Result<Self, E> {
|
||||
match value {
|
||||
deserr::Value::String(s) => match T::from_query_param(&s) {
|
||||
Ok(x) => Ok(Param(x)),
|
||||
Err(e) => Err(unwrap_any(E::merge(None, e, location))),
|
||||
},
|
||||
_ => Err(unwrap_any(E::error(
|
||||
None,
|
||||
deserr::ErrorKind::IncorrectValueKind {
|
||||
actual: value,
|
||||
accepted: &[ValueKind::String],
|
||||
},
|
||||
location,
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a value from a query parameter string.
|
||||
///
|
||||
/// This trait is functionally equivalent to `FromStr`.
|
||||
/// Having a separate trait trait allows us to return better
|
||||
/// deserializatio error messages.
|
||||
pub trait FromQueryParameter: Sized {
|
||||
type Err;
|
||||
fn from_query_param(p: &str) -> Result<Self, Self::Err>;
|
||||
}
|
||||
|
||||
/// Implement `FromQueryParameter` for the given type using its `FromStr`
|
||||
/// trait implementation.
|
||||
macro_rules! impl_from_query_param_from_str {
|
||||
($type:ty) => {
|
||||
impl FromQueryParameter for $type {
|
||||
type Err = <$type as FromStr>::Err;
|
||||
fn from_query_param(p: &str) -> Result<Self, Self::Err> {
|
||||
p.parse()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
impl_from_query_param_from_str!(Kind);
|
||||
impl_from_query_param_from_str!(Status);
|
||||
impl_from_query_param_from_str!(IndexUid);
|
||||
|
||||
/// Implement `FromQueryParameter` for the given type using its `FromStr`
|
||||
/// trait implementation, replacing the returned error with a struct
|
||||
/// that wraps the original query parameter.
|
||||
macro_rules! impl_from_query_param_wrap_original_value_in_error {
|
||||
($type:ty, $err_type:path) => {
|
||||
impl FromQueryParameter for $type {
|
||||
type Err = $err_type;
|
||||
fn from_query_param(p: &str) -> Result<Self, Self::Err> {
|
||||
p.parse().map_err(|_| $err_type(p.to_owned()))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
impl_from_query_param_wrap_original_value_in_error!(usize, DeserrParseIntError);
|
||||
impl_from_query_param_wrap_original_value_in_error!(u32, DeserrParseIntError);
|
||||
impl_from_query_param_wrap_original_value_in_error!(bool, DeserrParseBoolError);
|
||||
|
||||
impl FromQueryParameter for String {
|
||||
type Err = Infallible;
|
||||
fn from_query_param(p: &str) -> Result<Self, Infallible> {
|
||||
Ok(p.to_owned())
|
||||
}
|
||||
}
|
|
@ -1,23 +1,16 @@
|
|||
use std::convert::Infallible;
|
||||
use std::marker::PhantomData;
|
||||
use std::{fmt, io};
|
||||
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{self as aweb, HttpResponseBuilder};
|
||||
use aweb::rt::task::JoinError;
|
||||
use convert_case::Casing;
|
||||
use deserr::{DeserializeError, IntoValue, MergeWithError, ValuePointerRef};
|
||||
use milli::heed::{Error as HeedError, MdbError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use self::deserr_codes::MissingIndexUid;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[cfg_attr(feature = "test-traits", derive(proptest_derive::Arbitrary))]
|
||||
pub struct ResponseError {
|
||||
#[serde(skip)]
|
||||
#[cfg_attr(feature = "test-traits", proptest(strategy = "strategy::status_code_strategy()"))]
|
||||
code: StatusCode,
|
||||
message: String,
|
||||
#[serde(rename = "code")]
|
||||
|
@ -36,7 +29,7 @@ impl ResponseError {
|
|||
Self {
|
||||
code: code.http(),
|
||||
message,
|
||||
error_code: code.err_code().error_name,
|
||||
error_code: code.name(),
|
||||
error_type: code.type_(),
|
||||
error_link: code.url(),
|
||||
}
|
||||
|
@ -97,9 +90,9 @@ pub trait ErrorCode {
|
|||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
enum ErrorType {
|
||||
InternalError,
|
||||
InvalidRequestError,
|
||||
AuthenticationError,
|
||||
Internal,
|
||||
InvalidRequest,
|
||||
Auth,
|
||||
System,
|
||||
}
|
||||
|
||||
|
@ -108,14 +101,24 @@ impl fmt::Display for ErrorType {
|
|||
use ErrorType::*;
|
||||
|
||||
match self {
|
||||
InternalError => write!(f, "internal"),
|
||||
InvalidRequestError => write!(f, "invalid_request"),
|
||||
AuthenticationError => write!(f, "auth"),
|
||||
Internal => write!(f, "internal"),
|
||||
InvalidRequest => write!(f, "invalid_request"),
|
||||
Auth => write!(f, "auth"),
|
||||
System => write!(f, "system"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement all the error codes.
|
||||
///
|
||||
/// 1. Make an enum `Code` where each error code is a variant
|
||||
/// 2. Implement the `http`, `name`, and `type_` method on the enum
|
||||
/// 3. Make a unit type for each error code in the module `deserr_codes`.
|
||||
///
|
||||
/// The unit type's purpose is to be used as a marker type parameter, e.g.
|
||||
/// `DeserrJsonError<MyErrorCode>`. It implements `Default` and `ErrorCode`,
|
||||
/// so we can get a value of the `Code` enum with the correct variant by calling
|
||||
/// `MyErrorCode::default().error_code()`.
|
||||
macro_rules! make_error_codes {
|
||||
($($code_ident:ident, $err_type:ident, $status:ident);*) => {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
@ -123,37 +126,36 @@ macro_rules! make_error_codes {
|
|||
$($code_ident),*
|
||||
}
|
||||
impl Code {
|
||||
/// associate a `Code` variant to the actual ErrCode
|
||||
fn err_code(&self) -> ErrCode {
|
||||
match self {
|
||||
$(
|
||||
Code::$code_ident => {
|
||||
ErrCode::$err_type( stringify!($code_ident).to_case(convert_case::Case::Snake), StatusCode::$status)
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
/// return the HTTP status code associated with the `Code`
|
||||
fn http(&self) -> StatusCode {
|
||||
self.err_code().status_code
|
||||
match self {
|
||||
$(
|
||||
Code::$code_ident => StatusCode::$status
|
||||
),*
|
||||
}
|
||||
}
|
||||
|
||||
/// return error name, used as error code
|
||||
fn name(&self) -> String {
|
||||
self.err_code().error_name.to_string()
|
||||
match self {
|
||||
$(
|
||||
Code::$code_ident => stringify!($code_ident).to_case(convert_case::Case::Snake)
|
||||
),*
|
||||
}
|
||||
}
|
||||
|
||||
/// return the error type
|
||||
fn type_(&self) -> String {
|
||||
self.err_code().error_type.to_string()
|
||||
match self {
|
||||
$(
|
||||
Code::$code_ident => ErrorType::$err_type.to_string()
|
||||
),*
|
||||
}
|
||||
}
|
||||
|
||||
/// return the doc url associated with the error
|
||||
fn url(&self) -> String {
|
||||
format!(
|
||||
"https://docs.meilisearch.com/errors#{}",
|
||||
self.name().to_case(convert_case::Case::Kebab)
|
||||
)
|
||||
format!("https://docs.meilisearch.com/errors#{}", self.name())
|
||||
}
|
||||
}
|
||||
pub mod deserr_codes {
|
||||
|
@ -170,146 +172,120 @@ macro_rules! make_error_codes {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An exhaustive list of all the error codes used by meilisearch.
|
||||
make_error_codes! {
|
||||
ApiKeyAlreadyExists , invalid , CONFLICT ;
|
||||
ApiKeyNotFound , invalid , NOT_FOUND ;
|
||||
BadParameter , invalid , BAD_REQUEST;
|
||||
BadRequest , invalid , BAD_REQUEST;
|
||||
DatabaseSizeLimitReached , internal , INTERNAL_SERVER_ERROR;
|
||||
DocumentNotFound , invalid , NOT_FOUND;
|
||||
DumpAlreadyProcessing , invalid , CONFLICT;
|
||||
DumpNotFound , invalid , NOT_FOUND;
|
||||
DumpProcessFailed , internal , INTERNAL_SERVER_ERROR;
|
||||
DuplicateIndexFound , invalid , BAD_REQUEST;
|
||||
|
||||
ImmutableApiKeyUid , invalid , BAD_REQUEST;
|
||||
ImmutableApiKeyKey , invalid , BAD_REQUEST;
|
||||
ImmutableApiKeyActions , invalid , BAD_REQUEST;
|
||||
ImmutableApiKeyIndexes , invalid , BAD_REQUEST;
|
||||
ImmutableApiKeyExpiresAt , invalid , BAD_REQUEST;
|
||||
ImmutableApiKeyCreatedAt , invalid , BAD_REQUEST;
|
||||
ImmutableApiKeyUpdatedAt , invalid , BAD_REQUEST;
|
||||
|
||||
ImmutableIndexUid , invalid , BAD_REQUEST;
|
||||
ImmutableIndexCreatedAt , invalid , BAD_REQUEST;
|
||||
ImmutableIndexUpdatedAt , invalid , BAD_REQUEST;
|
||||
|
||||
IndexAlreadyExists , invalid , CONFLICT ;
|
||||
IndexCreationFailed , internal , INTERNAL_SERVER_ERROR;
|
||||
IndexNotFound , invalid , NOT_FOUND;
|
||||
IndexPrimaryKeyAlreadyExists , invalid , BAD_REQUEST ;
|
||||
IndexPrimaryKeyNoCandidateFound , invalid , BAD_REQUEST ;
|
||||
IndexPrimaryKeyMultipleCandidatesFound, invalid , BAD_REQUEST;
|
||||
Internal , internal , INTERNAL_SERVER_ERROR ;
|
||||
InvalidApiKeyActions , invalid , BAD_REQUEST ;
|
||||
InvalidApiKeyDescription , invalid , BAD_REQUEST ;
|
||||
InvalidApiKeyExpiresAt , invalid , BAD_REQUEST ;
|
||||
InvalidApiKeyIndexes , invalid , BAD_REQUEST ;
|
||||
InvalidApiKeyLimit , invalid , BAD_REQUEST ;
|
||||
InvalidApiKeyName , invalid , BAD_REQUEST ;
|
||||
InvalidApiKeyOffset , invalid , BAD_REQUEST ;
|
||||
InvalidApiKeyUid , invalid , BAD_REQUEST ;
|
||||
InvalidApiKey , authentication, FORBIDDEN ;
|
||||
InvalidContentType , invalid , UNSUPPORTED_MEDIA_TYPE ;
|
||||
InvalidDocumentFields , invalid , BAD_REQUEST ;
|
||||
InvalidDocumentGeoField , invalid , BAD_REQUEST ;
|
||||
InvalidDocumentId , invalid , BAD_REQUEST ;
|
||||
InvalidDocumentLimit , invalid , BAD_REQUEST ;
|
||||
InvalidDocumentOffset , invalid , BAD_REQUEST ;
|
||||
InvalidIndexLimit , invalid , BAD_REQUEST ;
|
||||
InvalidIndexOffset , invalid , BAD_REQUEST ;
|
||||
InvalidIndexPrimaryKey , invalid , BAD_REQUEST ;
|
||||
InvalidIndexUid , invalid , BAD_REQUEST ;
|
||||
InvalidMinWordLengthForTypo , invalid , BAD_REQUEST ;
|
||||
InvalidSearchAttributesToCrop , invalid , BAD_REQUEST ;
|
||||
InvalidSearchAttributesToHighlight , invalid , BAD_REQUEST ;
|
||||
InvalidSearchAttributesToRetrieve , invalid , BAD_REQUEST ;
|
||||
InvalidSearchCropLength , invalid , BAD_REQUEST ;
|
||||
InvalidSearchCropMarker , invalid , BAD_REQUEST ;
|
||||
InvalidSearchFacets , invalid , BAD_REQUEST ;
|
||||
InvalidSearchFilter , invalid , BAD_REQUEST ;
|
||||
InvalidSearchHighlightPostTag , invalid , BAD_REQUEST ;
|
||||
InvalidSearchHighlightPreTag , invalid , BAD_REQUEST ;
|
||||
InvalidSearchHitsPerPage , invalid , BAD_REQUEST ;
|
||||
InvalidSearchLimit , invalid , BAD_REQUEST ;
|
||||
InvalidSearchMatchingStrategy , invalid , BAD_REQUEST ;
|
||||
InvalidSearchOffset , invalid , BAD_REQUEST ;
|
||||
InvalidSearchPage , invalid , BAD_REQUEST ;
|
||||
InvalidSearchQ , invalid , BAD_REQUEST ;
|
||||
InvalidSearchShowMatchesPosition , invalid , BAD_REQUEST ;
|
||||
InvalidSearchSort , invalid , BAD_REQUEST ;
|
||||
InvalidSettingsDisplayedAttributes , invalid , BAD_REQUEST ;
|
||||
InvalidSettingsDistinctAttribute , invalid , BAD_REQUEST ;
|
||||
InvalidSettingsFaceting , invalid , BAD_REQUEST ;
|
||||
InvalidSettingsFilterableAttributes , invalid , BAD_REQUEST ;
|
||||
InvalidSettingsPagination , invalid , BAD_REQUEST ;
|
||||
InvalidSettingsRankingRules , invalid , BAD_REQUEST ;
|
||||
InvalidSettingsSearchableAttributes , invalid , BAD_REQUEST ;
|
||||
InvalidSettingsSortableAttributes , invalid , BAD_REQUEST ;
|
||||
InvalidSettingsStopWords , invalid , BAD_REQUEST ;
|
||||
InvalidSettingsSynonyms , invalid , BAD_REQUEST ;
|
||||
InvalidSettingsTypoTolerance , invalid , BAD_REQUEST ;
|
||||
InvalidState , internal , INTERNAL_SERVER_ERROR ;
|
||||
InvalidStoreFile , internal , INTERNAL_SERVER_ERROR ;
|
||||
InvalidSwapDuplicateIndexFound , invalid , BAD_REQUEST ;
|
||||
InvalidSwapIndexes , invalid , BAD_REQUEST ;
|
||||
InvalidTaskAfterEnqueuedAt , invalid , BAD_REQUEST ;
|
||||
InvalidTaskAfterFinishedAt , invalid , BAD_REQUEST ;
|
||||
InvalidTaskAfterStartedAt , invalid , BAD_REQUEST ;
|
||||
InvalidTaskBeforeEnqueuedAt , invalid , BAD_REQUEST ;
|
||||
InvalidTaskBeforeFinishedAt , invalid , BAD_REQUEST ;
|
||||
InvalidTaskBeforeStartedAt , invalid , BAD_REQUEST ;
|
||||
InvalidTaskCanceledBy , invalid , BAD_REQUEST ;
|
||||
InvalidTaskFrom , invalid , BAD_REQUEST ;
|
||||
InvalidTaskLimit , invalid , BAD_REQUEST ;
|
||||
InvalidTaskStatuses , invalid , BAD_REQUEST ;
|
||||
InvalidTaskTypes , invalid , BAD_REQUEST ;
|
||||
InvalidTaskUids , invalid , BAD_REQUEST ;
|
||||
IoError , system , UNPROCESSABLE_ENTITY;
|
||||
MalformedPayload , invalid , BAD_REQUEST ;
|
||||
MaxFieldsLimitExceeded , invalid , BAD_REQUEST ;
|
||||
MissingApiKeyActions , invalid , BAD_REQUEST ;
|
||||
MissingApiKeyExpiresAt , invalid , BAD_REQUEST ;
|
||||
MissingApiKeyIndexes , invalid , BAD_REQUEST ;
|
||||
MissingAuthorizationHeader , authentication, UNAUTHORIZED ;
|
||||
MissingContentType , invalid , UNSUPPORTED_MEDIA_TYPE ;
|
||||
MissingDocumentId , invalid , BAD_REQUEST ;
|
||||
MissingIndexUid , invalid , BAD_REQUEST ;
|
||||
MissingMasterKey , authentication, UNAUTHORIZED ;
|
||||
MissingPayload , invalid , BAD_REQUEST ;
|
||||
MissingTaskFilters , invalid , BAD_REQUEST ;
|
||||
NoSpaceLeftOnDevice , system , UNPROCESSABLE_ENTITY;
|
||||
PayloadTooLarge , invalid , PAYLOAD_TOO_LARGE ;
|
||||
TaskNotFound , invalid , NOT_FOUND ;
|
||||
TooManyOpenFiles , system , UNPROCESSABLE_ENTITY ;
|
||||
UnretrievableDocument , internal , BAD_REQUEST ;
|
||||
UnretrievableErrorCode , invalid , BAD_REQUEST ;
|
||||
UnsupportedMediaType , invalid , UNSUPPORTED_MEDIA_TYPE
|
||||
}
|
||||
|
||||
/// Internal structure providing a convenient way to create error codes
|
||||
struct ErrCode {
|
||||
status_code: StatusCode,
|
||||
error_type: ErrorType,
|
||||
error_name: String,
|
||||
}
|
||||
|
||||
impl ErrCode {
|
||||
fn authentication(error_name: String, status_code: StatusCode) -> ErrCode {
|
||||
ErrCode { status_code, error_name, error_type: ErrorType::AuthenticationError }
|
||||
}
|
||||
|
||||
fn internal(error_name: String, status_code: StatusCode) -> ErrCode {
|
||||
ErrCode { status_code, error_name, error_type: ErrorType::InternalError }
|
||||
}
|
||||
|
||||
fn invalid(error_name: String, status_code: StatusCode) -> ErrCode {
|
||||
ErrCode { status_code, error_name, error_type: ErrorType::InvalidRequestError }
|
||||
}
|
||||
|
||||
fn system(error_name: String, status_code: StatusCode) -> ErrCode {
|
||||
ErrCode { status_code, error_name, error_type: ErrorType::System }
|
||||
}
|
||||
ApiKeyAlreadyExists , InvalidRequest , CONFLICT ;
|
||||
ApiKeyNotFound , InvalidRequest , NOT_FOUND ;
|
||||
BadParameter , InvalidRequest , BAD_REQUEST;
|
||||
BadRequest , InvalidRequest , BAD_REQUEST;
|
||||
DatabaseSizeLimitReached , Internal , INTERNAL_SERVER_ERROR;
|
||||
DocumentNotFound , InvalidRequest , NOT_FOUND;
|
||||
DumpAlreadyProcessing , InvalidRequest , CONFLICT;
|
||||
DumpNotFound , InvalidRequest , NOT_FOUND;
|
||||
DumpProcessFailed , Internal , INTERNAL_SERVER_ERROR;
|
||||
DuplicateIndexFound , InvalidRequest , BAD_REQUEST;
|
||||
ImmutableApiKeyActions , InvalidRequest , BAD_REQUEST;
|
||||
ImmutableApiKeyCreatedAt , InvalidRequest , BAD_REQUEST;
|
||||
ImmutableApiKeyExpiresAt , InvalidRequest , BAD_REQUEST;
|
||||
ImmutableApiKeyIndexes , InvalidRequest , BAD_REQUEST;
|
||||
ImmutableApiKeyKey , InvalidRequest , BAD_REQUEST;
|
||||
ImmutableApiKeyUid , InvalidRequest , BAD_REQUEST;
|
||||
ImmutableApiKeyUpdatedAt , InvalidRequest , BAD_REQUEST;
|
||||
ImmutableIndexCreatedAt , InvalidRequest , BAD_REQUEST;
|
||||
ImmutableIndexUid , InvalidRequest , BAD_REQUEST;
|
||||
ImmutableIndexUpdatedAt , InvalidRequest , BAD_REQUEST;
|
||||
IndexAlreadyExists , InvalidRequest , CONFLICT ;
|
||||
IndexCreationFailed , Internal , INTERNAL_SERVER_ERROR;
|
||||
IndexNotFound , InvalidRequest , NOT_FOUND;
|
||||
IndexPrimaryKeyAlreadyExists , InvalidRequest , BAD_REQUEST ;
|
||||
IndexPrimaryKeyMultipleCandidatesFound, InvalidRequest , BAD_REQUEST;
|
||||
IndexPrimaryKeyNoCandidateFound , InvalidRequest , BAD_REQUEST ;
|
||||
Internal , Internal , INTERNAL_SERVER_ERROR ;
|
||||
InvalidApiKey , Auth , FORBIDDEN ;
|
||||
InvalidApiKeyActions , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidApiKeyDescription , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidApiKeyExpiresAt , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidApiKeyIndexes , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidApiKeyLimit , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidApiKeyName , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidApiKeyOffset , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidApiKeyUid , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidContentType , InvalidRequest , UNSUPPORTED_MEDIA_TYPE ;
|
||||
InvalidDocumentFields , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidDocumentGeoField , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidDocumentId , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidDocumentLimit , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidDocumentOffset , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidIndexLimit , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidIndexOffset , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidIndexPrimaryKey , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidIndexUid , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSearchAttributesToCrop , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSearchAttributesToHighlight , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSearchAttributesToRetrieve , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSearchCropLength , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSearchCropMarker , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSearchFacets , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSearchFilter , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSearchHighlightPostTag , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSearchHighlightPreTag , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSearchHitsPerPage , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSearchLimit , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSearchMatchingStrategy , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSearchOffset , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSearchPage , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSearchQ , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSearchShowMatchesPosition , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSearchSort , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSettingsDisplayedAttributes , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSettingsDistinctAttribute , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSettingsFaceting , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSettingsFilterableAttributes , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSettingsPagination , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSettingsRankingRules , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSettingsSearchableAttributes , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSettingsSortableAttributes , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSettingsStopWords , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSettingsSynonyms , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSettingsTypoTolerance , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidState , Internal , INTERNAL_SERVER_ERROR ;
|
||||
InvalidStoreFile , Internal , INTERNAL_SERVER_ERROR ;
|
||||
InvalidSwapDuplicateIndexFound , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidSwapIndexes , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidTaskAfterEnqueuedAt , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidTaskAfterFinishedAt , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidTaskAfterStartedAt , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidTaskBeforeEnqueuedAt , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidTaskBeforeFinishedAt , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidTaskBeforeStartedAt , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidTaskCanceledBy , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidTaskFrom , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidTaskLimit , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidTaskStatuses , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidTaskTypes , InvalidRequest , BAD_REQUEST ;
|
||||
InvalidTaskUids , InvalidRequest , BAD_REQUEST ;
|
||||
IoError , System , UNPROCESSABLE_ENTITY;
|
||||
MalformedPayload , InvalidRequest , BAD_REQUEST ;
|
||||
MaxFieldsLimitExceeded , InvalidRequest , BAD_REQUEST ;
|
||||
MissingApiKeyActions , InvalidRequest , BAD_REQUEST ;
|
||||
MissingApiKeyExpiresAt , InvalidRequest , BAD_REQUEST ;
|
||||
MissingApiKeyIndexes , InvalidRequest , BAD_REQUEST ;
|
||||
MissingAuthorizationHeader , Auth , UNAUTHORIZED ;
|
||||
MissingContentType , InvalidRequest , UNSUPPORTED_MEDIA_TYPE ;
|
||||
MissingDocumentId , InvalidRequest , BAD_REQUEST ;
|
||||
MissingIndexUid , InvalidRequest , BAD_REQUEST ;
|
||||
MissingMasterKey , Auth , UNAUTHORIZED ;
|
||||
MissingPayload , InvalidRequest , BAD_REQUEST ;
|
||||
MissingSwapIndexes , InvalidRequest , BAD_REQUEST ;
|
||||
MissingTaskFilters , InvalidRequest , BAD_REQUEST ;
|
||||
NoSpaceLeftOnDevice , System , UNPROCESSABLE_ENTITY;
|
||||
PayloadTooLarge , InvalidRequest , PAYLOAD_TOO_LARGE ;
|
||||
TaskNotFound , InvalidRequest , NOT_FOUND ;
|
||||
TooManyOpenFiles , System , UNPROCESSABLE_ENTITY ;
|
||||
UnretrievableDocument , Internal , BAD_REQUEST ;
|
||||
UnretrievableErrorCode , InvalidRequest , BAD_REQUEST ;
|
||||
UnsupportedMediaType , InvalidRequest , UNSUPPORTED_MEDIA_TYPE
|
||||
}
|
||||
|
||||
impl ErrorCode for JoinError {
|
||||
|
@ -348,13 +324,13 @@ impl ErrorCode for milli::Error {
|
|||
}
|
||||
UserError::PrimaryKeyCannotBeChanged(_) => Code::IndexPrimaryKeyAlreadyExists,
|
||||
UserError::SortRankingRuleMissing => Code::InvalidSearchSort,
|
||||
UserError::InvalidFacetsDistribution { .. } => Code::BadRequest,
|
||||
UserError::InvalidFacetsDistribution { .. } => Code::InvalidSearchFacets,
|
||||
UserError::InvalidSortableAttribute { .. } => Code::InvalidSearchSort,
|
||||
UserError::CriterionError(_) => Code::InvalidSettingsRankingRules,
|
||||
UserError::InvalidGeoField { .. } => Code::InvalidDocumentGeoField,
|
||||
UserError::SortError(_) => Code::InvalidSearchSort,
|
||||
UserError::InvalidMinTypoWordLenSetting(_, _) => {
|
||||
Code::InvalidMinWordLengthForTypo
|
||||
Code::InvalidSettingsTypoTolerance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -367,6 +343,7 @@ impl ErrorCode for file_store::Error {
|
|||
match self {
|
||||
Self::IoError(e) => e.error_code(),
|
||||
Self::PersistError(e) => e.error_code(),
|
||||
Self::CouldNotParseFileNameAsUtf8 | Self::UuidError(_) => Code::Internal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -404,6 +381,7 @@ impl ErrorCode for io::Error {
|
|||
}
|
||||
}
|
||||
|
||||
/// Unwrap a result, either its Ok or Err value.
|
||||
pub fn unwrap_any<T>(any: Result<T, T>) -> T {
|
||||
match any {
|
||||
Ok(any) => any,
|
||||
|
@ -411,90 +389,41 @@ pub fn unwrap_any<T>(any: Result<T, T>) -> T {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-traits")]
|
||||
mod strategy {
|
||||
use proptest::strategy::Strategy;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub(super) fn status_code_strategy() -> impl Strategy<Value = StatusCode> {
|
||||
(100..999u16).prop_map(|i| StatusCode::from_u16(i).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DeserrError<C: ErrorCode = deserr_codes::BadRequest> {
|
||||
pub msg: String,
|
||||
pub code: Code,
|
||||
_phantom: PhantomData<C>,
|
||||
}
|
||||
impl<C: ErrorCode> std::fmt::Debug for DeserrError<C> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("DeserrError").field("msg", &self.msg).field("code", &self.code).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: ErrorCode> std::fmt::Display for DeserrError<C> {
|
||||
/// Deserialization when `deserr` cannot parse an API key date.
|
||||
#[derive(Debug)]
|
||||
pub struct ParseOffsetDateTimeError(pub String);
|
||||
impl fmt::Display for ParseOffsetDateTimeError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.msg)
|
||||
writeln!(f, "`{original}` is not a valid date. It should follow the RFC 3339 format to represents a date or datetime in the future or specified as a null value. e.g. 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'.", original = self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: ErrorCode> std::error::Error for DeserrError<C> {}
|
||||
impl<C: ErrorCode> ErrorCode for DeserrError<C> {
|
||||
fn error_code(&self) -> Code {
|
||||
self.code
|
||||
/// Deserialization when `deserr` cannot parse a task date.
|
||||
#[derive(Debug)]
|
||||
pub struct InvalidTaskDateError(pub String);
|
||||
impl std::fmt::Display for InvalidTaskDateError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "`{}` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<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 })
|
||||
/// Deserialization error when `deserr` cannot parse a String
|
||||
/// into a bool.
|
||||
#[derive(Debug)]
|
||||
pub struct DeserrParseBoolError(pub String);
|
||||
impl fmt::Display for DeserrParseBoolError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "could not parse `{}` as a boolean, expected either `true` or `false`", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
/// Deserialization error when `deserr` cannot parse a String
|
||||
/// into an integer.
|
||||
#[derive(Debug)]
|
||||
pub struct DeserrParseIntError(pub String);
|
||||
impl fmt::Display for DeserrParseIntError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "could not parse `{}` as a positive integer", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,17 +2,15 @@ use std::error::Error;
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use deserr::DeserializeFromValue;
|
||||
|
||||
use crate::error::{Code, ErrorCode};
|
||||
|
||||
/// An index uid is composed of only ascii alphanumeric characters, - and _, between 1 and 400
|
||||
/// bytes long
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "test-traits", derive(proptest_derive::Arbitrary))]
|
||||
pub struct IndexUid(
|
||||
#[cfg_attr(feature = "test-traits", proptest(regex("[a-zA-Z0-9_-]{1,400}")))] String,
|
||||
);
|
||||
#[derive(Debug, Clone, PartialEq, Eq, DeserializeFromValue)]
|
||||
#[deserr(from(String) = IndexUid::try_from -> IndexUidFormatError)]
|
||||
pub struct IndexUid(String);
|
||||
|
||||
impl IndexUid {
|
||||
pub fn new_unchecked(s: impl AsRef<str>) -> Self {
|
||||
|
@ -29,6 +27,12 @@ impl IndexUid {
|
|||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for IndexUid {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for IndexUid {
|
||||
type Target = str;
|
||||
|
||||
|
|
|
@ -1,22 +1,25 @@
|
|||
use std::convert::Infallible;
|
||||
use std::fmt::Display;
|
||||
use std::hash::Hash;
|
||||
use std::str::FromStr;
|
||||
|
||||
use deserr::{DeserializeError, DeserializeFromValue, MergeWithError, ValuePointerRef};
|
||||
use enum_iterator::Sequence;
|
||||
use milli::update::Setting;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
use time::macros::{format_description, time};
|
||||
use time::{Date, OffsetDateTime, PrimitiveDateTime};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::deserr::error_messages::immutable_field_error;
|
||||
use crate::deserr::{DeserrError, DeserrJsonError};
|
||||
use crate::error::deserr_codes::*;
|
||||
use crate::error::{unwrap_any, Code, DeserrError, ErrorCode, TakeErrorMessage};
|
||||
use crate::error::{unwrap_any, Code, ErrorCode, ParseOffsetDateTimeError};
|
||||
use crate::index_uid_pattern::{IndexUidPattern, IndexUidPatternFormatError};
|
||||
|
||||
pub type KeyId = Uuid;
|
||||
|
||||
impl<C: Default + ErrorCode> MergeWithError<IndexUidPatternFormatError> for DeserrError<C> {
|
||||
impl<C: Default + ErrorCode> MergeWithError<IndexUidPatternFormatError> for DeserrJsonError<C> {
|
||||
fn merge(
|
||||
_self_: Option<Self>,
|
||||
other: IndexUidPatternFormatError,
|
||||
|
@ -30,24 +33,20 @@ impl<C: Default + ErrorCode> MergeWithError<IndexUidPatternFormatError> for Dese
|
|||
}
|
||||
}
|
||||
|
||||
fn parse_uuid_from_str(s: &str) -> Result<Uuid, TakeErrorMessage<uuid::Error>> {
|
||||
Uuid::parse_str(s).map_err(TakeErrorMessage)
|
||||
}
|
||||
|
||||
#[derive(Debug, DeserializeFromValue)]
|
||||
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)]
|
||||
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
|
||||
pub struct CreateApiKey {
|
||||
#[deserr(error = DeserrError<InvalidApiKeyDescription>)]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidApiKeyDescription>)]
|
||||
pub description: Option<String>,
|
||||
#[deserr(error = DeserrError<InvalidApiKeyName>)]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidApiKeyName>)]
|
||||
pub name: Option<String>,
|
||||
#[deserr(default = Uuid::new_v4(), error = DeserrError<InvalidApiKeyUid>, from(&String) = parse_uuid_from_str -> TakeErrorMessage<uuid::Error>)]
|
||||
#[deserr(default = Uuid::new_v4(), error = DeserrJsonError<InvalidApiKeyUid>, from(&String) = Uuid::from_str -> uuid::Error)]
|
||||
pub uid: KeyId,
|
||||
#[deserr(error = DeserrError<InvalidApiKeyActions>)]
|
||||
#[deserr(error = DeserrJsonError<InvalidApiKeyActions>, missing_field_error = DeserrJsonError::missing_api_key_actions)]
|
||||
pub actions: Vec<Action>,
|
||||
#[deserr(error = DeserrError<InvalidApiKeyIndexes>)]
|
||||
#[deserr(error = DeserrJsonError<InvalidApiKeyIndexes>, missing_field_error = DeserrJsonError::missing_api_key_indexes)]
|
||||
pub indexes: Vec<IndexUidPattern>,
|
||||
#[deserr(error = DeserrError<InvalidApiKeyExpiresAt>, default = None, from(&String) = parse_expiration_date -> TakeErrorMessage<ParseOffsetDateTimeError>)]
|
||||
#[deserr(error = DeserrJsonError<InvalidApiKeyExpiresAt>, from(Option<String>) = parse_expiration_date -> ParseOffsetDateTimeError, missing_field_error = DeserrJsonError::missing_api_key_expires_at)]
|
||||
pub expires_at: Option<OffsetDateTime>,
|
||||
}
|
||||
|
||||
|
@ -72,32 +71,29 @@ 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::ImmutableApiKeyUid,
|
||||
"actions" => Code::ImmutableApiKeyActions,
|
||||
"indexes" => Code::ImmutableApiKeyIndexes,
|
||||
"expiresAt" => Code::ImmutableApiKeyExpiresAt,
|
||||
"createdAt" => Code::ImmutableApiKeyCreatedAt,
|
||||
"updatedAt" => Code::ImmutableApiKeyUpdatedAt,
|
||||
_ => Code::BadRequest,
|
||||
};
|
||||
error
|
||||
) -> DeserrJsonError {
|
||||
match field {
|
||||
"uid" => immutable_field_error(field, accepted, Code::ImmutableApiKeyUid),
|
||||
"actions" => immutable_field_error(field, accepted, Code::ImmutableApiKeyActions),
|
||||
"indexes" => immutable_field_error(field, accepted, Code::ImmutableApiKeyIndexes),
|
||||
"expiresAt" => immutable_field_error(field, accepted, Code::ImmutableApiKeyExpiresAt),
|
||||
"createdAt" => immutable_field_error(field, accepted, Code::ImmutableApiKeyCreatedAt),
|
||||
"updatedAt" => immutable_field_error(field, accepted, Code::ImmutableApiKeyUpdatedAt),
|
||||
_ => unwrap_any(DeserrJsonError::<BadRequest>::error::<Infallible>(
|
||||
None,
|
||||
deserr::ErrorKind::UnknownKey { key: field, accepted },
|
||||
location,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, DeserializeFromValue)]
|
||||
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_api_key)]
|
||||
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_api_key)]
|
||||
pub struct PatchApiKey {
|
||||
#[deserr(error = DeserrError<InvalidApiKeyDescription>)]
|
||||
pub description: Option<String>,
|
||||
#[deserr(error = DeserrError<InvalidApiKeyName>)]
|
||||
pub name: Option<String>,
|
||||
#[deserr(default, error = DeserrJsonError<InvalidApiKeyDescription>)]
|
||||
pub description: Setting<String>,
|
||||
#[deserr(default, error = DeserrJsonError<InvalidApiKeyName>)]
|
||||
pub name: Setting<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
|
@ -149,46 +145,40 @@ impl Key {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParseOffsetDateTimeError(String);
|
||||
impl Display for ParseOffsetDateTimeError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
writeln!(f, "`{original}` is not a valid date. It should follow the RFC 3339 format to represents a date or datetime in the future or specified as a null value. e.g. 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'.", original = self.0)
|
||||
}
|
||||
}
|
||||
impl std::error::Error for ParseOffsetDateTimeError {}
|
||||
|
||||
fn parse_expiration_date(
|
||||
string: &str,
|
||||
) -> std::result::Result<Option<OffsetDateTime>, TakeErrorMessage<ParseOffsetDateTimeError>> {
|
||||
let datetime = if let Ok(datetime) = OffsetDateTime::parse(string, &Rfc3339) {
|
||||
string: Option<String>,
|
||||
) -> std::result::Result<Option<OffsetDateTime>, ParseOffsetDateTimeError> {
|
||||
let Some(string) = string else {
|
||||
return Ok(None)
|
||||
};
|
||||
let datetime = if let Ok(datetime) = OffsetDateTime::parse(&string, &Rfc3339) {
|
||||
datetime
|
||||
} else if let Ok(primitive_datetime) = PrimitiveDateTime::parse(
|
||||
string,
|
||||
&string,
|
||||
format_description!(
|
||||
"[year repr:full base:calendar]-[month repr:numerical]-[day]T[hour]:[minute]:[second]"
|
||||
),
|
||||
) {
|
||||
primitive_datetime.assume_utc()
|
||||
} else if let Ok(primitive_datetime) = PrimitiveDateTime::parse(
|
||||
string,
|
||||
&string,
|
||||
format_description!(
|
||||
"[year repr:full base:calendar]-[month repr:numerical]-[day] [hour]:[minute]:[second]"
|
||||
),
|
||||
) {
|
||||
primitive_datetime.assume_utc()
|
||||
} else if let Ok(date) = Date::parse(
|
||||
string,
|
||||
&string,
|
||||
format_description!("[year repr:full base:calendar]-[month repr:numerical]-[day]"),
|
||||
) {
|
||||
PrimitiveDateTime::new(date, time!(00:00)).assume_utc()
|
||||
} else {
|
||||
return Err(TakeErrorMessage(ParseOffsetDateTimeError(string.to_owned())));
|
||||
return Err(ParseOffsetDateTimeError(string));
|
||||
};
|
||||
if datetime > OffsetDateTime::now_utc() {
|
||||
Ok(Some(datetime))
|
||||
} else {
|
||||
Err(TakeErrorMessage(ParseOffsetDateTimeError(string.to_owned())))
|
||||
Err(ParseOffsetDateTimeError(string))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
pub mod compression;
|
||||
pub mod deserr;
|
||||
pub mod document_formats;
|
||||
pub mod error;
|
||||
pub mod index_uid;
|
||||
|
@ -8,11 +9,10 @@ pub mod settings;
|
|||
pub mod star_or;
|
||||
pub mod tasks;
|
||||
pub mod versioning;
|
||||
|
||||
pub use milli;
|
||||
pub use milli::{heed, Index};
|
||||
use uuid::Uuid;
|
||||
pub use versioning::VERSION_FILE_NAME;
|
||||
pub use {milli, serde_cs};
|
||||
|
||||
pub type Document = serde_json::Map<String, serde_json::Value>;
|
||||
pub type InstanceUid = Uuid;
|
||||
|
|
|
@ -11,8 +11,9 @@ use milli::update::Setting;
|
|||
use milli::{Criterion, CriterionError, Index, DEFAULT_VALUES_PER_FACET};
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
|
||||
use crate::deserr::DeserrJsonError;
|
||||
use crate::error::deserr_codes::*;
|
||||
use crate::error::{unwrap_any, DeserrError};
|
||||
use crate::error::unwrap_any;
|
||||
|
||||
/// The maximimum number of results that the engine
|
||||
/// will be able to return in one search call.
|
||||
|
@ -66,26 +67,31 @@ fn validate_min_word_size_for_typo_setting<E: DeserializeError>(
|
|||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
#[deserr(deny_unknown_fields, rename_all = camelCase, validate = validate_min_word_size_for_typo_setting -> DeserrError<InvalidMinWordLengthForTypo>)]
|
||||
#[deserr(deny_unknown_fields, rename_all = camelCase, validate = validate_min_word_size_for_typo_setting -> DeserrJsonError<InvalidSettingsTypoTolerance>)]
|
||||
pub struct MinWordSizeTyposSetting {
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
pub one_typo: Setting<u8>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
pub two_typos: Setting<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, DeserializeFromValue)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
#[deserr(deny_unknown_fields, rename_all = camelCase, where_predicate = __Deserr_E: deserr::MergeWithError<DeserrError<InvalidMinWordLengthForTypo>>)]
|
||||
#[deserr(deny_unknown_fields, rename_all = camelCase, where_predicate = __Deserr_E: deserr::MergeWithError<DeserrJsonError<InvalidSettingsTypoTolerance>>)]
|
||||
pub struct TypoSettings {
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
pub enabled: Setting<bool>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(error = DeserrError<InvalidMinWordLengthForTypo>)]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsTypoTolerance>)]
|
||||
pub min_word_size_for_typos: Setting<MinWordSizeTyposSetting>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
pub disable_on_words: Setting<BTreeSet<String>>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
pub disable_on_attributes: Setting<BTreeSet<String>>,
|
||||
}
|
||||
|
||||
|
@ -94,6 +100,7 @@ pub struct TypoSettings {
|
|||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
||||
pub struct FacetingSettings {
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
pub max_values_per_facet: Setting<usize>,
|
||||
}
|
||||
|
||||
|
@ -102,10 +109,11 @@ pub struct FacetingSettings {
|
|||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
||||
pub struct PaginationSettings {
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
pub max_total_hits: Setting<usize>,
|
||||
}
|
||||
|
||||
impl MergeWithError<milli::CriterionError> for DeserrError<InvalidSettingsRankingRules> {
|
||||
impl MergeWithError<milli::CriterionError> for DeserrJsonError<InvalidSettingsRankingRules> {
|
||||
fn merge(
|
||||
_self_: Option<Self>,
|
||||
other: milli::CriterionError,
|
||||
|
@ -128,14 +136,14 @@ impl MergeWithError<milli::CriterionError> for DeserrError<InvalidSettingsRankin
|
|||
rename_all = "camelCase",
|
||||
bound(serialize = "T: Serialize", deserialize = "T: Deserialize<'static>")
|
||||
)]
|
||||
#[deserr(error = DeserrError, rename_all = camelCase, deny_unknown_fields)]
|
||||
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
|
||||
pub struct Settings<T> {
|
||||
#[serde(
|
||||
default,
|
||||
serialize_with = "serialize_with_wildcard",
|
||||
skip_serializing_if = "Setting::is_not_set"
|
||||
)]
|
||||
#[deserr(error = DeserrError<InvalidSettingsDisplayedAttributes>)]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsDisplayedAttributes>)]
|
||||
pub displayed_attributes: Setting<Vec<String>>,
|
||||
|
||||
#[serde(
|
||||
|
@ -143,35 +151,35 @@ pub struct Settings<T> {
|
|||
serialize_with = "serialize_with_wildcard",
|
||||
skip_serializing_if = "Setting::is_not_set"
|
||||
)]
|
||||
#[deserr(error = DeserrError<InvalidSettingsSearchableAttributes>)]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsSearchableAttributes>)]
|
||||
pub searchable_attributes: Setting<Vec<String>>,
|
||||
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(error = DeserrError<InvalidSettingsFilterableAttributes>)]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsFilterableAttributes>)]
|
||||
pub filterable_attributes: Setting<BTreeSet<String>>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(error = DeserrError<InvalidSettingsSortableAttributes>)]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsSortableAttributes>)]
|
||||
pub sortable_attributes: Setting<BTreeSet<String>>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(error = DeserrError<InvalidSettingsRankingRules>)]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsRankingRules>)]
|
||||
pub ranking_rules: Setting<Vec<RankingRuleView>>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(error = DeserrError<InvalidSettingsStopWords>)]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsStopWords>)]
|
||||
pub stop_words: Setting<BTreeSet<String>>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(error = DeserrError<InvalidSettingsSynonyms>)]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsSynonyms>)]
|
||||
pub synonyms: Setting<BTreeMap<String, Vec<String>>>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(error = DeserrError<InvalidSettingsDistinctAttribute>)]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsDistinctAttribute>)]
|
||||
pub distinct_attribute: Setting<String>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(error = DeserrError<InvalidSettingsTypoTolerance>)]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsTypoTolerance>)]
|
||||
pub typo_tolerance: Setting<TypoSettings>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(error = DeserrError<InvalidSettingsFaceting>)]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsFaceting>)]
|
||||
pub faceting: Setting<FacetingSettings>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(error = DeserrError<InvalidSettingsPagination>)]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsPagination>)]
|
||||
pub pagination: Setting<PaginationSettings>,
|
||||
|
||||
#[serde(skip)]
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
use std::fmt::{Display, Formatter};
|
||||
use std::fmt;
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
|
||||
use deserr::{DeserializeError, DeserializeFromValue, MergeWithError, ValueKind};
|
||||
use serde::de::Visitor;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
use crate::deserr::query_params::FromQueryParameter;
|
||||
use crate::error::unwrap_any;
|
||||
|
||||
/// A type that tries to match either a star (*) or
|
||||
|
@ -17,35 +17,6 @@ pub enum StarOr<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> {
|
||||
type Err = T::Err;
|
||||
|
||||
|
@ -57,23 +28,11 @@ impl<T: FromStr> FromStr for StarOr<T> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Deref<Target = str>> Deref for StarOr<T> {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
impl<T: fmt::Display> fmt::Display for StarOr<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Star => "*",
|
||||
Self::Other(t) => t.deref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<String>> From<StarOr<T>> for String {
|
||||
fn from(s: StarOr<T>) -> Self {
|
||||
match s {
|
||||
StarOr::Star => "*".to_string(),
|
||||
StarOr::Other(t) => t.into(),
|
||||
StarOr::Star => write!(f, "*"),
|
||||
StarOr::Other(x) => fmt::Display::fmt(x, f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -93,7 +52,7 @@ impl<T: PartialEq + Eq> Eq for StarOr<T> {}
|
|||
impl<'de, T, E> Deserialize<'de> for StarOr<T>
|
||||
where
|
||||
T: FromStr<Err = E>,
|
||||
E: Display,
|
||||
E: fmt::Display,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
|
@ -109,11 +68,11 @@ where
|
|||
impl<'de, T, FE> Visitor<'de> for StarOrVisitor<T>
|
||||
where
|
||||
T: FromStr<Err = FE>,
|
||||
FE: Display,
|
||||
FE: fmt::Display,
|
||||
{
|
||||
type Value = StarOr<T>;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a string")
|
||||
}
|
||||
|
||||
|
@ -139,7 +98,7 @@ where
|
|||
|
||||
impl<T> Serialize for StarOr<T>
|
||||
where
|
||||
T: Deref<Target = str>,
|
||||
T: ToString,
|
||||
{
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
|
@ -147,7 +106,222 @@ where
|
|||
{
|
||||
match self {
|
||||
StarOr::Star => serializer.serialize_str("*"),
|
||||
StarOr::Other(other) => serializer.serialize_str(other.deref()),
|
||||
StarOr::Other(other) => serializer.serialize_str(&other.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> DeserializeFromValue<E> for StarOr<T>
|
||||
where
|
||||
T: FromStr,
|
||||
E: DeserializeError + MergeWithError<T::Err>,
|
||||
{
|
||||
fn deserialize_from_value<V: deserr::IntoValue>(
|
||||
value: deserr::Value<V>,
|
||||
location: deserr::ValuePointerRef,
|
||||
) -> Result<Self, E> {
|
||||
match value {
|
||||
deserr::Value::String(v) => {
|
||||
if v == "*" {
|
||||
Ok(StarOr::Star)
|
||||
} else {
|
||||
match T::from_str(&v) {
|
||||
Ok(parsed) => Ok(StarOr::Other(parsed)),
|
||||
Err(e) => Err(unwrap_any(E::merge(None, e, location))),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => Err(unwrap_any(E::error::<V>(
|
||||
None,
|
||||
deserr::ErrorKind::IncorrectValueKind {
|
||||
actual: value,
|
||||
accepted: &[ValueKind::String],
|
||||
},
|
||||
location,
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A type representing the content of a query parameter that can either not exist,
|
||||
/// be equal to a star (*), or another value
|
||||
///
|
||||
/// It is a convenient alternative to `Option<StarOr<T>>`.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub enum OptionStarOr<T> {
|
||||
#[default]
|
||||
None,
|
||||
Star,
|
||||
Other(T),
|
||||
}
|
||||
|
||||
impl<T> OptionStarOr<T> {
|
||||
pub fn is_some(&self) -> bool {
|
||||
match self {
|
||||
Self::None => false,
|
||||
Self::Star => false,
|
||||
Self::Other(_) => true,
|
||||
}
|
||||
}
|
||||
pub fn merge_star_and_none(self) -> Option<T> {
|
||||
match self {
|
||||
Self::None | Self::Star => None,
|
||||
Self::Other(x) => Some(x),
|
||||
}
|
||||
}
|
||||
pub fn try_map<U, E, F: Fn(T) -> Result<U, E>>(self, map_f: F) -> Result<OptionStarOr<U>, E> {
|
||||
match self {
|
||||
OptionStarOr::None => Ok(OptionStarOr::None),
|
||||
OptionStarOr::Star => Ok(OptionStarOr::Star),
|
||||
OptionStarOr::Other(x) => map_f(x).map(OptionStarOr::Other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FromQueryParameter for OptionStarOr<T>
|
||||
where
|
||||
T: FromQueryParameter,
|
||||
{
|
||||
type Err = T::Err;
|
||||
fn from_query_param(p: &str) -> Result<Self, Self::Err> {
|
||||
match p {
|
||||
"*" => Ok(OptionStarOr::Star),
|
||||
s => T::from_query_param(s).map(OptionStarOr::Other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> DeserializeFromValue<E> for OptionStarOr<T>
|
||||
where
|
||||
E: DeserializeError + MergeWithError<T::Err>,
|
||||
T: FromQueryParameter,
|
||||
{
|
||||
fn deserialize_from_value<V: deserr::IntoValue>(
|
||||
value: deserr::Value<V>,
|
||||
location: deserr::ValuePointerRef,
|
||||
) -> Result<Self, E> {
|
||||
match value {
|
||||
deserr::Value::String(s) => match s.as_str() {
|
||||
"*" => Ok(OptionStarOr::Star),
|
||||
s => match T::from_query_param(s) {
|
||||
Ok(x) => Ok(OptionStarOr::Other(x)),
|
||||
Err(e) => Err(unwrap_any(E::merge(None, e, location))),
|
||||
},
|
||||
},
|
||||
_ => Err(unwrap_any(E::error::<V>(
|
||||
None,
|
||||
deserr::ErrorKind::IncorrectValueKind {
|
||||
actual: value,
|
||||
accepted: &[ValueKind::String],
|
||||
},
|
||||
location,
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A type representing the content of a query parameter that can either not exist, be equal to a star (*), or represent a list of other values
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub enum OptionStarOrList<T> {
|
||||
#[default]
|
||||
None,
|
||||
Star,
|
||||
List(Vec<T>),
|
||||
}
|
||||
|
||||
impl<T> OptionStarOrList<T> {
|
||||
pub fn is_some(&self) -> bool {
|
||||
match self {
|
||||
Self::None => false,
|
||||
Self::Star => false,
|
||||
Self::List(_) => true,
|
||||
}
|
||||
}
|
||||
pub fn map<U, F: Fn(T) -> U>(self, map_f: F) -> OptionStarOrList<U> {
|
||||
match self {
|
||||
Self::None => OptionStarOrList::None,
|
||||
Self::Star => OptionStarOrList::Star,
|
||||
Self::List(xs) => OptionStarOrList::List(xs.into_iter().map(map_f).collect()),
|
||||
}
|
||||
}
|
||||
pub fn try_map<U, E, F: Fn(T) -> Result<U, E>>(
|
||||
self,
|
||||
map_f: F,
|
||||
) -> Result<OptionStarOrList<U>, E> {
|
||||
match self {
|
||||
Self::None => Ok(OptionStarOrList::None),
|
||||
Self::Star => Ok(OptionStarOrList::Star),
|
||||
Self::List(xs) => {
|
||||
xs.into_iter().map(map_f).collect::<Result<Vec<_>, _>>().map(OptionStarOrList::List)
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn merge_star_and_none(self) -> Option<Vec<T>> {
|
||||
match self {
|
||||
Self::None | Self::Star => None,
|
||||
Self::List(xs) => Some(xs),
|
||||
}
|
||||
}
|
||||
pub fn push(&mut self, el: T) {
|
||||
match self {
|
||||
Self::None => *self = Self::List(vec![el]),
|
||||
Self::Star => (),
|
||||
Self::List(xs) => xs.push(el),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> DeserializeFromValue<E> for OptionStarOrList<T>
|
||||
where
|
||||
E: DeserializeError + MergeWithError<T::Err>,
|
||||
T: FromQueryParameter,
|
||||
{
|
||||
fn deserialize_from_value<V: deserr::IntoValue>(
|
||||
value: deserr::Value<V>,
|
||||
location: deserr::ValuePointerRef,
|
||||
) -> Result<Self, E> {
|
||||
match value {
|
||||
deserr::Value::String(s) => {
|
||||
let mut error = None;
|
||||
let mut is_star = false;
|
||||
// CS::<String>::from_str is infaillible
|
||||
let cs = serde_cs::vec::CS::<String>::from_str(&s).unwrap();
|
||||
let len_cs = cs.0.len();
|
||||
let mut els = vec![];
|
||||
for (i, el_str) in cs.into_iter().enumerate() {
|
||||
if el_str == "*" {
|
||||
is_star = true;
|
||||
} else {
|
||||
match T::from_query_param(&el_str) {
|
||||
Ok(el) => {
|
||||
els.push(el);
|
||||
}
|
||||
Err(e) => {
|
||||
let location =
|
||||
if len_cs > 1 { location.push_index(i) } else { location };
|
||||
error = Some(E::merge(error, e, location)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(error) = error {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
if is_star {
|
||||
Ok(OptionStarOrList::Star)
|
||||
} else {
|
||||
Ok(OptionStarOrList::List(els))
|
||||
}
|
||||
}
|
||||
_ => Err(unwrap_any(E::error::<V>(
|
||||
None,
|
||||
deserr::ErrorKind::IncorrectValueKind {
|
||||
actual: value,
|
||||
accepted: &[ValueKind::String],
|
||||
},
|
||||
location,
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use core::fmt;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::{Display, Write};
|
||||
use std::str::FromStr;
|
||||
|
@ -9,7 +10,7 @@ use serde::{Deserialize, Serialize, Serializer};
|
|||
use time::{Duration, OffsetDateTime};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::error::{Code, ResponseError};
|
||||
use crate::error::ResponseError;
|
||||
use crate::keys::Key;
|
||||
use crate::settings::{Settings, Unchecked};
|
||||
use crate::InstanceUid;
|
||||
|
@ -332,7 +333,7 @@ impl Display for Status {
|
|||
}
|
||||
|
||||
impl FromStr for Status {
|
||||
type Err = ResponseError;
|
||||
type Err = ParseTaskStatusError;
|
||||
|
||||
fn from_str(status: &str) -> Result<Self, Self::Err> {
|
||||
if status.eq_ignore_ascii_case("enqueued") {
|
||||
|
@ -346,21 +347,28 @@ impl FromStr for Status {
|
|||
} else if status.eq_ignore_ascii_case("canceled") {
|
||||
Ok(Status::Canceled)
|
||||
} else {
|
||||
Err(ResponseError::from_msg(
|
||||
format!(
|
||||
"`{}` is not a status. Available status are {}.",
|
||||
status,
|
||||
enum_iterator::all::<Status>()
|
||||
.map(|s| format!("`{s}`"))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
),
|
||||
Code::BadRequest,
|
||||
))
|
||||
Err(ParseTaskStatusError(status.to_owned()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParseTaskStatusError(pub String);
|
||||
impl fmt::Display for ParseTaskStatusError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"`{}` is not a valid task status. Available statuses are {}.",
|
||||
self.0,
|
||||
enum_iterator::all::<Status>()
|
||||
.map(|s| format!("`{s}`"))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
impl std::error::Error for ParseTaskStatusError {}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Sequence)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Kind {
|
||||
|
@ -412,7 +420,7 @@ impl Display for Kind {
|
|||
}
|
||||
}
|
||||
impl FromStr for Kind {
|
||||
type Err = ResponseError;
|
||||
type Err = ParseTaskKindError;
|
||||
|
||||
fn from_str(kind: &str) -> Result<Self, Self::Err> {
|
||||
if kind.eq_ignore_ascii_case("indexCreation") {
|
||||
|
@ -438,25 +446,32 @@ impl FromStr for Kind {
|
|||
} else if kind.eq_ignore_ascii_case("snapshotCreation") {
|
||||
Ok(Kind::SnapshotCreation)
|
||||
} else {
|
||||
Err(ResponseError::from_msg(
|
||||
format!(
|
||||
"`{}` is not a type. Available types are {}.",
|
||||
kind,
|
||||
enum_iterator::all::<Kind>()
|
||||
.map(|k| format!(
|
||||
"`{}`",
|
||||
// by default serde is going to insert `"` around the value.
|
||||
serde_json::to_string(&k).unwrap().trim_matches('"')
|
||||
))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
),
|
||||
Code::BadRequest,
|
||||
))
|
||||
Err(ParseTaskKindError(kind.to_owned()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParseTaskKindError(pub String);
|
||||
impl fmt::Display for ParseTaskKindError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"`{}` is not a valid task type. Available types are {}.",
|
||||
self.0,
|
||||
enum_iterator::all::<Kind>()
|
||||
.map(|k| format!(
|
||||
"`{}`",
|
||||
// by default serde is going to insert `"` around the value.
|
||||
serde_json::to_string(&k).unwrap().trim_matches('"')
|
||||
))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
impl std::error::Error for ParseTaskKindError {}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
pub enum Details {
|
||||
DocumentAdditionOrUpdate { received_documents: u64, indexed_documents: Option<u64> },
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue