Skip to content

Commit 7021bce

Browse files
committed
northbound: enforce valid YANG CRUD callbacks at compile time
Not every YANG node structurally supports every CRUD operation (e.g. a leaf with a plain default and no when/case can't be deleted, list keys can't be modified or deleted). This was only checked at daemon startup (validate_callback), so an invalid registration like .delete_apply() on such a node compiled fine and only surfaced as a runtime crash. Make YangPath and CallbacksBuilder a typestate: yang_codegen now emits a zero-sized Caps marker per node and implements SupportsCreate/ SupportsModify/SupportsDelete/SupportsLookup on it based on the same CallbackOp::is_valid rules enforced at runtime today. CallbacksBuilder's create_*/modify_*/delete_*/lookup methods are now gated on the matching trait, so an invalid registration fails to compile instead of panicking at startup. Drop validate_callback, now fully superseded. validate_callbacks (checks for missing, not invalid, callbacks) is unaffected. Basically making sure invalid calls are caught at compile time not at runtime. Concern raised when invalid callbacks were made in a boolean Yang Path: PR: holo-routing#133 Comment: holo-routing#133 (comment) Signed-off-by: Paul Wekesa <paul1tw1@gmail.com>
1 parent 047398b commit 7021bce

4 files changed

Lines changed: 145 additions & 69 deletions

File tree

holo-daemon/src/northbound/core.rs

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -801,9 +801,8 @@ async fn load_callbacks(
801801
// Receive response.
802802
let provider_response = responder_rx.await.unwrap();
803803

804-
// Validate and store callback key.
804+
// Store callback key.
805805
for cb_key in provider_response.callbacks {
806-
validate_callback(&cb_key);
807806
callbacks.insert(cb_key, provider_tx.downgrade());
808807
}
809808
}
@@ -847,17 +846,3 @@ fn validate_callbacks(
847846
std::process::exit(1);
848847
}
849848
}
850-
851-
// Checks whether the callback key is valid.
852-
fn validate_callback(callback: &CallbackKey) {
853-
let yang_ctx = YANG_CTX.get().unwrap();
854-
855-
if let Ok(snode) = yang_ctx.find_path(&callback.path)
856-
&& !callback.operation.is_valid(&snode)
857-
{
858-
error!(xpath = %callback.path, operation = ?callback.operation,
859-
"invalid callback",
860-
);
861-
std::process::exit(1);
862-
}
863-
}

holo-northbound/src/configuration.rs

Lines changed: 91 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,20 @@ pub struct CallbacksNode<P: Provider> {
6767
pub apply: Option<CallbackPhaseTwo<P>>,
6868
}
6969

70-
pub struct CallbacksBuilder<P: Provider> {
71-
path: Option<YangPath>,
70+
/// Marker traits, implemented by generated per-node `Caps` marker types
71+
/// (see `holo_northbound::yang_codegen`), that record which CRUD operations
72+
/// are structurally valid for a given YANG node. `CallbacksBuilder`'s
73+
/// `create_*`/`modify_*`/`delete_*`/`lookup` methods are only available when
74+
/// the path's `Caps` type implements the corresponding trait, turning an
75+
/// invalid callback registration (e.g. `.delete_apply()` on a node that
76+
/// can't be deleted) into a compile error instead of a runtime one.
77+
pub trait SupportsCreate {}
78+
pub trait SupportsModify {}
79+
pub trait SupportsDelete {}
80+
pub trait SupportsLookup {}
81+
82+
pub struct CallbacksBuilder<P: Provider, Caps = ()> {
83+
path: Option<YangPath<Caps>>,
7284
callbacks: Callbacks<P>,
7385
}
7486

@@ -91,7 +103,7 @@ pub struct ValidationCallbacks(pub HashMap<String, ValidationCallback>);
91103

92104
#[derive(Default)]
93105
pub struct ValidationCallbacksBuilder {
94-
path: Option<YangPath>,
106+
path: Option<String>,
95107
callbacks: ValidationCallbacks,
96108
}
97109

@@ -298,23 +310,21 @@ where
298310

299311
// ===== impl CallbacksBuilder =====
300312

301-
impl<P> CallbacksBuilder<P>
313+
impl<P, Caps> CallbacksBuilder<P, Caps>
302314
where
303315
P: Provider,
304316
{
305-
pub fn new(callbacks: Callbacks<P>) -> Self {
317+
#[must_use]
318+
pub fn path<Caps2>(
319+
self,
320+
path: YangPath<Caps2>,
321+
) -> CallbacksBuilder<P, Caps2> {
306322
CallbacksBuilder {
307-
path: None,
308-
callbacks,
323+
path: Some(path),
324+
callbacks: self.callbacks,
309325
}
310326
}
311327

312-
#[must_use]
313-
pub fn path(mut self, path: YangPath) -> Self {
314-
self.path = Some(path);
315-
self
316-
}
317-
318328
#[must_use]
319329
fn load_prepare(
320330
mut self,
@@ -352,13 +362,40 @@ where
352362
}
353363

354364
#[must_use]
355-
pub fn lookup(mut self, cb: CallbackLookup<P>) -> Self {
356-
let path = self.path.unwrap().to_string();
357-
let key = CallbackKey::new(path, CallbackOp::Lookup);
358-
self.callbacks.0.entry(key).or_default().lookup = Some(cb);
359-
self
365+
pub fn build(self) -> Callbacks<P> {
366+
self.callbacks
360367
}
368+
}
361369

370+
impl<P> CallbacksBuilder<P, ()>
371+
where
372+
P: Provider,
373+
{
374+
pub fn new(callbacks: Callbacks<P>) -> Self {
375+
CallbacksBuilder {
376+
path: None,
377+
callbacks,
378+
}
379+
}
380+
}
381+
382+
impl<P> Default for CallbacksBuilder<P, ()>
383+
where
384+
P: Provider,
385+
{
386+
fn default() -> Self {
387+
CallbacksBuilder {
388+
path: None,
389+
callbacks: Callbacks::default(),
390+
}
391+
}
392+
}
393+
394+
impl<P, Caps> CallbacksBuilder<P, Caps>
395+
where
396+
P: Provider,
397+
Caps: SupportsCreate,
398+
{
362399
#[must_use]
363400
pub fn create_prepare(self, cb: CallbackPhaseOne<P>) -> Self {
364401
self.load_prepare(CallbackOp::Create, cb)
@@ -373,22 +410,13 @@ where
373410
pub fn create_apply(self, cb: CallbackPhaseTwo<P>) -> Self {
374411
self.load_apply(CallbackOp::Create, cb)
375412
}
413+
}
376414

377-
#[must_use]
378-
pub fn delete_prepare(self, cb: CallbackPhaseOne<P>) -> Self {
379-
self.load_prepare(CallbackOp::Delete, cb)
380-
}
381-
382-
#[must_use]
383-
pub fn delete_abort(self, cb: CallbackPhaseTwo<P>) -> Self {
384-
self.load_abort(CallbackOp::Delete, cb)
385-
}
386-
387-
#[must_use]
388-
pub fn delete_apply(self, cb: CallbackPhaseTwo<P>) -> Self {
389-
self.load_apply(CallbackOp::Delete, cb)
390-
}
391-
415+
impl<P, Caps> CallbacksBuilder<P, Caps>
416+
where
417+
P: Provider,
418+
Caps: SupportsModify,
419+
{
392420
#[must_use]
393421
pub fn modify_prepare(self, cb: CallbackPhaseOne<P>) -> Self {
394422
self.load_prepare(CallbackOp::Modify, cb)
@@ -403,29 +431,47 @@ where
403431
pub fn modify_apply(self, cb: CallbackPhaseTwo<P>) -> Self {
404432
self.load_apply(CallbackOp::Modify, cb)
405433
}
434+
}
406435

436+
impl<P, Caps> CallbacksBuilder<P, Caps>
437+
where
438+
P: Provider,
439+
Caps: SupportsDelete,
440+
{
407441
#[must_use]
408-
pub fn build(self) -> Callbacks<P> {
409-
self.callbacks
442+
pub fn delete_prepare(self, cb: CallbackPhaseOne<P>) -> Self {
443+
self.load_prepare(CallbackOp::Delete, cb)
444+
}
445+
446+
#[must_use]
447+
pub fn delete_abort(self, cb: CallbackPhaseTwo<P>) -> Self {
448+
self.load_abort(CallbackOp::Delete, cb)
449+
}
450+
451+
#[must_use]
452+
pub fn delete_apply(self, cb: CallbackPhaseTwo<P>) -> Self {
453+
self.load_apply(CallbackOp::Delete, cb)
410454
}
411455
}
412456

413-
impl<P> Default for CallbacksBuilder<P>
457+
impl<P, Caps> CallbacksBuilder<P, Caps>
414458
where
415459
P: Provider,
460+
Caps: SupportsLookup,
416461
{
417-
fn default() -> Self {
418-
CallbacksBuilder {
419-
path: None,
420-
callbacks: Callbacks::default(),
421-
}
462+
#[must_use]
463+
pub fn lookup(mut self, cb: CallbackLookup<P>) -> Self {
464+
let path = self.path.unwrap().to_string();
465+
let key = CallbackKey::new(path, CallbackOp::Lookup);
466+
self.callbacks.0.entry(key).or_default().lookup = Some(cb);
467+
self
422468
}
423469
}
424470

425471
// ===== impl ValidationCallbacks =====
426472

427473
impl ValidationCallbacks {
428-
pub fn load(&mut self, path: YangPath, cb: ValidationCallback) {
474+
pub fn load<Caps>(&mut self, path: YangPath<Caps>, cb: ValidationCallback) {
429475
let path = path.to_string();
430476
self.0.insert(path, cb);
431477
}
@@ -446,14 +492,14 @@ impl ValidationCallbacksBuilder {
446492
}
447493

448494
#[must_use]
449-
pub fn path(mut self, path: YangPath) -> Self {
450-
self.path = Some(path);
495+
pub fn path<Caps>(mut self, path: YangPath<Caps>) -> Self {
496+
self.path = Some(path.to_string());
451497
self
452498
}
453499

454500
#[must_use]
455501
pub fn validate(mut self, cb: ValidationCallback) -> Self {
456-
let path = self.path.unwrap().to_string();
502+
let path = self.path.take().unwrap();
457503
self.callbacks.0.insert(path, cb);
458504
self
459505
}

holo-northbound/src/lib.rs

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,15 @@ impl<T: YangObject> YangObjectDyn for T {
6565
// Instances of this structure are created automatically at build-time, and
6666
// their use should be preferred over regular strings for extra type safety.
6767
//
68-
#[derive(Clone, Copy, Debug)]
69-
pub struct YangPath(&'static str);
68+
// The `Caps` type parameter is a zero-sized marker, generated alongside each
69+
// path constant, that records which CRUD operations are structurally valid
70+
// for the corresponding YANG node (see `configuration::Supports*` traits).
71+
// It lets `CallbacksBuilder` reject invalid callback registrations (e.g.
72+
// `.delete_apply()` on a node that doesn't support delete) at compile time.
73+
pub struct YangPath<Caps = ()>(
74+
&'static str,
75+
std::marker::PhantomData<fn() -> Caps>,
76+
);
7077

7178
// A YANG data path, represented as a sequence of elements.
7279
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -96,19 +103,36 @@ pub type NbProviderReceiver = UnboundedReceiver<api::provider::Notification>;
96103

97104
// ===== impl YangPath =====
98105

99-
impl YangPath {
100-
pub const fn new(path: &'static str) -> YangPath {
101-
YangPath(path)
106+
impl<Caps> YangPath<Caps> {
107+
pub const fn new(path: &'static str) -> YangPath<Caps> {
108+
YangPath(path, std::marker::PhantomData)
102109
}
103110
}
104111

105-
impl std::fmt::Display for YangPath {
112+
// Hand-written instead of derived: a `#[derive(..)]` would add a spurious
113+
// `Caps: Clone/Copy/Debug` bound, even though `Caps` only ever appears
114+
// inside `PhantomData`.
115+
impl<Caps> Clone for YangPath<Caps> {
116+
fn clone(&self) -> Self {
117+
*self
118+
}
119+
}
120+
121+
impl<Caps> Copy for YangPath<Caps> {}
122+
123+
impl<Caps> std::fmt::Debug for YangPath<Caps> {
124+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125+
f.debug_tuple("YangPath").field(&self.0).finish()
126+
}
127+
}
128+
129+
impl<Caps> std::fmt::Display for YangPath<Caps> {
106130
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107131
write!(f, "{}", self.0)
108132
}
109133
}
110134

111-
impl AsRef<str> for YangPath {
135+
impl<Caps> AsRef<str> for YangPath<Caps> {
112136
fn as_ref(&self) -> &str {
113137
self.0
114138
}

holo-northbound/src/yang_codegen/mod.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,11 +231,32 @@ fn generate_paths(
231231
w: &mut CodeWriter,
232232
snode: &SchemaNode<'_>,
233233
) -> std::fmt::Result {
234+
// Zero-sized marker type recording which CRUD operations this node
235+
// structurally supports, computed with the same rules enforced at
236+
// runtime by `CallbackOp::is_valid`. This lets `CallbacksBuilder` reject
237+
// invalid callback registrations (e.g. `.delete_apply()` on a node that
238+
// doesn't support delete) at compile time.
239+
emit!(w, 1, "pub struct Caps;")?;
240+
for (op, trait_name) in [
241+
(crate::configuration::CallbackOp::Create, "SupportsCreate"),
242+
(crate::configuration::CallbackOp::Modify, "SupportsModify"),
243+
(crate::configuration::CallbackOp::Delete, "SupportsDelete"),
244+
(crate::configuration::CallbackOp::Lookup, "SupportsLookup"),
245+
] {
246+
if op.is_valid(snode) {
247+
emit!(
248+
w,
249+
1,
250+
"impl holo_northbound::configuration::{trait_name} for Caps {{}}"
251+
)?;
252+
}
253+
}
254+
234255
let path = snode.path(SchemaPathFormat::DATA);
235256
emit!(
236257
w,
237258
1,
238-
"pub const PATH: YangPath = YangPath::new(\"{path}\");"
259+
"pub const PATH: YangPath<Caps> = YangPath::new(\"{path}\");"
239260
)?;
240261

241262
// For notifications, also generate data path relative to the nearest

0 commit comments

Comments
 (0)