Skip to main content

nmbrs_runtime/wrappers/
if.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Conditional execution wrapper. Reads a per-cycle truthy/falsy
5//! value through the pull plan; if falsy, the op is skipped
6//! (no inner execution, no adapter call) and `skips_total` is
7//! incremented on the activity metrics.
8
9use std::sync::Arc;
10
11use crate::adapter::WrappingDispenser;
12use crate::adapter::{ExecutionError, OpDispenser, OpResult};
13use crate::wrapper_registry::{WrapperName, WrapperRegistration, WrapperSubject};
14
15/// SRD-32a wrapper name.
16pub const NAME: WrapperName = WrapperName::new("if");
17
18/// Trigger: op declares an `if:` condition.
19fn triggers(s: WrapperSubject) -> bool {
20    let Some(template) = s.op() else {
21        return false;
22    };
23    template.condition.is_some()
24}
25
26fn describe_assignment(s: WrapperSubject) -> Option<String> {
27    let template = s.op()?;
28    template.condition.as_ref().map(|cond| {
29        let trimmed = crate::wrapper_registrations::trim_braces(cond);
30        format!("if: {trimmed}")
31    })
32}
33
34inventory::submit! {
35    WrapperRegistration {
36        name: NAME,
37        owned_fields: &["if"],
38        triggers,
39        requires_inner: &[],
40        forbids_outer: &[],
41        mutually_exclusive_with: &[],
42        describe_assignment,
43        levels: &[crate::wrapper_registry::WrapperLevel::Op],
44    }
45}
46
47/// Wraps an inner OpDispenser with a conditional check.
48///
49/// Evaluates a named field in `ResolvedFields` before executing.
50/// If the field is falsy (0, 0.0, false, empty string, None), the
51/// op is skipped — no inner execution, no adapter call. Returns
52/// `OpResult::skipped()`.
53///
54/// The condition Polydat name is resolved at init time via
55/// `ScopeFixture::register_pull` and read at cycle time through
56/// the stored `PullHandle` against `ExecCtx::pulls`.
57pub struct ConditionalDispenser {
58    inner: Arc<dyn OpDispenser>,
59    /// Memoized handle for the condition Polydat name registered into
60    /// the scope fixture at init.
61    condition_handle: crate::fixture::PullHandle,
62    /// Metrics reference for counting skips.
63    metrics: Arc<crate::activity::ActivityMetrics>,
64}
65
66impl ConditionalDispenser {
67    /// Wrap an inner dispenser with a condition check, registering
68    /// `condition_field` into the supplied scope fixture so the
69    /// per-cycle read goes through the canonical PullPlan path
70    /// (SRD 32 §"Init-Time Fixture and Consumer Self-Registration").
71    ///
72    /// Errors if the kernel doesn't know `condition_field`.
73    pub fn wrap(
74        inner: Arc<dyn OpDispenser>,
75        condition_field: &str,
76        metrics: Arc<crate::activity::ActivityMetrics>,
77        fx: &mut crate::fixture::ScopeFixture,
78    ) -> Result<Arc<dyn OpDispenser>, String> {
79        let condition_handle = fx
80            .register_pull(condition_field)
81            .map_err(|e| format!("conditional `if`: {e}"))?;
82        Ok(Arc::new(Self {
83            inner,
84            condition_handle,
85            metrics,
86        }))
87    }
88}
89
90/// Test whether a resolved field value is truthy.
91
92impl WrappingDispenser for ConditionalDispenser {}
93
94impl OpDispenser for ConditionalDispenser {
95    fn execute<'a>(
96        &'a self,
97        cycle: u64,
98        ctx: &'a crate::fixture::ExecCtx<'a>,
99    ) -> std::pin::Pin<
100        Box<dyn std::future::Future<Output = Result<OpResult, ExecutionError>> + Send + 'a>,
101    > {
102        Box::pin(async move {
103            // Single read path: condition value via handle from
104            // the cycle's ResolvedPulls. Adapter never sees the
105            // condition — it's not in fields, so no strip step.
106            let value = ctx.pulls.get(self.condition_handle);
107            if !crate::wrappers::condition::is_truthy(value) {
108                self.metrics.skips_total.inc();
109                return Ok(OpResult::skipped());
110            }
111            self.inner.execute(cycle, ctx).await
112        })
113    }
114    fn inner_dispenser(&self) -> Option<&dyn OpDispenser> {
115        Some(self.inner.as_ref())
116    }
117}