mirror of
https://github.com/meilisearch/meilisearch.git
synced 2024-11-22 01:57:41 +08:00
Compare commits
26 Commits
4f561a8612
...
b4bf73d650
Author | SHA1 | Date | |
---|---|---|---|
|
b4bf73d650 | ||
|
057fcb3993 | ||
|
7882819795 | ||
|
c2b14b8f8f | ||
|
ccd79b07f7 | ||
|
146c87bd6f | ||
|
7e45125e7a | ||
|
46fc6bbc2e | ||
|
ba045a0000 | ||
|
b89fd49d63 | ||
|
544961372c | ||
|
0419b93032 | ||
|
e4745a61a8 | ||
|
04d86a4d9e | ||
|
e9668eff79 | ||
|
0403ec0a56 | ||
|
56b5289b2f | ||
|
4b8dd6dd6b | ||
|
3d3ae5aa0c | ||
|
a80bb8f77e | ||
|
f95bd11db2 | ||
|
e54fbb0d1e | ||
|
b6c5a57932 | ||
|
6688604a93 | ||
|
75969f9f68 | ||
|
07e7ddbc5b |
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -3507,6 +3507,7 @@ version = "1.11.0"
|
||||
dependencies = [
|
||||
"actix-web",
|
||||
"anyhow",
|
||||
"bitflags 2.6.0",
|
||||
"convert_case 0.6.0",
|
||||
"csv",
|
||||
"deserr",
|
||||
|
@ -105,7 +105,7 @@ impl HeedAuthStore {
|
||||
|
||||
let mut actions = HashSet::new();
|
||||
for action in &key.actions {
|
||||
match action {
|
||||
match *action {
|
||||
Action::All => actions.extend(enum_iterator::all::<Action>()),
|
||||
Action::DocumentsAll => {
|
||||
actions.extend(
|
||||
@ -128,23 +128,11 @@ impl HeedAuthStore {
|
||||
Action::SettingsAll => {
|
||||
actions.extend([Action::SettingsGet, Action::SettingsUpdate].iter());
|
||||
}
|
||||
Action::DumpsAll => {
|
||||
actions.insert(Action::DumpsCreate);
|
||||
}
|
||||
Action::SnapshotsAll => {
|
||||
actions.insert(Action::SnapshotsCreate);
|
||||
}
|
||||
Action::TasksAll => {
|
||||
actions.extend([Action::TasksGet, Action::TasksDelete, Action::TasksCancel]);
|
||||
}
|
||||
Action::StatsAll => {
|
||||
actions.insert(Action::StatsGet);
|
||||
}
|
||||
Action::MetricsAll => {
|
||||
actions.insert(Action::MetricsGet);
|
||||
}
|
||||
other => {
|
||||
actions.insert(*other);
|
||||
actions.insert(other);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -293,18 +281,24 @@ impl HeedAuthStore {
|
||||
/// optionally on a specific index, for a given key.
|
||||
pub struct KeyIdActionCodec;
|
||||
|
||||
impl KeyIdActionCodec {
|
||||
fn action_parts_to_32bits([p1, p2, p3, p4]: &[u8; 4]) -> u32 {
|
||||
((*p1 as u32) << 24) | ((*p2 as u32) << 16) | ((*p3 as u32) << 8) | (*p4 as u32)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> milli::heed::BytesDecode<'a> for KeyIdActionCodec {
|
||||
type DItem = (KeyId, Action, Option<&'a [u8]>);
|
||||
|
||||
fn bytes_decode(bytes: &'a [u8]) -> StdResult<Self::DItem, BoxedError> {
|
||||
let (key_id_bytes, action_bytes) = try_split_array_at(bytes).ok_or(SliceTooShortError)?;
|
||||
let (&action_byte, index) =
|
||||
match try_split_array_at(action_bytes).ok_or(SliceTooShortError)? {
|
||||
([action], []) => (action, None),
|
||||
([action], index) => (action, Some(index)),
|
||||
let (action_bits, index) =
|
||||
match try_split_array_at::<u8, 4>(action_bytes).ok_or(SliceTooShortError)? {
|
||||
(action_parts, []) => (Self::action_parts_to_32bits(action_parts), None),
|
||||
(action_parts, index) => (Self::action_parts_to_32bits(action_parts), Some(index)),
|
||||
};
|
||||
let key_id = Uuid::from_bytes(*key_id_bytes);
|
||||
let action = Action::from_repr(action_byte).ok_or(InvalidActionError { action_byte })?;
|
||||
let action = Action::from_bits(action_bits).ok_or(InvalidActionError { action_bits })?;
|
||||
|
||||
Ok((key_id, action, index))
|
||||
}
|
||||
@ -317,7 +311,7 @@ impl<'a> milli::heed::BytesEncode<'a> for KeyIdActionCodec {
|
||||
let mut bytes = Vec::new();
|
||||
|
||||
bytes.extend_from_slice(key_id.as_bytes());
|
||||
let action_bytes = u8::to_be_bytes(action.repr());
|
||||
let action_bytes = u32::to_be_bytes(action.bits());
|
||||
bytes.extend_from_slice(&action_bytes);
|
||||
if let Some(index) = index {
|
||||
bytes.extend_from_slice(index);
|
||||
@ -332,9 +326,9 @@ impl<'a> milli::heed::BytesEncode<'a> for KeyIdActionCodec {
|
||||
pub struct SliceTooShortError;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[error("cannot construct a valid Action from {action_byte}")]
|
||||
#[error("cannot construct a valid Action from {action_bits}")]
|
||||
pub struct InvalidActionError {
|
||||
pub action_byte: u8,
|
||||
pub action_bits: u32,
|
||||
}
|
||||
|
||||
pub fn generate_key_as_hexa(uid: Uuid, master_key: &[u8]) -> String {
|
||||
|
@ -38,6 +38,7 @@ time = { version = "0.3.36", features = [
|
||||
] }
|
||||
tokio = "1.38"
|
||||
uuid = { version = "1.10.0", features = ["serde", "v4"] }
|
||||
bitflags = "2.6.0"
|
||||
|
||||
[dev-dependencies]
|
||||
insta = "1.39.0"
|
||||
|
@ -2,10 +2,11 @@ use std::convert::Infallible;
|
||||
use std::hash::Hash;
|
||||
use std::str::FromStr;
|
||||
|
||||
use deserr::{DeserializeError, Deserr, MergeWithError, ValuePointerRef};
|
||||
use bitflags::{bitflags, Flags};
|
||||
use deserr::{take_cf_content, DeserializeError, Deserr, MergeWithError, ValuePointerRef};
|
||||
use enum_iterator::Sequence;
|
||||
use milli::update::Setting;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
use time::macros::{format_description, time};
|
||||
use time::{Date, OffsetDateTime, PrimitiveDateTime};
|
||||
@ -179,193 +180,284 @@ fn parse_expiration_date(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Sequence, Deserr)]
|
||||
#[repr(u8)]
|
||||
pub enum Action {
|
||||
#[serde(rename = "*")]
|
||||
#[deserr(rename = "*")]
|
||||
All = 0,
|
||||
#[serde(rename = "search")]
|
||||
#[deserr(rename = "search")]
|
||||
Search,
|
||||
#[serde(rename = "documents.*")]
|
||||
#[deserr(rename = "documents.*")]
|
||||
DocumentsAll,
|
||||
#[serde(rename = "documents.add")]
|
||||
#[deserr(rename = "documents.add")]
|
||||
DocumentsAdd,
|
||||
#[serde(rename = "documents.get")]
|
||||
#[deserr(rename = "documents.get")]
|
||||
DocumentsGet,
|
||||
#[serde(rename = "documents.delete")]
|
||||
#[deserr(rename = "documents.delete")]
|
||||
DocumentsDelete,
|
||||
#[serde(rename = "indexes.*")]
|
||||
#[deserr(rename = "indexes.*")]
|
||||
IndexesAll,
|
||||
#[serde(rename = "indexes.create")]
|
||||
#[deserr(rename = "indexes.create")]
|
||||
IndexesAdd,
|
||||
#[serde(rename = "indexes.get")]
|
||||
#[deserr(rename = "indexes.get")]
|
||||
IndexesGet,
|
||||
#[serde(rename = "indexes.update")]
|
||||
#[deserr(rename = "indexes.update")]
|
||||
IndexesUpdate,
|
||||
#[serde(rename = "indexes.delete")]
|
||||
#[deserr(rename = "indexes.delete")]
|
||||
IndexesDelete,
|
||||
#[serde(rename = "indexes.swap")]
|
||||
#[deserr(rename = "indexes.swap")]
|
||||
IndexesSwap,
|
||||
#[serde(rename = "tasks.*")]
|
||||
#[deserr(rename = "tasks.*")]
|
||||
TasksAll,
|
||||
#[serde(rename = "tasks.cancel")]
|
||||
#[deserr(rename = "tasks.cancel")]
|
||||
TasksCancel,
|
||||
#[serde(rename = "tasks.delete")]
|
||||
#[deserr(rename = "tasks.delete")]
|
||||
TasksDelete,
|
||||
#[serde(rename = "tasks.get")]
|
||||
#[deserr(rename = "tasks.get")]
|
||||
TasksGet,
|
||||
#[serde(rename = "settings.*")]
|
||||
#[deserr(rename = "settings.*")]
|
||||
SettingsAll,
|
||||
#[serde(rename = "settings.get")]
|
||||
#[deserr(rename = "settings.get")]
|
||||
SettingsGet,
|
||||
#[serde(rename = "settings.update")]
|
||||
#[deserr(rename = "settings.update")]
|
||||
SettingsUpdate,
|
||||
#[serde(rename = "stats.*")]
|
||||
#[deserr(rename = "stats.*")]
|
||||
StatsAll,
|
||||
#[serde(rename = "stats.get")]
|
||||
#[deserr(rename = "stats.get")]
|
||||
StatsGet,
|
||||
#[serde(rename = "metrics.*")]
|
||||
#[deserr(rename = "metrics.*")]
|
||||
MetricsAll,
|
||||
#[serde(rename = "metrics.get")]
|
||||
#[deserr(rename = "metrics.get")]
|
||||
MetricsGet,
|
||||
#[serde(rename = "dumps.*")]
|
||||
#[deserr(rename = "dumps.*")]
|
||||
DumpsAll,
|
||||
#[serde(rename = "dumps.create")]
|
||||
#[deserr(rename = "dumps.create")]
|
||||
DumpsCreate,
|
||||
#[serde(rename = "snapshots.*")]
|
||||
#[deserr(rename = "snapshots.*")]
|
||||
SnapshotsAll,
|
||||
#[serde(rename = "snapshots.create")]
|
||||
#[deserr(rename = "snapshots.create")]
|
||||
SnapshotsCreate,
|
||||
#[serde(rename = "version")]
|
||||
#[deserr(rename = "version")]
|
||||
Version,
|
||||
#[serde(rename = "keys.create")]
|
||||
#[deserr(rename = "keys.create")]
|
||||
KeysAdd,
|
||||
#[serde(rename = "keys.get")]
|
||||
#[deserr(rename = "keys.get")]
|
||||
KeysGet,
|
||||
#[serde(rename = "keys.update")]
|
||||
#[deserr(rename = "keys.update")]
|
||||
KeysUpdate,
|
||||
#[serde(rename = "keys.delete")]
|
||||
#[deserr(rename = "keys.delete")]
|
||||
KeysDelete,
|
||||
#[serde(rename = "experimental.get")]
|
||||
#[deserr(rename = "experimental.get")]
|
||||
ExperimentalFeaturesGet,
|
||||
#[serde(rename = "experimental.update")]
|
||||
#[deserr(rename = "experimental.update")]
|
||||
ExperimentalFeaturesUpdate,
|
||||
bitflags! {
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||
#[repr(transparent)]
|
||||
// NOTE: For `Sequence` impl to work, the values of these must be in ascending order
|
||||
pub struct Action: u32 {
|
||||
const Search = 1;
|
||||
// Documents
|
||||
const DocumentsAdd = 1 << 1;
|
||||
const DocumentsGet = 1 << 2;
|
||||
const DocumentsDelete = 1 << 3;
|
||||
const DocumentsAll = Self::DocumentsAdd.bits() | Self::DocumentsGet.bits() | Self::DocumentsDelete.bits();
|
||||
// Indexes
|
||||
const IndexesAdd = 1 << 4;
|
||||
const IndexesGet = 1 << 5;
|
||||
const IndexesUpdate = 1 << 6;
|
||||
const IndexesDelete = 1 << 7;
|
||||
const IndexesSwap = 1 << 8;
|
||||
const IndexesAll = Self::IndexesAdd.bits() | Self::IndexesGet.bits() | Self::IndexesUpdate.bits() | Self::IndexesDelete.bits() | Self::IndexesSwap.bits();
|
||||
// Tasks
|
||||
const TasksCancel = 1 << 9;
|
||||
const TasksDelete = 1 << 10;
|
||||
const TasksGet = 1 << 11;
|
||||
const TasksAll = Self::TasksCancel.bits() | Self::TasksDelete.bits() | Self::TasksGet.bits();
|
||||
// Settings
|
||||
const SettingsGet = 1 << 12;
|
||||
const SettingsUpdate = 1 << 13;
|
||||
const SettingsAll = Self::SettingsGet.bits() | Self::SettingsUpdate.bits();
|
||||
// Stats
|
||||
const StatsGet = 1 << 14;
|
||||
const StatsAll = Self::StatsGet.bits();
|
||||
// Metrics
|
||||
const MetricsGet = 1 << 15;
|
||||
const MetricsAll = Self::MetricsGet.bits();
|
||||
// Dumps
|
||||
const DumpsCreate = 1 << 16;
|
||||
const DumpsAll = Self::DumpsCreate.bits();
|
||||
// Snapshots
|
||||
const SnapshotsCreate = 1 << 17;
|
||||
const SnapshotsAll = Self::SnapshotsCreate.bits();
|
||||
// Keys without an "all" version
|
||||
const Version = 1 << 18;
|
||||
const KeysAdd = 1 << 19;
|
||||
const KeysGet = 1 << 20;
|
||||
const KeysUpdate = 1 << 21;
|
||||
const KeysDelete = 1 << 22;
|
||||
const ExperimentalFeaturesGet = 1 << 23;
|
||||
const ExperimentalFeaturesUpdate = 1 << 24;
|
||||
// All
|
||||
const All = 0xFFFFFFFF >> (32 - 1 - 24);
|
||||
}
|
||||
}
|
||||
|
||||
impl Action {
|
||||
pub const fn from_repr(repr: u8) -> Option<Self> {
|
||||
use actions::*;
|
||||
match repr {
|
||||
ALL => Some(Self::All),
|
||||
SEARCH => Some(Self::Search),
|
||||
DOCUMENTS_ALL => Some(Self::DocumentsAll),
|
||||
DOCUMENTS_ADD => Some(Self::DocumentsAdd),
|
||||
DOCUMENTS_GET => Some(Self::DocumentsGet),
|
||||
DOCUMENTS_DELETE => Some(Self::DocumentsDelete),
|
||||
INDEXES_ALL => Some(Self::IndexesAll),
|
||||
INDEXES_CREATE => Some(Self::IndexesAdd),
|
||||
INDEXES_GET => Some(Self::IndexesGet),
|
||||
INDEXES_UPDATE => Some(Self::IndexesUpdate),
|
||||
INDEXES_DELETE => Some(Self::IndexesDelete),
|
||||
INDEXES_SWAP => Some(Self::IndexesSwap),
|
||||
TASKS_ALL => Some(Self::TasksAll),
|
||||
TASKS_CANCEL => Some(Self::TasksCancel),
|
||||
TASKS_DELETE => Some(Self::TasksDelete),
|
||||
TASKS_GET => Some(Self::TasksGet),
|
||||
SETTINGS_ALL => Some(Self::SettingsAll),
|
||||
SETTINGS_GET => Some(Self::SettingsGet),
|
||||
SETTINGS_UPDATE => Some(Self::SettingsUpdate),
|
||||
STATS_ALL => Some(Self::StatsAll),
|
||||
STATS_GET => Some(Self::StatsGet),
|
||||
METRICS_ALL => Some(Self::MetricsAll),
|
||||
METRICS_GET => Some(Self::MetricsGet),
|
||||
DUMPS_ALL => Some(Self::DumpsAll),
|
||||
DUMPS_CREATE => Some(Self::DumpsCreate),
|
||||
SNAPSHOTS_CREATE => Some(Self::SnapshotsCreate),
|
||||
VERSION => Some(Self::Version),
|
||||
KEYS_CREATE => Some(Self::KeysAdd),
|
||||
KEYS_GET => Some(Self::KeysGet),
|
||||
KEYS_UPDATE => Some(Self::KeysUpdate),
|
||||
KEYS_DELETE => Some(Self::KeysDelete),
|
||||
EXPERIMENTAL_FEATURES_GET => Some(Self::ExperimentalFeaturesGet),
|
||||
EXPERIMENTAL_FEATURES_UPDATE => Some(Self::ExperimentalFeaturesUpdate),
|
||||
_otherwise => None,
|
||||
}
|
||||
const SERDE_MAP_ARR: [(&'static str, Self); 34] = [
|
||||
("search", Self::Search),
|
||||
("documents.add", Self::DocumentsAdd),
|
||||
("documents.get", Self::DocumentsGet),
|
||||
("documents.delete", Self::DocumentsDelete),
|
||||
("documents.*", Self::DocumentsAll),
|
||||
("indexes.create", Self::IndexesAdd),
|
||||
("indexes.get", Self::IndexesGet),
|
||||
("indexes.update", Self::IndexesUpdate),
|
||||
("indexes.delete", Self::IndexesDelete),
|
||||
("indexes.swap", Self::IndexesSwap),
|
||||
("indexes.*", Self::IndexesAll),
|
||||
("tasks.cancel", Self::TasksCancel),
|
||||
("tasks.delete", Self::TasksDelete),
|
||||
("tasks.get", Self::TasksGet),
|
||||
("tasks.*", Self::TasksAll),
|
||||
("settings.get", Self::SettingsGet),
|
||||
("settings.update", Self::SettingsUpdate),
|
||||
("settings.*", Self::SettingsAll),
|
||||
("stats.get", Self::StatsGet),
|
||||
("stats.*", Self::StatsAll),
|
||||
("metrics.get", Self::MetricsGet),
|
||||
("metrics.*", Self::MetricsAll),
|
||||
("dumps.create", Self::DumpsCreate),
|
||||
("dumps.*", Self::DumpsAll),
|
||||
("snapshots.create", Self::SnapshotsCreate),
|
||||
("snapshots.*", Self::SnapshotsAll),
|
||||
("version", Self::Version),
|
||||
("keys.create", Self::KeysAdd),
|
||||
("keys.get", Self::KeysGet),
|
||||
("keys.update", Self::KeysUpdate),
|
||||
("keys.delete", Self::KeysDelete),
|
||||
("experimental.get", Self::ExperimentalFeaturesGet),
|
||||
("experimental.update", Self::ExperimentalFeaturesUpdate),
|
||||
("*", Self::All),
|
||||
];
|
||||
|
||||
fn get_action(v: &str) -> Option<Action> {
|
||||
Self::SERDE_MAP_ARR
|
||||
.iter()
|
||||
.find(|(serde_name, _)| &v == serde_name)
|
||||
.map(|(_, action)| *action)
|
||||
}
|
||||
|
||||
pub const fn repr(&self) -> u8 {
|
||||
*self as u8
|
||||
fn get_action_serde_name(v: &Action) -> &'static str {
|
||||
Self::SERDE_MAP_ARR
|
||||
.iter()
|
||||
.find(|(_, action)| v == action)
|
||||
.map(|(serde_name, _)| serde_name)
|
||||
.expect("an action is missing a matching serialized value")
|
||||
}
|
||||
|
||||
// when we remove "all" flags, this will give us the exact index
|
||||
fn get_potential_index(&self) -> usize {
|
||||
if self.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// most significant bit for u32
|
||||
let msb = 1u32 << (31 - self.bits().leading_zeros());
|
||||
|
||||
// index of the single set bit
|
||||
msb.trailing_zeros() as usize
|
||||
}
|
||||
}
|
||||
|
||||
pub mod actions {
|
||||
use super::Action::*;
|
||||
use super::Action as A;
|
||||
|
||||
pub(crate) const ALL: u8 = All.repr();
|
||||
pub const SEARCH: u8 = Search.repr();
|
||||
pub const DOCUMENTS_ALL: u8 = DocumentsAll.repr();
|
||||
pub const DOCUMENTS_ADD: u8 = DocumentsAdd.repr();
|
||||
pub const DOCUMENTS_GET: u8 = DocumentsGet.repr();
|
||||
pub const DOCUMENTS_DELETE: u8 = DocumentsDelete.repr();
|
||||
pub const INDEXES_ALL: u8 = IndexesAll.repr();
|
||||
pub const INDEXES_CREATE: u8 = IndexesAdd.repr();
|
||||
pub const INDEXES_GET: u8 = IndexesGet.repr();
|
||||
pub const INDEXES_UPDATE: u8 = IndexesUpdate.repr();
|
||||
pub const INDEXES_DELETE: u8 = IndexesDelete.repr();
|
||||
pub const INDEXES_SWAP: u8 = IndexesSwap.repr();
|
||||
pub const TASKS_ALL: u8 = TasksAll.repr();
|
||||
pub const TASKS_CANCEL: u8 = TasksCancel.repr();
|
||||
pub const TASKS_DELETE: u8 = TasksDelete.repr();
|
||||
pub const TASKS_GET: u8 = TasksGet.repr();
|
||||
pub const SETTINGS_ALL: u8 = SettingsAll.repr();
|
||||
pub const SETTINGS_GET: u8 = SettingsGet.repr();
|
||||
pub const SETTINGS_UPDATE: u8 = SettingsUpdate.repr();
|
||||
pub const STATS_ALL: u8 = StatsAll.repr();
|
||||
pub const STATS_GET: u8 = StatsGet.repr();
|
||||
pub const METRICS_ALL: u8 = MetricsAll.repr();
|
||||
pub const METRICS_GET: u8 = MetricsGet.repr();
|
||||
pub const DUMPS_ALL: u8 = DumpsAll.repr();
|
||||
pub const DUMPS_CREATE: u8 = DumpsCreate.repr();
|
||||
pub const SNAPSHOTS_CREATE: u8 = SnapshotsCreate.repr();
|
||||
pub const VERSION: u8 = Version.repr();
|
||||
pub const KEYS_CREATE: u8 = KeysAdd.repr();
|
||||
pub const KEYS_GET: u8 = KeysGet.repr();
|
||||
pub const KEYS_UPDATE: u8 = KeysUpdate.repr();
|
||||
pub const KEYS_DELETE: u8 = KeysDelete.repr();
|
||||
pub const EXPERIMENTAL_FEATURES_GET: u8 = ExperimentalFeaturesGet.repr();
|
||||
pub const EXPERIMENTAL_FEATURES_UPDATE: u8 = ExperimentalFeaturesUpdate.repr();
|
||||
pub const SEARCH: u32 = A::Search.bits();
|
||||
pub const DOCUMENTS_ADD: u32 = A::DocumentsAdd.bits();
|
||||
pub const DOCUMENTS_GET: u32 = A::DocumentsGet.bits();
|
||||
pub const DOCUMENTS_DELETE: u32 = A::DocumentsDelete.bits();
|
||||
pub const DOCUMENTS_ALL: u32 = A::DocumentsAll.bits();
|
||||
pub const INDEXES_CREATE: u32 = A::IndexesAdd.bits();
|
||||
pub const INDEXES_GET: u32 = A::IndexesGet.bits();
|
||||
pub const INDEXES_UPDATE: u32 = A::IndexesUpdate.bits();
|
||||
pub const INDEXES_DELETE: u32 = A::IndexesDelete.bits();
|
||||
pub const INDEXES_SWAP: u32 = A::IndexesSwap.bits();
|
||||
pub const INDEXES_ALL: u32 = A::IndexesAll.bits();
|
||||
pub const TASKS_CANCEL: u32 = A::TasksCancel.bits();
|
||||
pub const TASKS_DELETE: u32 = A::TasksDelete.bits();
|
||||
pub const TASKS_GET: u32 = A::TasksGet.bits();
|
||||
pub const TASKS_ALL: u32 = A::TasksAll.bits();
|
||||
pub const SETTINGS_GET: u32 = A::SettingsGet.bits();
|
||||
pub const SETTINGS_UPDATE: u32 = A::SettingsUpdate.bits();
|
||||
pub const SETTINGS_ALL: u32 = A::SettingsAll.bits();
|
||||
pub const STATS_GET: u32 = A::StatsGet.bits();
|
||||
pub const STATS_ALL: u32 = A::StatsAll.bits();
|
||||
pub const METRICS_GET: u32 = A::MetricsGet.bits();
|
||||
pub const METRICS_ALL: u32 = A::MetricsAll.bits();
|
||||
pub const DUMPS_CREATE: u32 = A::DumpsCreate.bits();
|
||||
pub const DUMPS_ALL: u32 = A::DumpsAll.bits();
|
||||
pub const SNAPSHOTS_CREATE: u32 = A::SnapshotsCreate.bits();
|
||||
pub const SNAPSHOTS_ALL: u32 = A::SnapshotsAll.bits();
|
||||
pub const VERSION: u32 = A::Version.bits();
|
||||
pub const KEYS_CREATE: u32 = A::KeysAdd.bits();
|
||||
pub const KEYS_GET: u32 = A::KeysGet.bits();
|
||||
pub const KEYS_UPDATE: u32 = A::KeysUpdate.bits();
|
||||
pub const KEYS_DELETE: u32 = A::KeysDelete.bits();
|
||||
pub const EXPERIMENTAL_FEATURES_GET: u32 = A::ExperimentalFeaturesGet.bits();
|
||||
pub const EXPERIMENTAL_FEATURES_UPDATE: u32 = A::ExperimentalFeaturesUpdate.bits();
|
||||
pub const ALL: u32 = A::All.bits();
|
||||
}
|
||||
|
||||
impl<E: DeserializeError> Deserr<E> for Action {
|
||||
fn deserialize_from_value<V: deserr::IntoValue>(
|
||||
value: deserr::Value<V>,
|
||||
location: deserr::ValuePointerRef<'_>,
|
||||
) -> Result<Self, E> {
|
||||
match value {
|
||||
deserr::Value::String(s) => match Self::get_action(&s) {
|
||||
Some(action) => Ok(action),
|
||||
None => Err(deserr::take_cf_content(E::error::<std::convert::Infallible>(
|
||||
None,
|
||||
deserr::ErrorKind::UnknownValue {
|
||||
value: &s,
|
||||
accepted: &Self::SERDE_MAP_ARR.map(|(ser_action, _)| ser_action),
|
||||
},
|
||||
location,
|
||||
))),
|
||||
},
|
||||
_ => Err(take_cf_content(E::error(
|
||||
None,
|
||||
deserr::ErrorKind::IncorrectValueKind {
|
||||
actual: value,
|
||||
accepted: &[deserr::ValueKind::String],
|
||||
},
|
||||
location,
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Action {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(Self::get_action_serde_name(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Action {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct Visitor;
|
||||
impl<'de> serde::de::Visitor<'de> for Visitor {
|
||||
type Value = Action;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(formatter, "the name of a valid action (string)")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
match Self::Value::get_action(s) {
|
||||
Some(action) => Ok(action),
|
||||
None => Err(E::invalid_value(serde::de::Unexpected::Str(s), &"a valid action")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(Visitor)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Once "all" type flags are removed, simplify
|
||||
// Essentially `get_potential_index` will give the exact index, +1 the exact next, -1 the exact previous
|
||||
impl Sequence for Action {
|
||||
const CARDINALITY: usize = Self::FLAGS.len();
|
||||
|
||||
fn next(&self) -> Option<Self> {
|
||||
let mut potential_next_index = self.get_potential_index() + 1;
|
||||
|
||||
loop {
|
||||
if let Some(next_flag) = Self::FLAGS.get(potential_next_index).map(|v| v.value()) {
|
||||
if next_flag > self {
|
||||
return Some(*next_flag);
|
||||
}
|
||||
|
||||
potential_next_index += 1;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn previous(&self) -> Option<Self> {
|
||||
// -2 because of "all" type flags that represent a single flag, otherwise -1 would suffice
|
||||
let initial_potential_index = self.get_potential_index();
|
||||
if initial_potential_index == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut potential_previous_index: usize =
|
||||
if initial_potential_index == 1 { 0 } else { initial_potential_index - 2 };
|
||||
|
||||
let mut previous_item: Option<Self> = None;
|
||||
let mut pre_previous_item: Option<Self> = None;
|
||||
|
||||
loop {
|
||||
if let Some(next_flag) = Self::FLAGS.get(potential_previous_index).map(|v| v.value()) {
|
||||
if next_flag > self {
|
||||
return pre_previous_item;
|
||||
}
|
||||
|
||||
pre_previous_item = previous_item;
|
||||
previous_item = Some(*next_flag);
|
||||
potential_previous_index += 1;
|
||||
} else {
|
||||
return pre_previous_item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn first() -> Option<Self> {
|
||||
Self::FLAGS.first().map(|v| *v.value())
|
||||
}
|
||||
|
||||
fn last() -> Option<Self> {
|
||||
Self::FLAGS.last().map(|v| *v.value())
|
||||
}
|
||||
}
|
||||
|
@ -171,7 +171,7 @@ pub mod policies {
|
||||
#[error("Could not decode tenant token, {0}.")]
|
||||
CouldNotDecodeTenantToken(jsonwebtoken::errors::Error),
|
||||
#[error("Invalid action `{0}`.")]
|
||||
InternalInvalidAction(u8),
|
||||
InternalInvalidAction(u32),
|
||||
}
|
||||
|
||||
impl From<jsonwebtoken::errors::Error> for AuthError {
|
||||
@ -214,14 +214,14 @@ pub mod policies {
|
||||
Ok(api_key_uid)
|
||||
}
|
||||
|
||||
fn is_keys_action(action: u8) -> bool {
|
||||
fn is_keys_action(action: u32) -> bool {
|
||||
use actions::*;
|
||||
matches!(action, KEYS_GET | KEYS_CREATE | KEYS_UPDATE | KEYS_DELETE)
|
||||
}
|
||||
|
||||
pub struct ActionPolicy<const A: u8>;
|
||||
pub struct ActionPolicy<const A: u32>;
|
||||
|
||||
impl<const A: u8> Policy for ActionPolicy<A> {
|
||||
impl<const A: u32> Policy for ActionPolicy<A> {
|
||||
/// Attempts to grant authentication from a bearer token (that can be a tenant token or an API key), the requested Action,
|
||||
/// and a list of requested indexes.
|
||||
///
|
||||
@ -255,7 +255,7 @@ pub mod policies {
|
||||
};
|
||||
|
||||
// check that the indexes are allowed
|
||||
let action = Action::from_repr(A).ok_or(AuthError::InternalInvalidAction(A))?;
|
||||
let action = Action::from_bits(A).ok_or(AuthError::InternalInvalidAction(A))?;
|
||||
let auth_filter = auth
|
||||
.get_key_filters(key_uuid, search_rules)
|
||||
.map_err(|_e| AuthError::InvalidApiKey)?;
|
||||
@ -294,7 +294,7 @@ pub mod policies {
|
||||
}
|
||||
}
|
||||
|
||||
impl<const A: u8> ActionPolicy<A> {
|
||||
impl<const A: u32> ActionPolicy<A> {
|
||||
fn authenticate_tenant_token(
|
||||
auth: &AuthController,
|
||||
token: &str,
|
||||
|
@ -1733,46 +1733,51 @@ fn format_fields(
|
||||
// select the attributes to retrieve
|
||||
let displayable_names =
|
||||
displayable_ids.iter().map(|&fid| field_ids_map.name(fid).expect("Missing field name"));
|
||||
permissive_json_pointer::map_leaf_values(&mut document, displayable_names, |key, value| {
|
||||
// To get the formatting option of each key we need to see all the rules that applies
|
||||
// to the value and merge them together. eg. If a user said he wanted to highlight `doggo`
|
||||
// and crop `doggo.name`. `doggo.name` needs to be highlighted + cropped while `doggo.age` is only
|
||||
// highlighted.
|
||||
// Warn: The time to compute the format list scales with the number of fields to format;
|
||||
// cumulated with map_leaf_values that iterates over all the nested fields, it gives a quadratic complexity:
|
||||
// d*f where d is the total number of fields to display and f is the total number of fields to format.
|
||||
let format = formatting_fields_options
|
||||
.iter()
|
||||
.filter(|(name, _option)| {
|
||||
milli::is_faceted_by(name, key) || milli::is_faceted_by(key, name)
|
||||
})
|
||||
.map(|(_, option)| **option)
|
||||
.reduce(|acc, option| acc.merge(option));
|
||||
let mut infos = Vec::new();
|
||||
|
||||
// if no locales has been provided, we try to find the locales in the localized_attributes.
|
||||
let locales = locales.or_else(|| {
|
||||
localized_attributes
|
||||
permissive_json_pointer::map_leaf_values(
|
||||
&mut document,
|
||||
displayable_names,
|
||||
|key, array_indices, value| {
|
||||
// To get the formatting option of each key we need to see all the rules that applies
|
||||
// to the value and merge them together. eg. If a user said he wanted to highlight `doggo`
|
||||
// and crop `doggo.name`. `doggo.name` needs to be highlighted + cropped while `doggo.age` is only
|
||||
// highlighted.
|
||||
// Warn: The time to compute the format list scales with the number of fields to format;
|
||||
// cumulated with map_leaf_values that iterates over all the nested fields, it gives a quadratic complexity:
|
||||
// d*f where d is the total number of fields to display and f is the total number of fields to format.
|
||||
let format = formatting_fields_options
|
||||
.iter()
|
||||
.find(|rule| rule.match_str(key))
|
||||
.map(LocalizedAttributesRule::locales)
|
||||
});
|
||||
.filter(|(name, _option)| {
|
||||
milli::is_faceted_by(name, key) || milli::is_faceted_by(key, name)
|
||||
})
|
||||
.map(|(_, option)| **option)
|
||||
.reduce(|acc, option| acc.merge(option));
|
||||
let mut infos = Vec::new();
|
||||
|
||||
*value = format_value(
|
||||
std::mem::take(value),
|
||||
builder,
|
||||
format,
|
||||
&mut infos,
|
||||
compute_matches,
|
||||
locales,
|
||||
);
|
||||
// if no locales has been provided, we try to find the locales in the localized_attributes.
|
||||
let locales = locales.or_else(|| {
|
||||
localized_attributes
|
||||
.iter()
|
||||
.find(|rule| rule.match_str(key))
|
||||
.map(LocalizedAttributesRule::locales)
|
||||
});
|
||||
|
||||
if let Some(matches) = matches_position.as_mut() {
|
||||
if !infos.is_empty() {
|
||||
matches.insert(key.to_owned(), infos);
|
||||
*value = format_value(
|
||||
std::mem::take(value),
|
||||
builder,
|
||||
format,
|
||||
&mut infos,
|
||||
compute_matches,
|
||||
array_indices,
|
||||
locales,
|
||||
);
|
||||
|
||||
if let Some(matches) = matches_position.as_mut() {
|
||||
if !infos.is_empty() {
|
||||
matches.insert(key.to_owned(), infos);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let selectors = formatted_options
|
||||
.keys()
|
||||
@ -1790,13 +1795,14 @@ fn format_value(
|
||||
format_options: Option<FormatOptions>,
|
||||
infos: &mut Vec<MatchBounds>,
|
||||
compute_matches: bool,
|
||||
array_indices: &[usize],
|
||||
locales: Option<&[Language]>,
|
||||
) -> Value {
|
||||
match value {
|
||||
Value::String(old_string) => {
|
||||
let mut matcher = builder.build(&old_string, locales);
|
||||
if compute_matches {
|
||||
let matches = matcher.matches();
|
||||
let matches = matcher.matches(array_indices);
|
||||
infos.extend_from_slice(&matches[..]);
|
||||
}
|
||||
|
||||
@ -1808,51 +1814,15 @@ fn format_value(
|
||||
None => Value::String(old_string),
|
||||
}
|
||||
}
|
||||
Value::Array(values) => Value::Array(
|
||||
values
|
||||
.into_iter()
|
||||
.map(|v| {
|
||||
format_value(
|
||||
v,
|
||||
builder,
|
||||
format_options.map(|format_options| FormatOptions {
|
||||
highlight: format_options.highlight,
|
||||
crop: None,
|
||||
}),
|
||||
infos,
|
||||
compute_matches,
|
||||
locales,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
Value::Object(object) => Value::Object(
|
||||
object
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k,
|
||||
format_value(
|
||||
v,
|
||||
builder,
|
||||
format_options.map(|format_options| FormatOptions {
|
||||
highlight: format_options.highlight,
|
||||
crop: None,
|
||||
}),
|
||||
infos,
|
||||
compute_matches,
|
||||
locales,
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
// `map_leaf_values` makes sure this is only called for leaf fields
|
||||
Value::Array(_) => unreachable!(),
|
||||
Value::Object(_) => unreachable!(),
|
||||
Value::Number(number) => {
|
||||
let s = number.to_string();
|
||||
|
||||
let mut matcher = builder.build(&s, locales);
|
||||
if compute_matches {
|
||||
let matches = matcher.matches();
|
||||
let matches = matcher.matches(array_indices);
|
||||
infos.extend_from_slice(&matches[..]);
|
||||
}
|
||||
|
||||
|
@ -421,7 +421,7 @@ async fn error_add_api_key_invalid_parameters_actions() {
|
||||
meili_snap::snapshot!(code, @"400 Bad Request");
|
||||
meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###"
|
||||
{
|
||||
"message": "Unknown value `doc.add` at `.actions[0]`: expected one of `*`, `search`, `documents.*`, `documents.add`, `documents.get`, `documents.delete`, `indexes.*`, `indexes.create`, `indexes.get`, `indexes.update`, `indexes.delete`, `indexes.swap`, `tasks.*`, `tasks.cancel`, `tasks.delete`, `tasks.get`, `settings.*`, `settings.get`, `settings.update`, `stats.*`, `stats.get`, `metrics.*`, `metrics.get`, `dumps.*`, `dumps.create`, `snapshots.*`, `snapshots.create`, `version`, `keys.create`, `keys.get`, `keys.update`, `keys.delete`, `experimental.get`, `experimental.update`",
|
||||
"message": "Unknown value `doc.add` at `.actions[0]`: expected one of `search`, `documents.add`, `documents.get`, `documents.delete`, `documents.*`, `indexes.create`, `indexes.get`, `indexes.update`, `indexes.delete`, `indexes.swap`, `indexes.*`, `tasks.cancel`, `tasks.delete`, `tasks.get`, `tasks.*`, `settings.get`, `settings.update`, `settings.*`, `stats.get`, `stats.*`, `metrics.get`, `metrics.*`, `dumps.create`, `dumps.*`, `snapshots.create`, `snapshots.*`, `version`, `keys.create`, `keys.get`, `keys.update`, `keys.delete`, `experimental.get`, `experimental.update`, `*`",
|
||||
"code": "invalid_api_key_actions",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_api_key_actions"
|
||||
|
@ -53,14 +53,14 @@ pub static AUTHORIZATIONS: Lazy<HashMap<(&'static str, &'static str), HashSet<&'
|
||||
("PUT", "/indexes/products/settings/sortable-attributes") => hashset!{"settings.update", "settings.*", "*"},
|
||||
("PUT", "/indexes/products/settings/stop-words") => hashset!{"settings.update", "settings.*", "*"},
|
||||
("PUT", "/indexes/products/settings/synonyms") => hashset!{"settings.update", "settings.*", "*"},
|
||||
("GET", "/indexes/products/stats") => hashset!{"stats.get", "stats.*", "*"},
|
||||
("GET", "/stats") => hashset!{"stats.get", "stats.*", "*"},
|
||||
("POST", "/dumps") => hashset!{"dumps.create", "dumps.*", "*"},
|
||||
("POST", "/snapshots") => hashset!{"snapshots.create", "snapshots.*", "*"},
|
||||
("GET", "/indexes/products/stats") => hashset!{"stats.get", "*"},
|
||||
("GET", "/stats") => hashset!{"stats.get", "*"},
|
||||
("POST", "/dumps") => hashset!{"dumps.create", "*"},
|
||||
("POST", "/snapshots") => hashset!{"snapshots.create", "*"},
|
||||
("GET", "/version") => hashset!{"version", "*"},
|
||||
("GET", "/metrics") => hashset!{"metrics.get", "metrics.*", "*"},
|
||||
("POST", "/logs/stream") => hashset!{"metrics.get", "metrics.*", "*"},
|
||||
("DELETE", "/logs/stream") => hashset!{"metrics.get", "metrics.*", "*"},
|
||||
("GET", "/metrics") => hashset!{"metrics.get", "*"},
|
||||
("POST", "/logs/stream") => hashset!{"metrics.get", "*"},
|
||||
("DELETE", "/logs/stream") => hashset!{"metrics.get", "*"},
|
||||
("PATCH", "/keys/mykey/") => hashset!{"keys.update", "*"},
|
||||
("GET", "/keys/mykey/") => hashset!{"keys.get", "*"},
|
||||
("DELETE", "/keys/mykey/") => hashset!{"keys.delete", "*"},
|
||||
|
@ -93,7 +93,7 @@ async fn create_api_key_bad_actions() {
|
||||
snapshot!(code, @"400 Bad Request");
|
||||
snapshot!(json_string!(response), @r###"
|
||||
{
|
||||
"message": "Unknown value `doggo` at `.actions[0]`: expected one of `*`, `search`, `documents.*`, `documents.add`, `documents.get`, `documents.delete`, `indexes.*`, `indexes.create`, `indexes.get`, `indexes.update`, `indexes.delete`, `indexes.swap`, `tasks.*`, `tasks.cancel`, `tasks.delete`, `tasks.get`, `settings.*`, `settings.get`, `settings.update`, `stats.*`, `stats.get`, `metrics.*`, `metrics.get`, `dumps.*`, `dumps.create`, `snapshots.*`, `snapshots.create`, `version`, `keys.create`, `keys.get`, `keys.update`, `keys.delete`, `experimental.get`, `experimental.update`",
|
||||
"message": "Unknown value `doggo` at `.actions[0]`: expected one of `search`, `documents.add`, `documents.get`, `documents.delete`, `documents.*`, `indexes.create`, `indexes.get`, `indexes.update`, `indexes.delete`, `indexes.swap`, `indexes.*`, `tasks.cancel`, `tasks.delete`, `tasks.get`, `tasks.*`, `settings.get`, `settings.update`, `settings.*`, `stats.get`, `stats.*`, `metrics.get`, `metrics.*`, `dumps.create`, `dumps.*`, `snapshots.create`, `snapshots.*`, `version`, `keys.create`, `keys.get`, `keys.update`, `keys.delete`, `experimental.get`, `experimental.update`, `*`",
|
||||
"code": "invalid_api_key_actions",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#invalid_api_key_actions"
|
||||
|
@ -208,7 +208,10 @@ async fn format_nested() {
|
||||
"doggos.name": [
|
||||
{
|
||||
"start": 0,
|
||||
"length": 5
|
||||
"length": 5,
|
||||
"indices": [
|
||||
0
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -105,6 +105,8 @@ impl FormatOptions {
|
||||
pub struct MatchBounds {
|
||||
pub start: usize,
|
||||
pub length: usize,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub indices: Option<Vec<usize>>,
|
||||
}
|
||||
|
||||
/// Structure used to analyze a string, compute words that match,
|
||||
@ -220,15 +222,20 @@ impl<'t, 'tokenizer> Matcher<'t, 'tokenizer, '_, '_> {
|
||||
}
|
||||
|
||||
/// Returns boundaries of the words that match the query.
|
||||
pub fn matches(&mut self) -> Vec<MatchBounds> {
|
||||
pub fn matches(&mut self, array_indices: &[usize]) -> Vec<MatchBounds> {
|
||||
match &self.matches {
|
||||
None => self.compute_matches().matches(),
|
||||
None => self.compute_matches().matches(array_indices),
|
||||
Some((tokens, matches)) => matches
|
||||
.iter()
|
||||
.map(|m| MatchBounds {
|
||||
start: tokens[m.get_first_token_pos()].byte_start,
|
||||
// TODO: Why is this in chars, while start is in bytes?
|
||||
length: m.char_count,
|
||||
indices: if array_indices.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(array_indices.to_owned())
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ fn contained_in(selector: &str, key: &str) -> bool {
|
||||
/// map_leaf_values(
|
||||
/// value.as_object_mut().unwrap(),
|
||||
/// ["jean.race.name"],
|
||||
/// |key, value| match (value, key) {
|
||||
/// |key, _array_indices, value| match (value, key) {
|
||||
/// (Value::String(name), "jean.race.name") => *name = "patou".to_string(),
|
||||
/// _ => unreachable!(),
|
||||
/// },
|
||||
@ -66,17 +66,18 @@ fn contained_in(selector: &str, key: &str) -> bool {
|
||||
pub fn map_leaf_values<'a>(
|
||||
value: &mut Map<String, Value>,
|
||||
selectors: impl IntoIterator<Item = &'a str>,
|
||||
mut mapper: impl FnMut(&str, &mut Value),
|
||||
mut mapper: impl FnMut(&str, &[usize], &mut Value),
|
||||
) {
|
||||
let selectors: Vec<_> = selectors.into_iter().collect();
|
||||
map_leaf_values_in_object(value, &selectors, "", &mut mapper);
|
||||
map_leaf_values_in_object(value, &selectors, "", &[], &mut mapper);
|
||||
}
|
||||
|
||||
pub fn map_leaf_values_in_object(
|
||||
value: &mut Map<String, Value>,
|
||||
selectors: &[&str],
|
||||
base_key: &str,
|
||||
mapper: &mut impl FnMut(&str, &mut Value),
|
||||
array_indices: &[usize],
|
||||
mapper: &mut impl FnMut(&str, &[usize], &mut Value),
|
||||
) {
|
||||
for (key, value) in value.iter_mut() {
|
||||
let base_key = if base_key.is_empty() {
|
||||
@ -94,12 +95,12 @@ pub fn map_leaf_values_in_object(
|
||||
if should_continue {
|
||||
match value {
|
||||
Value::Object(object) => {
|
||||
map_leaf_values_in_object(object, selectors, &base_key, mapper)
|
||||
map_leaf_values_in_object(object, selectors, &base_key, array_indices, mapper)
|
||||
}
|
||||
Value::Array(array) => {
|
||||
map_leaf_values_in_array(array, selectors, &base_key, mapper)
|
||||
map_leaf_values_in_array(array, selectors, &base_key, array_indices, mapper)
|
||||
}
|
||||
value => mapper(&base_key, value),
|
||||
value => mapper(&base_key, array_indices, value),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -109,13 +110,24 @@ pub fn map_leaf_values_in_array(
|
||||
values: &mut [Value],
|
||||
selectors: &[&str],
|
||||
base_key: &str,
|
||||
mapper: &mut impl FnMut(&str, &mut Value),
|
||||
base_array_indices: &[usize],
|
||||
mapper: &mut impl FnMut(&str, &[usize], &mut Value),
|
||||
) {
|
||||
for value in values.iter_mut() {
|
||||
// This avoids allocating twice
|
||||
let mut array_indices = Vec::with_capacity(base_array_indices.len() + 1);
|
||||
array_indices.extend_from_slice(base_array_indices);
|
||||
array_indices.push(0);
|
||||
|
||||
for (i, value) in values.iter_mut().enumerate() {
|
||||
*array_indices.last_mut().unwrap() = i;
|
||||
match value {
|
||||
Value::Object(object) => map_leaf_values_in_object(object, selectors, base_key, mapper),
|
||||
Value::Array(array) => map_leaf_values_in_array(array, selectors, base_key, mapper),
|
||||
value => mapper(base_key, value),
|
||||
Value::Object(object) => {
|
||||
map_leaf_values_in_object(object, selectors, base_key, &array_indices, mapper)
|
||||
}
|
||||
Value::Array(array) => {
|
||||
map_leaf_values_in_array(array, selectors, base_key, &array_indices, mapper)
|
||||
}
|
||||
value => mapper(base_key, &array_indices, value),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -743,12 +755,14 @@ mod tests {
|
||||
}
|
||||
});
|
||||
|
||||
map_leaf_values(value.as_object_mut().unwrap(), ["jean.race.name"], |key, value| {
|
||||
match (value, key) {
|
||||
map_leaf_values(
|
||||
value.as_object_mut().unwrap(),
|
||||
["jean.race.name"],
|
||||
|key, _, value| match (value, key) {
|
||||
(Value::String(name), "jean.race.name") => *name = S("patou"),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
value,
|
||||
@ -775,7 +789,7 @@ mod tests {
|
||||
});
|
||||
|
||||
let mut calls = 0;
|
||||
map_leaf_values(value.as_object_mut().unwrap(), ["jean"], |key, value| {
|
||||
map_leaf_values(value.as_object_mut().unwrap(), ["jean"], |key, _, value| {
|
||||
calls += 1;
|
||||
match (value, key) {
|
||||
(Value::String(name), "jean.race.name") => *name = S("patou"),
|
||||
@ -798,4 +812,52 @@ mod tests {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_array() {
|
||||
let mut value: Value = json!({
|
||||
"no_array": "peter",
|
||||
"simple": ["foo", "bar"],
|
||||
"nested": [
|
||||
{
|
||||
"a": [
|
||||
["cat", "dog"],
|
||||
["fox", "bear"],
|
||||
],
|
||||
"b": "hi",
|
||||
},
|
||||
{
|
||||
"a": ["green", "blue"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
map_leaf_values(
|
||||
value.as_object_mut().unwrap(),
|
||||
["no_array", "simple", "nested"],
|
||||
|_key, array_indices, value| {
|
||||
*value = format!("{array_indices:?}").into();
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({
|
||||
"no_array": "[]",
|
||||
"simple": ["[0]", "[1]"],
|
||||
"nested": [
|
||||
{
|
||||
"a": [
|
||||
["[0, 0, 0]", "[0, 0, 1]"],
|
||||
["[0, 1, 0]", "[0, 1, 1]"],
|
||||
],
|
||||
"b": "[0]",
|
||||
},
|
||||
{
|
||||
"a": ["[1, 0]", "[1, 1]"],
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user