mdbook_validator/
dependency.rs

1//! Dependency checking for jq and Docker availability.
2//!
3//! Checks for required external dependencies at startup and warns if missing.
4//! Uses trait-based design for testability.
5
6use std::process::Command;
7
8/// Result of dependency checks.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct DependencyStatus {
11    /// Whether jq is available (jq --version exits 0).
12    pub jq_available: bool,
13    /// Whether Docker is running (docker info exits 0).
14    pub docker_available: bool,
15}
16
17/// Trait for checking command availability.
18///
19/// Enables mocking in tests to verify both success and failure paths.
20pub trait DependencyChecker {
21    /// Check if a command is available and working.
22    ///
23    /// # Arguments
24    /// * `cmd` - The command to run
25    /// * `args` - Arguments to pass to the command
26    ///
27    /// # Returns
28    /// `true` if the command exits successfully, `false` otherwise.
29    fn check_command(&self, cmd: &str, args: &[&str]) -> bool;
30}
31
32/// Real implementation using [`std::process::Command`].
33#[derive(Debug, Default, Clone, Copy)]
34pub struct RealChecker;
35
36impl DependencyChecker for RealChecker {
37    fn check_command(&self, cmd: &str, args: &[&str]) -> bool {
38        Command::new(cmd)
39            .args(args)
40            .output()
41            .map(|o| o.status.success())
42            .unwrap_or(false)
43    }
44}
45
46/// Check if jq is available.
47pub fn check_jq<C: DependencyChecker>(checker: &C) -> bool {
48    checker.check_command("jq", &["--version"])
49}
50
51/// Check if Docker daemon is running.
52pub fn check_docker<C: DependencyChecker>(checker: &C) -> bool {
53    checker.check_command("docker", &["info"])
54}
55
56/// Check all dependencies and return status.
57///
58/// Does not log warnings - caller is responsible for logging based on status.
59pub fn check_all<C: DependencyChecker>(checker: &C) -> DependencyStatus {
60    DependencyStatus {
61        jq_available: check_jq(checker),
62        docker_available: check_docker(checker),
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    #![allow(clippy::panic, clippy::expect_used, clippy::unwrap_used)]
69
70    use super::*;
71
72    /// Mock checker that returns configured values.
73    struct MockChecker {
74        jq_available: bool,
75        docker_available: bool,
76    }
77
78    impl MockChecker {
79        fn new(jq_available: bool, docker_available: bool) -> Self {
80            Self {
81                jq_available,
82                docker_available,
83            }
84        }
85    }
86
87    impl DependencyChecker for MockChecker {
88        fn check_command(&self, cmd: &str, _args: &[&str]) -> bool {
89            match cmd {
90                "jq" => self.jq_available,
91                "docker" => self.docker_available,
92                _ => false,
93            }
94        }
95    }
96
97    #[test]
98    fn test_check_jq_available() {
99        let checker = MockChecker::new(true, false);
100        assert!(check_jq(&checker));
101    }
102
103    #[test]
104    fn test_check_jq_unavailable() {
105        let checker = MockChecker::new(false, false);
106        assert!(!check_jq(&checker));
107    }
108
109    #[test]
110    fn test_check_docker_running() {
111        let checker = MockChecker::new(false, true);
112        assert!(check_docker(&checker));
113    }
114
115    #[test]
116    fn test_check_docker_not_running() {
117        let checker = MockChecker::new(false, false);
118        assert!(!check_docker(&checker));
119    }
120
121    #[test]
122    fn test_check_all_both_present() {
123        let checker = MockChecker::new(true, true);
124        let status = check_all(&checker);
125        assert!(status.jq_available);
126        assert!(status.docker_available);
127    }
128
129    #[test]
130    fn test_check_all_jq_missing() {
131        let checker = MockChecker::new(false, true);
132        let status = check_all(&checker);
133        assert!(!status.jq_available);
134        assert!(status.docker_available);
135    }
136
137    #[test]
138    fn test_check_all_docker_missing() {
139        let checker = MockChecker::new(true, false);
140        let status = check_all(&checker);
141        assert!(status.jq_available);
142        assert!(!status.docker_available);
143    }
144
145    #[test]
146    fn test_check_all_both_missing() {
147        let checker = MockChecker::new(false, false);
148        let status = check_all(&checker);
149        assert!(!status.jq_available);
150        assert!(!status.docker_available);
151    }
152
153    #[test]
154    fn test_real_checker_jq() {
155        // Integration test - depends on jq being installed
156        let checker = RealChecker;
157        // jq should be available on dev machines
158        let result = check_jq(&checker);
159        // We just verify it doesn't panic - actual availability depends on environment
160        let _ = result;
161    }
162
163    #[test]
164    fn test_real_checker_docker() {
165        // Integration test - depends on Docker running
166        let checker = RealChecker;
167        // Docker may or may not be running
168        let result = check_docker(&checker);
169        // We just verify it doesn't panic - actual availability depends on environment
170        let _ = result;
171    }
172
173    #[test]
174    fn test_dependency_status_equality() {
175        let status1 = DependencyStatus {
176            jq_available: true,
177            docker_available: false,
178        };
179        let status2 = DependencyStatus {
180            jq_available: true,
181            docker_available: false,
182        };
183        let status3 = DependencyStatus {
184            jq_available: false,
185            docker_available: false,
186        };
187        assert_eq!(status1, status2);
188        assert_ne!(status1, status3);
189    }
190
191    #[test]
192    fn test_dependency_status_clone() {
193        let status = DependencyStatus {
194            jq_available: true,
195            docker_available: true,
196        };
197        let cloned = status.clone();
198        assert_eq!(status, cloned);
199    }
200}