Compare commits

...

26 Commits

Author SHA1 Message Date
F. Levi
b4bf73d650
Merge 7882819795 into 057fcb3993 2024-11-20 15:56:32 +02:00
Lukas Kalbertodt
057fcb3993
Add indices field to _matchesPosition to specify where in an array a match comes from (#5005)
Some checks are pending
Indexing bench (push) / Run and upload benchmarks (push) Waiting to run
Benchmarks of indexing (push) / Run and upload benchmarks (push) Waiting to run
Benchmarks of search for geo (push) / Run and upload benchmarks (push) Waiting to run
Benchmarks of search for songs (push) / Run and upload benchmarks (push) Waiting to run
Benchmarks of search for Wikipedia articles (push) / Run and upload benchmarks (push) Waiting to run
Run the indexing fuzzer / Setup the action (push) Successful in 1h4m31s
* Remove unreachable code

* Add `indices` field to `MatchBounds`

For matches inside arrays, this field holds the indices of the array
elements that matched. For example, searching for `cat` inside
`{ "a": ["dog", "cat", "fox"] }` would return `indices: [1]`. For nested
arrays, this contains multiple indices, starting with the one for the
top-most array. For matches in fields without arrays, `indices` is not
serialized (does not exist) to save space.
2024-11-20 01:00:43 +01:00
F. Levi
7882819795 Fix erroneous Sequence impl 2024-11-13 15:58:54 +02:00
F. Levi
c2b14b8f8f Fix erroneous Sequence impl 2024-11-13 15:08:55 +02:00
F. Levi
ccd79b07f7 Fix test snapshots 2024-11-13 11:13:27 +02:00
F. Levi
146c87bd6f Make Sequence impl work without changing dumps 2024-11-13 10:59:42 +02:00
F. Levi
7e45125e7a Merge branch 'main' into key-actions-to-bitflags 2024-11-06 11:19:05 +02:00
F. Levi
46fc6bbc2e Merge branch 'main' into key-actions-to-bitflags 2024-10-31 15:31:20 +02:00
F. Levi
ba045a0000 Adapt tests 2024-10-21 11:43:58 +03:00
F. Levi
b89fd49d63 Fix some 'all' flags making problems 2024-10-21 11:12:47 +03:00
F. Levi
544961372c Attempt pushing bad code in order to stop stuck GitHub action 2024-10-21 00:19:33 +03:00
F. Levi
0419b93032 Try fix Sequence impl, refactor, improve 2024-10-20 21:46:39 +03:00
F. Levi
e4745a61a8 Resolve clippy error temporarily 2024-10-20 15:37:13 +03:00
F. Levi
04d86a4d9e Adapt code to new flags structure 2024-10-20 10:18:09 +03:00
F. Levi
e9668eff79 Misc 2024-10-05 13:40:38 +03:00
F. Levi
0403ec0a56 Fix All flag 2024-10-04 14:33:35 +03:00
F. Levi
56b5289b2f Make Actions usable as bitflags 2024-10-04 14:26:49 +03:00
F. Levi
4b8dd6dd6b Merge branch 'main' into key-actions-to-bitflags 2024-10-04 11:45:58 +03:00
F. Levi
3d3ae5aa0c Refactoring 2024-09-18 16:01:53 +03:00
F. Levi
a80bb8f77e Misc 2024-09-18 15:40:17 +03:00
F. Levi
f95bd11db2 Improve deserr error for unknown values 2024-09-18 10:46:05 +03:00
F. Levi
e54fbb0d1e Refactor 2024-09-18 09:54:15 +03:00
F. Levi
b6c5a57932 Add Sequence impl, other changes/adjustments 2024-09-17 23:10:59 +03:00
F. Levi
6688604a93 Add Serialize impl 2024-09-17 12:55:45 +03:00
F. Levi
75969f9f68 Add manual serde Deserialize impl 2024-09-16 21:14:47 +03:00
F. Levi
07e7ddbc5b Change Actions enum to bitflags 2024-09-09 12:14:19 +03:00
12 changed files with 445 additions and 315 deletions

1
Cargo.lock generated
View File

@ -3507,6 +3507,7 @@ version = "1.11.0"
dependencies = [ dependencies = [
"actix-web", "actix-web",
"anyhow", "anyhow",
"bitflags 2.6.0",
"convert_case 0.6.0", "convert_case 0.6.0",
"csv", "csv",
"deserr", "deserr",

View File

@ -105,7 +105,7 @@ impl HeedAuthStore {
let mut actions = HashSet::new(); let mut actions = HashSet::new();
for action in &key.actions { for action in &key.actions {
match action { match *action {
Action::All => actions.extend(enum_iterator::all::<Action>()), Action::All => actions.extend(enum_iterator::all::<Action>()),
Action::DocumentsAll => { Action::DocumentsAll => {
actions.extend( actions.extend(
@ -128,23 +128,11 @@ impl HeedAuthStore {
Action::SettingsAll => { Action::SettingsAll => {
actions.extend([Action::SettingsGet, Action::SettingsUpdate].iter()); actions.extend([Action::SettingsGet, Action::SettingsUpdate].iter());
} }
Action::DumpsAll => {
actions.insert(Action::DumpsCreate);
}
Action::SnapshotsAll => {
actions.insert(Action::SnapshotsCreate);
}
Action::TasksAll => { Action::TasksAll => {
actions.extend([Action::TasksGet, Action::TasksDelete, Action::TasksCancel]); actions.extend([Action::TasksGet, Action::TasksDelete, Action::TasksCancel]);
} }
Action::StatsAll => {
actions.insert(Action::StatsGet);
}
Action::MetricsAll => {
actions.insert(Action::MetricsGet);
}
other => { other => {
actions.insert(*other); actions.insert(other);
} }
} }
} }
@ -293,18 +281,24 @@ impl HeedAuthStore {
/// optionally on a specific index, for a given key. /// optionally on a specific index, for a given key.
pub struct KeyIdActionCodec; 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 { impl<'a> milli::heed::BytesDecode<'a> for KeyIdActionCodec {
type DItem = (KeyId, Action, Option<&'a [u8]>); type DItem = (KeyId, Action, Option<&'a [u8]>);
fn bytes_decode(bytes: &'a [u8]) -> StdResult<Self::DItem, BoxedError> { 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 (key_id_bytes, action_bytes) = try_split_array_at(bytes).ok_or(SliceTooShortError)?;
let (&action_byte, index) = let (action_bits, index) =
match try_split_array_at(action_bytes).ok_or(SliceTooShortError)? { match try_split_array_at::<u8, 4>(action_bytes).ok_or(SliceTooShortError)? {
([action], []) => (action, None), (action_parts, []) => (Self::action_parts_to_32bits(action_parts), None),
([action], index) => (action, Some(index)), (action_parts, index) => (Self::action_parts_to_32bits(action_parts), Some(index)),
}; };
let key_id = Uuid::from_bytes(*key_id_bytes); 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)) Ok((key_id, action, index))
} }
@ -317,7 +311,7 @@ impl<'a> milli::heed::BytesEncode<'a> for KeyIdActionCodec {
let mut bytes = Vec::new(); let mut bytes = Vec::new();
bytes.extend_from_slice(key_id.as_bytes()); 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); bytes.extend_from_slice(&action_bytes);
if let Some(index) = index { if let Some(index) = index {
bytes.extend_from_slice(index); bytes.extend_from_slice(index);
@ -332,9 +326,9 @@ impl<'a> milli::heed::BytesEncode<'a> for KeyIdActionCodec {
pub struct SliceTooShortError; pub struct SliceTooShortError;
#[derive(Error, Debug)] #[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 struct InvalidActionError {
pub action_byte: u8, pub action_bits: u32,
} }
pub fn generate_key_as_hexa(uid: Uuid, master_key: &[u8]) -> String { pub fn generate_key_as_hexa(uid: Uuid, master_key: &[u8]) -> String {

View File

@ -38,6 +38,7 @@ time = { version = "0.3.36", features = [
] } ] }
tokio = "1.38" tokio = "1.38"
uuid = { version = "1.10.0", features = ["serde", "v4"] } uuid = { version = "1.10.0", features = ["serde", "v4"] }
bitflags = "2.6.0"
[dev-dependencies] [dev-dependencies]
insta = "1.39.0" insta = "1.39.0"

View File

@ -2,10 +2,11 @@ use std::convert::Infallible;
use std::hash::Hash; use std::hash::Hash;
use std::str::FromStr; 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 enum_iterator::Sequence;
use milli::update::Setting; use milli::update::Setting;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use time::format_description::well_known::Rfc3339; use time::format_description::well_known::Rfc3339;
use time::macros::{format_description, time}; use time::macros::{format_description, time};
use time::{Date, OffsetDateTime, PrimitiveDateTime}; use time::{Date, OffsetDateTime, PrimitiveDateTime};
@ -179,193 +180,284 @@ fn parse_expiration_date(
} }
} }
#[derive(Copy, Clone, Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Sequence, Deserr)] bitflags! {
#[repr(u8)] #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
pub enum Action { #[repr(transparent)]
#[serde(rename = "*")] // NOTE: For `Sequence` impl to work, the values of these must be in ascending order
#[deserr(rename = "*")] pub struct Action: u32 {
All = 0, const Search = 1;
#[serde(rename = "search")] // Documents
#[deserr(rename = "search")] const DocumentsAdd = 1 << 1;
Search, const DocumentsGet = 1 << 2;
#[serde(rename = "documents.*")] const DocumentsDelete = 1 << 3;
#[deserr(rename = "documents.*")] const DocumentsAll = Self::DocumentsAdd.bits() | Self::DocumentsGet.bits() | Self::DocumentsDelete.bits();
DocumentsAll, // Indexes
#[serde(rename = "documents.add")] const IndexesAdd = 1 << 4;
#[deserr(rename = "documents.add")] const IndexesGet = 1 << 5;
DocumentsAdd, const IndexesUpdate = 1 << 6;
#[serde(rename = "documents.get")] const IndexesDelete = 1 << 7;
#[deserr(rename = "documents.get")] const IndexesSwap = 1 << 8;
DocumentsGet, const IndexesAll = Self::IndexesAdd.bits() | Self::IndexesGet.bits() | Self::IndexesUpdate.bits() | Self::IndexesDelete.bits() | Self::IndexesSwap.bits();
#[serde(rename = "documents.delete")] // Tasks
#[deserr(rename = "documents.delete")] const TasksCancel = 1 << 9;
DocumentsDelete, const TasksDelete = 1 << 10;
#[serde(rename = "indexes.*")] const TasksGet = 1 << 11;
#[deserr(rename = "indexes.*")] const TasksAll = Self::TasksCancel.bits() | Self::TasksDelete.bits() | Self::TasksGet.bits();
IndexesAll, // Settings
#[serde(rename = "indexes.create")] const SettingsGet = 1 << 12;
#[deserr(rename = "indexes.create")] const SettingsUpdate = 1 << 13;
IndexesAdd, const SettingsAll = Self::SettingsGet.bits() | Self::SettingsUpdate.bits();
#[serde(rename = "indexes.get")] // Stats
#[deserr(rename = "indexes.get")] const StatsGet = 1 << 14;
IndexesGet, const StatsAll = Self::StatsGet.bits();
#[serde(rename = "indexes.update")] // Metrics
#[deserr(rename = "indexes.update")] const MetricsGet = 1 << 15;
IndexesUpdate, const MetricsAll = Self::MetricsGet.bits();
#[serde(rename = "indexes.delete")] // Dumps
#[deserr(rename = "indexes.delete")] const DumpsCreate = 1 << 16;
IndexesDelete, const DumpsAll = Self::DumpsCreate.bits();
#[serde(rename = "indexes.swap")] // Snapshots
#[deserr(rename = "indexes.swap")] const SnapshotsCreate = 1 << 17;
IndexesSwap, const SnapshotsAll = Self::SnapshotsCreate.bits();
#[serde(rename = "tasks.*")] // Keys without an "all" version
#[deserr(rename = "tasks.*")] const Version = 1 << 18;
TasksAll, const KeysAdd = 1 << 19;
#[serde(rename = "tasks.cancel")] const KeysGet = 1 << 20;
#[deserr(rename = "tasks.cancel")] const KeysUpdate = 1 << 21;
TasksCancel, const KeysDelete = 1 << 22;
#[serde(rename = "tasks.delete")] const ExperimentalFeaturesGet = 1 << 23;
#[deserr(rename = "tasks.delete")] const ExperimentalFeaturesUpdate = 1 << 24;
TasksDelete, // All
#[serde(rename = "tasks.get")] const All = 0xFFFFFFFF >> (32 - 1 - 24);
#[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,
} }
impl Action { impl Action {
pub const fn from_repr(repr: u8) -> Option<Self> { const SERDE_MAP_ARR: [(&'static str, Self); 34] = [
use actions::*; ("search", Self::Search),
match repr { ("documents.add", Self::DocumentsAdd),
ALL => Some(Self::All), ("documents.get", Self::DocumentsGet),
SEARCH => Some(Self::Search), ("documents.delete", Self::DocumentsDelete),
DOCUMENTS_ALL => Some(Self::DocumentsAll), ("documents.*", Self::DocumentsAll),
DOCUMENTS_ADD => Some(Self::DocumentsAdd), ("indexes.create", Self::IndexesAdd),
DOCUMENTS_GET => Some(Self::DocumentsGet), ("indexes.get", Self::IndexesGet),
DOCUMENTS_DELETE => Some(Self::DocumentsDelete), ("indexes.update", Self::IndexesUpdate),
INDEXES_ALL => Some(Self::IndexesAll), ("indexes.delete", Self::IndexesDelete),
INDEXES_CREATE => Some(Self::IndexesAdd), ("indexes.swap", Self::IndexesSwap),
INDEXES_GET => Some(Self::IndexesGet), ("indexes.*", Self::IndexesAll),
INDEXES_UPDATE => Some(Self::IndexesUpdate), ("tasks.cancel", Self::TasksCancel),
INDEXES_DELETE => Some(Self::IndexesDelete), ("tasks.delete", Self::TasksDelete),
INDEXES_SWAP => Some(Self::IndexesSwap), ("tasks.get", Self::TasksGet),
TASKS_ALL => Some(Self::TasksAll), ("tasks.*", Self::TasksAll),
TASKS_CANCEL => Some(Self::TasksCancel), ("settings.get", Self::SettingsGet),
TASKS_DELETE => Some(Self::TasksDelete), ("settings.update", Self::SettingsUpdate),
TASKS_GET => Some(Self::TasksGet), ("settings.*", Self::SettingsAll),
SETTINGS_ALL => Some(Self::SettingsAll), ("stats.get", Self::StatsGet),
SETTINGS_GET => Some(Self::SettingsGet), ("stats.*", Self::StatsAll),
SETTINGS_UPDATE => Some(Self::SettingsUpdate), ("metrics.get", Self::MetricsGet),
STATS_ALL => Some(Self::StatsAll), ("metrics.*", Self::MetricsAll),
STATS_GET => Some(Self::StatsGet), ("dumps.create", Self::DumpsCreate),
METRICS_ALL => Some(Self::MetricsAll), ("dumps.*", Self::DumpsAll),
METRICS_GET => Some(Self::MetricsGet), ("snapshots.create", Self::SnapshotsCreate),
DUMPS_ALL => Some(Self::DumpsAll), ("snapshots.*", Self::SnapshotsAll),
DUMPS_CREATE => Some(Self::DumpsCreate), ("version", Self::Version),
SNAPSHOTS_CREATE => Some(Self::SnapshotsCreate), ("keys.create", Self::KeysAdd),
VERSION => Some(Self::Version), ("keys.get", Self::KeysGet),
KEYS_CREATE => Some(Self::KeysAdd), ("keys.update", Self::KeysUpdate),
KEYS_GET => Some(Self::KeysGet), ("keys.delete", Self::KeysDelete),
KEYS_UPDATE => Some(Self::KeysUpdate), ("experimental.get", Self::ExperimentalFeaturesGet),
KEYS_DELETE => Some(Self::KeysDelete), ("experimental.update", Self::ExperimentalFeaturesUpdate),
EXPERIMENTAL_FEATURES_GET => Some(Self::ExperimentalFeaturesGet), ("*", Self::All),
EXPERIMENTAL_FEATURES_UPDATE => Some(Self::ExperimentalFeaturesUpdate), ];
_otherwise => None,
} 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 { fn get_action_serde_name(v: &Action) -> &'static str {
*self as u8 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 { pub mod actions {
use super::Action::*; use super::Action as A;
pub(crate) const ALL: u8 = All.repr(); pub const SEARCH: u32 = A::Search.bits();
pub const SEARCH: u8 = Search.repr(); pub const DOCUMENTS_ADD: u32 = A::DocumentsAdd.bits();
pub const DOCUMENTS_ALL: u8 = DocumentsAll.repr(); pub const DOCUMENTS_GET: u32 = A::DocumentsGet.bits();
pub const DOCUMENTS_ADD: u8 = DocumentsAdd.repr(); pub const DOCUMENTS_DELETE: u32 = A::DocumentsDelete.bits();
pub const DOCUMENTS_GET: u8 = DocumentsGet.repr(); pub const DOCUMENTS_ALL: u32 = A::DocumentsAll.bits();
pub const DOCUMENTS_DELETE: u8 = DocumentsDelete.repr(); pub const INDEXES_CREATE: u32 = A::IndexesAdd.bits();
pub const INDEXES_ALL: u8 = IndexesAll.repr(); pub const INDEXES_GET: u32 = A::IndexesGet.bits();
pub const INDEXES_CREATE: u8 = IndexesAdd.repr(); pub const INDEXES_UPDATE: u32 = A::IndexesUpdate.bits();
pub const INDEXES_GET: u8 = IndexesGet.repr(); pub const INDEXES_DELETE: u32 = A::IndexesDelete.bits();
pub const INDEXES_UPDATE: u8 = IndexesUpdate.repr(); pub const INDEXES_SWAP: u32 = A::IndexesSwap.bits();
pub const INDEXES_DELETE: u8 = IndexesDelete.repr(); pub const INDEXES_ALL: u32 = A::IndexesAll.bits();
pub const INDEXES_SWAP: u8 = IndexesSwap.repr(); pub const TASKS_CANCEL: u32 = A::TasksCancel.bits();
pub const TASKS_ALL: u8 = TasksAll.repr(); pub const TASKS_DELETE: u32 = A::TasksDelete.bits();
pub const TASKS_CANCEL: u8 = TasksCancel.repr(); pub const TASKS_GET: u32 = A::TasksGet.bits();
pub const TASKS_DELETE: u8 = TasksDelete.repr(); pub const TASKS_ALL: u32 = A::TasksAll.bits();
pub const TASKS_GET: u8 = TasksGet.repr(); pub const SETTINGS_GET: u32 = A::SettingsGet.bits();
pub const SETTINGS_ALL: u8 = SettingsAll.repr(); pub const SETTINGS_UPDATE: u32 = A::SettingsUpdate.bits();
pub const SETTINGS_GET: u8 = SettingsGet.repr(); pub const SETTINGS_ALL: u32 = A::SettingsAll.bits();
pub const SETTINGS_UPDATE: u8 = SettingsUpdate.repr(); pub const STATS_GET: u32 = A::StatsGet.bits();
pub const STATS_ALL: u8 = StatsAll.repr(); pub const STATS_ALL: u32 = A::StatsAll.bits();
pub const STATS_GET: u8 = StatsGet.repr(); pub const METRICS_GET: u32 = A::MetricsGet.bits();
pub const METRICS_ALL: u8 = MetricsAll.repr(); pub const METRICS_ALL: u32 = A::MetricsAll.bits();
pub const METRICS_GET: u8 = MetricsGet.repr(); pub const DUMPS_CREATE: u32 = A::DumpsCreate.bits();
pub const DUMPS_ALL: u8 = DumpsAll.repr(); pub const DUMPS_ALL: u32 = A::DumpsAll.bits();
pub const DUMPS_CREATE: u8 = DumpsCreate.repr(); pub const SNAPSHOTS_CREATE: u32 = A::SnapshotsCreate.bits();
pub const SNAPSHOTS_CREATE: u8 = SnapshotsCreate.repr(); pub const SNAPSHOTS_ALL: u32 = A::SnapshotsAll.bits();
pub const VERSION: u8 = Version.repr(); pub const VERSION: u32 = A::Version.bits();
pub const KEYS_CREATE: u8 = KeysAdd.repr(); pub const KEYS_CREATE: u32 = A::KeysAdd.bits();
pub const KEYS_GET: u8 = KeysGet.repr(); pub const KEYS_GET: u32 = A::KeysGet.bits();
pub const KEYS_UPDATE: u8 = KeysUpdate.repr(); pub const KEYS_UPDATE: u32 = A::KeysUpdate.bits();
pub const KEYS_DELETE: u8 = KeysDelete.repr(); pub const KEYS_DELETE: u32 = A::KeysDelete.bits();
pub const EXPERIMENTAL_FEATURES_GET: u8 = ExperimentalFeaturesGet.repr(); pub const EXPERIMENTAL_FEATURES_GET: u32 = A::ExperimentalFeaturesGet.bits();
pub const EXPERIMENTAL_FEATURES_UPDATE: u8 = ExperimentalFeaturesUpdate.repr(); 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())
}
} }

View File

@ -171,7 +171,7 @@ pub mod policies {
#[error("Could not decode tenant token, {0}.")] #[error("Could not decode tenant token, {0}.")]
CouldNotDecodeTenantToken(jsonwebtoken::errors::Error), CouldNotDecodeTenantToken(jsonwebtoken::errors::Error),
#[error("Invalid action `{0}`.")] #[error("Invalid action `{0}`.")]
InternalInvalidAction(u8), InternalInvalidAction(u32),
} }
impl From<jsonwebtoken::errors::Error> for AuthError { impl From<jsonwebtoken::errors::Error> for AuthError {
@ -214,14 +214,14 @@ pub mod policies {
Ok(api_key_uid) Ok(api_key_uid)
} }
fn is_keys_action(action: u8) -> bool { fn is_keys_action(action: u32) -> bool {
use actions::*; use actions::*;
matches!(action, KEYS_GET | KEYS_CREATE | KEYS_UPDATE | KEYS_DELETE) 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, /// 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. /// and a list of requested indexes.
/// ///
@ -255,7 +255,7 @@ pub mod policies {
}; };
// check that the indexes are allowed // 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 let auth_filter = auth
.get_key_filters(key_uuid, search_rules) .get_key_filters(key_uuid, search_rules)
.map_err(|_e| AuthError::InvalidApiKey)?; .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( fn authenticate_tenant_token(
auth: &AuthController, auth: &AuthController,
token: &str, token: &str,

View File

@ -1733,46 +1733,51 @@ fn format_fields(
// select the attributes to retrieve // select the attributes to retrieve
let displayable_names = let displayable_names =
displayable_ids.iter().map(|&fid| field_ids_map.name(fid).expect("Missing field name")); 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| { permissive_json_pointer::map_leaf_values(
// To get the formatting option of each key we need to see all the rules that applies &mut document,
// to the value and merge them together. eg. If a user said he wanted to highlight `doggo` displayable_names,
// and crop `doggo.name`. `doggo.name` needs to be highlighted + cropped while `doggo.age` is only |key, array_indices, value| {
// highlighted. // To get the formatting option of each key we need to see all the rules that applies
// Warn: The time to compute the format list scales with the number of fields to format; // to the value and merge them together. eg. If a user said he wanted to highlight `doggo`
// cumulated with map_leaf_values that iterates over all the nested fields, it gives a quadratic complexity: // and crop `doggo.name`. `doggo.name` needs to be highlighted + cropped while `doggo.age` is only
// d*f where d is the total number of fields to display and f is the total number of fields to format. // highlighted.
let format = formatting_fields_options // Warn: The time to compute the format list scales with the number of fields to format;
.iter() // cumulated with map_leaf_values that iterates over all the nested fields, it gives a quadratic complexity:
.filter(|(name, _option)| { // d*f where d is the total number of fields to display and f is the total number of fields to format.
milli::is_faceted_by(name, key) || milli::is_faceted_by(key, name) let format = formatting_fields_options
})
.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
.iter() .iter()
.find(|rule| rule.match_str(key)) .filter(|(name, _option)| {
.map(LocalizedAttributesRule::locales) 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( // if no locales has been provided, we try to find the locales in the localized_attributes.
std::mem::take(value), let locales = locales.or_else(|| {
builder, localized_attributes
format, .iter()
&mut infos, .find(|rule| rule.match_str(key))
compute_matches, .map(LocalizedAttributesRule::locales)
locales, });
);
if let Some(matches) = matches_position.as_mut() { *value = format_value(
if !infos.is_empty() { std::mem::take(value),
matches.insert(key.to_owned(), infos); 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 let selectors = formatted_options
.keys() .keys()
@ -1790,13 +1795,14 @@ fn format_value(
format_options: Option<FormatOptions>, format_options: Option<FormatOptions>,
infos: &mut Vec<MatchBounds>, infos: &mut Vec<MatchBounds>,
compute_matches: bool, compute_matches: bool,
array_indices: &[usize],
locales: Option<&[Language]>, locales: Option<&[Language]>,
) -> Value { ) -> Value {
match value { match value {
Value::String(old_string) => { Value::String(old_string) => {
let mut matcher = builder.build(&old_string, locales); let mut matcher = builder.build(&old_string, locales);
if compute_matches { if compute_matches {
let matches = matcher.matches(); let matches = matcher.matches(array_indices);
infos.extend_from_slice(&matches[..]); infos.extend_from_slice(&matches[..]);
} }
@ -1808,51 +1814,15 @@ fn format_value(
None => Value::String(old_string), None => Value::String(old_string),
} }
} }
Value::Array(values) => Value::Array( // `map_leaf_values` makes sure this is only called for leaf fields
values Value::Array(_) => unreachable!(),
.into_iter() Value::Object(_) => unreachable!(),
.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(),
),
Value::Number(number) => { Value::Number(number) => {
let s = number.to_string(); let s = number.to_string();
let mut matcher = builder.build(&s, locales); let mut matcher = builder.build(&s, locales);
if compute_matches { if compute_matches {
let matches = matcher.matches(); let matches = matcher.matches(array_indices);
infos.extend_from_slice(&matches[..]); infos.extend_from_slice(&matches[..]);
} }

View File

@ -421,7 +421,7 @@ async fn error_add_api_key_invalid_parameters_actions() {
meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(code, @"400 Bad Request");
meili_snap::snapshot!(meili_snap::json_string!(response, { ".createdAt" => "[ignored]", ".updatedAt" => "[ignored]" }), @r###" 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", "code": "invalid_api_key_actions",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid_api_key_actions" "link": "https://docs.meilisearch.com/errors#invalid_api_key_actions"

View File

@ -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/sortable-attributes") => hashset!{"settings.update", "settings.*", "*"},
("PUT", "/indexes/products/settings/stop-words") => hashset!{"settings.update", "settings.*", "*"}, ("PUT", "/indexes/products/settings/stop-words") => hashset!{"settings.update", "settings.*", "*"},
("PUT", "/indexes/products/settings/synonyms") => hashset!{"settings.update", "settings.*", "*"}, ("PUT", "/indexes/products/settings/synonyms") => hashset!{"settings.update", "settings.*", "*"},
("GET", "/indexes/products/stats") => hashset!{"stats.get", "stats.*", "*"}, ("GET", "/indexes/products/stats") => hashset!{"stats.get", "*"},
("GET", "/stats") => hashset!{"stats.get", "stats.*", "*"}, ("GET", "/stats") => hashset!{"stats.get", "*"},
("POST", "/dumps") => hashset!{"dumps.create", "dumps.*", "*"}, ("POST", "/dumps") => hashset!{"dumps.create", "*"},
("POST", "/snapshots") => hashset!{"snapshots.create", "snapshots.*", "*"}, ("POST", "/snapshots") => hashset!{"snapshots.create", "*"},
("GET", "/version") => hashset!{"version", "*"}, ("GET", "/version") => hashset!{"version", "*"},
("GET", "/metrics") => hashset!{"metrics.get", "metrics.*", "*"}, ("GET", "/metrics") => hashset!{"metrics.get", "*"},
("POST", "/logs/stream") => hashset!{"metrics.get", "metrics.*", "*"}, ("POST", "/logs/stream") => hashset!{"metrics.get", "*"},
("DELETE", "/logs/stream") => hashset!{"metrics.get", "metrics.*", "*"}, ("DELETE", "/logs/stream") => hashset!{"metrics.get", "*"},
("PATCH", "/keys/mykey/") => hashset!{"keys.update", "*"}, ("PATCH", "/keys/mykey/") => hashset!{"keys.update", "*"},
("GET", "/keys/mykey/") => hashset!{"keys.get", "*"}, ("GET", "/keys/mykey/") => hashset!{"keys.get", "*"},
("DELETE", "/keys/mykey/") => hashset!{"keys.delete", "*"}, ("DELETE", "/keys/mykey/") => hashset!{"keys.delete", "*"},

View File

@ -93,7 +93,7 @@ async fn create_api_key_bad_actions() {
snapshot!(code, @"400 Bad Request"); snapshot!(code, @"400 Bad Request");
snapshot!(json_string!(response), @r###" 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", "code": "invalid_api_key_actions",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#invalid_api_key_actions" "link": "https://docs.meilisearch.com/errors#invalid_api_key_actions"

View File

@ -208,7 +208,10 @@ async fn format_nested() {
"doggos.name": [ "doggos.name": [
{ {
"start": 0, "start": 0,
"length": 5 "length": 5,
"indices": [
0
]
} }
] ]
} }

View File

@ -105,6 +105,8 @@ impl FormatOptions {
pub struct MatchBounds { pub struct MatchBounds {
pub start: usize, pub start: usize,
pub length: 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, /// 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. /// 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 { match &self.matches {
None => self.compute_matches().matches(), None => self.compute_matches().matches(array_indices),
Some((tokens, matches)) => matches Some((tokens, matches)) => matches
.iter() .iter()
.map(|m| MatchBounds { .map(|m| MatchBounds {
start: tokens[m.get_first_token_pos()].byte_start, start: tokens[m.get_first_token_pos()].byte_start,
// TODO: Why is this in chars, while start is in bytes? // TODO: Why is this in chars, while start is in bytes?
length: m.char_count, length: m.char_count,
indices: if array_indices.is_empty() {
None
} else {
Some(array_indices.to_owned())
},
}) })
.collect(), .collect(),
} }

View File

@ -45,7 +45,7 @@ fn contained_in(selector: &str, key: &str) -> bool {
/// map_leaf_values( /// map_leaf_values(
/// value.as_object_mut().unwrap(), /// value.as_object_mut().unwrap(),
/// ["jean.race.name"], /// ["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(), /// (Value::String(name), "jean.race.name") => *name = "patou".to_string(),
/// _ => unreachable!(), /// _ => unreachable!(),
/// }, /// },
@ -66,17 +66,18 @@ fn contained_in(selector: &str, key: &str) -> bool {
pub fn map_leaf_values<'a>( pub fn map_leaf_values<'a>(
value: &mut Map<String, Value>, value: &mut Map<String, Value>,
selectors: impl IntoIterator<Item = &'a str>, 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(); 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( pub fn map_leaf_values_in_object(
value: &mut Map<String, Value>, value: &mut Map<String, Value>,
selectors: &[&str], selectors: &[&str],
base_key: &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() { for (key, value) in value.iter_mut() {
let base_key = if base_key.is_empty() { let base_key = if base_key.is_empty() {
@ -94,12 +95,12 @@ pub fn map_leaf_values_in_object(
if should_continue { if should_continue {
match value { match value {
Value::Object(object) => { 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) => { 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], values: &mut [Value],
selectors: &[&str], selectors: &[&str],
base_key: &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 { match value {
Value::Object(object) => map_leaf_values_in_object(object, selectors, base_key, mapper), Value::Object(object) => {
Value::Array(array) => map_leaf_values_in_array(array, selectors, base_key, mapper), map_leaf_values_in_object(object, selectors, base_key, &array_indices, mapper)
value => mapper(base_key, value), }
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| { map_leaf_values(
match (value, key) { value.as_object_mut().unwrap(),
["jean.race.name"],
|key, _, value| match (value, key) {
(Value::String(name), "jean.race.name") => *name = S("patou"), (Value::String(name), "jean.race.name") => *name = S("patou"),
_ => unreachable!(), _ => unreachable!(),
} },
}); );
assert_eq!( assert_eq!(
value, value,
@ -775,7 +789,7 @@ mod tests {
}); });
let mut calls = 0; 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; calls += 1;
match (value, key) { match (value, key) {
(Value::String(name), "jean.race.name") => *name = S("patou"), (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]"],
},
],
})
);
}
} }