Skip to main content

nemo_flow/
registry.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Priority-sorted named registry.
5//!
6//! [`SortedRegistry`] is the backbone data structure for all guardrail and intercept
7//! registries in the NeMo Flow runtime. It stores entries by unique name and provides
8//! iteration in ascending priority order, with eager re-sorting on every mutation.
9
10use std::collections::HashMap;
11
12/// A named registry that maintains a sorted order by priority.
13///
14/// Items are stored by unique string name and sorted by an integer priority
15/// extracted via a caller-provided function. The sort is performed eagerly:
16/// every [`register`](SortedRegistry::register) or
17/// [`deregister`](SortedRegistry::deregister) call re-sorts immediately, so
18/// [`sorted_values`](SortedRegistry::sorted_values) is a read-only lookup.
19///
20/// # Priority ordering
21///
22/// Entries are sorted in **ascending** priority order (lower numbers run first).
23/// This means a guardrail with priority `1` executes before one with priority `10`.
24///
25/// # Uniqueness
26///
27/// Names must be unique within a registry. Attempting to [`register`](SortedRegistry::register)
28/// a duplicate name returns an error. Use [`deregister`](SortedRegistry::deregister) first
29/// to remove an existing entry before re-registering.
30pub struct SortedRegistry<T> {
31    entries: HashMap<String, T>,
32    sorted_keys: Vec<String>,
33    priority_fn: fn(&T) -> i32,
34}
35
36impl<T> SortedRegistry<T> {
37    /// Create a new empty registry with the given priority extraction function.
38    ///
39    /// The runtime calls `priority_fn` on each stored entry to determine its
40    /// sort key. Lower values are ordered first.
41    ///
42    /// # Parameters
43    /// - `priority_fn`: Function used to extract the integer priority from a
44    ///   stored entry.
45    ///
46    /// # Returns
47    /// A new empty [`SortedRegistry`] with no entries.
48    pub fn new(priority_fn: fn(&T) -> i32) -> Self {
49        Self {
50            entries: HashMap::new(),
51            sorted_keys: Vec::new(),
52            priority_fn,
53        }
54    }
55
56    /// Re-sorts the cached key order by priority. Called eagerly on every mutation.
57    fn resort(&mut self) {
58        let pf = self.priority_fn;
59        let entries = &self.entries;
60        let mut keys: Vec<String> = entries.keys().cloned().collect();
61        keys.sort_by_key(|k| pf(entries.get(k).unwrap()));
62        self.sorted_keys = keys;
63    }
64
65    /// Register a new entry under a unique name.
66    ///
67    /// # Parameters
68    /// - `name`: Unique name used to address the entry later.
69    /// - `entry`: Value to store in the registry.
70    ///
71    /// # Returns
72    /// `Ok(())` when the entry was inserted.
73    ///
74    /// # Errors
75    /// Returns `Err(String)` when `name` is already present in the registry.
76    ///
77    /// # Notes
78    /// Successful registration eagerly re-sorts the cached priority order.
79    pub fn register(&mut self, name: String, entry: T) -> Result<(), String> {
80        if self.entries.contains_key(&name) {
81            return Err(format!("{name} already exists"));
82        }
83        self.entries.insert(name, entry);
84        self.resort();
85        Ok(())
86    }
87
88    /// Deregister an entry by name.
89    ///
90    /// # Parameters
91    /// - `name`: Name of the entry to remove.
92    ///
93    /// # Returns
94    /// `true` when an entry was removed and `false` when `name` was not
95    /// present.
96    ///
97    /// # Notes
98    /// Successful removal eagerly re-sorts the cached priority order.
99    pub fn deregister(&mut self, name: &str) -> bool {
100        if self.entries.remove(name).is_some() {
101            self.resort();
102            true
103        } else {
104            false
105        }
106    }
107
108    /// Return entries sorted by priority (ascending).
109    ///
110    /// This is a read-only operation — the sort order is maintained eagerly
111    /// on every [`register`](SortedRegistry::register) / [`deregister`](SortedRegistry::deregister) call.
112    ///
113    /// # Returns
114    /// A newly allocated [`Vec`] of shared references ordered from lowest
115    /// priority to highest priority.
116    pub fn sorted_values(&self) -> Vec<&T> {
117        self.sorted_keys
118            .iter()
119            .filter_map(|k| self.entries.get(k))
120            .collect()
121    }
122
123    /// Return a shared reference to an entry by name.
124    ///
125    /// # Parameters
126    /// - `name`: Name of the entry to resolve.
127    ///
128    /// # Returns
129    /// `Some(&T)` when an entry exists under `name`, otherwise `None`.
130    pub fn get(&self, name: &str) -> Option<&T> {
131        self.entries.get(name)
132    }
133
134    /// Remove and return an entry by name.
135    ///
136    /// # Parameters
137    /// - `name`: Name of the entry to remove.
138    ///
139    /// # Returns
140    /// `Some(T)` when an entry was removed, otherwise `None`.
141    ///
142    /// # Notes
143    /// Successful removal eagerly re-sorts the cached priority order.
144    pub fn remove(&mut self, name: &str) -> Option<T> {
145        let removed = self.entries.remove(name);
146        if removed.is_some() {
147            self.resort();
148        }
149        removed
150    }
151
152    /// Report whether an entry with the given name exists.
153    ///
154    /// # Parameters
155    /// - `name`: Name to test for membership.
156    ///
157    /// # Returns
158    /// `true` when the registry contains `name`, otherwise `false`.
159    pub fn contains(&self, name: &str) -> bool {
160        self.entries.contains_key(name)
161    }
162}
163
164#[cfg(test)]
165#[path = "../tests/unit/registry_tests.rs"]
166mod tests;