Skip to content

Commit d87613c

Browse files
committed
refactor: improve InventoryTemplate API by taking ownership of context
- Change InventoryTemplate::new() to take InventoryContext by value instead of reference - Add Clone trait to InventoryContext to support the API change - Update all call sites throughout the codebase (e2e_tests, integration tests, unit tests) - Simplify internal implementation by removing manual cloning - Move ansible_host.rs and ssh_private_key_file.rs to context/ subdirectory - Move InventoryContext* types and tests to context/mod.rs for better organization This improves the API ergonomics and eliminates unnecessary complexity in the InventoryTemplate constructor while maintaining backward compatibility in usage patterns.
1 parent 81b11cb commit d87613c

File tree

6 files changed

+258
-244
lines changed

6 files changed

+258
-244
lines changed

src/bin/e2e_tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ impl TestEnvironment {
157157
.context("Failed to create InventoryContext")?
158158
};
159159
let inventory_template =
160-
InventoryTemplate::new(&inventory_template_file, &inventory_context)
160+
InventoryTemplate::new(&inventory_template_file, inventory_context)
161161
.context("Failed to create InventoryTemplate")?;
162162

163163
inventory_template

src/template/wrappers/ansible/inventory/ansible_host.rs renamed to src/template/wrappers/ansible/inventory/context/ansible_host.rs

File renamed without changes.
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
pub mod ansible_host;
2+
pub mod ssh_private_key_file;
3+
4+
use serde::Serialize;
5+
use thiserror::Error;
6+
7+
#[cfg(test)]
8+
use std::str::FromStr;
9+
10+
pub use ansible_host::{AnsibleHost, AnsibleHostError};
11+
pub use ssh_private_key_file::{SshPrivateKeyFile, SshPrivateKeyFileError};
12+
13+
/// Errors that can occur when creating an `InventoryContext`
14+
#[derive(Debug, Error)]
15+
pub enum InventoryContextError {
16+
#[error("Invalid ansible host: {0}")]
17+
InvalidAnsibleHost(#[from] AnsibleHostError),
18+
19+
#[error("Invalid SSH private key file: {0}")]
20+
InvalidSshPrivateKeyFile(#[from] SshPrivateKeyFileError),
21+
22+
#[error("Missing ansible host - must be set before building")]
23+
MissingAnsibleHost,
24+
25+
#[error("Missing SSH private key file - must be set before building")]
26+
MissingSshPrivateKeyFile,
27+
}
28+
29+
#[derive(Serialize, Debug, Clone)]
30+
pub struct InventoryContext {
31+
ansible_host: AnsibleHost,
32+
ansible_ssh_private_key_file: SshPrivateKeyFile,
33+
}
34+
35+
/// Builder for `InventoryContext` with fluent interface
36+
#[derive(Debug, Default)]
37+
pub struct InventoryContextBuilder {
38+
ansible_host: Option<AnsibleHost>,
39+
ansible_ssh_private_key_file: Option<SshPrivateKeyFile>,
40+
}
41+
42+
impl InventoryContextBuilder {
43+
/// Creates a new empty builder
44+
#[must_use]
45+
pub fn new() -> Self {
46+
Self::default()
47+
}
48+
49+
/// Sets the Ansible host for the builder.
50+
#[must_use]
51+
pub fn with_host(mut self, ansible_host: AnsibleHost) -> Self {
52+
self.ansible_host = Some(ansible_host);
53+
self
54+
}
55+
56+
/// Sets the SSH private key file path for the builder.
57+
#[must_use]
58+
pub fn with_ssh_priv_key_path(mut self, ssh_private_key_file: SshPrivateKeyFile) -> Self {
59+
self.ansible_ssh_private_key_file = Some(ssh_private_key_file);
60+
self
61+
}
62+
63+
/// Builds the `InventoryContext`
64+
///
65+
/// # Errors
66+
///
67+
/// Returns an error if either `ansible_host` or `ansible_ssh_private_key_file` is missing
68+
pub fn build(self) -> Result<InventoryContext, InventoryContextError> {
69+
let ansible_host = self
70+
.ansible_host
71+
.ok_or(InventoryContextError::MissingAnsibleHost)?;
72+
73+
let ansible_ssh_private_key_file = self
74+
.ansible_ssh_private_key_file
75+
.ok_or(InventoryContextError::MissingSshPrivateKeyFile)?;
76+
77+
Ok(InventoryContext {
78+
ansible_host,
79+
ansible_ssh_private_key_file,
80+
})
81+
}
82+
}
83+
84+
impl InventoryContext {
85+
/// Creates a new `InventoryContext` using typed parameters
86+
///
87+
/// # Errors
88+
///
89+
/// This method cannot fail with the current implementation since it takes
90+
/// already validated types, but returns Result for consistency with builder pattern
91+
pub fn new(
92+
ansible_host: AnsibleHost,
93+
ansible_ssh_private_key_file: SshPrivateKeyFile,
94+
) -> Result<Self, InventoryContextError> {
95+
Ok(Self {
96+
ansible_host,
97+
ansible_ssh_private_key_file,
98+
})
99+
}
100+
101+
/// Creates a new builder for `InventoryContext` with fluent interface
102+
#[must_use]
103+
pub fn builder() -> InventoryContextBuilder {
104+
InventoryContextBuilder::new()
105+
}
106+
107+
/// Get the ansible host value as a string
108+
#[must_use]
109+
pub fn ansible_host(&self) -> String {
110+
self.ansible_host.as_str()
111+
}
112+
113+
/// Get the ansible SSH private key file path as a string
114+
#[must_use]
115+
pub fn ansible_ssh_private_key_file(&self) -> String {
116+
self.ansible_ssh_private_key_file.as_str()
117+
}
118+
119+
/// Get the ansible host wrapper
120+
#[must_use]
121+
pub fn ansible_host_wrapper(&self) -> &AnsibleHost {
122+
&self.ansible_host
123+
}
124+
125+
/// Get the ansible SSH private key file wrapper
126+
#[must_use]
127+
pub fn ansible_ssh_private_key_file_wrapper(&self) -> &SshPrivateKeyFile {
128+
&self.ansible_ssh_private_key_file
129+
}
130+
}
131+
132+
#[cfg(test)]
133+
mod tests {
134+
use super::*;
135+
136+
#[test]
137+
fn it_should_provide_access_to_wrapper_types() {
138+
let host = AnsibleHost::from_str("10.0.0.1").unwrap();
139+
let ssh_key = SshPrivateKeyFile::new("/path/to/key").unwrap();
140+
let context = InventoryContext::builder()
141+
.with_host(host)
142+
.with_ssh_priv_key_path(ssh_key)
143+
.build()
144+
.unwrap();
145+
146+
// Test wrapper access
147+
let host_wrapper = context.ansible_host_wrapper();
148+
let key_wrapper = context.ansible_ssh_private_key_file_wrapper();
149+
150+
assert_eq!(host_wrapper.as_str(), "10.0.0.1");
151+
assert_eq!(key_wrapper.as_str(), "/path/to/key");
152+
}
153+
154+
#[test]
155+
fn it_should_support_builder_pattern_fluent_interface() {
156+
// Test the fluent builder interface as requested
157+
let host = AnsibleHost::from_str("192.168.1.100").unwrap();
158+
let ssh_key = SshPrivateKeyFile::new("/home/user/.ssh/id_rsa").unwrap();
159+
let inventory_context = InventoryContext::builder()
160+
.with_host(host)
161+
.with_ssh_priv_key_path(ssh_key)
162+
.build()
163+
.unwrap();
164+
165+
assert_eq!(inventory_context.ansible_host(), "192.168.1.100");
166+
assert_eq!(
167+
inventory_context.ansible_ssh_private_key_file(),
168+
"/home/user/.ssh/id_rsa"
169+
);
170+
}
171+
172+
#[test]
173+
fn it_should_work_with_builder_typed_parameters() {
174+
// Test builder with typed parameters instead of strings
175+
let host = AnsibleHost::from_str("10.0.0.1").unwrap();
176+
let ssh_key = SshPrivateKeyFile::new("/path/to/key").unwrap();
177+
178+
let inventory_context = InventoryContext::builder()
179+
.with_host(host)
180+
.with_ssh_priv_key_path(ssh_key)
181+
.build()
182+
.unwrap();
183+
184+
assert_eq!(inventory_context.ansible_host(), "10.0.0.1");
185+
assert_eq!(
186+
inventory_context.ansible_ssh_private_key_file(),
187+
"/path/to/key"
188+
);
189+
}
190+
191+
#[test]
192+
fn it_should_fail_when_builder_missing_host() {
193+
// Test that builder fails when host is missing
194+
let ssh_key = SshPrivateKeyFile::new("/path/to/key").unwrap();
195+
let result = InventoryContext::builder()
196+
.with_ssh_priv_key_path(ssh_key)
197+
.build();
198+
199+
assert!(result.is_err());
200+
let error_msg = result.unwrap_err().to_string();
201+
assert!(error_msg.contains("Missing ansible host"));
202+
}
203+
204+
#[test]
205+
fn it_should_fail_when_builder_missing_ssh_key() {
206+
// Test that builder fails when SSH key is missing
207+
let host = AnsibleHost::from_str("192.168.1.100").unwrap();
208+
let result = InventoryContext::builder().with_host(host).build();
209+
210+
assert!(result.is_err());
211+
let error_msg = result.unwrap_err().to_string();
212+
assert!(error_msg.contains("Missing SSH private key file"));
213+
}
214+
215+
#[test]
216+
fn it_should_create_new_inventory_context_with_typed_parameters() {
217+
// Test the new direct constructor with typed parameters
218+
let host = AnsibleHost::from_str("192.168.1.50").unwrap();
219+
let ssh_key = SshPrivateKeyFile::new("/etc/ssh/test_key").unwrap();
220+
221+
let inventory_context = InventoryContext::new(host, ssh_key).unwrap();
222+
223+
assert_eq!(inventory_context.ansible_host(), "192.168.1.50");
224+
assert_eq!(
225+
inventory_context.ansible_ssh_private_key_file(),
226+
"/etc/ssh/test_key"
227+
);
228+
}
229+
}

src/template/wrappers/ansible/inventory/ssh_private_key_file.rs renamed to src/template/wrappers/ansible/inventory/context/ssh_private_key_file.rs

File renamed without changes.

0 commit comments

Comments
 (0)