mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
feat(value): introduce Set storage abstraction (#740)
Add an opaque `Set` newtype paralleling `Object`, living under `src/value/set/` with the same module structure (`mod.rs` / `iter.rs` / `serde.rs`). `Set` wraps `BTreeSet<Value>` today but exposes only a curated surface: `contains`, `insert`, `remove`, `iter`, `iter_sorted`, `cursor` (resumable), `is_subset`, `intersection`, `difference`, serde, and a hand-written `Ord`. The cursor types are re-exported behind the `rvm` feature so the follow-up `IterationState::Set` swap can land additively. To free the `Set` name for the new public type, the crate-internal `BTreeSet as Set` / `HashSet as Set` aliases in `lib.rs` are renamed to `MapSet`. All in-tree consumers of the old alias are updated in lockstep. `Value::Set` is unchanged in this commit (still wraps `Rc<BTreeSet<Value>>`); the payload swap and call-site migration ship in the next PR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
9b42239327
commit
41e1303213
79
docs/value/set.md
Normal file
79
docs/value/set.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Set
|
||||||
|
|
||||||
|
Opaque container for `Value::Set`'s element storage, enabling alternative
|
||||||
|
backends without call-site changes. Pairs with [`Object`](object.md) under
|
||||||
|
a shared design philosophy.
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
`Set` wraps a `BTreeSet<Value>` today but exposes only a curated method
|
||||||
|
surface (`contains`, `insert`, `remove`, `iter`, `iter_sorted`, `cursor`,
|
||||||
|
`is_subset`, `intersection`, `union`, `difference`, serde). The inner set is
|
||||||
|
private — callers cannot pattern-match it or hand out references to the
|
||||||
|
backing store, so the backend can change without churn at the ~400 call
|
||||||
|
sites that name `Set`.
|
||||||
|
|
||||||
|
Two iteration methods reflect a real distinction: `iter()` makes no
|
||||||
|
ordering promise (lets future hash/lazy backends skip sorting work);
|
||||||
|
`iter_sorted()` guarantees deterministic order (used by serialization and
|
||||||
|
`Ord`). Cursor types support incremental traversal needed by the RVM
|
||||||
|
iteration state without exposing iterator internals.
|
||||||
|
|
||||||
|
`Ord` is hand-written against `iter_sorted` rather than derived, so two
|
||||||
|
backends that store elements differently still compare equal when their
|
||||||
|
sorted contents match.
|
||||||
|
|
||||||
|
## Scenarios enabled
|
||||||
|
|
||||||
|
- **Hash-backed storage** — `FxHashSet`-backed inner turns O(log n)
|
||||||
|
membership checks into O(1); swap in for policies where elements aren't
|
||||||
|
compared ordinally.
|
||||||
|
- **Lazy/streaming** — wrap a `LazySetProvider` (DB query, CBOR slice,
|
||||||
|
REST endpoint) and materialize elements on demand.
|
||||||
|
- **Arena allocation** — bumpalo-backed inner for eval-time temporaries;
|
||||||
|
drop the whole arena at query end with zero per-element free cost.
|
||||||
|
- **FFI-backed** — host-language collections (Python set, JS Set) without
|
||||||
|
copying into Rust.
|
||||||
|
- **Bloom-filter pre-check** — front a large backing set with a Bloom
|
||||||
|
filter for fast negative-membership tests on read-mostly allowlists.
|
||||||
|
|
||||||
|
## Known use cases
|
||||||
|
|
||||||
|
- **Azure Policy allowed-values lists** — large allowlists (allowed
|
||||||
|
regions, allowed SKUs, allowed image publishers) compared against
|
||||||
|
single resource values. Hash-backed Set turns O(log n) membership
|
||||||
|
checks into O(1).
|
||||||
|
- **SARIF rule deduplication** — collapsing duplicate rule references
|
||||||
|
across thousands of result records. Set-of-objects with structural
|
||||||
|
hashing avoids the BTreeSet sort cost on every insert.
|
||||||
|
- **RBAC role membership** — checking whether a principal belongs to any
|
||||||
|
of dozens of role groups. Hash-backed Set scales to thousands of
|
||||||
|
members with constant-time membership.
|
||||||
|
- **Azure Policy denied-resource-type sets** — exclusion lists used by
|
||||||
|
deny-effect policies; same hash-backed pattern as allowed-values.
|
||||||
|
|
||||||
|
## Precedents
|
||||||
|
|
||||||
|
- **`indexmap::IndexSet`** — opaque newtype that pairs hash lookup with
|
||||||
|
insertion-order iteration; precedent for "Set with alternative
|
||||||
|
ordering semantics behind a stable surface."
|
||||||
|
- **`hashbrown::HashSet`** — backs Rust's `std::collections::HashSet`
|
||||||
|
and demonstrates a fully swappable backend behind a stable API.
|
||||||
|
- **`roaring::RoaringBitmap`** — bitmap-backed integer set. Not
|
||||||
|
applicable to `Value` keys directly, but a precedent for the broader
|
||||||
|
idea of "Set with alternative storage representations chosen by
|
||||||
|
workload shape."
|
||||||
|
- **`serde_json`** — note that `serde_json` has no Set equivalent: its
|
||||||
|
Value enum collapses sets into arrays. Regorus's first-class Set with
|
||||||
|
storage abstraction is therefore unusually well-positioned among JSON
|
||||||
|
value libraries.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
Cursor types are `pub` (referenced by public `IterationState`) but not
|
||||||
|
re-exported at the crate root. The crate-internal `Set`/`Map`/`MapEntry`
|
||||||
|
aliases for `BTreeSet`/`BTreeMap` in `lib.rs` were renamed to
|
||||||
|
`MapSet`/`Map`/`MapEntry` when this type landed, to free the `Set` name
|
||||||
|
for the new public type. Future Array and String abstractions follow the
|
||||||
|
same shape — see `docs/value/array.md` and `docs/value/string.md` when
|
||||||
|
they land.
|
||||||
@@ -217,7 +217,7 @@ pub(crate) struct CompiledPolicyData {
|
|||||||
pub(crate) default_rules: Map<String, Vec<DefaultRuleInfo>>,
|
pub(crate) default_rules: Map<String, Vec<DefaultRuleInfo>>,
|
||||||
pub(crate) imports: BTreeMap<String, Ref<Expr>>,
|
pub(crate) imports: BTreeMap<String, Ref<Expr>>,
|
||||||
pub(crate) functions: FunctionTable,
|
pub(crate) functions: FunctionTable,
|
||||||
pub(crate) rule_paths: Set<String>,
|
pub(crate) rule_paths: MapSet<String>,
|
||||||
#[cfg(feature = "azure_policy")]
|
#[cfg(feature = "azure_policy")]
|
||||||
pub(crate) target_info: Option<TargetInfo>,
|
pub(crate) target_info: Option<TargetInfo>,
|
||||||
#[cfg(feature = "azure_policy")]
|
#[cfg(feature = "azure_policy")]
|
||||||
|
|||||||
@@ -205,10 +205,10 @@ pub use alloc::sync::Arc as Rc;
|
|||||||
pub use alloc::rc::Rc;
|
pub use alloc::rc::Rc;
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
use std::collections::{hash_map::Entry as MapEntry, HashMap as Map, HashSet as Set};
|
use std::collections::{hash_map::Entry as MapEntry, HashMap as Map, HashSet as MapSet};
|
||||||
|
|
||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
use alloc::collections::{btree_map::Entry as MapEntry, BTreeMap as Map, BTreeSet as Set};
|
use alloc::collections::{btree_map::Entry as MapEntry, BTreeMap as Map, BTreeSet as MapSet};
|
||||||
|
|
||||||
use alloc::{
|
use alloc::{
|
||||||
borrow::ToOwned as _,
|
borrow::ToOwned as _,
|
||||||
|
|||||||
@@ -12,16 +12,22 @@
|
|||||||
)] // value helpers index paths directly for performance
|
)] // value helpers index paths directly for performance
|
||||||
|
|
||||||
mod object;
|
mod object;
|
||||||
|
mod set;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
#[allow(unused_imports)] // surface for downstream PRs
|
#[allow(unused_imports)] // surface for downstream PRs
|
||||||
pub use object::{IntoIter, Iter, IterMut, Object};
|
pub use object::{IntoIter, Iter, IterMut, Object};
|
||||||
|
#[allow(unused_imports)] // surface for downstream PRs
|
||||||
|
pub use set::Set;
|
||||||
|
|
||||||
#[cfg(feature = "rvm")]
|
#[cfg(feature = "rvm")]
|
||||||
#[allow(unused_imports)] // surface for downstream PRs
|
#[allow(unused_imports)] // surface for downstream PRs
|
||||||
pub use object::ObjectCursor;
|
pub use object::ObjectCursor;
|
||||||
|
#[cfg(feature = "rvm")]
|
||||||
|
#[allow(unused_imports)] // surface for downstream PRs
|
||||||
|
pub use set::SetCursor;
|
||||||
|
|
||||||
use crate::number::Number;
|
use crate::number::Number;
|
||||||
|
|
||||||
|
|||||||
103
src/value/set/iter.rs
Normal file
103
src/value/set/iter.rs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
|
||||||
|
//! Opaque iterator types for [`Set`].
|
||||||
|
//!
|
||||||
|
//! These newtypes wrap the storage backend's iterators so the backend can be
|
||||||
|
//! swapped without changing any iterator type signatures observed by callers.
|
||||||
|
|
||||||
|
use alloc::collections::btree_set;
|
||||||
|
use core::iter::FusedIterator;
|
||||||
|
|
||||||
|
use super::Set;
|
||||||
|
use crate::value::Value;
|
||||||
|
|
||||||
|
/// Owned iterator over `Value` elements.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct IntoIter {
|
||||||
|
pub(super) inner: btree_set::IntoIter<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for IntoIter {
|
||||||
|
type Item = Value;
|
||||||
|
#[inline]
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
self.inner.next()
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
|
self.inner.size_hint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DoubleEndedIterator for IntoIter {
|
||||||
|
#[inline]
|
||||||
|
fn next_back(&mut self) -> Option<Self::Item> {
|
||||||
|
self.inner.next_back()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExactSizeIterator for IntoIter {
|
||||||
|
#[inline]
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.inner.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FusedIterator for IntoIter {}
|
||||||
|
|
||||||
|
/// Borrowed iterator over `&Value` elements.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Iter<'a> {
|
||||||
|
pub(super) inner: btree_set::Iter<'a, Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for Iter<'a> {
|
||||||
|
type Item = &'a Value;
|
||||||
|
#[inline]
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
self.inner.next()
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
|
self.inner.size_hint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> DoubleEndedIterator for Iter<'a> {
|
||||||
|
#[inline]
|
||||||
|
fn next_back(&mut self) -> Option<Self::Item> {
|
||||||
|
self.inner.next_back()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ExactSizeIterator for Iter<'a> {
|
||||||
|
#[inline]
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.inner.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> FusedIterator for Iter<'a> {}
|
||||||
|
|
||||||
|
impl IntoIterator for Set {
|
||||||
|
type Item = Value;
|
||||||
|
type IntoIter = IntoIter;
|
||||||
|
#[inline]
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
IntoIter {
|
||||||
|
inner: self.inner.into_iter(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoIterator for &'a Set {
|
||||||
|
type Item = &'a Value;
|
||||||
|
type IntoIter = Iter<'a>;
|
||||||
|
#[inline]
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
Iter {
|
||||||
|
inner: self.inner.iter(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
269
src/value/set/mod.rs
Normal file
269
src/value/set/mod.rs
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
|
||||||
|
//! See [`Set`].
|
||||||
|
|
||||||
|
mod iter;
|
||||||
|
mod serde;
|
||||||
|
|
||||||
|
use alloc::collections::BTreeSet;
|
||||||
|
use core::cmp::Ordering;
|
||||||
|
use core::fmt;
|
||||||
|
use core::ops::Bound;
|
||||||
|
|
||||||
|
use crate::value::Value;
|
||||||
|
|
||||||
|
#[allow(unused_imports)] // surface for downstream PRs
|
||||||
|
pub use iter::{IntoIter, Iter};
|
||||||
|
|
||||||
|
/// Opaque, ordered set of [`Value`]s.
|
||||||
|
///
|
||||||
|
/// The current backing storage is `BTreeSet<Value>`. The inner field is
|
||||||
|
/// private so the representation can change (hash-backed, lazy, bloom-fronted,
|
||||||
|
/// FFI-backed) without touching call sites.
|
||||||
|
///
|
||||||
|
/// # Iteration
|
||||||
|
///
|
||||||
|
/// - [`Set::iter`] — implementation-defined order; non-resumable.
|
||||||
|
/// - [`Set::iter_sorted`] — sorted by `Value::Ord`; non-resumable.
|
||||||
|
/// - [`Set::cursor`] / [`Set::next`] — implementation-defined order,
|
||||||
|
/// resumable; cheapest per-step cost. Used by interpreter/RVM when iteration
|
||||||
|
/// must yield mid-flight.
|
||||||
|
#[derive(Default, Clone, Eq, PartialEq)]
|
||||||
|
pub struct Set {
|
||||||
|
inner: BTreeSet<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Set {
|
||||||
|
/// Create an empty `Set`.
|
||||||
|
#[inline]
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
inner: BTreeSet::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.inner.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.inner.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn contains(&self, value: &Value) -> bool {
|
||||||
|
self.inner.contains(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn get(&self, value: &Value) -> Option<&Value> {
|
||||||
|
self.inner.get(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// First element in sorted order (by `Value::Ord`).
|
||||||
|
#[inline]
|
||||||
|
pub fn first(&self) -> Option<&Value> {
|
||||||
|
self.iter_sorted().next()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Last element in sorted order (by `Value::Ord`).
|
||||||
|
#[inline]
|
||||||
|
pub fn last(&self) -> Option<&Value> {
|
||||||
|
self.iter_sorted().next_back()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iteration in implementation-defined order. Non-resumable.
|
||||||
|
///
|
||||||
|
/// For the current BTree-backed storage this happens to be sorted, but
|
||||||
|
/// callers MUST NOT depend on that. Use [`Set::iter_sorted`] when
|
||||||
|
/// deterministic order is required, or [`Set::cursor`] when iteration
|
||||||
|
/// must yield and resume.
|
||||||
|
#[inline]
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = &Value> + '_ {
|
||||||
|
self.inner.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iteration in sorted order (by `Value::Ord`). Non-resumable.
|
||||||
|
///
|
||||||
|
/// Use this for serialization, snapshots, hashing, `Debug`, etc.
|
||||||
|
#[inline]
|
||||||
|
pub fn iter_sorted(&self) -> Iter<'_> {
|
||||||
|
// BTree backend iterates sorted natively.
|
||||||
|
Iter {
|
||||||
|
inner: self.inner.iter(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert `value`. Returns `true` if the value was newly inserted.
|
||||||
|
#[inline]
|
||||||
|
pub fn insert(&mut self, value: Value) -> bool {
|
||||||
|
self.inner.insert(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn remove(&mut self, value: &Value) -> bool {
|
||||||
|
self.inner.remove(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn retain<F>(&mut self, f: F)
|
||||||
|
where
|
||||||
|
F: FnMut(&Value) -> bool,
|
||||||
|
{
|
||||||
|
self.inner.retain(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.inner.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn append(&mut self, other: &mut Set) {
|
||||||
|
self.inner.append(&mut other.inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set intersection. Returns a new `Set` containing the elements
|
||||||
|
/// present in both `self` and `other`.
|
||||||
|
pub fn intersection(&self, other: &Set) -> Set {
|
||||||
|
Set {
|
||||||
|
inner: self.inner.intersection(&other.inner).cloned().collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set union. Returns a new `Set` containing the elements present in
|
||||||
|
/// either `self` or `other`.
|
||||||
|
pub fn union(&self, other: &Set) -> Set {
|
||||||
|
Set {
|
||||||
|
inner: self.inner.union(&other.inner).cloned().collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set difference. Returns a new `Set` containing the elements present
|
||||||
|
/// in `self` but not in `other`.
|
||||||
|
pub fn difference(&self, other: &Set) -> Set {
|
||||||
|
Set {
|
||||||
|
inner: self.inner.difference(&other.inner).cloned().collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn is_subset(&self, other: &Set) -> bool {
|
||||||
|
self.inner.is_subset(&other.inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrap into a `Value::Set`.
|
||||||
|
#[inline]
|
||||||
|
pub fn into_value(self) -> Value {
|
||||||
|
Value::Set(crate::Rc::new(self.inner))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a resumable cursor over elements in implementation-defined
|
||||||
|
/// order. Stable for the lifetime of `&self`. O(1).
|
||||||
|
///
|
||||||
|
/// The cursor is fully self-owned (it stores a clone of the last-seen
|
||||||
|
/// element, not a reference) so it can be stored as a field of a
|
||||||
|
/// long-lived state struct — e.g. an RVM iteration frame that persists
|
||||||
|
/// across instruction dispatches. As a consequence, mutating the `Set`
|
||||||
|
/// between `next()` calls is not rejected by the borrow checker; the
|
||||||
|
/// resulting iteration order in that case is unspecified.
|
||||||
|
#[inline]
|
||||||
|
pub const fn cursor(&self) -> SetCursor {
|
||||||
|
SetCursor {
|
||||||
|
inner: SetCursorInner::BTree(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Advance `cursor` and yield the next element. O(log n) for the BTree
|
||||||
|
/// backend (range probe); future hash/inline variants may be O(1).
|
||||||
|
pub fn next<'a>(&'a self, cursor: &mut SetCursor) -> Option<&'a Value> {
|
||||||
|
let SetCursorInner::BTree(ref mut last) = cursor.inner;
|
||||||
|
let next = last.as_ref().map_or_else(
|
||||||
|
|| self.inner.iter().next(),
|
||||||
|
|prev| {
|
||||||
|
// `(Bound<&T>, Bound<&T>)` impls `RangeBounds<T>` — no clone
|
||||||
|
// needed to build the resume bound.
|
||||||
|
self.inner
|
||||||
|
.range((Bound::Excluded(prev), Bound::Unbounded))
|
||||||
|
.next()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let v = next?;
|
||||||
|
*last = Some(v.clone());
|
||||||
|
Some(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opaque resumable cursor over a [`Set`]'s elements in
|
||||||
|
/// implementation-defined order.
|
||||||
|
///
|
||||||
|
/// Self-owned: holds no borrow on the `Set`, so it can be stored as a
|
||||||
|
/// field of a long-lived state struct (e.g. an RVM iteration frame).
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SetCursor {
|
||||||
|
inner: SetCursorInner,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum SetCursorInner {
|
||||||
|
/// BTree backend cursor: tracks last-seen element. `None` means "before start".
|
||||||
|
BTree(Option<Value>),
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Hand-written Ord/PartialOrd ----------------------------------------
|
||||||
|
//
|
||||||
|
// Implemented in terms of `iter_sorted()` so ordering is consistent with the
|
||||||
|
// canonical (sorted) view of the elements and is therefore independent of
|
||||||
|
// the storage variant.
|
||||||
|
|
||||||
|
impl Ord for Set {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
self.iter_sorted().cmp(other.iter_sorted())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Set {
|
||||||
|
#[inline]
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Set {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
// Use sorted iteration so Debug output is stable across storage
|
||||||
|
// variants.
|
||||||
|
f.debug_set().entries(self.iter_sorted()).finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Extend<Value> for Set {
|
||||||
|
fn extend<I: IntoIterator<Item = Value>>(&mut self, iter: I) {
|
||||||
|
self.inner.extend(iter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromIterator<Value> for Set {
|
||||||
|
fn from_iter<I: IntoIterator<Item = Value>>(iter: I) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: BTreeSet::from_iter(iter),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BTreeSet<Value>> for Set {
|
||||||
|
#[inline]
|
||||||
|
fn from(set: BTreeSet<Value>) -> Self {
|
||||||
|
Self { inner: set }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Set> for Value {
|
||||||
|
#[inline]
|
||||||
|
fn from(s: Set) -> Self {
|
||||||
|
s.into_value()
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/value/set/serde.rs
Normal file
44
src/value/set/serde.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
|
||||||
|
//! Serde `Serialize`/`Deserialize` impls for [`Set`].
|
||||||
|
|
||||||
|
use core::fmt;
|
||||||
|
|
||||||
|
use serde::de::{Deserialize, Deserializer, Error as _, SeqAccess, Visitor};
|
||||||
|
use serde::ser::{Serialize, Serializer};
|
||||||
|
|
||||||
|
use super::Set;
|
||||||
|
use crate::value::Value;
|
||||||
|
|
||||||
|
impl Serialize for Set {
|
||||||
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
// Sets serialize as JSON arrays. Sorted iteration: canonical output.
|
||||||
|
serializer.collect_seq(self.iter_sorted())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SetVisitor;
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for SetVisitor {
|
||||||
|
type Value = Set;
|
||||||
|
|
||||||
|
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str("a sequence of Values")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_seq<A: SeqAccess<'de>>(self, mut access: A) -> Result<Self::Value, A::Error> {
|
||||||
|
let mut set = Set::new();
|
||||||
|
while let Some(v) = access.next_element::<Value>()? {
|
||||||
|
set.insert(v);
|
||||||
|
crate::utils::limits::check_memory_limit_if_needed().map_err(A::Error::custom)?;
|
||||||
|
}
|
||||||
|
Ok(set)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Set {
|
||||||
|
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
deserializer.deserialize_seq(SetVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,11 +13,11 @@
|
|||||||
clippy::pattern_type_mismatch
|
clippy::pattern_type_mismatch
|
||||||
)]
|
)]
|
||||||
|
|
||||||
use alloc::collections::BTreeMap;
|
use alloc::collections::{BTreeMap, BTreeSet};
|
||||||
use alloc::format;
|
use alloc::format;
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
|
|
||||||
use super::Object;
|
use super::{Object, Set};
|
||||||
use crate::value::Value;
|
use crate::value::Value;
|
||||||
|
|
||||||
fn val(i: u64) -> Value {
|
fn val(i: u64) -> Value {
|
||||||
@@ -562,3 +562,194 @@ fn object_insert_returns_previous_value() {
|
|||||||
assert_eq!(obj.insert(val(0), val(2)), Some(val(1)));
|
assert_eq!(obj.insert(val(0), val(2)), Some(val(1)));
|
||||||
assert_eq!(obj.get(&val(0)), Some(&val(2)));
|
assert_eq!(obj.get(&val(0)), Some(&val(2)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Set tests
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
const SET_SIZES: &[u64] = &[0, 1, 2, 4, 8, 64, 256, 1024];
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_iter_sorted_matches_btreeset_oracle() {
|
||||||
|
for &n in SET_SIZES {
|
||||||
|
let values: Vec<Value> = (0..n).map(val).collect();
|
||||||
|
let oracle: BTreeSet<Value> = values.iter().cloned().collect();
|
||||||
|
let s: Set = values.into_iter().collect();
|
||||||
|
let actual: Vec<&Value> = s.iter_sorted().collect();
|
||||||
|
let expected: Vec<&Value> = oracle.iter().collect();
|
||||||
|
assert_eq!(actual, expected, "size {n}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_iter_multiset_equality_with_oracle() {
|
||||||
|
for &n in SET_SIZES {
|
||||||
|
let values: Vec<Value> = (0..n).map(val).collect();
|
||||||
|
let oracle: BTreeSet<Value> = values.iter().cloned().collect();
|
||||||
|
let s: Set = values.into_iter().collect();
|
||||||
|
let mut a: Vec<Value> = s.iter().cloned().collect();
|
||||||
|
let mut b: Vec<Value> = oracle.iter().cloned().collect();
|
||||||
|
a.sort();
|
||||||
|
b.sort();
|
||||||
|
assert_eq!(a, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_algebra_matches_btreeset() {
|
||||||
|
let a_vals: Vec<Value> = (0..32_u64).map(val).collect();
|
||||||
|
let b_vals: Vec<Value> = (16..48_u64).map(val).collect();
|
||||||
|
let a_btree: BTreeSet<Value> = a_vals.iter().cloned().collect();
|
||||||
|
let b_btree: BTreeSet<Value> = b_vals.iter().cloned().collect();
|
||||||
|
let a: Set = a_vals.into_iter().collect();
|
||||||
|
let b: Set = b_vals.into_iter().collect();
|
||||||
|
|
||||||
|
fn sorted<'a, I: Iterator<Item = &'a Value>>(it: I) -> Vec<&'a Value> {
|
||||||
|
let mut v: Vec<&Value> = it.collect();
|
||||||
|
v.sort();
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
let inter_set = a.intersection(&b);
|
||||||
|
assert_eq!(
|
||||||
|
sorted(inter_set.iter_sorted()),
|
||||||
|
sorted(a_btree.intersection(&b_btree))
|
||||||
|
);
|
||||||
|
|
||||||
|
let union_set = a.union(&b);
|
||||||
|
assert_eq!(
|
||||||
|
sorted(union_set.iter_sorted()),
|
||||||
|
sorted(a_btree.union(&b_btree))
|
||||||
|
);
|
||||||
|
|
||||||
|
let diff_set = a.difference(&b);
|
||||||
|
assert_eq!(
|
||||||
|
sorted(diff_set.iter_sorted()),
|
||||||
|
sorted(a_btree.difference(&b_btree))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Subset: trivial + non-trivial cases.
|
||||||
|
let proper_subset: Set = (0..16_u64).map(val).collect();
|
||||||
|
let non_subset: Set = (30..50_u64).map(val).collect();
|
||||||
|
assert!(a.is_subset(&a));
|
||||||
|
assert!(proper_subset.is_subset(&a));
|
||||||
|
assert!(!non_subset.is_subset(&a));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_first_last() {
|
||||||
|
let s: Set = (0..16_u64).map(val).collect();
|
||||||
|
assert_eq!(s.first(), Some(&val(0)));
|
||||||
|
assert_eq!(s.last(), Some(&val(15)));
|
||||||
|
assert!(Set::new().first().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_serde_roundtrip() {
|
||||||
|
for &n in &[0_u64, 1, 8, 64] {
|
||||||
|
let s: Set = (0..n).map(val).collect();
|
||||||
|
let json = serde_json::to_string(&s).expect("ser");
|
||||||
|
let back: Set = serde_json::from_str(&json).expect("de");
|
||||||
|
assert_eq!(s, back, "size {n}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_append_drains_other() {
|
||||||
|
let mut a: Set = (0..4_u64).map(val).collect();
|
||||||
|
let mut b: Set = (4..8_u64).map(val).collect();
|
||||||
|
a.append(&mut b);
|
||||||
|
assert_eq!(a.len(), 8);
|
||||||
|
assert!(b.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_value_cow_make_mut_isolates_clones() {
|
||||||
|
let a = Value::new_set();
|
||||||
|
let b = a.clone();
|
||||||
|
let mut b_owned = b;
|
||||||
|
b_owned.as_set_mut().expect("set").insert(Value::from("x"));
|
||||||
|
assert_eq!(a.as_set().expect("set").len(), 0);
|
||||||
|
assert_eq!(b_owned.as_set().expect("set").len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_from_iter_dedups_duplicates() {
|
||||||
|
let s: Set = [val(1), val(1), val(2), val(2), val(2)]
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
assert_eq!(s.len(), 2);
|
||||||
|
assert!(s.contains(&val(1)));
|
||||||
|
assert!(s.contains(&val(2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_accessor_coverage() {
|
||||||
|
let mut s: Set = (0..4_u64).map(val).collect();
|
||||||
|
|
||||||
|
assert!(s.contains(&val(2)));
|
||||||
|
assert!(!s.contains(&val(100)));
|
||||||
|
|
||||||
|
assert_eq!(s.get(&val(2)), Some(&val(2)));
|
||||||
|
assert!(s.get(&val(100)).is_none());
|
||||||
|
|
||||||
|
assert!(s.remove(&val(2)));
|
||||||
|
assert!(!s.remove(&val(2)));
|
||||||
|
assert_eq!(s.len(), 3);
|
||||||
|
|
||||||
|
s.retain(|v| v != &val(0));
|
||||||
|
assert!(!s.contains(&val(0)));
|
||||||
|
assert_eq!(s.len(), 2);
|
||||||
|
|
||||||
|
s.clear();
|
||||||
|
assert!(s.is_empty());
|
||||||
|
assert!(!s.contains(&val(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_into_iterator_ref() {
|
||||||
|
let s: Set = (0..4_u64).map(val).collect();
|
||||||
|
let mut count = 0;
|
||||||
|
for _v in &s {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
assert_eq!(count, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_cursor_yields_every_element_once() {
|
||||||
|
for &n in SET_SIZES {
|
||||||
|
let vals: Vec<Value> = (0..n).map(val).collect();
|
||||||
|
let s: Set = vals.clone().into_iter().collect();
|
||||||
|
let mut cursor = s.cursor();
|
||||||
|
let mut collected: Vec<Value> = Vec::new();
|
||||||
|
while let Some(v) = s.next(&mut cursor) {
|
||||||
|
collected.push(v.clone());
|
||||||
|
}
|
||||||
|
let mut a = collected;
|
||||||
|
a.sort();
|
||||||
|
let mut b = vals;
|
||||||
|
b.sort();
|
||||||
|
assert_eq!(a, b, "size {n}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_cursor_empty_returns_none_immediately() {
|
||||||
|
let s = Set::new();
|
||||||
|
let mut c = s.cursor();
|
||||||
|
assert!(s.next(&mut c).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_ord_invariant_to_insertion_order() {
|
||||||
|
let mut a = Set::new();
|
||||||
|
let mut b = Set::new();
|
||||||
|
for i in 0..16_u64 {
|
||||||
|
a.insert(val(i));
|
||||||
|
}
|
||||||
|
for i in (0..16_u64).rev() {
|
||||||
|
b.insert(val(i));
|
||||||
|
}
|
||||||
|
assert_eq!(a.cmp(&b), core::cmp::Ordering::Equal);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user