Skip to main content

ad_plugins_rs/
attr_plot_args.rs

1//! `NDAttrPlotConfig`'s positional-argument mapping, kept apart from the
2//! command that installs it.
3//!
4//! The mapping is the parity-critical half of that command and it is pure:
5//! `&[ArgValue]` in, six fields out, no port manager, no IOC, no reactor. It
6//! lives here rather than in `crate::ioc` because that module is gated on
7//! `tokio_backend` — it stands an IOC up on `epics_ca_rs::server::ioc_app`,
8//! which the reactor-free backend does not have — and a gate sized for the
9//! `IocApplication` type surface took this parsing and its three boundary
10//! cases down with it.
11//!
12//! The predicate here is `not(epics_embedded_target)` instead, which is what
13//! [`ArgValue`] itself is gated on: the iocsh registry is host-and-VxWorks-
14//! and-RTEMS-absent, not reactor-dependent, so a host `exec_backend` build
15//! keeps both it and this.
16//!
17//! `crate::ioc` is named in a code span rather than linked for the same
18//! reason this module exists: it is not there to link to in the very
19//! configuration this paragraph is explaining.
20
21use epics_base_rs::server::iocsh::registry::ArgValue;
22
23/// Parsed `NDAttrPlotConfig` arguments.
24pub struct AttrPlotArgs {
25    pub port_name: String,
26    pub n_attributes: usize,
27    pub cache_size: usize,
28    pub n_data_blocks: usize,
29    pub in_port: String,
30    pub queue_size: usize,
31}
32
33/// Parse `NDAttrPlotConfig` positional args in C order
34/// (`NDPluginAttrPlot.cpp:308`): `port, n_attributes, cache_size,
35/// n_selected_blocks, in_port, in_addr, queue_size, ...`.
36///
37/// A present integer is honoured exactly — including an explicit `0`, which is
38/// meaningful for `cache_size` (`0` = unlimited per-buffer cache). Fallbacks
39/// apply only when an arg is absent; a real st.cmd always passes them, so the
40/// fallbacks only affect malformed calls.
41pub fn parse_attr_plot_args(args: &[ArgValue]) -> Result<AttrPlotArgs, String> {
42    let port_name = match args.first() {
43        Some(ArgValue::String(s)) if !s.is_empty() => s.clone(),
44        _ => return Err("NDAttrPlotConfig: portName required".into()),
45    };
46    let usize_arg = |i: usize, default: usize| match args.get(i) {
47        Some(ArgValue::Int(n)) => (*n).max(0) as usize,
48        _ => default,
49    };
50    let in_port = match args.get(4) {
51        Some(ArgValue::String(s)) => s.clone(),
52        _ => String::new(),
53    };
54    Ok(AttrPlotArgs {
55        port_name,
56        n_attributes: usize_arg(1, 8),
57        cache_size: usize_arg(2, 1000),
58        n_data_blocks: usize_arg(3, 4),
59        in_port,
60        queue_size: usize_arg(6, 20),
61    })
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn parse_attr_plot_args_maps_c_positional_order() {
70        // C NDAttrPlotConfig(port, n_attributes, cache_size, n_selected_blocks,
71        // in_port, in_addr, queue_size, ...) — NDPluginAttrPlot.cpp:308. The
72        // distinct order (n_attributes/cache/blocks before in_port, queue at
73        // index 6) is the parity-critical mapping this guards.
74        let args = vec![
75            ArgValue::String("AP1".to_string()),
76            ArgValue::Int(10),                    // n_attributes
77            ArgValue::Int(500),                   // cache_size
78            ArgValue::Int(3),                     // n_selected_blocks
79            ArgValue::String("DET1".to_string()), // in_port
80            ArgValue::Int(0),                     // in_addr
81            ArgValue::Int(50),                    // queue_size
82            ArgValue::Int(0),                     // blocking_callbacks
83        ];
84        let p = parse_attr_plot_args(&args).unwrap();
85        assert_eq!(p.port_name, "AP1");
86        assert_eq!(p.n_attributes, 10);
87        assert_eq!(p.cache_size, 500);
88        assert_eq!(p.n_data_blocks, 3);
89        assert_eq!(p.in_port, "DET1");
90        assert_eq!(p.queue_size, 50);
91    }
92
93    #[test]
94    fn parse_attr_plot_args_requires_port_name() {
95        assert!(parse_attr_plot_args(&[]).is_err());
96        assert!(parse_attr_plot_args(&[ArgValue::String(String::new())]).is_err());
97        assert!(parse_attr_plot_args(&[ArgValue::Int(1)]).is_err());
98    }
99
100    #[test]
101    fn parse_attr_plot_args_honours_explicit_zero_and_defaults_absent() {
102        // Boundary: an explicit cache_size=0 is meaningful (unlimited) and must be
103        // honoured; absent n_attributes/n_data_blocks/queue_size fall back.
104        let args = vec![
105            ArgValue::String("AP2".to_string()),
106            ArgValue::Missing, // n_attributes absent
107            ArgValue::Int(0),  // cache_size = unlimited (explicit 0, not a fallback)
108        ];
109        let p = parse_attr_plot_args(&args).unwrap();
110        assert_eq!(p.n_attributes, 8, "absent n_attributes -> fallback");
111        assert_eq!(
112            p.cache_size, 0,
113            "explicit 0 cache_size honoured (unlimited)"
114        );
115        assert_eq!(p.n_data_blocks, 4, "absent n_data_blocks -> fallback");
116        assert_eq!(p.in_port, "", "absent in_port -> empty");
117        assert_eq!(p.queue_size, 20, "absent queue_size -> fallback");
118    }
119}