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}