torrust_tracker_deployer_lib/adapters/tofu/
json_parser.rs1use std::net::IpAddr;
18use std::str::FromStr;
19
20use serde_json::Value;
21use thiserror::Error;
22
23use super::client::InstanceInfo;
24
25#[derive(Error, Debug)]
27pub enum ParseError {
28 #[error("Failed to parse JSON: {message}")]
30 JsonError { message: String },
31
32 #[error("Field error: {message}")]
34 FieldError { message: String },
35}
36
37pub(crate) struct OpenTofuJsonParser;
43
44impl OpenTofuJsonParser {
45 pub fn parse_instance_info(json_output: &str) -> Result<InstanceInfo, ParseError> {
63 let outputs: Value =
64 serde_json::from_str(json_output).map_err(|e| ParseError::JsonError {
65 message: format!("Failed to parse OpenTofu output as JSON: {e}"),
66 })?;
67
68 let instance_info_value = outputs
69 .get("instance_info")
70 .and_then(|v| v.get("value"))
71 .ok_or_else(|| ParseError::FieldError {
72 message: "instance_info section not found in OpenTofu outputs".to_string(),
73 })?;
74
75 let image = instance_info_value
76 .get("image")
77 .and_then(|v| v.as_str())
78 .ok_or_else(|| ParseError::FieldError {
79 message: "image field missing or not a string".to_string(),
80 })?
81 .to_string();
82
83 let ip_address_str = instance_info_value
84 .get("ip_address")
85 .and_then(|v| v.as_str())
86 .ok_or_else(|| ParseError::FieldError {
87 message: "ip_address field missing or not a string".to_string(),
88 })?;
89
90 let ip_address = IpAddr::from_str(ip_address_str).map_err(|e| ParseError::FieldError {
91 message: format!("ip_address field is not a valid IP address: {e}"),
92 })?;
93
94 let name = instance_info_value
95 .get("name")
96 .and_then(|v| v.as_str())
97 .ok_or_else(|| ParseError::FieldError {
98 message: "name field missing or not a string".to_string(),
99 })?
100 .to_string();
101
102 let status = instance_info_value
103 .get("status")
104 .and_then(|v| v.as_str())
105 .ok_or_else(|| ParseError::FieldError {
106 message: "status field missing or not a string".to_string(),
107 })?
108 .to_string();
109
110 Ok(InstanceInfo {
111 image,
112 ip_address,
113 name,
114 status,
115 })
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn it_should_parse_instance_info_from_valid_json() {
125 let json_output = r#"{
126 "instance_info": {
127 "value": {
128 "image": "ubuntu:24.04",
129 "ip_address": "10.140.190.68",
130 "name": "torrust-tracker-vm",
131 "status": "Running"
132 }
133 }
134 }"#;
135
136 let result = OpenTofuJsonParser::parse_instance_info(json_output).unwrap();
137
138 assert_eq!(result.image, "ubuntu:24.04");
139 assert_eq!(
140 result.ip_address,
141 IpAddr::from_str("10.140.190.68").unwrap()
142 );
143 assert_eq!(result.name, "torrust-tracker-vm");
144 assert_eq!(result.status, "Running");
145 }
146
147 #[test]
148 fn it_should_fail_with_invalid_json() {
149 let invalid_json = "not valid json";
150
151 let result = OpenTofuJsonParser::parse_instance_info(invalid_json);
152
153 assert!(result.is_err());
154 assert!(matches!(result.unwrap_err(), ParseError::JsonError { .. }));
155 }
156
157 #[test]
158 fn it_should_fail_when_instance_info_section_missing() {
159 let json_output = r#"{
160 "other_output": {
161 "value": "some value"
162 }
163 }"#;
164
165 let result = OpenTofuJsonParser::parse_instance_info(json_output);
166
167 assert!(result.is_err());
168 let error = result.unwrap_err();
169 assert!(matches!(error, ParseError::FieldError { .. }));
170 assert!(error
171 .to_string()
172 .contains("instance_info section not found"));
173 }
174
175 #[test]
176 fn it_should_fail_when_required_field_missing() {
177 let json_output = r#"{
178 "instance_info": {
179 "value": {
180 "image": "ubuntu:24.04",
181 "ip_address": "10.140.190.68",
182 "name": "torrust-tracker-vm"
183 }
184 }
185 }"#;
186
187 let result = OpenTofuJsonParser::parse_instance_info(json_output);
188
189 assert!(result.is_err());
190 let error = result.unwrap_err();
191 assert!(matches!(error, ParseError::FieldError { .. }));
192 assert!(error.to_string().contains("status field missing"));
193 }
194
195 #[test]
196 fn it_should_fail_when_field_has_wrong_type() {
197 let json_output = r#"{
198 "instance_info": {
199 "value": {
200 "image": 123,
201 "ip_address": "10.140.190.68",
202 "name": "torrust-tracker-vm",
203 "status": "Running"
204 }
205 }
206 }"#;
207
208 let result = OpenTofuJsonParser::parse_instance_info(json_output);
209
210 assert!(result.is_err());
211 let error = result.unwrap_err();
212 assert!(matches!(error, ParseError::FieldError { .. }));
213 assert!(error
214 .to_string()
215 .contains("image field missing or not a string"));
216 }
217
218 #[test]
219 fn it_should_fail_when_ip_address_is_invalid() {
220 let json_output = r#"{
221 "instance_info": {
222 "value": {
223 "image": "ubuntu:24.04",
224 "ip_address": "invalid-ip-address",
225 "name": "torrust-tracker-vm",
226 "status": "Running"
227 }
228 }
229 }"#;
230
231 let result = OpenTofuJsonParser::parse_instance_info(json_output);
232
233 assert!(result.is_err());
234 let error = result.unwrap_err();
235 assert!(matches!(error, ParseError::FieldError { .. }));
236 assert!(error
237 .to_string()
238 .contains("ip_address field is not a valid IP address"));
239 }
240}