1use alloc::collections::BTreeMap;
30use alloc::string::{String, ToString};
31use alloc::vec::Vec;
32
33use crate::plan::{
34 DeploymentPlan, ImplementationDescription, InstanceDeploymentDescription, PlanConnection,
35};
36
37#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum ParseError {
40 ExpectedTag(String),
42 UnterminatedTag,
44 ValidationFailed(String),
46}
47
48impl core::fmt::Display for ParseError {
49 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
50 match self {
51 Self::ExpectedTag(t) => write!(f, "expected tag <{t}>"),
52 Self::UnterminatedTag => f.write_str("unterminated tag"),
53 Self::ValidationFailed(msg) => write!(f, "validation: {msg}"),
54 }
55 }
56}
57
58#[cfg(feature = "std")]
59impl std::error::Error for ParseError {}
60
61pub fn parse_plan_xml(input: &str) -> Result<DeploymentPlan, ParseError> {
70 let body = inner_text(input, "deploymentPlan")?;
71 let label = inner_text(&body, "label").unwrap_or_default();
72 let uuid = inner_text(&body, "UUID").unwrap_or_default();
73 let realizes = inner_text(&body, "realizes").unwrap_or_default();
74
75 let mut plan = DeploymentPlan {
76 label,
77 uuid,
78 realizes,
79 ..DeploymentPlan::default()
80 };
81
82 let instance_blocks = iter_blocks(&body, "instance");
89 let connection_blocks = iter_blocks(&body, "connection");
90 let body_without_instances = strip_blocks(&body, "instance");
91 let body_without_inst_conn = strip_blocks(&body_without_instances, "connection");
92
93 for impl_block in iter_blocks(&body_without_inst_conn, "implementation") {
94 plan.implementations.push(parse_implementation(&impl_block));
95 }
96 for inst_block in instance_blocks {
97 plan.instances.push(parse_instance(&inst_block));
98 }
99 for conn_block in connection_blocks {
100 plan.connections.push(parse_connection(&conn_block));
101 }
102
103 plan.validate()
104 .map_err(|e| ParseError::ValidationFailed(format_err(&e)))?;
105 Ok(plan)
106}
107
108fn strip_blocks(input: &str, tag: &str) -> String {
109 let open = alloc::format!("<{tag}>");
110 let close = alloc::format!("</{tag}>");
111 let mut out = String::new();
112 let mut cursor = 0;
113 while let Some(s) = input[cursor..].find(&open) {
114 let start = cursor + s;
115 out.push_str(&input[cursor..start]);
116 if let Some(e) = input[start..].find(&close) {
117 cursor = start + e + close.len();
118 } else {
119 cursor = input.len();
120 break;
121 }
122 }
123 out.push_str(&input[cursor..]);
124 out
125}
126
127fn parse_implementation(block: &str) -> ImplementationDescription {
128 let label = inner_text(block, "label").unwrap_or_default();
129 let uuid = inner_text(block, "UUID").unwrap_or_default();
130 let realizes = inner_text(block, "realizes").unwrap_or_default();
131 let artifacts: Vec<String> = collect_inner_texts(block, "artifact");
132 ImplementationDescription {
133 label,
134 uuid,
135 artifacts,
136 realizes,
137 exec_params: BTreeMap::new(),
138 depends_on: Vec::new(),
139 }
140}
141
142fn parse_instance(block: &str) -> InstanceDeploymentDescription {
143 let name = inner_text(block, "name").unwrap_or_default();
144 let implementation = inner_text(block, "implementation").unwrap_or_default();
145 let node = inner_text(block, "node").unwrap_or_default();
146 let co_locate_with = collect_inner_texts(block, "coLocateWith");
147 InstanceDeploymentDescription {
148 name,
149 implementation,
150 node,
151 config_props: BTreeMap::new(),
152 co_locate_with,
153 }
154}
155
156fn parse_connection(block: &str) -> PlanConnection {
157 PlanConnection {
158 name: inner_text(block, "name").unwrap_or_default(),
159 source_instance: inner_text(block, "sourceInstance").unwrap_or_default(),
160 source_port: inner_text(block, "sourcePort").unwrap_or_default(),
161 target_instance: inner_text(block, "targetInstance").unwrap_or_default(),
162 target_port: inner_text(block, "targetPort").unwrap_or_default(),
163 }
164}
165
166fn inner_text(input: &str, tag: &str) -> Result<String, ParseError> {
167 let open = alloc::format!("<{tag}>");
168 let close = alloc::format!("</{tag}>");
169 let start = input
170 .find(&open)
171 .ok_or_else(|| ParseError::ExpectedTag(tag.to_string()))?
172 + open.len();
173 let end = input[start..]
174 .find(&close)
175 .ok_or(ParseError::UnterminatedTag)?
176 + start;
177 Ok(input[start..end].trim().to_string())
178}
179
180fn collect_inner_texts(input: &str, tag: &str) -> Vec<String> {
181 let open = alloc::format!("<{tag}>");
182 let close = alloc::format!("</{tag}>");
183 let mut out = Vec::new();
184 let mut cursor = 0;
185 while let Some(s) = input[cursor..].find(&open) {
186 let start = cursor + s + open.len();
187 if let Some(e) = input[start..].find(&close) {
188 let end = start + e;
189 out.push(input[start..end].trim().to_string());
190 cursor = end + close.len();
191 } else {
192 break;
193 }
194 }
195 out
196}
197
198fn iter_blocks(input: &str, tag: &str) -> Vec<String> {
199 let open = alloc::format!("<{tag}>");
200 let close = alloc::format!("</{tag}>");
201 let mut blocks = Vec::new();
202 let mut cursor = 0;
203 while let Some(s) = input[cursor..].find(&open) {
204 let start = cursor + s + open.len();
205 if let Some(e) = input[start..].find(&close) {
206 let end = start + e;
207 blocks.push(input[start..end].to_string());
208 cursor = end + close.len();
209 } else {
210 break;
211 }
212 }
213 blocks
214}
215
216fn format_err(e: &crate::plan::PlanError) -> String {
217 alloc::format!("{e}")
218}
219
220#[cfg(test)]
221#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
222mod tests {
223 use super::*;
224
225 const SAMPLE: &str = r#"
226<deploymentPlan>
227 <label>Plan1</label>
228 <UUID>uuid-1</UUID>
229 <realizes>RootApp</realizes>
230 <implementation>
231 <label>EchoImpl</label>
232 <UUID>echo-uuid</UUID>
233 <artifact>lib/echo.so</artifact>
234 <realizes>IDL:demo/Echo:1.0</realizes>
235 </implementation>
236 <instance>
237 <name>echo1</name>
238 <implementation>EchoImpl</implementation>
239 <node>Node1</node>
240 </instance>
241</deploymentPlan>
242"#;
243
244 #[test]
245 fn parses_minimal_plan() {
246 let plan = parse_plan_xml(SAMPLE).unwrap();
247 assert_eq!(plan.label, "Plan1");
248 assert_eq!(plan.uuid, "uuid-1");
249 assert_eq!(plan.implementations.len(), 1);
250 assert_eq!(
251 plan.implementations[0].artifacts,
252 alloc::vec!["lib/echo.so".to_string()]
253 );
254 assert_eq!(plan.instances.len(), 1);
255 assert_eq!(plan.instances[0].node, "Node1");
256 }
257
258 #[test]
259 fn parses_multiple_artifacts() {
260 let xml = r#"
261<deploymentPlan>
262 <label>P</label>
263 <UUID>u</UUID>
264 <implementation>
265 <label>I1</label>
266 <UUID>i1</UUID>
267 <artifact>a.so</artifact>
268 <artifact>b.so</artifact>
269 <realizes>IDL:X:1.0</realizes>
270 </implementation>
271</deploymentPlan>
272"#;
273 let plan = parse_plan_xml(xml).unwrap();
274 assert_eq!(plan.implementations[0].artifacts.len(), 2);
275 }
276
277 #[test]
278 fn validation_fails_on_unknown_impl_reference() {
279 let xml = r#"
280<deploymentPlan>
281 <label>P</label>
282 <UUID>u</UUID>
283 <instance>
284 <name>x</name>
285 <implementation>NoSuchImpl</implementation>
286 <node>N</node>
287 </instance>
288</deploymentPlan>
289"#;
290 let err = parse_plan_xml(xml).unwrap_err();
291 match err {
292 ParseError::ValidationFailed(_) => {}
293 e => panic!("unexpected error variant: {e}"),
294 }
295 }
296
297 #[test]
298 fn missing_root_tag_returns_expected_tag() {
299 let err = parse_plan_xml("<empty/>").unwrap_err();
300 assert_eq!(err, ParseError::ExpectedTag("deploymentPlan".into()));
301 }
302
303 #[test]
304 fn co_locate_with_parsed() {
305 let xml = r#"
306<deploymentPlan>
307 <label>P</label>
308 <UUID>u</UUID>
309 <implementation>
310 <label>I</label>
311 <UUID>i</UUID>
312 <artifact>a.so</artifact>
313 <realizes>IDL:X:1.0</realizes>
314 </implementation>
315 <instance>
316 <name>a</name>
317 <implementation>I</implementation>
318 <node>N</node>
319 </instance>
320 <instance>
321 <name>b</name>
322 <implementation>I</implementation>
323 <node>N</node>
324 <coLocateWith>a</coLocateWith>
325 </instance>
326</deploymentPlan>
327"#;
328 let plan = parse_plan_xml(xml).unwrap();
329 assert_eq!(
330 plan.instances[1].co_locate_with,
331 alloc::vec!["a".to_string()]
332 );
333 }
334}