2414: Improve index uid validation upon API key creation r=Kerollmops a=pierre-l

- ~Use an IndexUid newtype to enforce stronger constraints~
- ~`cargo update -p vergen`~ (`rustup update` was the proper fix for this)
- Add a new `meilisearch_types` crate
- Move `meilisearch_error` to `meilisearch_types::error`
- Move `meilisearch_lib::index_resolver::IndexUid` to `meilisearch_types::index_uid`
- Add a new `InvalidIndexUid` error in `meilisearch_types::index_uid`
- Move `meilisearch_http::routes::StarOr` to `meilisearch_types::star_or`
- Use the `IndexUid` and `StarOr` in `meilisearch_auth::Key`

Fixes #2158


Co-authored-by: pierre-l <pierre.larger@gmail.com>
This commit is contained in:
bors[bot] 2022-06-09 15:41:51 +00:00 committed by GitHub
commit de356061db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 362 additions and 193 deletions

28
Cargo.lock generated
View File

@ -2006,7 +2006,7 @@ dependencies = [
"base64", "base64",
"enum-iterator", "enum-iterator",
"hmac", "hmac",
"meilisearch-error", "meilisearch-types",
"milli", "milli",
"rand", "rand",
"serde", "serde",
@ -2017,17 +2017,6 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "meilisearch-error"
version = "0.28.0"
dependencies = [
"actix-web",
"proptest",
"proptest-derive",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "meilisearch-http" name = "meilisearch-http"
version = "0.28.0" version = "0.28.0"
@ -2061,8 +2050,8 @@ dependencies = [
"manifest-dir-macros", "manifest-dir-macros",
"maplit", "maplit",
"meilisearch-auth", "meilisearch-auth",
"meilisearch-error",
"meilisearch-lib", "meilisearch-lib",
"meilisearch-types",
"mime", "mime",
"num_cpus", "num_cpus",
"obkv", "obkv",
@ -2129,7 +2118,7 @@ dependencies = [
"lazy_static", "lazy_static",
"log", "log",
"meilisearch-auth", "meilisearch-auth",
"meilisearch-error", "meilisearch-types",
"milli", "milli",
"mime", "mime",
"mockall", "mockall",
@ -2163,6 +2152,17 @@ dependencies = [
"whoami", "whoami",
] ]
[[package]]
name = "meilisearch-types"
version = "0.28.0"
dependencies = [
"actix-web",
"proptest",
"proptest-derive",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.5.0" version = "2.5.0"

View File

@ -2,7 +2,7 @@
resolver = "2" resolver = "2"
members = [ members = [
"meilisearch-http", "meilisearch-http",
"meilisearch-error", "meilisearch-types",
"meilisearch-lib", "meilisearch-lib",
"meilisearch-auth", "meilisearch-auth",
"permissive-json-pointer", "permissive-json-pointer",

View File

@ -7,7 +7,7 @@ edition = "2021"
base64 = "0.13.0" base64 = "0.13.0"
enum-iterator = "0.7.0" enum-iterator = "0.7.0"
hmac = "0.12.1" hmac = "0.12.1"
meilisearch-error = { path = "../meilisearch-error" } meilisearch-types = { path = "../meilisearch-types" }
milli = { git = "https://github.com/meilisearch/milli.git", tag = "v0.29.2" } milli = { git = "https://github.com/meilisearch/milli.git", tag = "v0.29.2" }
rand = "0.8.4" rand = "0.8.4"
serde = { version = "1.0.136", features = ["derive"] } serde = { version = "1.0.136", features = ["derive"] }

View File

@ -1,7 +1,7 @@
use std::error::Error; use std::error::Error;
use meilisearch_error::ErrorCode; use meilisearch_types::error::{Code, ErrorCode};
use meilisearch_error::{internal_error, Code}; use meilisearch_types::internal_error;
use serde_json::Value; use serde_json::Value;
pub type Result<T> = std::result::Result<T, AuthControllerError>; pub type Result<T> = std::result::Result<T, AuthControllerError>;

View File

@ -2,6 +2,8 @@ use crate::action::Action;
use crate::error::{AuthControllerError, Result}; use crate::error::{AuthControllerError, Result};
use crate::store::KeyId; use crate::store::KeyId;
use meilisearch_types::index_uid::IndexUid;
use meilisearch_types::star_or::StarOr;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{from_value, Value}; use serde_json::{from_value, Value};
use time::format_description::well_known::Rfc3339; use time::format_description::well_known::Rfc3339;
@ -17,7 +19,7 @@ pub struct Key {
pub name: Option<String>, pub name: Option<String>,
pub uid: KeyId, pub uid: KeyId,
pub actions: Vec<Action>, pub actions: Vec<Action>,
pub indexes: Vec<String>, pub indexes: Vec<StarOr<IndexUid>>,
#[serde(with = "time::serde::rfc3339::option")] #[serde(with = "time::serde::rfc3339::option")]
pub expires_at: Option<OffsetDateTime>, pub expires_at: Option<OffsetDateTime>,
#[serde(with = "time::serde::rfc3339")] #[serde(with = "time::serde::rfc3339")]
@ -136,7 +138,7 @@ impl Key {
description: Some("Use it for anything that is not a search operation. Caution! Do not expose it on a public frontend".to_string()), description: Some("Use it for anything that is not a search operation. Caution! Do not expose it on a public frontend".to_string()),
uid, uid,
actions: vec![Action::All], actions: vec![Action::All],
indexes: vec!["*".to_string()], indexes: vec![StarOr::Star],
expires_at: None, expires_at: None,
created_at: now, created_at: now,
updated_at: now, updated_at: now,
@ -151,7 +153,7 @@ impl Key {
description: Some("Use it to search from the frontend".to_string()), description: Some("Use it to search from the frontend".to_string()),
uid, uid,
actions: vec![Action::Search], actions: vec![Action::Search],
indexes: vec!["*".to_string()], indexes: vec![StarOr::Star],
expires_at: None, expires_at: None,
created_at: now, created_at: now,
updated_at: now, updated_at: now,

View File

@ -5,6 +5,7 @@ mod key;
mod store; mod store;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::ops::Deref;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
@ -16,6 +17,7 @@ use uuid::Uuid;
pub use action::{actions, Action}; pub use action::{actions, Action};
use error::{AuthControllerError, Result}; use error::{AuthControllerError, Result};
pub use key::Key; pub use key::Key;
use meilisearch_types::star_or::StarOr;
use store::generate_key_as_base64; use store::generate_key_as_base64;
pub use store::open_auth_store_env; pub use store::open_auth_store_env;
use store::HeedAuthStore; use store::HeedAuthStore;
@ -87,20 +89,22 @@ impl AuthController {
.get_api_key(uid)? .get_api_key(uid)?
.ok_or_else(|| AuthControllerError::ApiKeyNotFound(uid.to_string()))?; .ok_or_else(|| AuthControllerError::ApiKeyNotFound(uid.to_string()))?;
if !key.indexes.iter().any(|i| i.as_str() == "*") { if !key.indexes.iter().any(|i| i == &StarOr::Star) {
filters.search_rules = match search_rules { filters.search_rules = match search_rules {
// Intersect search_rules with parent key authorized indexes. // Intersect search_rules with parent key authorized indexes.
Some(search_rules) => SearchRules::Map( Some(search_rules) => SearchRules::Map(
key.indexes key.indexes
.into_iter() .into_iter()
.filter_map(|index| { .filter_map(|index| {
search_rules search_rules.get_index_search_rules(index.deref()).map(
.get_index_search_rules(&index) |index_search_rules| {
.map(|index_search_rules| (index, Some(index_search_rules))) (String::from(index), Some(index_search_rules))
},
)
}) })
.collect(), .collect(),
), ),
None => SearchRules::Set(key.indexes.into_iter().collect()), None => SearchRules::Set(key.indexes.into_iter().map(String::from).collect()),
}; };
} else if let Some(search_rules) = search_rules { } else if let Some(search_rules) = search_rules {
filters.search_rules = search_rules; filters.search_rules = search_rules;

View File

@ -3,12 +3,14 @@ use std::cmp::Reverse;
use std::convert::TryFrom; use std::convert::TryFrom;
use std::convert::TryInto; use std::convert::TryInto;
use std::fs::create_dir_all; use std::fs::create_dir_all;
use std::ops::Deref;
use std::path::Path; use std::path::Path;
use std::str; use std::str;
use std::sync::Arc; use std::sync::Arc;
use enum_iterator::IntoEnumIterator; use enum_iterator::IntoEnumIterator;
use hmac::{Hmac, Mac}; use hmac::{Hmac, Mac};
use meilisearch_types::star_or::StarOr;
use milli::heed::types::{ByteSlice, DecodeIgnore, SerdeJson}; use milli::heed::types::{ByteSlice, DecodeIgnore, SerdeJson};
use milli::heed::{Database, Env, EnvOpenOptions, RwTxn}; use milli::heed::{Database, Env, EnvOpenOptions, RwTxn};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
@ -92,7 +94,7 @@ impl HeedAuthStore {
key.actions.clone() key.actions.clone()
}; };
let no_index_restriction = key.indexes.contains(&"*".to_owned()); let no_index_restriction = key.indexes.contains(&StarOr::Star);
for action in actions { for action in actions {
if no_index_restriction { if no_index_restriction {
// If there is no index restriction we put None. // If there is no index restriction we put None.
@ -102,7 +104,7 @@ impl HeedAuthStore {
for index in key.indexes.iter() { for index in key.indexes.iter() {
db.put( db.put(
&mut wtxn, &mut wtxn,
&(&uid, &action, Some(index.as_bytes())), &(&uid, &action, Some(index.deref().as_bytes())),
&key.expires_at, &key.expires_at,
)?; )?;
} }

View File

@ -45,7 +45,7 @@ itertools = "0.10.3"
jsonwebtoken = "8.0.1" jsonwebtoken = "8.0.1"
log = "0.4.14" log = "0.4.14"
meilisearch-auth = { path = "../meilisearch-auth" } meilisearch-auth = { path = "../meilisearch-auth" }
meilisearch-error = { path = "../meilisearch-error" } meilisearch-types = { path = "../meilisearch-types" }
meilisearch-lib = { path = "../meilisearch-lib" } meilisearch-lib = { path = "../meilisearch-lib" }
mime = "0.3.16" mime = "0.3.16"
num_cpus = "1.13.1" num_cpus = "1.13.1"

View File

@ -1,6 +1,6 @@
use actix_web as aweb; use actix_web as aweb;
use aweb::error::{JsonPayloadError, QueryPayloadError}; use aweb::error::{JsonPayloadError, QueryPayloadError};
use meilisearch_error::{Code, ErrorCode, ResponseError}; use meilisearch_types::error::{Code, ErrorCode, ResponseError};
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum MeilisearchHttpError { pub enum MeilisearchHttpError {

View File

@ -1,4 +1,4 @@
use meilisearch_error::{Code, ErrorCode}; use meilisearch_types::error::{Code, ErrorCode};
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum AuthenticationError { pub enum AuthenticationError {

View File

@ -5,12 +5,11 @@ use std::ops::Deref;
use std::pin::Pin; use std::pin::Pin;
use actix_web::FromRequest; use actix_web::FromRequest;
use error::AuthenticationError;
use futures::future::err; use futures::future::err;
use futures::Future; use futures::Future;
use meilisearch_error::{Code, ResponseError};
use error::AuthenticationError;
use meilisearch_auth::{AuthController, AuthFilter}; use meilisearch_auth::{AuthController, AuthFilter};
use meilisearch_types::error::{Code, ResponseError};
pub struct GuardedData<P, D> { pub struct GuardedData<P, D> {
data: D, data: D,

View File

@ -148,10 +148,10 @@ macro_rules! create_app {
use actix_web::middleware::TrailingSlash; use actix_web::middleware::TrailingSlash;
use actix_web::App; use actix_web::App;
use actix_web::{middleware, web}; use actix_web::{middleware, web};
use meilisearch_error::ResponseError;
use meilisearch_http::error::MeilisearchHttpError; use meilisearch_http::error::MeilisearchHttpError;
use meilisearch_http::routes; use meilisearch_http::routes;
use meilisearch_http::{configure_data, dashboard}; use meilisearch_http::{configure_data, dashboard};
use meilisearch_types::error::ResponseError;
App::new() App::new()
.configure(|s| configure_data(s, $data.clone(), $auth.clone(), &$opt, $analytics)) .configure(|s| configure_data(s, $data.clone(), $auth.clone(), &$opt, $analytics))

View File

@ -7,7 +7,7 @@ use time::OffsetDateTime;
use uuid::Uuid; use uuid::Uuid;
use meilisearch_auth::{error::AuthControllerError, Action, AuthController, Key}; use meilisearch_auth::{error::AuthControllerError, Action, AuthController, Key};
use meilisearch_error::{Code, ResponseError}; use meilisearch_types::error::{Code, ResponseError};
use crate::extractors::{ use crate::extractors::{
authentication::{policies::*, GuardedData}, authentication::{policies::*, GuardedData},
@ -151,7 +151,7 @@ impl KeyView {
key: generated_key, key: generated_key,
uid: key.uid, uid: key.uid,
actions: key.actions, actions: key.actions,
indexes: key.indexes, indexes: key.indexes.into_iter().map(String::from).collect(),
expires_at: key.expires_at, expires_at: key.expires_at,
created_at: key.created_at, created_at: key.created_at,
updated_at: key.updated_at, updated_at: key.updated_at,

View File

@ -1,7 +1,7 @@
use actix_web::{web, HttpRequest, HttpResponse}; use actix_web::{web, HttpRequest, HttpResponse};
use log::debug; use log::debug;
use meilisearch_error::ResponseError;
use meilisearch_lib::MeiliSearch; use meilisearch_lib::MeiliSearch;
use meilisearch_types::error::ResponseError;
use serde_json::json; use serde_json::json;
use crate::analytics::Analytics; use crate::analytics::Analytics;

View File

@ -6,10 +6,11 @@ use actix_web::{web, HttpRequest, HttpResponse};
use bstr::ByteSlice; use bstr::ByteSlice;
use futures::{Stream, StreamExt}; use futures::{Stream, StreamExt};
use log::debug; use log::debug;
use meilisearch_error::ResponseError;
use meilisearch_lib::index_controller::{DocumentAdditionFormat, Update}; use meilisearch_lib::index_controller::{DocumentAdditionFormat, Update};
use meilisearch_lib::milli::update::IndexDocumentsMethod; use meilisearch_lib::milli::update::IndexDocumentsMethod;
use meilisearch_lib::MeiliSearch; use meilisearch_lib::MeiliSearch;
use meilisearch_types::error::ResponseError;
use meilisearch_types::star_or::StarOr;
use mime::Mime; use mime::Mime;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::Deserialize; use serde::Deserialize;
@ -22,7 +23,7 @@ use crate::error::MeilisearchHttpError;
use crate::extractors::authentication::{policies::*, GuardedData}; use crate::extractors::authentication::{policies::*, GuardedData};
use crate::extractors::payload::Payload; use crate::extractors::payload::Payload;
use crate::extractors::sequential_extractor::SeqHandler; use crate::extractors::sequential_extractor::SeqHandler;
use crate::routes::{fold_star_or, PaginationView, StarOr}; use crate::routes::{fold_star_or, PaginationView};
use crate::task::SummarizedTaskView; use crate::task::SummarizedTaskView;
static ACCEPTED_CONTENT_TYPE: Lazy<Vec<String>> = Lazy::new(|| { static ACCEPTED_CONTENT_TYPE: Lazy<Vec<String>> = Lazy::new(|| {

View File

@ -1,8 +1,8 @@
use actix_web::{web, HttpRequest, HttpResponse}; use actix_web::{web, HttpRequest, HttpResponse};
use log::debug; use log::debug;
use meilisearch_error::ResponseError;
use meilisearch_lib::index_controller::Update; use meilisearch_lib::index_controller::Update;
use meilisearch_lib::MeiliSearch; use meilisearch_lib::MeiliSearch;
use meilisearch_types::error::ResponseError;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use time::OffsetDateTime; use time::OffsetDateTime;

View File

@ -1,12 +1,12 @@
use actix_web::{web, HttpRequest, HttpResponse}; use actix_web::{web, HttpRequest, HttpResponse};
use log::debug; use log::debug;
use meilisearch_auth::IndexSearchRules; use meilisearch_auth::IndexSearchRules;
use meilisearch_error::ResponseError;
use meilisearch_lib::index::{ use meilisearch_lib::index::{
SearchQuery, DEFAULT_CROP_LENGTH, DEFAULT_CROP_MARKER, DEFAULT_HIGHLIGHT_POST_TAG, SearchQuery, DEFAULT_CROP_LENGTH, DEFAULT_CROP_MARKER, DEFAULT_HIGHLIGHT_POST_TAG,
DEFAULT_HIGHLIGHT_PRE_TAG, DEFAULT_SEARCH_LIMIT, DEFAULT_HIGHLIGHT_PRE_TAG, DEFAULT_SEARCH_LIMIT,
}; };
use meilisearch_lib::MeiliSearch; use meilisearch_lib::MeiliSearch;
use meilisearch_types::error::ResponseError;
use serde::Deserialize; use serde::Deserialize;
use serde_cs::vec::CS; use serde_cs::vec::CS;
use serde_json::Value; use serde_json::Value;

View File

@ -1,10 +1,10 @@
use log::debug; use log::debug;
use actix_web::{web, HttpRequest, HttpResponse}; use actix_web::{web, HttpRequest, HttpResponse};
use meilisearch_error::ResponseError;
use meilisearch_lib::index::{Settings, Unchecked}; use meilisearch_lib::index::{Settings, Unchecked};
use meilisearch_lib::index_controller::Update; use meilisearch_lib::index_controller::Update;
use meilisearch_lib::MeiliSearch; use meilisearch_lib::MeiliSearch;
use meilisearch_types::error::ResponseError;
use serde_json::json; use serde_json::json;
use crate::analytics::Analytics; use crate::analytics::Analytics;
@ -21,7 +21,7 @@ macro_rules! make_setting_route {
use meilisearch_lib::milli::update::Setting; use meilisearch_lib::milli::update::Setting;
use meilisearch_lib::{index::Settings, index_controller::Update, MeiliSearch}; use meilisearch_lib::{index::Settings, index_controller::Update, MeiliSearch};
use meilisearch_error::ResponseError; use meilisearch_types::error::ResponseError;
use $crate::analytics::Analytics; use $crate::analytics::Analytics;
use $crate::extractors::authentication::{policies::*, GuardedData}; use $crate::extractors::authentication::{policies::*, GuardedData};
use $crate::extractors::sequential_extractor::SeqHandler; use $crate::extractors::sequential_extractor::SeqHandler;

View File

@ -1,14 +1,13 @@
use std::str::FromStr;
use actix_web::{web, HttpResponse}; use actix_web::{web, HttpResponse};
use log::debug; use log::debug;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use time::OffsetDateTime; use time::OffsetDateTime;
use meilisearch_error::ResponseError;
use meilisearch_lib::index::{Settings, Unchecked}; use meilisearch_lib::index::{Settings, Unchecked};
use meilisearch_lib::MeiliSearch; use meilisearch_lib::MeiliSearch;
use meilisearch_types::error::ResponseError;
use meilisearch_types::star_or::StarOr;
use crate::extractors::authentication::{policies::*, GuardedData}; use crate::extractors::authentication::{policies::*, GuardedData};
@ -27,26 +26,6 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
.service(web::scope("/indexes").configure(indexes::configure)); .service(web::scope("/indexes").configure(indexes::configure));
} }
/// A type that tries to match either a star (*) or
/// any other thing that implements `FromStr`.
#[derive(Debug)]
pub enum StarOr<T> {
Star,
Other(T),
}
impl<T: FromStr> FromStr for StarOr<T> {
type Err = T::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.trim() == "*" {
Ok(StarOr::Star)
} else {
T::from_str(s).map(StarOr::Other)
}
}
}
/// Extracts the raw values from the `StarOr` types and /// Extracts the raw values from the `StarOr` types and
/// return None if a `StarOr::Star` is encountered. /// return None if a `StarOr::Star` is encountered.
pub fn fold_star_or<T, O>(content: impl IntoIterator<Item = StarOr<T>>) -> Option<O> pub fn fold_star_or<T, O>(content: impl IntoIterator<Item = StarOr<T>>) -> Option<O>

View File

@ -1,8 +1,10 @@
use actix_web::{web, HttpRequest, HttpResponse}; use actix_web::{web, HttpRequest, HttpResponse};
use meilisearch_error::ResponseError;
use meilisearch_lib::tasks::task::{TaskContent, TaskEvent, TaskId}; use meilisearch_lib::tasks::task::{TaskContent, TaskEvent, TaskId};
use meilisearch_lib::tasks::TaskFilter; use meilisearch_lib::tasks::TaskFilter;
use meilisearch_lib::{IndexUid, MeiliSearch}; use meilisearch_lib::MeiliSearch;
use meilisearch_types::error::ResponseError;
use meilisearch_types::index_uid::IndexUid;
use meilisearch_types::star_or::StarOr;
use serde::Deserialize; use serde::Deserialize;
use serde_cs::vec::CS; use serde_cs::vec::CS;
use serde_json::json; use serde_json::json;
@ -12,7 +14,7 @@ use crate::extractors::authentication::{policies::*, GuardedData};
use crate::extractors::sequential_extractor::SeqHandler; use crate::extractors::sequential_extractor::SeqHandler;
use crate::task::{TaskListView, TaskStatus, TaskType, TaskView}; use crate::task::{TaskListView, TaskStatus, TaskType, TaskView};
use super::{fold_star_or, StarOr}; use super::fold_star_or;
const DEFAULT_LIMIT: fn() -> usize = || 20; const DEFAULT_LIMIT: fn() -> usize = || 20;

View File

@ -3,12 +3,12 @@ use std::fmt::{self, Write};
use std::str::FromStr; use std::str::FromStr;
use std::write; use std::write;
use meilisearch_error::ResponseError;
use meilisearch_lib::index::{Settings, Unchecked}; use meilisearch_lib::index::{Settings, Unchecked};
use meilisearch_lib::tasks::batch::BatchId; use meilisearch_lib::tasks::batch::BatchId;
use meilisearch_lib::tasks::task::{ use meilisearch_lib::tasks::task::{
DocumentDeletion, Task, TaskContent, TaskEvent, TaskId, TaskResult, DocumentDeletion, Task, TaskContent, TaskEvent, TaskId, TaskResult,
}; };
use meilisearch_types::error::ResponseError;
use serde::{Deserialize, Serialize, Serializer}; use serde::{Deserialize, Serialize, Serializer};
use time::{Duration, OffsetDateTime}; use time::{Duration, OffsetDateTime};

View File

@ -358,6 +358,32 @@ async fn error_add_api_key_invalid_parameters_indexes() {
assert_eq!(response, expected_response); assert_eq!(response, expected_response);
} }
#[actix_rt::test]
async fn error_add_api_key_invalid_index_uids() {
let mut server = Server::new_auth().await;
server.use_api_key("MASTER_KEY");
let content = json!({
"description": Value::Null,
"indexes": ["invalid index # / \\name with spaces"],
"actions": [
"documents.add"
],
"expiresAt": "2050-11-13T00:00:00"
});
let (response, code) = server.add_api_key(content).await;
let expected_response = json!({
"message": r#"`indexes` field value `["invalid index # / \\name with spaces"]` is invalid. It should be an array of string representing index names."#,
"code": "invalid_api_key_indexes",
"type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid_api_key_indexes"
});
assert_eq!(response, expected_response);
assert_eq!(code, 400);
}
#[actix_rt::test] #[actix_rt::test]
async fn error_add_api_key_invalid_parameters_actions() { async fn error_add_api_key_invalid_parameters_actions() {
let mut server = Server::new_auth().await; let mut server = Server::new_auth().await;

View File

@ -638,7 +638,7 @@ async fn error_document_add_create_index_bad_uid() {
let (response, code) = index.add_documents(json!([{"id": 1}]), None).await; let (response, code) = index.add_documents(json!([{"id": 1}]), None).await;
let expected_response = json!({ let expected_response = json!({
"message": "`883 fj!` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", "message": "invalid index uid `883 fj!`, the uid must be an integer or a string containing only alphanumeric characters a-z A-Z 0-9, hyphens - and underscores _.",
"code": "invalid_index_uid", "code": "invalid_index_uid",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid_index_uid" "link": "https://docs.meilisearch.com/errors#invalid_index_uid"
@ -655,7 +655,7 @@ async fn error_document_update_create_index_bad_uid() {
let (response, code) = index.update_documents(json!([{"id": 1}]), None).await; let (response, code) = index.update_documents(json!([{"id": 1}]), None).await;
let expected_response = json!({ let expected_response = json!({
"message": "`883 fj!` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", "message": "invalid index uid `883 fj!`, the uid must be an integer or a string containing only alphanumeric characters a-z A-Z 0-9, hyphens - and underscores _.",
"code": "invalid_index_uid", "code": "invalid_index_uid",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid_index_uid" "link": "https://docs.meilisearch.com/errors#invalid_index_uid"

View File

@ -102,7 +102,7 @@ async fn error_create_with_invalid_index_uid() {
let (response, code) = index.create(None).await; let (response, code) = index.create(None).await;
let expected_response = json!({ let expected_response = json!({
"message": "`test test#!` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", "message": "invalid index uid `test test#!`, the uid must be an integer or a string containing only alphanumeric characters a-z A-Z 0-9, hyphens - and underscores _.",
"code": "invalid_index_uid", "code": "invalid_index_uid",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid_index_uid" "link": "https://docs.meilisearch.com/errors#invalid_index_uid"

View File

@ -197,7 +197,7 @@ async fn error_update_setting_unexisting_index_invalid_uid() {
assert_eq!(code, 400); assert_eq!(code, 400);
let expected = json!({ let expected = json!({
"message": "`test##! ` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).", "message": "invalid index uid `test##! `, the uid must be an integer or a string containing only alphanumeric characters a-z A-Z 0-9, hyphens - and underscores _.",
"code": "invalid_index_uid", "code": "invalid_index_uid",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid_index_uid"}); "link": "https://docs.meilisearch.com/errors#invalid_index_uid"});

View File

@ -29,7 +29,7 @@ itertools = "0.10.3"
lazy_static = "1.4.0" lazy_static = "1.4.0"
log = "0.4.14" log = "0.4.14"
meilisearch-auth = { path = "../meilisearch-auth" } meilisearch-auth = { path = "../meilisearch-auth" }
meilisearch-error = { path = "../meilisearch-error" } meilisearch-types = { path = "../meilisearch-types" }
milli = { git = "https://github.com/meilisearch/milli.git", tag = "v0.29.2" } milli = { git = "https://github.com/meilisearch/milli.git", tag = "v0.29.2" }
mime = "0.3.16" mime = "0.3.16"
num_cpus = "1.13.1" num_cpus = "1.13.1"
@ -59,7 +59,7 @@ whoami = { version = "1.2.1", optional = true }
[dev-dependencies] [dev-dependencies]
actix-rt = "2.7.0" actix-rt = "2.7.0"
meilisearch-error = { path = "../meilisearch-error", features = ["test-traits"] } meilisearch-types = { path = "../meilisearch-types", features = ["test-traits"] }
mockall = "0.11.0" mockall = "0.11.0"
nelson = { git = "https://github.com/meilisearch/nelson.git", rev = "675f13885548fb415ead8fbb447e9e6d9314000a"} nelson = { git = "https://github.com/meilisearch/nelson.git", rev = "675f13885548fb415ead8fbb447e9e6d9314000a"}
paste = "1.0.6" paste = "1.0.6"

View File

@ -2,7 +2,8 @@ use std::borrow::Borrow;
use std::fmt::{self, Debug, Display}; use std::fmt::{self, Debug, Display};
use std::io::{self, BufRead, BufReader, BufWriter, Cursor, Read, Seek, Write}; use std::io::{self, BufRead, BufReader, BufWriter, Cursor, Read, Seek, Write};
use meilisearch_error::{internal_error, Code, ErrorCode}; use meilisearch_types::error::{Code, ErrorCode};
use meilisearch_types::internal_error;
use milli::documents::DocumentBatchBuilder; use milli::documents::DocumentBatchBuilder;
type Result<T> = std::result::Result<T, DocumentFormatError>; type Result<T> = std::result::Result<T, DocumentFormatError>;

View File

@ -1,5 +1,5 @@
use anyhow::bail; use anyhow::bail;
use meilisearch_error::Code; use meilisearch_types::error::Code;
use milli::update::IndexDocumentsMethod; use milli::update::IndexDocumentsMethod;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use time::OffsetDateTime; use time::OffsetDateTime;

View File

@ -1,4 +1,5 @@
use meilisearch_error::{Code, ResponseError}; use meilisearch_types::error::{Code, ResponseError};
use meilisearch_types::index_uid::IndexUid;
use milli::update::IndexDocumentsMethod; use milli::update::IndexDocumentsMethod;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use time::OffsetDateTime; use time::OffsetDateTime;
@ -6,7 +7,6 @@ use uuid::Uuid;
use super::v4::{Task, TaskContent, TaskEvent}; use super::v4::{Task, TaskContent, TaskEvent};
use crate::index::{Settings, Unchecked}; use crate::index::{Settings, Unchecked};
use crate::index_resolver::IndexUid;
use crate::tasks::task::{DocumentDeletion, TaskId, TaskResult}; use crate::tasks::task::{DocumentDeletion, TaskId, TaskResult};
use super::v2; use super::v2;

View File

@ -1,4 +1,5 @@
use meilisearch_error::ResponseError; use meilisearch_types::error::ResponseError;
use meilisearch_types::index_uid::IndexUid;
use milli::update::IndexDocumentsMethod; use milli::update::IndexDocumentsMethod;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use time::OffsetDateTime; use time::OffsetDateTime;
@ -9,7 +10,6 @@ use crate::tasks::batch::BatchId;
use crate::tasks::task::{ use crate::tasks::task::{
DocumentDeletion, TaskContent as NewTaskContent, TaskEvent as NewTaskEvent, TaskId, TaskResult, DocumentDeletion, TaskContent as NewTaskContent, TaskEvent as NewTaskEvent, TaskId, TaskResult,
}; };
use crate::IndexUid;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Task { pub struct Task {

View File

@ -1,5 +1,6 @@
use meilisearch_auth::error::AuthControllerError; use meilisearch_auth::error::AuthControllerError;
use meilisearch_error::{internal_error, Code, ErrorCode}; use meilisearch_types::error::{Code, ErrorCode};
use meilisearch_types::internal_error;
use crate::{index_resolver::error::IndexResolverError, tasks::error::TaskError}; use crate::{index_resolver::error::IndexResolverError, tasks::error::TaskError};

View File

@ -1,7 +1,7 @@
use std::error::Error; use std::error::Error;
use std::fmt; use std::fmt;
use meilisearch_error::{Code, ErrorCode}; use meilisearch_types::error::{Code, ErrorCode};
use milli::UserError; use milli::UserError;
#[derive(Debug)] #[derive(Debug)]

View File

@ -1,6 +1,7 @@
use std::error::Error; use std::error::Error;
use meilisearch_error::{internal_error, Code, ErrorCode}; use meilisearch_types::error::{Code, ErrorCode};
use meilisearch_types::internal_error;
use serde_json::Value; use serde_json::Value;
use crate::{error::MilliError, update_file_store}; use crate::{error::MilliError, update_file_store};

View File

@ -1,7 +1,8 @@
use std::error::Error; use std::error::Error;
use meilisearch_error::Code; use meilisearch_types::error::{Code, ErrorCode};
use meilisearch_error::{internal_error, ErrorCode}; use meilisearch_types::index_uid::IndexUidFormatError;
use meilisearch_types::internal_error;
use tokio::task::JoinError; use tokio::task::JoinError;
use super::DocumentAdditionFormat; use super::DocumentAdditionFormat;
@ -63,3 +64,9 @@ impl ErrorCode for IndexControllerError {
} }
} }
} }
impl From<IndexUidFormatError> for IndexControllerError {
fn from(err: IndexUidFormatError) -> Self {
IndexResolverError::from(err).into()
}
}

View File

@ -11,6 +11,7 @@ use actix_web::error::PayloadError;
use bytes::Bytes; use bytes::Bytes;
use futures::Stream; use futures::Stream;
use futures::StreamExt; use futures::StreamExt;
use meilisearch_types::index_uid::IndexUid;
use milli::update::IndexDocumentsMethod; use milli::update::IndexDocumentsMethod;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use time::OffsetDateTime; use time::OffsetDateTime;
@ -37,7 +38,6 @@ use error::Result;
use self::error::IndexControllerError; use self::error::IndexControllerError;
use crate::index_resolver::index_store::{IndexStore, MapIndexStore}; use crate::index_resolver::index_store::{IndexStore, MapIndexStore};
use crate::index_resolver::meta_store::{HeedMetaStore, IndexMetaStore}; use crate::index_resolver::meta_store::{HeedMetaStore, IndexMetaStore};
pub use crate::index_resolver::IndexUid;
use crate::index_resolver::{create_index_resolver, IndexResolver}; use crate::index_resolver::{create_index_resolver, IndexResolver};
use crate::update_file_store::UpdateFileStore; use crate::update_file_store::UpdateFileStore;

View File

@ -1,7 +1,7 @@
use std::error::Error; use std::error::Error;
use std::fmt; use std::fmt;
use meilisearch_error::{internal_error, Code, ErrorCode}; use meilisearch_types::{internal_error, Code, ErrorCode};
use crate::{ use crate::{
document_formats::DocumentFormatError, document_formats::DocumentFormatError,

View File

@ -1,6 +1,8 @@
use std::fmt; use std::fmt;
use meilisearch_error::{internal_error, Code, ErrorCode}; use meilisearch_types::error::{Code, ErrorCode};
use meilisearch_types::index_uid::IndexUidFormatError;
use meilisearch_types::internal_error;
use tokio::sync::mpsc::error::SendError as MpscSendError; use tokio::sync::mpsc::error::SendError as MpscSendError;
use tokio::sync::oneshot::error::RecvError as OneshotRecvError; use tokio::sync::oneshot::error::RecvError as OneshotRecvError;
use uuid::Uuid; use uuid::Uuid;
@ -25,8 +27,8 @@ pub enum IndexResolverError {
UuidAlreadyExists(Uuid), UuidAlreadyExists(Uuid),
#[error("{0}")] #[error("{0}")]
Milli(#[from] milli::Error), Milli(#[from] milli::Error),
#[error("`{0}` is not a valid index uid. Index uid can be an integer or a string containing only alphanumeric characters, hyphens (-) and underscores (_).")] #[error("{0}")]
BadlyFormatted(String), BadlyFormatted(#[from] IndexUidFormatError),
} }
impl<T> From<MpscSendError<T>> for IndexResolverError impl<T> From<MpscSendError<T>> for IndexResolverError

View File

@ -2,20 +2,17 @@ pub mod error;
pub mod index_store; pub mod index_store;
pub mod meta_store; pub mod meta_store;
use std::convert::{TryFrom, TryInto}; use std::convert::TryFrom;
use std::error::Error;
use std::fmt;
use std::path::Path; use std::path::Path;
use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use error::{IndexResolverError, Result}; use error::{IndexResolverError, Result};
use index_store::{IndexStore, MapIndexStore}; use index_store::{IndexStore, MapIndexStore};
use meilisearch_error::ResponseError; use meilisearch_types::error::ResponseError;
use meilisearch_types::index_uid::IndexUid;
use meta_store::{HeedMetaStore, IndexMetaStore}; use meta_store::{HeedMetaStore, IndexMetaStore};
use milli::heed::Env; use milli::heed::Env;
use milli::update::{DocumentDeletionResult, IndexerConfig}; use milli::update::{DocumentDeletionResult, IndexerConfig};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime; use time::OffsetDateTime;
use tokio::task::spawn_blocking; use tokio::task::spawn_blocking;
use uuid::Uuid; use uuid::Uuid;
@ -35,12 +32,6 @@ pub use real::IndexResolver;
#[cfg(test)] #[cfg(test)]
pub use test::MockIndexResolver as IndexResolver; pub use test::MockIndexResolver as IndexResolver;
/// An index uid is composed of only ascii alphanumeric characters, - and _, between 1 and 400
/// bytes long
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
pub struct IndexUid(#[cfg_attr(test, proptest(regex("[a-zA-Z0-9_-]{1,400}")))] String);
pub fn create_index_resolver( pub fn create_index_resolver(
path: impl AsRef<Path>, path: impl AsRef<Path>,
index_size: usize, index_size: usize,
@ -53,81 +44,6 @@ pub fn create_index_resolver(
Ok(IndexResolver::new(uuid_store, index_store, file_store)) Ok(IndexResolver::new(uuid_store, index_store, file_store))
} }
impl IndexUid {
pub fn new_unchecked(s: impl AsRef<str>) -> Self {
Self(s.as_ref().to_string())
}
pub fn into_inner(self) -> String {
self.0
}
/// Return a reference over the inner str.
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::ops::Deref for IndexUid {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl TryInto<IndexUid> for String {
type Error = IndexUidFormatError;
fn try_into(self) -> std::result::Result<IndexUid, IndexUidFormatError> {
IndexUid::from_str(&self)
}
}
#[derive(Debug)]
pub struct IndexUidFormatError {
invalid_uid: String,
}
impl fmt::Display for IndexUidFormatError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"invalid index uid `{}`, the uid must be an integer \
or a string containing only alphanumeric characters \
a-z A-Z 0-9, hyphens - and underscores _.",
self.invalid_uid,
)
}
}
impl Error for IndexUidFormatError {}
impl From<IndexUidFormatError> for IndexResolverError {
fn from(error: IndexUidFormatError) -> Self {
Self::BadlyFormatted(error.invalid_uid)
}
}
impl FromStr for IndexUid {
type Err = IndexUidFormatError;
fn from_str(uid: &str) -> std::result::Result<IndexUid, IndexUidFormatError> {
if !uid
.chars()
.all(|x| x.is_ascii_alphanumeric() || x == '-' || x == '_')
|| uid.is_empty()
|| uid.len() > 400
{
Err(IndexUidFormatError {
invalid_uid: uid.to_string(),
})
} else {
Ok(IndexUid(uid.to_string()))
}
}
}
mod real { mod real {
use super::*; use super::*;

View File

@ -13,7 +13,7 @@ mod update_file_store;
use std::path::Path; use std::path::Path;
pub use index_controller::{IndexUid, MeiliSearch}; pub use index_controller::MeiliSearch;
pub use milli; pub use milli;
pub use milli::heed; pub use milli::heed;

View File

@ -1,4 +1,5 @@
use meilisearch_error::{internal_error, Code, ErrorCode}; use meilisearch_types::error::{Code, ErrorCode};
use meilisearch_types::internal_error;
use tokio::task::JoinError; use tokio::task::JoinError;
use crate::update_file_store::UpdateFileStoreError; use crate::update_file_store::UpdateFileStoreError;

View File

@ -55,9 +55,9 @@ mod test {
task::{Task, TaskContent}, task::{Task, TaskContent},
}; };
use crate::update_file_store::{Result as FileStoreResult, UpdateFileStore}; use crate::update_file_store::{Result as FileStoreResult, UpdateFileStore};
use crate::IndexUid;
use super::*; use super::*;
use meilisearch_types::index_uid::IndexUid;
use milli::update::IndexDocumentsMethod; use milli::update::IndexDocumentsMethod;
use nelson::Mocker; use nelson::Mocker;
use proptest::prelude::*; use proptest::prelude::*;

View File

@ -534,10 +534,11 @@ fn make_batch(tasks: &mut TaskQueue, config: &SchedulerConfig) -> Processing {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use meilisearch_types::index_uid::IndexUid;
use milli::update::IndexDocumentsMethod; use milli::update::IndexDocumentsMethod;
use uuid::Uuid; use uuid::Uuid;
use crate::{index_resolver::IndexUid, tasks::task::TaskContent}; use crate::tasks::task::TaskContent;
use super::*; use super::*;

View File

@ -1,4 +1,5 @@
use meilisearch_error::ResponseError; use meilisearch_types::error::ResponseError;
use meilisearch_types::index_uid::IndexUid;
use milli::update::{DocumentAdditionResult, IndexDocumentsMethod}; use milli::update::{DocumentAdditionResult, IndexDocumentsMethod};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use time::OffsetDateTime; use time::OffsetDateTime;
@ -6,7 +7,6 @@ use uuid::Uuid;
use super::batch::BatchId; use super::batch::BatchId;
use crate::index::{Settings, Unchecked}; use crate::index::{Settings, Unchecked};
use crate::index_resolver::IndexUid;
pub type TaskId = u32; pub type TaskId = u32;

View File

@ -267,13 +267,11 @@ impl TaskStore {
#[cfg(test)] #[cfg(test)]
pub mod test { pub mod test {
use crate::{ use crate::tasks::{scheduler::Processing, task_store::store::test::tmp_env};
tasks::{scheduler::Processing, task_store::store::test::tmp_env},
IndexUid,
};
use super::*; use super::*;
use meilisearch_types::index_uid::IndexUid;
use nelson::Mocker; use nelson::Mocker;
use proptest::{ use proptest::{
strategy::Strategy, strategy::Strategy,

View File

@ -179,11 +179,11 @@ impl Store {
#[cfg(test)] #[cfg(test)]
pub mod test { pub mod test {
use itertools::Itertools; use itertools::Itertools;
use meilisearch_types::index_uid::IndexUid;
use milli::heed::EnvOpenOptions; use milli::heed::EnvOpenOptions;
use nelson::Mocker; use nelson::Mocker;
use tempfile::TempDir; use tempfile::TempDir;
use crate::index_resolver::IndexUid;
use crate::tasks::task::TaskContent; use crate::tasks::task::TaskContent;
use super::*; use super::*;

View File

@ -1,5 +1,5 @@
[package] [package]
name = "meilisearch-error" name = "meilisearch-types"
version = "0.28.0" version = "0.28.0"
authors = ["marin <postma.marin@protonmail.com>"] authors = ["marin <postma.marin@protonmail.com>"]
edition = "2021" edition = "2021"

View File

@ -0,0 +1,85 @@
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::fmt;
use std::str::FromStr;
/// An index uid is composed of only ascii alphanumeric characters, - and _, between 1 and 400
/// bytes long
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[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,
);
impl IndexUid {
pub fn new_unchecked(s: impl AsRef<str>) -> Self {
Self(s.as_ref().to_string())
}
pub fn into_inner(self) -> String {
self.0
}
/// Return a reference over the inner str.
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::ops::Deref for IndexUid {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl TryFrom<String> for IndexUid {
type Error = IndexUidFormatError;
fn try_from(uid: String) -> Result<Self, Self::Error> {
if !uid
.chars()
.all(|x| x.is_ascii_alphanumeric() || x == '-' || x == '_')
|| uid.is_empty()
|| uid.len() > 400
{
Err(IndexUidFormatError { invalid_uid: uid })
} else {
Ok(IndexUid(uid))
}
}
}
impl FromStr for IndexUid {
type Err = IndexUidFormatError;
fn from_str(uid: &str) -> Result<IndexUid, IndexUidFormatError> {
uid.to_string().try_into()
}
}
impl From<IndexUid> for String {
fn from(uid: IndexUid) -> Self {
uid.into_inner()
}
}
#[derive(Debug)]
pub struct IndexUidFormatError {
pub invalid_uid: String,
}
impl fmt::Display for IndexUidFormatError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"invalid index uid `{}`, the uid must be an integer \
or a string containing only alphanumeric characters \
a-z A-Z 0-9, hyphens - and underscores _.",
self.invalid_uid,
)
}
}
impl Error for IndexUidFormatError {}

View File

@ -0,0 +1,3 @@
pub mod error;
pub mod index_uid;
pub mod star_or;

View File

@ -0,0 +1,138 @@
use serde::de::Visitor;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt::{Display, Formatter};
use std::marker::PhantomData;
use std::ops::Deref;
use std::str::FromStr;
/// A type that tries to match either a star (*) or
/// any other thing that implements `FromStr`.
#[derive(Debug)]
pub enum StarOr<T> {
Star,
Other(T),
}
impl<T: FromStr> FromStr for StarOr<T> {
type Err = T::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.trim() == "*" {
Ok(StarOr::Star)
} else {
T::from_str(s).map(StarOr::Other)
}
}
}
impl<T: Deref<Target = str>> Deref for StarOr<T> {
type Target = str;
fn deref(&self) -> &Self::Target {
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(),
}
}
}
impl<T: PartialEq> PartialEq for StarOr<T> {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Star, Self::Star) => true,
(Self::Other(left), Self::Other(right)) if left.eq(right) => true,
_ => false,
}
}
}
impl<T: PartialEq + Eq> Eq for StarOr<T> {}
impl<'de, T, E> Deserialize<'de> for StarOr<T>
where
T: FromStr<Err = E>,
E: Display,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
/// Serde can't differentiate between `StarOr::Star` and `StarOr::Other` without a tag.
/// Simply using `#[serde(untagged)]` + `#[serde(rename="*")]` will lead to attempting to
/// deserialize everything as a `StarOr::Other`, including "*".
/// [`#[serde(other)]`](https://serde.rs/variant-attrs.html#other) might have helped but is
/// not supported on untagged enums.
struct StarOrVisitor<T>(PhantomData<T>);
impl<'de, T, FE> Visitor<'de> for StarOrVisitor<T>
where
T: FromStr<Err = FE>,
FE: Display,
{
type Value = StarOr<T>;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("a string")
}
fn visit_str<SE>(self, v: &str) -> Result<Self::Value, SE>
where
SE: serde::de::Error,
{
match v {
"*" => Ok(StarOr::Star),
v => {
let other = FromStr::from_str(v).map_err(|e: T::Err| {
SE::custom(format!("Invalid `other` value: {}", e))
})?;
Ok(StarOr::Other(other))
}
}
}
}
deserializer.deserialize_str(StarOrVisitor(PhantomData))
}
}
impl<T> Serialize for StarOr<T>
where
T: Deref<Target = str>,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
StarOr::Star => serializer.serialize_str("*"),
StarOr::Other(other) => serializer.serialize_str(other.deref()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::{json, Value};
#[test]
fn star_or_serde_roundtrip() {
fn roundtrip(content: Value, expected: StarOr<String>) {
let deserialized: StarOr<String> = serde_json::from_value(content.clone()).unwrap();
assert_eq!(deserialized, expected);
assert_eq!(content, serde_json::to_value(deserialized).unwrap());
}
roundtrip(json!("products"), StarOr::Other("products".to_string()));
roundtrip(json!("*"), StarOr::Star);
}
}

View File

@ -206,7 +206,7 @@ fn create_value(value: &Document, mut selectors: HashSet<&str>) -> Document {
new_value new_value
} }
fn create_array(array: &Vec<Value>, selectors: &HashSet<&str>) -> Vec<Value> { fn create_array(array: &[Value], selectors: &HashSet<&str>) -> Vec<Value> {
let mut res = Vec::new(); let mut res = Vec::new();
for value in array { for value in array {