async_snmp/handler/traits.rs
1//! MibHandler trait and related types.
2
3use std::future::Future;
4use std::pin::Pin;
5
6use crate::oid::Oid;
7use crate::value::Value;
8
9use super::{GetNextResult, GetResult, RequestContext, SetResult};
10
11/// Type alias for boxed async return type (dyn-compatible).
12///
13/// This type is required because async trait methods cannot be object-safe.
14/// All handler methods return `BoxFuture` to allow handlers to be stored
15/// as trait objects in the agent.
16///
17/// # Example
18///
19/// ```rust
20/// use async_snmp::handler::{BoxFuture, GetResult};
21///
22/// fn example_async_fn<'a>(value: &'a i32) -> BoxFuture<'a, GetResult> {
23/// Box::pin(async move {
24/// // Async work here
25/// GetResult::Value(async_snmp::Value::Integer(*value))
26/// })
27/// }
28/// ```
29pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
30
31/// Handler for SNMP MIB operations.
32///
33/// Implement this trait to provide values for a subtree of OIDs.
34/// Register handlers with [`AgentBuilder::handler()`](crate::agent::AgentBuilder::handler)
35/// using a prefix OID.
36///
37/// # Required Methods
38///
39/// - [`get`](MibHandler::get): Handle GET requests for specific OIDs
40/// - [`get_next`](MibHandler::get_next): Handle GETNEXT/GETBULK requests
41///
42/// # Optional Methods
43///
44/// - [`test_set`](MibHandler::test_set): Validate SET operations (default: read-only)
45/// - [`commit_set`](MibHandler::commit_set): Apply SET operations (default: read-only)
46/// - [`undo_set`](MibHandler::undo_set): Rollback failed SET operations
47/// - [`handles`](MibHandler::handles): Custom OID matching logic
48///
49/// # GET Implementation
50///
51/// The [`get`](MibHandler::get) method should return:
52/// - [`GetResult::Value`] if the OID exists and has a value
53/// - [`GetResult::NoSuchObject`] if the object type is not implemented
54/// - [`GetResult::NoSuchInstance`] if the object exists but this instance doesn't
55///
56/// # GETNEXT and Lexicographic Ordering
57///
58/// The [`get_next`](MibHandler::get_next) method must return the lexicographically
59/// next OID after the requested one. OIDs are compared arc-by-arc as unsigned integers.
60/// For example: `1.3.6.1.2` < `1.3.6.1.2.1` < `1.3.6.1.3`.
61///
62/// Key considerations:
63/// - The returned OID must be strictly greater than the input OID
64/// - GETBULK uses GETNEXT repeatedly, so efficient implementation matters
65/// - Use [`OidTable`](super::OidTable) to simplify sorted OID management
66///
67/// # SET Two-Phase Commit (RFC 3416)
68///
69/// SET operations use a two-phase commit protocol for atomicity:
70///
71/// 1. **Test phase**: [`test_set`](MibHandler::test_set) is called for ALL varbinds
72/// before any commits. If any test fails, no changes are made.
73///
74/// 2. **Commit phase**: [`commit_set`](MibHandler::commit_set) is called for each
75/// varbind in order. If a commit fails, [`undo_set`](MibHandler::undo_set) is
76/// called for all previously committed varbinds (in reverse order).
77///
78/// By default, handlers are read-only and return [`SetResult::NotWritable`].
79///
80/// # Bounds
81///
82/// The `'static` bound is required because handlers are stored as
83/// `Arc<dyn MibHandler>` within the agent. This allows the agent to
84/// hold handlers for its entire lifetime without lifetime annotations.
85/// In practice, most handlers naturally satisfy this bound.
86///
87/// # Thread Safety
88///
89/// Handlers must be `Send + Sync` because the agent may process
90/// requests concurrently from multiple tasks.
91///
92/// # Example: Read-Only Handler
93///
94/// ```rust
95/// use async_snmp::handler::{MibHandler, RequestContext, GetResult, GetNextResult, BoxFuture};
96/// use async_snmp::{Oid, Value, VarBind, oid};
97///
98/// struct SystemInfoHandler {
99/// sys_descr: String,
100/// sys_uptime: u32,
101/// }
102///
103/// impl MibHandler for SystemInfoHandler {
104/// fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
105/// Box::pin(async move {
106/// // sysDescr.0
107/// if oid == &oid!(1, 3, 6, 1, 2, 1, 1, 1, 0) {
108/// return GetResult::Value(Value::OctetString(self.sys_descr.clone().into()));
109/// }
110/// // sysUpTime.0
111/// if oid == &oid!(1, 3, 6, 1, 2, 1, 1, 3, 0) {
112/// return GetResult::Value(Value::TimeTicks(self.sys_uptime));
113/// }
114/// GetResult::NoSuchObject
115/// })
116/// }
117///
118/// fn get_next<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetNextResult> {
119/// Box::pin(async move {
120/// let sys_descr = oid!(1, 3, 6, 1, 2, 1, 1, 1, 0);
121/// let sys_uptime = oid!(1, 3, 6, 1, 2, 1, 1, 3, 0);
122///
123/// // Return the next OID in lexicographic order
124/// if oid < &sys_descr {
125/// return GetNextResult::Value(VarBind::new(
126/// sys_descr,
127/// Value::OctetString("My System".into())
128/// ));
129/// }
130/// if oid < &sys_uptime {
131/// return GetNextResult::Value(VarBind::new(
132/// sys_uptime,
133/// Value::TimeTicks(12345)
134/// ));
135/// }
136/// GetNextResult::EndOfMibView
137/// })
138/// }
139/// }
140/// ```
141///
142/// # Example: Writable Handler
143///
144/// ```rust
145/// use async_snmp::handler::{
146/// MibHandler, RequestContext, GetResult, GetNextResult, SetResult, BoxFuture
147/// };
148/// use async_snmp::{Oid, Value, VarBind, oid};
149/// use std::sync::atomic::{AtomicI32, Ordering};
150///
151/// struct WritableHandler {
152/// counter: AtomicI32,
153/// }
154///
155/// impl MibHandler for WritableHandler {
156/// fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
157/// Box::pin(async move {
158/// if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0) {
159/// return GetResult::Value(Value::Integer(
160/// self.counter.load(Ordering::Relaxed)
161/// ));
162/// }
163/// GetResult::NoSuchObject
164/// })
165/// }
166///
167/// fn get_next<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetNextResult> {
168/// Box::pin(async move {
169/// let my_oid = oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0);
170/// if oid < &my_oid {
171/// return GetNextResult::Value(VarBind::new(
172/// my_oid,
173/// Value::Integer(self.counter.load(Ordering::Relaxed))
174/// ));
175/// }
176/// GetNextResult::EndOfMibView
177/// })
178/// }
179///
180/// fn test_set<'a>(
181/// &'a self,
182/// _ctx: &'a RequestContext,
183/// oid: &'a Oid,
184/// value: &'a Value,
185/// ) -> BoxFuture<'a, SetResult> {
186/// Box::pin(async move {
187/// if oid != &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0) {
188/// return SetResult::NotWritable;
189/// }
190/// // Validate the value type
191/// match value {
192/// Value::Integer(_) => SetResult::Ok,
193/// _ => SetResult::WrongType,
194/// }
195/// })
196/// }
197///
198/// fn commit_set<'a>(
199/// &'a self,
200/// _ctx: &'a RequestContext,
201/// _oid: &'a Oid,
202/// value: &'a Value,
203/// ) -> BoxFuture<'a, SetResult> {
204/// Box::pin(async move {
205/// if let Value::Integer(v) = value {
206/// self.counter.store(*v, Ordering::Relaxed);
207/// SetResult::Ok
208/// } else {
209/// SetResult::CommitFailed
210/// }
211/// })
212/// }
213/// }
214/// ```
215pub trait MibHandler: Send + Sync + 'static {
216 /// Handle a GET request for a specific OID.
217 ///
218 /// Return [`GetResult::Value`] if the OID exists, [`GetResult::NoSuchObject`]
219 /// if the object type is not implemented, or [`GetResult::NoSuchInstance`]
220 /// if the object type exists but this specific instance doesn't.
221 ///
222 /// See [`GetResult`] documentation for details on when to use each variant.
223 fn get<'a>(&'a self, ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult>;
224
225 /// Handle a GETNEXT request.
226 ///
227 /// Return [`GetNextResult::Value`] with the lexicographically next OID and value
228 /// after `oid`, or [`GetNextResult::EndOfMibView`] if there are no more OIDs
229 /// in this handler's subtree.
230 fn get_next<'a>(
231 &'a self,
232 ctx: &'a RequestContext,
233 oid: &'a Oid,
234 ) -> BoxFuture<'a, GetNextResult>;
235
236 /// Test if a SET operation would succeed (phase 1 of two-phase commit).
237 ///
238 /// Called for ALL varbinds before any commits. Must NOT modify state.
239 /// Return `SetResult::Ok` if the SET would succeed, or an appropriate
240 /// error otherwise.
241 ///
242 /// Default implementation returns `NotWritable` (read-only handler).
243 fn test_set<'a>(
244 &'a self,
245 _ctx: &'a RequestContext,
246 _oid: &'a Oid,
247 _value: &'a Value,
248 ) -> BoxFuture<'a, SetResult> {
249 Box::pin(async { SetResult::NotWritable })
250 }
251
252 /// Commit a SET operation (phase 2 of two-phase commit).
253 ///
254 /// Only called after ALL `test_set` calls succeed. Should apply the change.
255 /// If this fails, `undo_set` will be called for all previously committed
256 /// varbinds in this request.
257 ///
258 /// Default implementation returns `NotWritable` (read-only handler).
259 fn commit_set<'a>(
260 &'a self,
261 _ctx: &'a RequestContext,
262 _oid: &'a Oid,
263 _value: &'a Value,
264 ) -> BoxFuture<'a, SetResult> {
265 Box::pin(async { SetResult::NotWritable })
266 }
267
268 /// Undo a committed SET operation (rollback on partial failure).
269 ///
270 /// Called if a later `commit_set` fails. Should restore the previous value.
271 /// This is best-effort: if undo fails, log a warning but continue.
272 ///
273 /// Default implementation does nothing (no rollback support).
274 fn undo_set<'a>(
275 &'a self,
276 _ctx: &'a RequestContext,
277 _oid: &'a Oid,
278 _value: &'a Value,
279 ) -> BoxFuture<'a, ()> {
280 Box::pin(async {})
281 }
282
283 /// Check if this handler handles the given OID prefix.
284 ///
285 /// Default implementation returns true if the OID starts with
286 /// the registered prefix or is lexicographically before it
287 /// (for GETNEXT support). Override for more complex matching.
288 fn handles(&self, registered_prefix: &Oid, oid: &Oid) -> bool {
289 oid.starts_with(registered_prefix) || oid < registered_prefix
290 }
291}