1mod utils;
2use crate::host::utils::{is_duplicate_entry, parse_line};
3use serde::Serialize;
4use std::{fmt, io, net::IpAddr};
5
6#[derive(Debug, Clone, PartialEq, Serialize)]
7pub enum HostStatus {
8 Active,
9 Inactive,
10}
11
12impl fmt::Display for HostStatus {
13 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
14 match self {
15 HostStatus::Active => write!(f, "Active"),
16 HostStatus::Inactive => write!(f, "Inactive"),
17 }
18 }
19}
20
21#[derive(Debug, Clone, PartialEq, Serialize)]
22pub struct HostEntry {
23 pub status: HostStatus,
24 pub ip: IpAddr,
25 pub name: String,
26}
27
28impl HostEntry {
29 pub fn toggle(&mut self) {
30 self.status = match self.status {
31 HostStatus::Active => HostStatus::Inactive,
32 HostStatus::Inactive => HostStatus::Active,
33 };
34 }
35}
36
37impl fmt::Display for HostEntry {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 write!(f, "{}: {} is {}", self.ip, self.name, self.status)
40 }
41}
42
43#[derive(Debug)]
44pub enum HostError {
45 DuplicateEntry,
46 EntryNotFound,
47}
48
49impl fmt::Display for HostError {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 match self {
52 HostError::DuplicateEntry => write!(f, "You have inserted a duplicate entry."),
53 HostError::EntryNotFound => write!(f, "Please check the name and try again."),
54 }
55 }
56}
57
58impl std::error::Error for HostError {}
59
60pub fn parse_contents(contents: impl Iterator<Item = io::Result<String>>) -> Vec<HostEntry> {
61 contents
62 .map_while(Result::ok)
63 .filter_map(|line| parse_line(&line))
64 .collect()
65}
66
67pub fn add_entry(
68 entries: &mut Vec<HostEntry>,
69 ip: IpAddr,
70 name: impl Into<String>,
71) -> Result<(), HostError> {
72 let name = name.into();
73 if is_duplicate_entry(entries, ip, &name) {
74 return Err(HostError::DuplicateEntry);
75 }
76 entries.push(HostEntry {
77 status: HostStatus::Active,
78 ip,
79 name,
80 });
81 Ok(())
82}
83
84pub fn remove_entry(entries: &mut Vec<HostEntry>, partial_name: &str) -> Result<(), HostError> {
85 let original_len = entries.len();
86 entries.retain(|e| !e.name.contains(partial_name));
87 if entries.len() == original_len {
88 return Err(HostError::EntryNotFound);
89 }
90 Ok(())
91}
92
93pub fn toggle_entry(entries: &mut Vec<HostEntry>, partial_name: &str) -> Result<(), HostError> {
94 let mut found = false;
95 for entry in entries.iter_mut() {
96 if entry.name.contains(partial_name) {
97 entry.toggle();
98 found = true;
99 }
100 }
101 if !found {
102 return Err(HostError::EntryNotFound);
103 }
104 Ok(())
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use std::io;
111 use std::net::{IpAddr, Ipv4Addr};
112
113 fn ip(a: u8, b: u8, c: u8, d: u8) -> IpAddr {
116 IpAddr::V4(Ipv4Addr::new(a, b, c, d))
117 }
118
119 fn make_entry(ip_addr: IpAddr, name: &str, status: HostStatus) -> HostEntry {
120 HostEntry {
121 ip: ip_addr,
122 name: name.to_string(),
123 status,
124 }
125 }
126
127 fn sample_entries() -> Vec<HostEntry> {
128 vec![
129 make_entry(ip(127, 0, 0, 1), "alpha.com", HostStatus::Active),
130 make_entry(ip(192, 168, 0, 1), "beta.com", HostStatus::Inactive),
131 ]
132 }
133
134 fn to_lines(lines: &[&str]) -> std::vec::IntoIter<io::Result<String>> {
135 lines
136 .iter()
137 .map(|l| Ok(l.to_string()))
138 .collect::<Vec<_>>()
139 .into_iter()
140 }
141
142 #[test]
145 fn parse_contents_mixed_lines_returns_correct_entries() {
146 let lines = to_lines(&["127.0.0.1 active.com", "# 192.168.0.1 inactive.com"]);
147 let entries = parse_contents(lines);
148 assert_eq!(entries.len(), 2);
149 assert_eq!(entries[0].ip, ip(127, 0, 0, 1));
150 assert_eq!(entries[0].name, "active.com");
151 assert_eq!(entries[0].status, HostStatus::Active);
152 assert_eq!(entries[1].ip, ip(192, 168, 0, 1));
153 assert_eq!(entries[1].name, "inactive.com");
154 assert_eq!(entries[1].status, HostStatus::Inactive);
155 }
156
157 #[test]
158 fn parse_contents_skips_non_matching_lines() {
159 let lines = to_lines(&[
160 "127.0.0.1 active.com",
161 "# This is a comment",
162 "",
163 "192.168.0.1 another.com",
164 ]);
165 let entries = parse_contents(lines);
166 assert_eq!(entries.len(), 2);
167 }
168
169 #[test]
170 fn parse_contents_empty_input_returns_empty_vec() {
171 let entries = parse_contents(std::iter::empty::<io::Result<String>>());
172 assert!(entries.is_empty());
173 }
174
175 #[test]
178 fn add_entry_new_entry_returns_ok_and_appends() {
179 let mut entries = sample_entries();
180 let result = add_entry(&mut entries, ip(10, 0, 0, 1), "new.com".to_string());
181 assert!(result.is_ok());
182 assert_eq!(entries.len(), 3);
183 assert_eq!(entries[2].ip, ip(10, 0, 0, 1));
184 assert_eq!(entries[2].name, "new.com");
185 assert_eq!(entries[2].status, HostStatus::Active);
186 }
187
188 #[test]
189 fn add_entry_duplicate_returns_err_and_leaves_entries_unchanged() {
190 let mut entries = sample_entries();
191 let result = add_entry(&mut entries, ip(127, 0, 0, 1), "alpha.com".to_string());
192 assert!(matches!(result, Err(HostError::DuplicateEntry)));
193 assert_eq!(entries.len(), 2);
194 }
195
196 #[test]
199 fn remove_entry_exact_name_match_removes_entry() {
200 let mut entries = sample_entries();
201 let result = remove_entry(&mut entries, "alpha.com");
202 assert!(result.is_ok());
203 assert_eq!(entries.len(), 1);
204 assert_eq!(entries[0].name, "beta.com");
205 }
206
207 #[test]
208 fn remove_entry_partial_name_match_removes_all_matching() {
209 let mut entries = vec![
210 make_entry(ip(127, 0, 0, 1), "dev.hostcraft.com", HostStatus::Active),
211 make_entry(
212 ip(192, 168, 0, 1),
213 "staging.hostcraft.com",
214 HostStatus::Active,
215 ),
216 make_entry(ip(10, 0, 0, 1), "unrelated.com", HostStatus::Active),
217 ];
218 let result = remove_entry(&mut entries, "hostcraft");
219 assert!(result.is_ok());
220 assert_eq!(entries.len(), 1);
221 assert_eq!(entries[0].name, "unrelated.com");
222 }
223
224 #[test]
225 fn remove_entry_no_match_returns_err_and_leaves_entries_unchanged() {
226 let mut entries = sample_entries();
227 let result = remove_entry(&mut entries, "nonexistent.com");
228 assert!(matches!(result, Err(HostError::EntryNotFound)));
229 assert_eq!(entries.len(), 2);
230 }
231
232 #[test]
235 fn toggle_entry_active_becomes_inactive() {
236 let mut entries = sample_entries();
237 let result = toggle_entry(&mut entries, "alpha.com");
238 assert!(result.is_ok());
239 assert_eq!(entries[0].status, HostStatus::Inactive);
240 }
241
242 #[test]
243 fn toggle_entry_inactive_becomes_active() {
244 let mut entries = sample_entries();
245 let result = toggle_entry(&mut entries, "beta.com");
246 assert!(result.is_ok());
247 assert_eq!(entries[1].status, HostStatus::Active);
248 }
249
250 #[test]
251 fn toggle_entry_partial_match_toggles_all_matching_only() {
252 let mut entries = vec![
253 make_entry(ip(127, 0, 0, 1), "dev.hostcraft.com", HostStatus::Active),
254 make_entry(
255 ip(192, 168, 0, 1),
256 "staging.hostcraft.com",
257 HostStatus::Active,
258 ),
259 make_entry(ip(10, 0, 0, 1), "unrelated.com", HostStatus::Active),
260 ];
261 let result = toggle_entry(&mut entries, "hostcraft");
262 assert!(result.is_ok());
263 assert_eq!(entries[0].status, HostStatus::Inactive);
264 assert_eq!(entries[1].status, HostStatus::Inactive);
265 assert_eq!(entries[2].status, HostStatus::Active);
266 }
267
268 #[test]
269 fn toggle_entry_no_match_returns_err() {
270 let mut entries = sample_entries();
271 let result = toggle_entry(&mut entries, "nonexistent.com");
272 assert!(matches!(result, Err(HostError::EntryNotFound)));
273 }
274}