behaviortree 0.7.4

A #![no_std] compatible behavior tree library similar to 'BehaviorTree.CPP'.
Documentation
// Copyright © 2025 Stephan Kunz

//! XML writer for `behaviortree`

#[doc(hidden)]
#[cfg(feature = "std")]
extern crate std;

// region:      --- modules
use alloc::{
	collections::btree_map::BTreeMap,
	string::{String, ToString},
	vec::Vec,
};
use core::convert::TryFrom;

use crate::{
	ConstString, ID, NAME, SUBTREE,
	behavior::{
		behavior_description::BehaviorDescription,
		pre_post_conditions::{POST_CONDITIONS, PRE_CONDITIONS},
	},
	factory::BehaviorTreeFactory,
	tree::{BehaviorTree, BehaviorTreeElement, TreeElementKind},
};
use woxml::{Write, XmlWriter};

// endregion:   --- modules

// region:      --- XmlWriter
/// Write different kinds of XML from various sources.
#[derive(Default)]
pub struct XmlCreator;

impl XmlCreator {
	/// Create XML `TreeNodesModel` from factories registered nodes.
	/// # Errors
	pub fn write_tree_nodes_model(factory: &BehaviorTreeFactory, pretty: bool) -> Result<ConstString, woxml::Error> {
		let mut writer = if pretty {
			XmlWriter::pretty_mode(Vec::new())
		} else {
			XmlWriter::compact_mode(Vec::new())
		};

		writer.begin_elem("root")?;
		writer.attr("BTCPP_format", "4")?;
		writer.begin_elem("TreeNodesModel")?;
		// loop over factories behavior entries in registry
		for item in factory.registry().behaviors() {
			if !item.1.0.groot2() {
				writer.begin_elem(item.1.0.kind_str())?;
				writer.attr(ID, item.0)?;
				// look for a PortsList
				for port in &item.1.0.ports().0 {
					writer.begin_elem(port.direction().type_str())?;
					writer.attr(NAME, port.name())?;
					writer.attr("type", port.type_name())?;
					writer.end_elem()?;
				}
				writer.end_elem()?;
			}
		}
		writer.end_elem()?; // TreeNodesModel
		writer.end_elem()?; // root
		writer.flush()?;

		Ok(String::try_from(writer)?.into())
	}

	/// Create XML from tree including `TreeNodesModel`.
	/// # Errors
	/// - if it cannot create an xml entry
	pub fn write_tree(
		tree: &BehaviorTree,
		metadata: bool,
		builtin_models: bool,
		pretty: bool,
	) -> Result<ConstString, woxml::Error> {
		let mut writer = if pretty {
			XmlWriter::pretty_mode(Vec::new())
		} else {
			XmlWriter::compact_mode(Vec::new())
		};

		writer.begin_elem("root")?;
		writer.attr("BTCPP_format", "4")?;
		// scan the tree
		let (behaviors, subtrees) = Self::scan_tree(tree, builtin_models);
		// ensure lifetimes
		{
			// create the BehaviorTree's
			Self::create_behavior_trees(&mut writer, &subtrees, metadata)?;

			// create the TreeNodesModel
			Self::create_tree_nodes_model(&mut writer, &behaviors, builtin_models, pretty, false)?;
		}
		writer.end_elem()?; // root
		writer.flush()?;

		Ok(String::try_from(writer)?.into())
	}

	fn create_behavior_trees<'a>(
		writer: &mut XmlWriter<'a, impl Write>,
		subtrees: &'a Vec<&BehaviorTreeElement>,
		metadata: bool,
	) -> Result<(), woxml::Error> {
		for subtree in subtrees {
			writer.begin_elem("BehaviorTree")?;
			writer.attr(ID, subtree.name())?;
			writer.attr("_fullpath", subtree.groot2_path())?;

			// recursive dive into children
			for element in subtree.children().iter() {
				Self::write_subtree(element, writer, metadata)?;
			}
			writer.end_elem()?; // BehaviorTree
		}

		Ok(())
	}

	fn write_subtree<'a>(
		element: &'a BehaviorTreeElement,
		writer: &mut XmlWriter<'a, impl Write>,
		metadata: bool,
	) -> Result<(), woxml::Error> {
		let is_subtree = match element.kind() {
			TreeElementKind::Leaf | TreeElementKind::Node => {
				writer.begin_elem(element.id())?;
				writer.attr(NAME, element.name())?;
				false
			}
			TreeElementKind::SubTree => {
				writer.begin_elem(SUBTREE)?;
				writer.attr(ID, element.name())?;
				if metadata {
					writer.attr("_fullpath", element.groot2_path())?;
				}
				true
			}
		};
		if metadata {
			writer.attr("_uid", &element.uid().to_string())?;
		}

		if is_subtree {
			// subtree port mappings/values are in blackboard
			if let Some(remappings) = element.blackboard().remappings() {
				for remapping in remappings.iter() {
					writer.attr(&remapping.0, &remapping.1.to_string())?;
				}
			}
		} else {
			// behavior port mappings/values
			for remapping in element.remappings().iter() {
				writer.attr(&remapping.0, &remapping.1.to_string())?;
			}
		}

		// Pre-conditions
		if let Some(conditions) = &element.pre_conditions().0 {
			for i in 0..PRE_CONDITIONS.len() {
				if let Some(cond) = &conditions[i] {
					writer.attr(PRE_CONDITIONS[i], cond)?;
				}
			}
		}

		// Post-conditions
		if let Some(conditions) = &element.post_conditions().0 {
			for i in 0..POST_CONDITIONS.len() {
				if let Some(cond) = &conditions[i] {
					writer.attr(POST_CONDITIONS[i], cond)?;
				}
			}
		}

		if !is_subtree {
			// recursive dive into children, ignoring subtrees
			for element in element.children().iter() {
				Self::write_subtree(element, writer, metadata)?;
			}
		}

		writer.end_elem()?;

		Ok(())
	}

	fn create_tree_nodes_model<'a>(
		writer: &mut XmlWriter<'a, impl Write>,
		behaviors: &'a BTreeMap<ConstString, BehaviorDescription>,
		builtin_models: bool,
		pretty: bool,
		groot: bool,
	) -> Result<(), woxml::Error> {
		writer.begin_elem("TreeNodesModel")?;
		// loop over collected behavior entries
		for (name, item) in behaviors {
			if builtin_models || !item.groot2() {
				writer.begin_elem(item.kind_str())?;
				writer.attr(ID, name)?;
				// look for a PortsList
				for port in &item.ports().0 {
					writer.begin_elem(port.direction().type_str())?;
					writer.attr(NAME, port.name())?;
					if groot {
						writer.attr("type", Self::groot_map_types(port.type_name()))?;
					} else {
						writer.attr("type", port.type_name())?;
					}
					if !port.description().is_empty() {
						writer.set_compact_mode();
						writer.text(port.description())?;
					}
					writer.end_elem()?;
					if pretty {
						writer.set_pretty_mode();
					}
				}
				writer.end_elem()?;
			}
		}

		writer.end_elem()?; // TreeNodesModel

		Ok(())
	}

	fn scan_tree(
		tree: &BehaviorTree,
		builtin_models: bool,
	) -> (BTreeMap<ConstString, BehaviorDescription>, Vec<&BehaviorTreeElement>) {
		let mut behaviors: BTreeMap<ConstString, BehaviorDescription> = BTreeMap::new();
		let mut subtrees: Vec<&BehaviorTreeElement> = Vec::new();

		// scan the tree
		for item in tree.iter() {
			match item.kind() {
				TreeElementKind::Leaf | TreeElementKind::Node => {
					let desc = item.data().description();
					if builtin_models || !desc.groot2() {
						behaviors.insert(desc.name().clone(), desc.clone());
					}
				}
				TreeElementKind::SubTree => {
					subtrees.push(item);
				}
			}
		}

		(behaviors, subtrees)
	}

	/// Create XML from tree including `TreeNodesModel`.
	/// # Errors
	/// - if it cannot create an xml entry
	pub fn groot_write_tree(tree: &BehaviorTree) -> Result<bytes::Bytes, woxml::Error> {
		let mut writer = XmlWriter::compact_mode(bytes::BytesMut::new());

		writer.begin_elem("root")?;
		writer.attr("BTCPP_format", "4")?;
		// scan the tree
		let (behaviors, subtrees) = Self::scan_tree(tree, false);
		// ensure lifetimes
		{
			// create the BehaviorTree's
			Self::create_behavior_trees(&mut writer, &subtrees, true)?;

			// create the TreeNodesModel
			Self::create_tree_nodes_model(&mut writer, &behaviors, false, false, true)?;
		}
		writer.end_elem()?; // root
		writer.flush()?;

		Ok(String::try_from(writer)?.into())
	}

	// @TODO: things like: SharedQueue<T: FromStr + ToString>(pub Arc<Mutex<VecDeque<T>>>);
	fn groot_map_types(input: &str) -> &str {
		match input {
			"char" => "char",
			"i16" => "short",
			"u16" => "unsigned short",
			"i32" => "int",
			"u32" => "unsigned int",
			"i64" => "long",
			"u64" => "unsigned long",
			"f32" => "float",
			"f64" => "double",
			"String" => "std::string",
			"BehaviorState" => "BT::NodeStatus",
			_ => "BT::Any",
		}
	}
}
// endregion:   --- XmlWriter