behaviortree-core 0.1.0

Core implementaion of behaviortree
Documentation
// Copyright © 2025 Stephan Kunz
//! XML writer for `behaviortree`

use core::convert::TryFrom;

use alloc::{
	collections::btree_map::BTreeMap,
	string::{String, ToString},
	vec::Vec,
};

use woxml::{Write, XmlWriter};

use crate::{
	ConstString, ID, NAME, SUBTREE,
	tree::{BehaviorTree, BehaviorTreeElement, TreeElementKind},
};

/// 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())
	// 	};

	// 	// this is an Arc<...> so its cheap to clone
	// 	let dummy_board = databoard::Databoard::default();
	// 	writer.begin_elem("root")?;
	// 	writer.attr("BTCPP_format", "4")?;
	// 	writer.begin_elem("TreeNodesModel")?;
	// 	// loop over factories behavior entries in registry
	// 	for (bhvr_name, (bhvr_description, bhvr_creation_fn)) in factory.registry().behaviors() {
	// 		// if it is not a known behavior to groot, we have to send detail information
	// 		// as a TreeNodesModel
	// 		if !bhvr_description.groot2() {
	// 			// create a behavior instance
	// 			let bhvr = bhvr_creation_fn(dummy_board.clone());
	// 			writer.begin_elem(bhvr.kind().as_str())?;
	// 			writer.attr(ID, bhvr_name)?;
	// 			for (port_name, port_variant) in bhvr.portlist().iter() {
	// 				writer.begin_elem(port_variant.direction())?;
	// 				writer.attr(NAME, port_name)?;
	// 				writer.attr("type", port_variant.data_type())?;
	// 				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())
	}

	/// Creates the XML for all SubTrees, including the main tree.
	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())?;
			// @TODO: do we need to do ths only if metadata are needed?
			// if metadata {
			writer.attr("_fullpath", subtree.groot2_path())?;
			// writer.attr("_uid", &subtree.uid().to_string())?;
			// }

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

		Ok(())
	}

	/// Writes the xml for a tree element. The flag `metadata` indicates whether to include
	/// metadata like `uid` and `fullpath` into the XML which are necessary for Groot.
	fn write_tree_element<'a>(
		element: &'a BehaviorTreeElement,
		writer: &mut XmlWriter<'a, impl Write>,
		metadata: bool,
	) -> Result<(), woxml::Error> {
		let name = element.name().as_ref();
		let is_subtree = match element.kind() {
			TreeElementKind::Leaf | TreeElementKind::Node => {
				writer.begin_elem(element.id())?;
				writer.attr(NAME, name)?;
				false
			}
			TreeElementKind::SubTree => {
				writer.begin_elem(SUBTREE)?;
				writer.attr(ID, name)?;
				if metadata {
					writer.attr("_fullpath", element.groot2_path())?;
				}
				true
			}
		};
		if metadata {
			writer.attr("_uid", &element.uid().to_string())?;
		}

		// write the tree items configuration
		let txt = element.data().description().configuration();
		if !txt.is_empty() {
			writer.write(" ")?;
			writer.write(txt)?;
		}

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

		writer.end_elem()?;

		Ok(())
	}

	/// Create a `TreeNodesModel` for all `Behaviors` registered in the factory.
	fn create_tree_nodes_model<'a>(
		writer: &mut XmlWriter<'a, impl Write>,
		behaviors: &'a BTreeMap<ConstString, &BehaviorTreeElement>,
		builtin_models: bool,
		pretty: bool,
		groot: bool,
	) -> Result<(), woxml::Error> {
		writer.begin_elem("TreeNodesModel")?;
		// loop over collected behavior entries
		// in the tree nodes model we always use the ID, never the name
		for (_, item) in behaviors {
			if builtin_models || !item.data().description().groot2() {
				writer.begin_elem(item.behavior().kind().as_str())?;
				writer.attr(ID, item.id())?;
				for (port_name, port_variant) in item.behavior().portlist().iter() {
					writer.begin_elem(port_variant.direction())?;
					writer.attr(NAME, port_name)?;
					if groot {
						writer.attr("type", Self::groot_map_types(port_variant.data_type()))?;
					} else {
						writer.attr("type", port_variant.data_type())?;
					}
					// @TODO: the old port description is removed
					// 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(())
	}

	/// Method returns a map of `BehaviorTreeElements` and a list of all `SubTrees`.
	/// The flag `builtin_models` signals whether to collect also Groots builtin behaviors.
	fn scan_tree(
		tree: &BehaviorTree,
		builtin_models: bool,
	) -> (BTreeMap<ConstString, &BehaviorTreeElement>, Vec<&BehaviorTreeElement>) {
		let mut behaviors: BTreeMap<ConstString, &BehaviorTreeElement> = 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(), item);
					}
				}
				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>>>);
	/// Map the Rust data types to corresponding C++ data types, so that Groot understands them.
	/// This is clearly possible for the fundamental types and the well defined library types,
	/// but very difficult for application/custom types. So any ambigious type is mapped to BT::Any.
	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",
		}
	}
}

#[cfg(test)]
mod tests {
	use super::XmlCreator;

	/// Covers all branches of `groot_map_types` (lines 269-280).
	#[test]
	fn groot_map_types_all_branches() {
		assert_eq!(XmlCreator::groot_map_types("char"), "char");
		assert_eq!(XmlCreator::groot_map_types("i16"), "short");
		assert_eq!(XmlCreator::groot_map_types("u16"), "unsigned short");
		assert_eq!(XmlCreator::groot_map_types("i32"), "int");
		assert_eq!(XmlCreator::groot_map_types("u32"), "unsigned int");
		assert_eq!(XmlCreator::groot_map_types("i64"), "long");
		assert_eq!(XmlCreator::groot_map_types("u64"), "unsigned long");
		assert_eq!(XmlCreator::groot_map_types("f32"), "float");
		assert_eq!(XmlCreator::groot_map_types("f64"), "double");
		assert_eq!(XmlCreator::groot_map_types("String"), "std::string");
		assert_eq!(XmlCreator::groot_map_types("BehaviorState"), "BT::NodeStatus");
		assert_eq!(XmlCreator::groot_map_types("SomeCustomType"), "BT::Any");
	}
}