Skip to content

Commit 70882a8

Browse files
anakrishCopilot
andcommitted
feat(value): introduce Set storage abstraction
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>
1 parent 6a3b744 commit 70882a8

8 files changed

Lines changed: 697 additions & 5 deletions

File tree

docs/value/set.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Set
2+
3+
Opaque container for `Value::Set`'s element storage, enabling alternative
4+
backends without call-site changes. Pairs with [`Object`](object.md) under
5+
a shared design philosophy.
6+
7+
## Design
8+
9+
`Set` wraps a `BTreeSet<Value>` today but exposes only a curated method
10+
surface (`contains`, `insert`, `remove`, `iter`, `iter_sorted`, `cursor`,
11+
`is_subset`, `intersection`, `difference`, serde). The inner set is
12+
private — callers cannot pattern-match it or hand out references to the
13+
backing store, so the backend can change without churn at the ~400 call
14+
sites that name `Set`.
15+
16+
Two iteration methods reflect a real distinction: `iter()` makes no
17+
ordering promise (lets future hash/lazy backends skip sorting work);
18+
`iter_sorted()` guarantees deterministic order (used by serialization and
19+
`Ord`). Cursor types support incremental traversal needed by the RVM
20+
iteration state without exposing iterator internals.
21+
22+
`Ord` is hand-written against `iter_sorted` rather than derived, so two
23+
backends that store elements differently still compare equal when their
24+
sorted contents match.
25+
26+
## Scenarios enabled
27+
28+
- **Hash-backed storage**`FxHashSet`-backed inner turns O(log n)
29+
membership checks into O(1); swap in for policies where elements aren't
30+
compared ordinally.
31+
- **Lazy/streaming** — wrap a `LazySetProvider` (DB query, CBOR slice,
32+
REST endpoint) and materialize elements on demand.
33+
- **Arena allocation** — bumpalo-backed inner for eval-time temporaries;
34+
drop the whole arena at query end with zero per-element free cost.
35+
- **FFI-backed** — host-language collections (Python set, JS Set) without
36+
copying into Rust.
37+
- **Bloom-filter pre-check** — front a large backing set with a Bloom
38+
filter for fast negative-membership tests on read-mostly allowlists.
39+
40+
## Known use cases
41+
42+
- **Azure Policy allowed-values lists** — large allowlists (allowed
43+
regions, allowed SKUs, allowed image publishers) compared against
44+
single resource values. Hash-backed Set turns O(log n) membership
45+
checks into O(1).
46+
- **SARIF rule deduplication** — collapsing duplicate rule references
47+
across thousands of result records. Set-of-objects with structural
48+
hashing avoids the BTreeSet sort cost on every insert.
49+
- **RBAC role membership** — checking whether a principal belongs to any
50+
of dozens of role groups. Hash-backed Set scales to thousands of
51+
members with constant-time membership.
52+
- **Azure Policy denied-resource-type sets** — exclusion lists used by
53+
deny-effect policies; same hash-backed pattern as allowed-values.
54+
55+
## Precedents
56+
57+
- **`indexmap::IndexSet`** — opaque newtype that pairs hash lookup with
58+
insertion-order iteration; precedent for "Set with alternative
59+
ordering semantics behind a stable surface."
60+
- **`hashbrown::HashSet`** — backs Rust's `std::collections::HashSet`
61+
and demonstrates a fully swappable backend behind a stable API.
62+
- **`roaring::RoaringBitmap`** — bitmap-backed integer set. Not
63+
applicable to `Value` keys directly, but a precedent for the broader
64+
idea of "Set with alternative storage representations chosen by
65+
workload shape."
66+
- **`serde_json`** — note that `serde_json` has no Set equivalent: its
67+
Value enum collapses sets into arrays. Regorus's first-class Set with
68+
storage abstraction is therefore unusually well-positioned among JSON
69+
value libraries.
70+
71+
## Notes
72+
73+
Cursor types are `pub` (referenced by public `IterationState`) but not
74+
re-exported at the crate root. The crate-internal `Set`/`Map`/`MapEntry`
75+
aliases for `BTreeSet`/`BTreeMap` in `lib.rs` were renamed to
76+
`MapSet`/`Map`/`MapEntry` when this type landed, to free the `Set` name
77+
for the new public type. Future Array and String abstractions follow the
78+
same shape — see `docs/value/array.md` and `docs/value/string.md` when
79+
they land.

src/compiled_policy.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ pub(crate) struct CompiledPolicyData {
217217
pub(crate) default_rules: Map<String, Vec<DefaultRuleInfo>>,
218218
pub(crate) imports: BTreeMap<String, Ref<Expr>>,
219219
pub(crate) functions: FunctionTable,
220-
pub(crate) rule_paths: Set<String>,
220+
pub(crate) rule_paths: MapSet<String>,
221221
#[cfg(feature = "azure_policy")]
222222
pub(crate) target_info: Option<TargetInfo>,
223223
#[cfg(feature = "azure_policy")]

src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,10 +205,10 @@ pub use alloc::sync::Arc as Rc;
205205
pub use alloc::rc::Rc;
206206

207207
#[cfg(feature = "std")]
208-
use std::collections::{hash_map::Entry as MapEntry, HashMap as Map, HashSet as Set};
208+
use std::collections::{hash_map::Entry as MapEntry, HashMap as Map, HashSet as MapSet};
209209

210210
#[cfg(not(feature = "std"))]
211-
use alloc::collections::{btree_map::Entry as MapEntry, BTreeMap as Map, BTreeSet as Set};
211+
use alloc::collections::{btree_map::Entry as MapEntry, BTreeMap as Map, BTreeSet as MapSet};
212212

213213
use alloc::{
214214
borrow::ToOwned as _,

src/value/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,22 @@
1212
)] // value helpers index paths directly for performance
1313

1414
mod object;
15+
mod set;
1516

1617
#[cfg(test)]
1718
mod tests;
1819

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

2225
#[cfg(feature = "rvm")]
2326
#[allow(unused_imports)] // surface for downstream PRs
2427
pub use object::ObjectCursor;
28+
#[cfg(feature = "rvm")]
29+
#[allow(unused_imports)] // surface for downstream PRs
30+
pub use set::SetCursor;
2531

2632
use crate::number::Number;
2733

src/value/set/iter.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
//! Opaque iterator types for [`Set`].
5+
//!
6+
//! These newtypes wrap the storage backend's iterators so the backend can be
7+
//! swapped without changing any iterator type signatures observed by callers.
8+
9+
use alloc::collections::btree_set;
10+
use core::iter::FusedIterator;
11+
12+
use super::Set;
13+
use crate::value::Value;
14+
15+
/// Owned iterator over `Value` elements.
16+
#[derive(Debug)]
17+
pub struct IntoIter {
18+
pub(super) inner: btree_set::IntoIter<Value>,
19+
}
20+
21+
impl Iterator for IntoIter {
22+
type Item = Value;
23+
#[inline]
24+
fn next(&mut self) -> Option<Self::Item> {
25+
self.inner.next()
26+
}
27+
#[inline]
28+
fn size_hint(&self) -> (usize, Option<usize>) {
29+
self.inner.size_hint()
30+
}
31+
}
32+
33+
impl DoubleEndedIterator for IntoIter {
34+
#[inline]
35+
fn next_back(&mut self) -> Option<Self::Item> {
36+
self.inner.next_back()
37+
}
38+
}
39+
40+
impl ExactSizeIterator for IntoIter {
41+
#[inline]
42+
fn len(&self) -> usize {
43+
self.inner.len()
44+
}
45+
}
46+
47+
impl FusedIterator for IntoIter {}
48+
49+
/// Borrowed iterator over `&Value` elements.
50+
#[derive(Debug, Clone)]
51+
pub struct Iter<'a> {
52+
pub(super) inner: btree_set::Iter<'a, Value>,
53+
}
54+
55+
impl<'a> Iterator for Iter<'a> {
56+
type Item = &'a Value;
57+
#[inline]
58+
fn next(&mut self) -> Option<Self::Item> {
59+
self.inner.next()
60+
}
61+
#[inline]
62+
fn size_hint(&self) -> (usize, Option<usize>) {
63+
self.inner.size_hint()
64+
}
65+
}
66+
67+
impl<'a> DoubleEndedIterator for Iter<'a> {
68+
#[inline]
69+
fn next_back(&mut self) -> Option<Self::Item> {
70+
self.inner.next_back()
71+
}
72+
}
73+
74+
impl<'a> ExactSizeIterator for Iter<'a> {
75+
#[inline]
76+
fn len(&self) -> usize {
77+
self.inner.len()
78+
}
79+
}
80+
81+
impl<'a> FusedIterator for Iter<'a> {}
82+
83+
impl IntoIterator for Set {
84+
type Item = Value;
85+
type IntoIter = IntoIter;
86+
#[inline]
87+
fn into_iter(self) -> Self::IntoIter {
88+
IntoIter {
89+
inner: self.inner.into_iter(),
90+
}
91+
}
92+
}
93+
94+
impl<'a> IntoIterator for &'a Set {
95+
type Item = &'a Value;
96+
type IntoIter = Iter<'a>;
97+
#[inline]
98+
fn into_iter(self) -> Self::IntoIter {
99+
Iter {
100+
inner: self.inner.iter(),
101+
}
102+
}
103+
}

0 commit comments

Comments
 (0)