Skip to main content

winrm_rs/
builder.rs

1// Typestate builder for WinrmClient.
2//
3// Provides a compile-time-safe builder pattern that requires credentials
4// to be set before building the client.
5
6use crate::client::WinrmClient;
7use crate::config::{WinrmConfig, WinrmCredentials};
8use crate::error::WinrmError;
9
10/// Typestate marker: credentials have not yet been provided.
11pub struct NeedsCredentials;
12/// Typestate marker: all required fields are set and [`build`](WinrmClientBuilder::build) can be called.
13pub struct Ready;
14
15/// Builder for [`WinrmClient`] with compile-time state tracking.
16///
17/// Ensures that credentials are always provided before the client is built.
18///
19/// # Example
20/// ```no_run
21/// use winrm_rs::{WinrmClientBuilder, WinrmConfig, WinrmCredentials};
22///
23/// let client = WinrmClientBuilder::new(WinrmConfig::default())
24///     .credentials(WinrmCredentials::new("admin", "pass", ""))
25///     .build()
26///     .unwrap();
27/// ```
28pub struct WinrmClientBuilder<S = NeedsCredentials> {
29    config: WinrmConfig,
30    credentials: Option<WinrmCredentials>,
31    _state: std::marker::PhantomData<S>,
32}
33
34impl WinrmClientBuilder<NeedsCredentials> {
35    /// Create a new builder with the given configuration.
36    pub fn new(config: WinrmConfig) -> Self {
37        Self {
38            config,
39            credentials: None,
40            _state: std::marker::PhantomData,
41        }
42    }
43
44    /// Set the authentication credentials, transitioning to the `Ready` state.
45    pub fn credentials(self, creds: WinrmCredentials) -> WinrmClientBuilder<Ready> {
46        WinrmClientBuilder {
47            config: self.config,
48            credentials: Some(creds),
49            _state: std::marker::PhantomData,
50        }
51    }
52}
53
54impl WinrmClientBuilder<Ready> {
55    /// Build the [`WinrmClient`].
56    ///
57    /// Returns [`WinrmError::Http`] if the underlying HTTP client cannot be
58    /// constructed.
59    pub fn build(self) -> Result<WinrmClient, WinrmError> {
60        // SAFETY: The typestate pattern guarantees that `credentials` is
61        // `Some` when `build()` is callable (state = Ready).
62        WinrmClient::new(
63            self.config,
64            self.credentials
65                .expect("typestate guarantees credentials are set"),
66        )
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn builder_creates_client() {
76        let client = WinrmClientBuilder::new(WinrmConfig::default())
77            .credentials(WinrmCredentials::new("admin", "pass", ""))
78            .build()
79            .unwrap();
80        // Verify the client was constructed (endpoint uses config values)
81        assert_eq!(client.endpoint("test-host"), "http://test-host:5985/wsman");
82    }
83
84    #[test]
85    fn builder_preserves_custom_config() {
86        let config = WinrmConfig {
87            port: 5986,
88            use_tls: true,
89            max_envelope_size: 512_000,
90            ..Default::default()
91        };
92        let client = WinrmClientBuilder::new(config)
93            .credentials(WinrmCredentials::new("user", "pw", "DOM"))
94            .build()
95            .unwrap();
96        assert_eq!(client.endpoint("srv"), "https://srv:5986/wsman");
97        assert_eq!(client.config().max_envelope_size, 512_000);
98    }
99
100    #[test]
101    fn builder_via_client_static_method() {
102        let client = WinrmClient::builder(WinrmConfig::default())
103            .credentials(WinrmCredentials::new("admin", "pass", ""))
104            .build()
105            .unwrap();
106        assert_eq!(client.endpoint("host"), "http://host:5985/wsman");
107    }
108}