Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions docs/value/set.md
Original file line number Diff line number Diff line change
@@ -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
Comment thread
anakrish marked this conversation as resolved.
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.
2 changes: 1 addition & 1 deletion src/compiled_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ pub(crate) struct CompiledPolicyData {
pub(crate) default_rules: Map<String, Vec<DefaultRuleInfo>>,
pub(crate) imports: BTreeMap<String, Ref<Expr>>,
pub(crate) functions: FunctionTable,
pub(crate) rule_paths: Set<String>,
pub(crate) rule_paths: MapSet<String>,
#[cfg(feature = "azure_policy")]
pub(crate) target_info: Option<TargetInfo>,
#[cfg(feature = "azure_policy")]
Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,10 @@ pub use alloc::sync::Arc as Rc;
pub use alloc::rc::Rc;

#[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"))]
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::{
borrow::ToOwned as _,
Expand Down
6 changes: 6 additions & 0 deletions src/value/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,22 @@
)] // value helpers index paths directly for performance

mod object;
mod set;

#[cfg(test)]
mod tests;

#[allow(unused_imports)] // surface for downstream PRs
pub use object::{IntoIter, Iter, IterMut, Object};
#[allow(unused_imports)] // surface for downstream PRs
pub use set::Set;

#[cfg(feature = "rvm")]
#[allow(unused_imports)] // surface for downstream PRs
pub use object::ObjectCursor;
#[cfg(feature = "rvm")]
#[allow(unused_imports)] // surface for downstream PRs
pub use set::SetCursor;

use crate::number::Number;

Expand Down
103 changes: 103 additions & 0 deletions src/value/set/iter.rs
Original file line number Diff line number Diff line change
@@ -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(),
}
}
}
Loading
Loading