maven_rs/pom/editor.rs
1//! Pom Editor
2//!
3//! ## What is a Pom Editor?
4//! A pom editor is a struct that allows you to edit and create pom files.
5//! ## What is the difference between a [PomEditor] and a [crate::pom::Pom]?
6//!
7//! [crate::pom::Pom] is a struct that represents the data in a pom file. If you were to use it to edit a pom file. The data could be lost due
8//! How XML works with Serde
9//!
10//! [PomEditor] uses the crate edit-xml to parse the xml into a dom like structure.
11//! This allows for easy editing of the xml file. Without losing any data or original structure.
12
13use std::io::Write;
14
15use edit_xml::{Document, Element, ReadOptions, WriteOptions};
16mod build;
17mod dependency_management;
18mod distribution_management;
19use super::{depend::Dependency, Developer, Parent, Repository, Scm};
20use crate::editor::{
21 utils::{add_or_update_item, get_all_children_of_element, MissingElementError},
22 ElementConverter, UpdatableElement, XMLEditorError,
23};
24pub use build::*;
25pub use dependency_management::*;
26pub use distribution_management::*;
27/// A struct that allows editing and creating pom files
28/// A pom file is an xml file that follows the maven pom schema
29#[derive(Debug)]
30pub struct PomEditor {
31 /// The document is kept private to prevent the root element from being changed.
32 ///
33 /// The root element must always be a project element. As this code assumes it exists and without it. Panicking would occur.
34 document: Document,
35 pub ident_level: usize,
36}
37impl Default for PomEditor {
38 fn default() -> Self {
39 let document = Document::new_with_root("project", |project| {
40 project
41 .attribute("xmlns", "http://maven.apache.org/POM/4.0.0")
42 .attribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
43 .attribute(
44 "xsi:schemaLocation",
45 "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd",
46 )
47 });
48 let mut editor = Self {
49 document,
50 ident_level: 2,
51 };
52
53 editor.set_model_version("4.0.0");
54 editor
55 }
56}
57macro_rules! simple_type_getter_setter {
58 (
59 $(#[$shared_docs:meta])*
60 $name:literal {
61 $(#[$set_docs:meta])*
62 set: $set:ident,
63 $(#[$get_docs:meta])*
64 get: $get:ident,
65 }
66 ) => {
67 $(#[$set_docs])*
68 $(#[$shared_docs])*
69 pub fn $set<S, O>(&mut self, value: O)
70 where
71 S: Into<String>,
72 O: Into<Option<S>>
73 {
74 let root = self.root();
75 let value: Option<S> = value.into();
76 if let Some(value) = value {
77 let value = value.into();
78 let element = crate::editor::utils::get_or_create_top_level_element(
79 $name,
80 &mut self.document,
81 root,
82 );
83 element.set_text_content(&mut self.document, value);
84 }else{
85 let element = root.find(&self.document, $name);
86 if let Some(element) = element {
87 element.detach(&mut self.document).expect("Failed to remove element");
88 }
89 }
90 }
91 $(#[$get_docs])*
92 $(#[$shared_docs])*
93 pub fn $get(&self) -> Option<String> {
94 let root = self.root();
95 let element = root.find(&self.document, $name);
96 return element.map(|x| x.text_content(&self.document));
97 }
98
99 };
100 [
101 $(
102 $(#[$shared_docs:meta])*
103 $name:literal {
104 $(#[$set_docs:meta])*
105 set: $set:ident,
106 $(#[$get_docs:meta])*
107 get: $get:ident,
108 }
109 ),*
110 ] => {
111 $(
112
113 simple_type_getter_setter! {
114 $(#[$shared_docs])*
115 $name {
116 $(#[$set_docs])*
117 set: $set,
118 $(#[$get_docs])*
119 get: $get,
120 }
121 }
122 )*
123 };
124
125}
126macro_rules! top_level_structured_type {
127 (
128 $(#[$set_docs:meta])*
129 set: $set:ident,
130 $(#[$get_docs:meta])*
131 get: $get:ident,
132 $element_name:literal => $structured_type:ident,
133 ) => {
134 $(#[$get_docs])*
135 pub fn $get(&self) -> Result<Option<$structured_type>, XMLEditorError> {
136 let root = self.root();
137 root.find(&self.document, $element_name)
138 .map(|x| $structured_type::from_element(x, &self.document))
139 .transpose()
140 }
141 $(#[$set_docs])*
142 pub fn $set<U>(&mut self, value: U) -> Result<(), XMLEditorError>
143 where
144 U: Into<Option<$structured_type>> {
145 let value: Option<$structured_type> = value.into();
146 let root = self.root();
147 let existing_element = root.find(&self.document, $element_name);
148 if let Some(value) = value{
149 if let Some(element) = existing_element {
150 value.update_element(element, &mut self.document)?;
151 return Ok(());
152 }
153 let new_element = value.into_element(&mut self.document)?;
154 root.push_child(&mut self.document, new_element)?;
155 }else{
156 if let Some(element) = existing_element {
157 element.detach(&mut self.document)?;
158 }
159 }
160
161 Ok(())
162 }
163 };
164}
165
166macro_rules! list_item_getter_and_add {
167 (
168 $(#[$get_docs:meta])*
169 get: $get:ident,
170 $(#[$add_docs:meta])*
171 add: $add:ident,
172 $(#[$clear_docs:meta])*
173 clear: $clear:ident,
174 $parent:literal => $list_element:ident
175 ) => {
176 $(#[$get_docs])*
177 pub fn $get(&self) -> Result<Vec<$list_element>, XMLEditorError> {
178 let root = self.root();
179 let Some(parent_element) = root.find(&self.document, $parent)
180 else {
181 return Ok(vec![]);
182 };
183 let result =
184 get_all_children_of_element::<$list_element>(&self.document, parent_element)?;
185 Ok(result.into_iter().map(|(v, _)| v).collect())
186 }
187 $(#[$add_docs])*
188 pub fn $add(
189 &mut self,
190 value: $list_element,
191 ) -> Result<Option<$list_element>, XMLEditorError> {
192 let root = self.root();
193 let parent_element = root.find(&self.document, $parent);
194 add_or_update_item(&mut self.document, parent_element, root, value)
195 }
196 $(#[$clear_docs])*
197 pub fn $clear(&mut self)-> Result<(), XMLEditorError> {
198 let root = self.root();
199 let parent_element = root.find(&self.document, $parent);
200 if let Some(parent_element) = parent_element {
201 parent_element.clear_children(&mut self.document);
202 }
203 Ok(())
204 }
205 };
206}
207impl PomEditor {
208 /// Creates a new [PomEditor] with the group id and artifact id set
209 pub fn new_with_group_and_artifact(group_id: &str, artifact_id: &str) -> Self {
210 let mut editor = Self::default();
211 editor.set_group_id(group_id);
212 editor.set_artifact_id(artifact_id);
213 editor
214 }
215 top_level_structured_type!(
216 /// Sets the parent of the pom file
217 ///
218 /// If [None] is passed in. The parent element will be removed
219 set: set_parent,
220 /// Gets the parent of the pom file
221 get: get_parent,
222 "parent" => Parent,
223 );
224 top_level_structured_type!(
225 /// Sets the scm of the pom file
226 ///
227 /// If [None] is passed in. The scm element will be removed
228 set: set_scm,
229 /// Gets the scm of the pom file
230 get: get_scm,
231 "scm" => Scm,
232 );
233 simple_type_getter_setter
237 /// Example Usage:
238 /// ```rust
239 /// use maven_rs::pom::editor::PomEditor;
240 /// let mut editor = PomEditor::default();
241 /// editor.set_group_id("dev.wyatt-herkamp");
242 /// assert_eq!(editor.get_group_id(), Some("dev.wyatt-herkamp".to_string()));
243 /// ```
244 "groupId" {
245 /// Sets the group Id in the pom file. For the maven project
246 set: set_group_id,
247 get: get_group_id,
248 },
249 /// The artifact id of the pom
250 /// [More Info](https://maven.apache.org/pom.html#maven-coordinates)
251 ///
252 /// Example Usage:
253 /// ```rust
254 /// use maven_rs::pom::editor::PomEditor;
255 /// let mut editor = PomEditor::default();
256 /// editor.set_artifact_id("test");
257 /// assert_eq!(editor.get_artifact_id(), Some("test".to_string()));
258 /// ```
259 "artifactId" {
260 set: set_artifact_id,
261 get: get_artifact_id,
262 },
263 /// The version of the project file
264 /// [More Info](https://maven.apache.org/pom.html#maven-coordinates)
265 ///
266 /// Example Usage:
267 /// ```rust
268 /// use maven_rs::pom::editor::PomEditor;
269 /// let mut editor = PomEditor::default();
270 /// editor.set_version("1.0.0");
271 /// assert_eq!(editor.get_version(), Some("1.0.0".to_string()));
272 /// ```
273 "version" {
274 set: set_version,
275 get: get_version,
276 },
277 /// The name of the maven project
278 ///
279 /// [More Info](https://maven.apache.org/pom.html#More_Project_Information)
280 "name" {
281 set: set_name,
282 get: get_name,
283 },
284 /// The description of the maven project
285 ///
286 /// [More Info](https://maven.apache.org/pom.html#More_Project_Information)
287 "description" {
288 set: set_description,
289 get: get_description,
290 },
291 /// [More Info](https://maven.apache.org/pom.html#More_Project_Information)
292 "url" {
293 set: set_url,
294 get: get_url,
295 },
296 /// The inception year of the project
297 "inceptionYear" {
298 set: set_inception_year,
299 get: get_inception_year,
300 },
301 /// Sets the model version of the pom file
302 ///
303 /// The model version is currently always 4.0.0
304 "modelVersion" {
305 /// Sets the model version of the pom file
306 set: set_model_version,
307 /// Gets the model version of the pom file
308 get: get_model_version,
309 },
310 /// [More Info](https://maven.apache.org/pom.html#Packaging)
311 "packaging" {
312 set: set_packaging,
313 get: get_packaging,
314 }
315 ];
316
317 list_item_getter_and_add!(
318 /// Gets all the repositories in the pom file
319 /// ```rust
320 /// use maven_rs::pom::editor::PomEditor;
321 /// use maven_rs::pom::Repository;
322 /// let xml = r#"
323 /// <project>
324 /// <repositories>
325 /// <repository>
326 /// <id>central</id>
327 /// <name>Maven Central Repository</name>
328 /// <url>https://repo.maven.apache.org/maven2</url>
329 /// </repository>
330 /// </repositories>
331 /// </project>
332 /// "#;
333 /// let editor = PomEditor::load_from_str(xml).unwrap();
334 /// let repositories = editor.get_repositories().unwrap();
335 /// assert_eq!(repositories.len(), 1);
336 /// assert_eq!(repositories[0].id, Some("central".to_string()));
337 get: get_repositories,
338 /// Adds or Updates a repository in the pom file
339 /// ```rust
340 /// use maven_rs::pom::editor::PomEditor;
341 /// use maven_rs::pom::Repository;
342 /// let mut editor = PomEditor::default();
343 /// editor.add_or_update_repository(Repository {
344 /// id: Some("central".to_string()),
345 /// name: Some("Maven Central Repository".to_string()),
346 /// url: "https://repo.maven.apache.org/maven2".to_string(),
347 /// ..Default::default()
348 /// }).unwrap();
349 /// let repositories = editor.get_repositories().unwrap();
350 /// assert_eq!(repositories.len(), 1);
351 /// assert_eq!(repositories[0].id, Some("central".to_string()));
352 /// ```
353 add: add_or_update_repository,
354 /// Clears all the repositories in the pom file
355 clear: clear_repositories,
356 "repositories" => Repository
357 );
358 list_item_getter_and_add!(
359 /// Gets all the developers in the pom file
360 /// ```rust
361 /// use maven_rs::pom::editor::PomEditor;
362 /// use maven_rs::pom::Developer;
363 /// let xml = r#"
364 /// <project>
365 /// <developers>
366 /// <developer>
367 /// <id>dev.wyatt-herkamp</id>
368 /// <name>Wyatt Herkamp</name>
369 /// </developer>
370 /// </developers>
371 /// </project>
372 /// "#;
373 /// let editor = PomEditor::load_from_str(xml).unwrap();
374 /// let developers = editor.get_developers().unwrap();
375 /// assert_eq!(developers.len(), 1);
376 /// assert_eq!(developers[0].id, Some("dev.wyatt-herkamp".to_string()));
377 /// ```
378 get: get_developers,
379 /// Adds or Updates a developer in the pom file
380 /// ```rust
381 /// use maven_rs::pom::editor::PomEditor;
382 /// use maven_rs::pom::Developer;
383 /// let mut editor = PomEditor::default();
384 /// editor.add_or_update_developer(Developer {
385 /// id: Some("dev.wyatt-herkamp".to_string()),
386 /// name: Some("Wyatt Herkamp".to_string()),
387 /// ..Default::default()
388 /// }).unwrap();
389 /// let developers = editor.get_developers().unwrap();
390 /// assert_eq!(developers.len(), 1);
391 /// assert_eq!(developers[0].id, Some("dev.wyatt-herkamp".to_string()));
392 /// ```
393 add: add_or_update_developer,
394 /// Clears all the developers in the pom file
395 clear: clear_developers,
396 "developers" => Developer
397 );
398 list_item_getter_and_add!(
399 /// Gets all the dependencies in the pom file
400 /// ```rust
401 /// use maven_rs::pom::editor::PomEditor;
402 /// use maven_rs::pom::Dependency;
403 /// let xml = r#"
404 /// <project>
405 /// <dependencies>
406 /// <dependency>
407 /// <groupId>com.google.guava</groupId>
408 /// <artifactId>guava</artifactId>
409 /// <version>30.1-jre</version>
410 /// </dependency>
411 /// </dependencies>
412 /// </project>
413 /// "#;
414 /// let editor = PomEditor::load_from_str(xml).unwrap();
415 /// let dependencies = editor.get_dependencies().unwrap();
416 /// assert_eq!(dependencies.len(), 1);
417 /// assert_eq!(dependencies[0].group_id, "com.google.guava".to_string());
418 /// ```
419 get: get_dependencies,
420 /// Adds or Updates a dependency in the pom file
421 ///
422 /// ```rust
423 /// use maven_rs::pom::editor::PomEditor;
424 /// use maven_rs::pom::Dependency;
425 /// let mut editor = PomEditor::default();
426 /// editor.add_or_update_dependency(Dependency {
427 /// group_id: "com.google.guava".to_string(),
428 /// artifact_id: "guava".to_string(),
429 /// version: Some("30.1-jre".parse().unwrap()),
430 /// ..Default::default()
431 /// }).unwrap();
432 /// let dependencies = editor.get_dependencies().unwrap();
433 /// assert_eq!(dependencies.len(), 1);
434 /// assert_eq!(dependencies[0].group_id, "com.google.guava".to_string());
435 /// ```
436 add: add_or_update_dependency,
437 /// Clears all the dependencies in the pom file
438 clear: clear_dependencies,
439 "dependencies" => Dependency
440 );
441 // TODO: pluginRepositories
442 // Loads a pom from a string
443 pub fn load_from_str(value: &str) -> Result<Self, XMLEditorError> {
444 let document = Document::parse_str_with_opts(
445 value,
446 ReadOptions {
447 require_decl: false,
448 ..Default::default()
449 },
450 )?;
451 Self::assert_requirements_for_pom(&document)?;
452 Ok(Self {
453 document,
454 ident_level: 2,
455 })
456 }
457 /// Loads a pom from a reader
458 ///
459 /// # Errors
460 /// If the xml is not a valid pom file
461 pub fn load_from_reader<R: std::io::Read>(reader: R) -> Result<Self, XMLEditorError> {
462 let document = Document::parse_reader_with_opts(
463 reader,
464 ReadOptions {
465 require_decl: false,
466 ..Default::default()
467 },
468 )?;
469 Self::assert_requirements_for_pom(&document)?;
470 Ok(Self {
471 document,
472 ident_level: 2,
473 })
474 }
475
476 /// Assets that the document has an root element of project
477 fn assert_requirements_for_pom(document: &Document) -> Result<(), XMLEditorError> {
478 let root = document
479 .root_element()
480 .ok_or(MissingElementError("project"))?;
481 if root.name(document) != "project" {
482 return Err(XMLEditorError::UnexpectedElementType {
483 expected: "project",
484 found: root.name(document).to_owned(),
485 });
486 }
487 Ok(())
488 }
489
490 pub(crate) fn root(&self) -> Element {
491 self.document.root_element().unwrap()
492 }
493 pub fn write_to_str(&self) -> Result<String, XMLEditorError> {
494 self.document
495 .write_str_with_opts(WriteOptions {
496 write_decl: true,
497 indent_size: self.ident_level,
498 ..Default::default()
499 })
500 .map_err(XMLEditorError::from)
501 }
502 pub fn write<W: Write>(&self, writer: &mut W) -> Result<(), XMLEditorError> {
503 self.document
504 .write_with_opts(
505 writer,
506 WriteOptions {
507 write_decl: true,
508 indent_size: self.ident_level,
509 ..Default::default()
510 },
511 )
512 .map_err(XMLEditorError::from)
513 }
514}
515
516#[cfg(test)]
517mod tests {
518
519 use super::*;
520 #[test]
521 pub fn create() -> anyhow::Result<()> {
522 let mut editor = PomEditor::default();
523 editor.set_group_id("dev.wyatt-herkamp");
524 editor.set_artifact_id("test");
525 let value = editor.write_to_str()?;
526 println!("{}", value);
527 let mut new_editor = PomEditor::load_from_str(value.as_str())?;
528
529 // Make sure the group id and artifact id are correct
530 assert_eq!(
531 new_editor.get_group_id(),
532 Some("dev.wyatt-herkamp".to_string())
533 );
534 assert_eq!(new_editor.get_artifact_id(), Some("test".to_string()));
535 // Try Changing the group id and artifact id
536 new_editor.set_group_id("dev.wyatt-herkamp2");
537 new_editor.set_artifact_id("test2");
538 assert_eq!(
539 new_editor.get_group_id(),
540 Some("dev.wyatt-herkamp2".to_string())
541 );
542 assert_eq!(new_editor.get_artifact_id(), Some("test2".to_string()));
543 let value = new_editor.write_to_str()?;
544 println!("{}", value);
545 Ok(())
546 }
547 #[test]
548 pub fn dependencies() -> anyhow::Result<()> {
549 let mut editor = PomEditor::new_with_group_and_artifact("dev.wyatt-herkamp", "test");
550 let dependency = Dependency {
551 group_id: "com.google.guava".to_string(),
552 artifact_id: "guava".to_string(),
553 version: Some("30.1-jre".parse().unwrap()),
554 depend_type: None,
555 scope: None,
556 classifier: None,
557 };
558 editor.add_or_update_dependency(dependency.clone())?;
559
560 let value = editor.write_to_str()?;
561 println!("{}", value);
562
563 let new_editor = PomEditor::load_from_str(value.as_str())?;
564 let dependencies = new_editor.get_dependencies()?;
565 println!("{:#?}", dependencies);
566 assert!(dependencies.len() == 1);
567 assert_eq!(dependencies[0], dependency);
568
569 Ok(())
570 }
571
572 #[test]
573 pub fn repositories() -> anyhow::Result<()> {
574 let mut editor = PomEditor::new_with_group_and_artifact("dev.wyatt-herkamp", "test");
575 let repository = Repository {
576 id: Some("central".to_string()),
577 name: Some("Maven Central Repository".to_string()),
578 url: "https://repo.maven.apache.org/maven2".to_string(),
579 ..Default::default()
580 };
581 editor.add_or_update_repository(repository.clone())?;
582
583 let value = editor.write_to_str()?;
584 println!("{}", value);
585
586 let new_editor = PomEditor::load_from_str(value.as_str())?;
587 let repositories = new_editor.get_repositories()?;
588 println!("{:#?}", repositories);
589 assert!(repositories.len() == 1);
590 assert_eq!(repositories[0], repository);
591
592 Ok(())
593 }
594}