Skip to main content

mq_rest_admin/
ensure.rs

1//! Idempotent ensure methods for MQ object management.
2
3use std::collections::HashMap;
4
5use serde_json::Value;
6
7use crate::error::{MqRestError, Result};
8use crate::session::MqRestSession;
9
10/// Action taken by an ensure operation.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum EnsureAction {
13    /// The object did not exist and was defined.
14    Created,
15    /// The object existed but attributes differed and were altered.
16    Updated,
17    /// The object existed and all specified attributes already matched.
18    Unchanged,
19}
20
21/// Result of an ensure operation.
22#[derive(Debug, Clone)]
23pub struct EnsureResult {
24    /// The action indicating what happened.
25    pub action: EnsureAction,
26    /// Attribute names that triggered the ALTER.
27    pub changed: Vec<String>,
28}
29
30impl MqRestSession {
31    /// Ensure the queue manager has the specified attributes.
32    ///
33    /// # Errors
34    ///
35    /// Returns an error if the DISPLAY or ALTER command fails.
36    pub fn ensure_qmgr(
37        &mut self,
38        request_parameters: Option<&HashMap<String, Value>>,
39    ) -> Result<EnsureResult> {
40        let params: HashMap<String, Value> = request_parameters.cloned().unwrap_or_default();
41        if params.is_empty() {
42            return Ok(EnsureResult {
43                action: EnsureAction::Unchanged,
44                changed: Vec::new(),
45            });
46        }
47
48        let all_params: &[&str] = &["all"];
49        let current_objects =
50            self.mqsc_command("DISPLAY", "QMGR", None, None, Some(all_params), None)?;
51
52        let current = current_objects.into_iter().next().unwrap_or_default();
53        let mut changed: HashMap<String, Value> = HashMap::new();
54        for (key, desired_value) in &params {
55            let current_value = current.get(key);
56            if !values_match(desired_value, current_value) {
57                changed.insert(key.clone(), desired_value.clone());
58            }
59        }
60
61        if changed.is_empty() {
62            return Ok(EnsureResult {
63                action: EnsureAction::Unchanged,
64                changed: Vec::new(),
65            });
66        }
67
68        self.mqsc_command("ALTER", "QMGR", None, Some(&changed), None, None)?;
69        Ok(EnsureResult {
70            action: EnsureAction::Updated,
71            changed: changed.keys().cloned().collect(),
72        })
73    }
74
75    /// Ensure a local queue exists with the specified attributes.
76    ///
77    /// # Errors
78    ///
79    /// Returns an error if the DISPLAY, DEFINE, or ALTER command fails.
80    pub fn ensure_qlocal(
81        &mut self,
82        name: &str,
83        request_parameters: Option<&HashMap<String, Value>>,
84    ) -> Result<EnsureResult> {
85        self.ensure_object(name, request_parameters, "QUEUE", "QLOCAL", "QLOCAL")
86    }
87
88    /// Ensure a remote queue exists with the specified attributes.
89    ///
90    /// # Errors
91    ///
92    /// Returns an error if the DISPLAY, DEFINE, or ALTER command fails.
93    pub fn ensure_qremote(
94        &mut self,
95        name: &str,
96        request_parameters: Option<&HashMap<String, Value>>,
97    ) -> Result<EnsureResult> {
98        self.ensure_object(name, request_parameters, "QUEUE", "QREMOTE", "QREMOTE")
99    }
100
101    /// Ensure an alias queue exists with the specified attributes.
102    ///
103    /// # Errors
104    ///
105    /// Returns an error if the DISPLAY, DEFINE, or ALTER command fails.
106    pub fn ensure_qalias(
107        &mut self,
108        name: &str,
109        request_parameters: Option<&HashMap<String, Value>>,
110    ) -> Result<EnsureResult> {
111        self.ensure_object(name, request_parameters, "QUEUE", "QALIAS", "QALIAS")
112    }
113
114    /// Ensure a model queue exists with the specified attributes.
115    ///
116    /// # Errors
117    ///
118    /// Returns an error if the DISPLAY, DEFINE, or ALTER command fails.
119    pub fn ensure_qmodel(
120        &mut self,
121        name: &str,
122        request_parameters: Option<&HashMap<String, Value>>,
123    ) -> Result<EnsureResult> {
124        self.ensure_object(name, request_parameters, "QUEUE", "QMODEL", "QMODEL")
125    }
126
127    /// Ensure a channel exists with the specified attributes.
128    ///
129    /// # Errors
130    ///
131    /// Returns an error if the DISPLAY, DEFINE, or ALTER command fails.
132    pub fn ensure_channel(
133        &mut self,
134        name: &str,
135        request_parameters: Option<&HashMap<String, Value>>,
136    ) -> Result<EnsureResult> {
137        self.ensure_object(name, request_parameters, "CHANNEL", "CHANNEL", "CHANNEL")
138    }
139
140    /// Ensure an authentication information object exists with the specified attributes.
141    ///
142    /// # Errors
143    ///
144    /// Returns an error if the DISPLAY, DEFINE, or ALTER command fails.
145    pub fn ensure_authinfo(
146        &mut self,
147        name: &str,
148        request_parameters: Option<&HashMap<String, Value>>,
149    ) -> Result<EnsureResult> {
150        self.ensure_object(name, request_parameters, "AUTHINFO", "AUTHINFO", "AUTHINFO")
151    }
152
153    /// Ensure a listener exists with the specified attributes.
154    ///
155    /// # Errors
156    ///
157    /// Returns an error if the DISPLAY, DEFINE, or ALTER command fails.
158    pub fn ensure_listener(
159        &mut self,
160        name: &str,
161        request_parameters: Option<&HashMap<String, Value>>,
162    ) -> Result<EnsureResult> {
163        self.ensure_object(name, request_parameters, "LISTENER", "LISTENER", "LISTENER")
164    }
165
166    /// Ensure a namelist exists with the specified attributes.
167    ///
168    /// # Errors
169    ///
170    /// Returns an error if the DISPLAY, DEFINE, or ALTER command fails.
171    pub fn ensure_namelist(
172        &mut self,
173        name: &str,
174        request_parameters: Option<&HashMap<String, Value>>,
175    ) -> Result<EnsureResult> {
176        self.ensure_object(name, request_parameters, "NAMELIST", "NAMELIST", "NAMELIST")
177    }
178
179    /// Ensure a process exists with the specified attributes.
180    ///
181    /// # Errors
182    ///
183    /// Returns an error if the DISPLAY, DEFINE, or ALTER command fails.
184    pub fn ensure_process(
185        &mut self,
186        name: &str,
187        request_parameters: Option<&HashMap<String, Value>>,
188    ) -> Result<EnsureResult> {
189        self.ensure_object(name, request_parameters, "PROCESS", "PROCESS", "PROCESS")
190    }
191
192    /// Ensure a service exists with the specified attributes.
193    ///
194    /// # Errors
195    ///
196    /// Returns an error if the DISPLAY, DEFINE, or ALTER command fails.
197    pub fn ensure_service(
198        &mut self,
199        name: &str,
200        request_parameters: Option<&HashMap<String, Value>>,
201    ) -> Result<EnsureResult> {
202        self.ensure_object(name, request_parameters, "SERVICE", "SERVICE", "SERVICE")
203    }
204
205    /// Ensure a topic exists with the specified attributes.
206    ///
207    /// # Errors
208    ///
209    /// Returns an error if the DISPLAY, DEFINE, or ALTER command fails.
210    pub fn ensure_topic(
211        &mut self,
212        name: &str,
213        request_parameters: Option<&HashMap<String, Value>>,
214    ) -> Result<EnsureResult> {
215        self.ensure_object(name, request_parameters, "TOPIC", "TOPIC", "TOPIC")
216    }
217
218    /// Ensure a subscription exists with the specified attributes.
219    ///
220    /// # Errors
221    ///
222    /// Returns an error if the DISPLAY, DEFINE, or ALTER command fails.
223    pub fn ensure_sub(
224        &mut self,
225        name: &str,
226        request_parameters: Option<&HashMap<String, Value>>,
227    ) -> Result<EnsureResult> {
228        self.ensure_object(name, request_parameters, "SUB", "SUB", "SUB")
229    }
230
231    /// Ensure a storage class exists with the specified attributes.
232    ///
233    /// # Errors
234    ///
235    /// Returns an error if the DISPLAY, DEFINE, or ALTER command fails.
236    pub fn ensure_stgclass(
237        &mut self,
238        name: &str,
239        request_parameters: Option<&HashMap<String, Value>>,
240    ) -> Result<EnsureResult> {
241        self.ensure_object(name, request_parameters, "STGCLASS", "STGCLASS", "STGCLASS")
242    }
243
244    /// Ensure a communication information object exists with the specified attributes.
245    ///
246    /// # Errors
247    ///
248    /// Returns an error if the DISPLAY, DEFINE, or ALTER command fails.
249    pub fn ensure_comminfo(
250        &mut self,
251        name: &str,
252        request_parameters: Option<&HashMap<String, Value>>,
253    ) -> Result<EnsureResult> {
254        self.ensure_object(name, request_parameters, "COMMINFO", "COMMINFO", "COMMINFO")
255    }
256
257    /// Ensure a CF structure exists with the specified attributes.
258    ///
259    /// # Errors
260    ///
261    /// Returns an error if the DISPLAY, DEFINE, or ALTER command fails.
262    pub fn ensure_cfstruct(
263        &mut self,
264        name: &str,
265        request_parameters: Option<&HashMap<String, Value>>,
266    ) -> Result<EnsureResult> {
267        self.ensure_object(name, request_parameters, "CFSTRUCT", "CFSTRUCT", "CFSTRUCT")
268    }
269
270    fn ensure_object(
271        &mut self,
272        name: &str,
273        request_parameters: Option<&HashMap<String, Value>>,
274        display_qualifier: &str,
275        define_qualifier: &str,
276        alter_qualifier: &str,
277    ) -> Result<EnsureResult> {
278        let all_params: &[&str] = &["all"];
279        let current_objects = match self.mqsc_command(
280            "DISPLAY",
281            display_qualifier,
282            Some(name),
283            None,
284            Some(all_params),
285            None,
286        ) {
287            Ok(objects) => objects,
288            Err(MqRestError::Command { .. }) => Vec::new(),
289            Err(e) => return Err(e),
290        };
291
292        let params: HashMap<String, Value> = request_parameters.cloned().unwrap_or_default();
293
294        if current_objects.is_empty() {
295            let define_params = if params.is_empty() {
296                None
297            } else {
298                Some(&params)
299            };
300            self.mqsc_command(
301                "DEFINE",
302                define_qualifier,
303                Some(name),
304                define_params,
305                None,
306                None,
307            )?;
308            return Ok(EnsureResult {
309                action: EnsureAction::Created,
310                changed: Vec::new(),
311            });
312        }
313
314        if params.is_empty() {
315            return Ok(EnsureResult {
316                action: EnsureAction::Unchanged,
317                changed: Vec::new(),
318            });
319        }
320
321        let current = &current_objects[0];
322        let mut changed: HashMap<String, Value> = HashMap::new();
323        for (key, desired_value) in &params {
324            let current_value = current.get(key);
325            if !values_match(desired_value, current_value) {
326                changed.insert(key.clone(), desired_value.clone());
327            }
328        }
329
330        if changed.is_empty() {
331            return Ok(EnsureResult {
332                action: EnsureAction::Unchanged,
333                changed: Vec::new(),
334            });
335        }
336
337        self.mqsc_command(
338            "ALTER",
339            alter_qualifier,
340            Some(name),
341            Some(&changed),
342            None,
343            None,
344        )?;
345        Ok(EnsureResult {
346            action: EnsureAction::Updated,
347            changed: changed.keys().cloned().collect(),
348        })
349    }
350}
351
352fn values_match(desired: &Value, current: Option<&Value>) -> bool {
353    let Some(current) = current else {
354        return false;
355    };
356    let desired_str = value_to_string(desired);
357    let current_str = value_to_string(current);
358    desired_str.trim().eq_ignore_ascii_case(current_str.trim())
359}
360
361fn value_to_string(value: &Value) -> String {
362    match value {
363        Value::String(s) => s.clone(),
364        Value::Number(n) => n.to_string(),
365        Value::Bool(b) => b.to_string(),
366        Value::Null => String::new(),
367        other => other.to_string(),
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374    use crate::test_helpers::{
375        MockTransport, command_error_response, empty_success_response, mock_session,
376        success_response,
377    };
378    use serde_json::json;
379
380    // ---- ensure_qmgr ----
381
382    #[test]
383    fn ensure_qmgr_empty_params_unchanged() {
384        let transport = MockTransport::new(vec![]);
385        let mut session = mock_session(transport);
386        let result = session.ensure_qmgr(None).unwrap();
387        assert_eq!(result.action, EnsureAction::Unchanged);
388    }
389
390    #[test]
391    fn ensure_qmgr_matching_params_unchanged() {
392        let mut current = HashMap::new();
393        current.insert("DESCR".into(), json!("test"));
394        let transport = MockTransport::new(vec![success_response(vec![current])]);
395        let mut session = mock_session(transport);
396        let mut params = HashMap::new();
397        params.insert("DESCR".into(), json!("test"));
398        let result = session.ensure_qmgr(Some(&params)).unwrap();
399        assert_eq!(result.action, EnsureAction::Unchanged);
400    }
401
402    #[test]
403    fn ensure_qmgr_differing_params_updated() {
404        let mut current = HashMap::new();
405        current.insert("DESCR".into(), json!("old"));
406        let transport = MockTransport::new(vec![
407            success_response(vec![current]),
408            empty_success_response(),
409        ]);
410        let mut session = mock_session(transport);
411        let mut params = HashMap::new();
412        params.insert("DESCR".into(), json!("new"));
413        let result = session.ensure_qmgr(Some(&params)).unwrap();
414        assert_eq!(result.action, EnsureAction::Updated);
415        assert!(result.changed.contains(&"DESCR".to_owned()));
416    }
417
418    // ---- ensure_object (via ensure_qlocal) ----
419
420    #[test]
421    fn ensure_qlocal_not_found_created() {
422        let transport =
423            MockTransport::new(vec![command_error_response(), empty_success_response()]);
424        let mut session = mock_session(transport);
425        let result = session.ensure_qlocal("MY.Q", None).unwrap();
426        assert_eq!(result.action, EnsureAction::Created);
427    }
428
429    #[test]
430    fn ensure_qlocal_exists_unchanged() {
431        let mut current = HashMap::new();
432        current.insert("DESCR".into(), json!("test"));
433        let transport = MockTransport::new(vec![success_response(vec![current])]);
434        let mut session = mock_session(transport);
435        let mut params = HashMap::new();
436        params.insert("DESCR".into(), json!("test"));
437        let result = session.ensure_qlocal("MY.Q", Some(&params)).unwrap();
438        assert_eq!(result.action, EnsureAction::Unchanged);
439    }
440
441    #[test]
442    fn ensure_qlocal_exists_updated() {
443        let mut current = HashMap::new();
444        current.insert("DESCR".into(), json!("old"));
445        let transport = MockTransport::new(vec![
446            success_response(vec![current]),
447            empty_success_response(),
448        ]);
449        let mut session = mock_session(transport);
450        let mut params = HashMap::new();
451        params.insert("DESCR".into(), json!("new"));
452        let result = session.ensure_qlocal("MY.Q", Some(&params)).unwrap();
453        assert_eq!(result.action, EnsureAction::Updated);
454    }
455
456    #[test]
457    fn ensure_qlocal_empty_params_not_found_created() {
458        let transport =
459            MockTransport::new(vec![command_error_response(), empty_success_response()]);
460        let mut session = mock_session(transport);
461        let result = session.ensure_qlocal("MY.Q", None).unwrap();
462        assert_eq!(result.action, EnsureAction::Created);
463    }
464
465    #[test]
466    fn ensure_qlocal_empty_params_exists_unchanged() {
467        let current = HashMap::new();
468        let transport = MockTransport::new(vec![success_response(vec![current])]);
469        let mut session = mock_session(transport);
470        let result = session.ensure_qlocal("MY.Q", None).unwrap();
471        assert_eq!(result.action, EnsureAction::Unchanged);
472    }
473
474    #[test]
475    fn ensure_qlocal_non_command_error_propagated() {
476        let transport = MockTransport::new(vec![]);
477        let mut session = mock_session(transport);
478        let result = session.ensure_qlocal("MY.Q", None);
479        assert!(result.is_err());
480    }
481
482    // ---- values_match ----
483
484    #[test]
485    fn values_match_case_insensitive() {
486        assert!(values_match(&json!("YES"), Some(&json!("yes"))));
487    }
488
489    #[test]
490    fn values_match_trimmed() {
491        assert!(values_match(&json!("YES"), Some(&json!(" YES "))));
492    }
493
494    #[test]
495    fn values_match_no_match() {
496        assert!(!values_match(&json!("YES"), Some(&json!("NO"))));
497    }
498
499    #[test]
500    fn values_match_none_current() {
501        assert!(!values_match(&json!("YES"), None));
502    }
503
504    // ---- value_to_string ----
505
506    #[test]
507    fn value_to_string_string() {
508        assert_eq!(value_to_string(&json!("hello")), "hello");
509    }
510
511    #[test]
512    fn value_to_string_number() {
513        assert_eq!(value_to_string(&json!(42)), "42");
514    }
515
516    #[test]
517    fn value_to_string_bool() {
518        assert_eq!(value_to_string(&json!(true)), "true");
519    }
520
521    #[test]
522    fn value_to_string_null() {
523        assert_eq!(value_to_string(&json!(null)), "");
524    }
525
526    #[test]
527    fn value_to_string_other() {
528        let val = json!({"key": "val"});
529        let result = value_to_string(&val);
530        assert!(result.contains("key"));
531    }
532
533    // ---- Macro-generated per-method ensure tests ----
534
535    macro_rules! test_ensure_created {
536        ($method:ident) => {
537            paste::paste! {
538                #[test]
539                fn [<test_ $method _created>]() {
540                    let transport = MockTransport::new(vec![
541                        command_error_response(),
542                        empty_success_response(),
543                    ]);
544                    let mut session = mock_session(transport);
545                    let result = session.$method("OBJ", None).unwrap();
546                    assert_eq!(result.action, EnsureAction::Created);
547                }
548            }
549        };
550    }
551
552    test_ensure_created!(ensure_qlocal);
553    test_ensure_created!(ensure_qremote);
554    test_ensure_created!(ensure_qalias);
555    test_ensure_created!(ensure_qmodel);
556    test_ensure_created!(ensure_channel);
557    test_ensure_created!(ensure_authinfo);
558    test_ensure_created!(ensure_listener);
559    test_ensure_created!(ensure_namelist);
560    test_ensure_created!(ensure_process);
561    test_ensure_created!(ensure_service);
562    test_ensure_created!(ensure_topic);
563    test_ensure_created!(ensure_sub);
564    test_ensure_created!(ensure_stgclass);
565    test_ensure_created!(ensure_comminfo);
566    test_ensure_created!(ensure_cfstruct);
567
568    #[test]
569    fn ensure_qlocal_not_found_with_params_created() {
570        let mut params = HashMap::new();
571        params.insert("DESCR".into(), json!("my queue"));
572        let transport =
573            MockTransport::new(vec![command_error_response(), empty_success_response()]);
574        let mut session = mock_session(transport);
575        let result = session.ensure_qlocal("MY.Q", Some(&params)).unwrap();
576        assert_eq!(result.action, EnsureAction::Created);
577    }
578
579    #[test]
580    fn ensure_qmgr_display_transport_error() {
581        let transport = MockTransport::new(vec![]);
582        let mut session = mock_session(transport);
583        let mut params = HashMap::new();
584        params.insert("DESCR".into(), json!("new"));
585        let result = session.ensure_qmgr(Some(&params));
586        assert!(result.is_err());
587    }
588
589    #[test]
590    fn ensure_qmgr_alter_transport_error() {
591        let mut current = HashMap::new();
592        current.insert("DESCR".into(), json!("old"));
593        let transport = MockTransport::new(vec![
594            success_response(vec![current]),
595            // No response for ALTER → transport error
596        ]);
597        let mut session = mock_session(transport);
598        let mut params = HashMap::new();
599        params.insert("DESCR".into(), json!("new"));
600        let result = session.ensure_qmgr(Some(&params));
601        assert!(result.is_err());
602    }
603
604    #[test]
605    fn ensure_qlocal_define_fails() {
606        // Object not found → DEFINE fails
607        let transport = MockTransport::new(vec![
608            command_error_response(),
609            // No response for DEFINE → transport error
610        ]);
611        let mut session = mock_session(transport);
612        let result = session.ensure_qlocal("MY.Q", None);
613        assert!(result.is_err());
614    }
615
616    #[test]
617    fn ensure_qlocal_alter_fails() {
618        // Object exists with different params → ALTER fails
619        let mut current = HashMap::new();
620        current.insert("DESCR".into(), json!("old"));
621        let transport = MockTransport::new(vec![
622            success_response(vec![current]),
623            // No response for ALTER → transport error
624        ]);
625        let mut session = mock_session(transport);
626        let mut params = HashMap::new();
627        params.insert("DESCR".into(), json!("new"));
628        let result = session.ensure_qlocal("MY.Q", Some(&params));
629        assert!(result.is_err());
630    }
631}