diff --git a/tools/xmlschema.py b/tools/xmlschema.py
old mode 100644
new mode 100755
index 26746c8c7..5ce572dd0
--- a/tools/xmlschema.py
+++ b/tools/xmlschema.py
@@ -1,236 +1,306 @@
-import xml.etree.ElementTree as ElementTree
+#!/usr/bin/env python3
+# Copyright 2023 Open Source Robotics Foundation, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Conversion script for SDF definitions to XML XSD files
+"""
+
+from xml.etree import ElementTree
import os
from typing import List, Dict, Tuple, Optional
-class XmlSchema:
- '''
- This defines all of the current known mappings between the types found
- in .sdf files and the corresponding XSD standard types.
- While there are more XSD standard types, these are the ones currently used.
- '''
- SDF_TYPES_TO_XSD_STD_TYPES = {
- "bool": "boolean",
- "char": "char",
- "int": "int",
- "double": "double",
- "float": "float",
- "string": "string",
- "unsigned int": "unsignedInt",
- "unsigned long": "unsignedLong",
- }
-
- SDF_REQUIRED_TO_MIN_MAX_OCCURS: Dict[str, Tuple[str, str]] = {
- '0': ('0', '1'),
- '1': ('1', '1'),
- '+': ('1', 'unbounded'),
- '*': ('0', 'unbounded'),
- '-1': ('0', 'unbounded'),
- }
-
- def __init__(self, sdf_root_dir: str):
- self.sdf_root_dir: str = sdf_root_dir
-
- def _indent_lines(self, lines: List[str], indent) -> List[str]:
- return [' ' * indent + line for line in lines]
-
- def _get_attribute(self, element: ElementTree.Element, attrib: str) -> Optional[str]:
- return element.attrib[attrib] if attrib in element.attrib else None
-
- def _is_std_type(self, sdf_type: str) -> bool:
- return sdf_type in self.SDF_TYPES_TO_XSD_STD_TYPES.keys()
-
- def _xsd_type_string(self, sdf_type: str) -> Optional[str]:
- if self._is_std_type(sdf_type):
- xsd_type = self.SDF_TYPES_TO_XSD_STD_TYPES[sdf_type]
- return 'xsd:' + xsd_type
- else:
- return None
-
- def _print_documentation(self, element: ElementTree.Element) -> List[str]:
- lines = []
- description = element.find('description')
- if (description is not None and
- description.text is not None and
- len(description.text)):
- lines.append(f"")
- lines.append(f" ")
- lines.append(f" ")
- lines.append(f" ")
- lines.append(f"")
- return lines
-
- def _print_include(self, element: ElementTree.Element) -> List[str]:
- lines = []
- filename = self._get_attribute(element, 'filename')
- if filename is not None:
- loc = 'http://sdformat.org/schemas/'
- loc += filename.replace('.sdf', '.xsd')
- lines.append(f"")
- return lines
-
- def _print_include_ref(self, element: ElementTree.Element) -> List[str]:
- lines = []
- filename = self._get_attribute(element, 'filename')
- if filename is not None:
- sdf_path = os.path.join(self.sdf_root_dir, filename)
- if os.path.exists(sdf_path):
- include_tree = ElementTree.parse(sdf_path)
- root = include_tree.getroot()
- include_element_name = root.attrib['name']
- lines.append(f"")
- else:
- print(f'Invalid sdf included {filename}')
- return lines
-
- def _print_plugin_element(self, element: ElementTree.Element) -> List[str]:
- lines = []
- # Short circuit for plugin.sdf copy_data element
- if 'copy_data' in element.attrib:
- lines.append('')
- lines.append(" ")
- lines.append('')
- return lines
-
- def _print_element(self, element: ElementTree.Element) -> List[str]:
- lines = []
-
- elem_name = self._get_attribute(element, 'name')
- elem_type = self._get_attribute(element, 'type')
- elem_reqd = self._get_attribute(element, 'required')
-
- if elem_type and self._is_std_type(elem_type):
- elem_type = self._xsd_type_string(elem_type)
-
- if elem_reqd:
- minOccurs, maxOccurs = self.SDF_REQUIRED_TO_MIN_MAX_OCCURS[elem_reqd]
- lines.append(f"")
-
- if elem_type is None:
- lines.append(f"")
- lines.extend(self._indent_lines(self._print_documentation(element), 2))
- lines.append(" ")
- lines.append(" ")
-
- for e in element.findall('element'):
- lines.extend(self._indent_lines(self._print_element(e), 6))
-
- lines.append(" ")
-
- for e in element.findall('attribute'):
- lines.extend(self._indent_lines(self._print_attribute(e), 4))
-
- lines.append(" ")
- else:
- lines.append(f"")
- lines.extend(self._indent_lines(self._print_documentation(element), 2))
-
- lines.append("")
- lines.append("")
- return lines
-
- def _print_attribute(self, element: ElementTree.Element) -> List[str]:
- lines = []
-
- elem_name = self._get_attribute(element, 'name')
- elem_type = self._get_attribute(element, 'type')
- elem_reqd = self._get_attribute(element, 'required')
- elem_default = self._get_attribute(element, 'default')
+SDF_TYPES_TO_XSD_STD_TYPES = {
+ "bool": "boolean",
+ "char": "char",
+ "int": "int",
+ "double": "double",
+ "float": "float",
+ "string": "string",
+ "unsigned int": "unsignedInt",
+ "unsigned long": "unsignedLong",
+}
+
+SDF_REQUIRED_TO_MIN_MAX_OCCURS: Dict[str, Tuple[str, str]] = {
+ "0": ("0", "1"),
+ "1": ("1", "1"),
+ "+": ("1", "unbounded"),
+ "*": ("0", "unbounded"),
+ "-1": ("0", "unbounded"),
+}
+
+
+def indent_lines(lines: List[str], indent: int) -> List[str]:
+ """
+ Indent a group of lines by a set number of spaces
+ """
+ return [" " * indent + line for line in lines]
+
+
+def get_attribute(element: ElementTree.Element, attrib: str) -> Optional[str]:
+ """
+ Retrieve XML attribute from an element
+ """
+ return element.attrib[attrib] if attrib in element.attrib else None
+
+
+def is_std_type(sdf_type: str) -> bool:
+ """
+ Check if sdf_type is a known XSD standard type
+ """
+ return sdf_type in SDF_TYPES_TO_XSD_STD_TYPES
+
+
+def xsd_type_string(sdf_type: str) -> Optional[str]:
+ """
+ If sdf_type is a known XSD standard type, return it.
+ Otherwise, return None
+ """
+ if is_std_type(sdf_type):
+ xsd_type = SDF_TYPES_TO_XSD_STD_TYPES[sdf_type]
+ return "xsd:" + xsd_type
+ return None
+
+
+def print_documentation(element: ElementTree.Element) -> List[str]:
+ """
+ Print the documentation associated with an element
+ """
+ lines = []
+ description = element.find("description")
+ if (
+ description is not None
+ and description.text is not None
+ and len(description.text)
+ ):
+ lines.append("")
+ lines.append(" ")
+ lines.append(f" ")
+ lines.append(" ")
+ lines.append("")
+ return lines
+
+
+def print_include(element: ElementTree.Element) -> List[str]:
+ """
+ Print include tag information
+ """
+ lines = []
+ filename = get_attribute(element, "filename")
+ if filename is not None:
+ loc = "http://sdformat.org/schemas/"
+ loc += filename.replace(".sdf", ".xsd")
+ lines.append(f"")
+ return lines
+
+
+def print_include_ref(element: ElementTree.Element, sdf_root_dir: str) -> List[str]:
+ """
+ Print include tag reference information
+ """
+ lines = []
+ filename = get_attribute(element, "filename")
+ if filename is not None:
+ sdf_path = os.path.join(sdf_root_dir, filename)
+ if os.path.exists(sdf_path):
+ include_tree = ElementTree.parse(sdf_path)
+ root = include_tree.getroot()
+ include_element_name = root.attrib["name"]
+ lines.append(f"")
+ return lines
+
+
+def print_plugin_element(element: ElementTree.Element) -> List[str]:
+ """
+ Separate handling of the 'plugin' element
+ """
+ lines = []
+ # Short circuit for plugin.sdf copy_data element
+ if "copy_data" in element.attrib:
+ lines.append("")
+ lines.append(
+ " "
+ )
+ lines.append("")
+ return lines
+
+
+def print_element(element: ElementTree.Element) -> List[str]:
+ """
+ Print a child element of the sdf definition
+ """
+ lines = []
+
+ elem_name = get_attribute(element, "name")
+ elem_type = get_attribute(element, "type")
+ elem_reqd = get_attribute(element, "required")
+
+ if elem_type and is_std_type(elem_type):
+ elem_type = xsd_type_string(elem_type)
+
+ if elem_reqd:
+ min_occurs, max_occurs = SDF_REQUIRED_TO_MIN_MAX_OCCURS[elem_reqd]
+ lines.append(f"")
+
+ if elem_type is None:
+ lines.append(f"")
+ lines.extend(indent_lines(print_documentation(element), 2))
+ lines.append(" ")
+ lines.append(" ")
+
+ for child_element in element.findall("element"):
+ lines.extend(indent_lines(print_element(child_element), 6))
+
+ lines.append(" ")
+
+ for attribute in element.findall("attribute"):
+ lines.extend(indent_lines(print_attribute(attribute), 4))
+
+ lines.append(" ")
+ else:
+ lines.append(f"")
+ lines.extend(indent_lines(print_documentation(element), 2))
+
+ lines.append("")
+ lines.append("")
+ return lines
+
+
+def print_attribute(element: ElementTree.Element) -> List[str]:
+ """
+ Print an attribute of the sdf definition
+ """
+ lines = []
+
+ elem_name = get_attribute(element, "name")
+ elem_type = get_attribute(element, "type")
+ elem_reqd = get_attribute(element, "required")
+ elem_default = get_attribute(element, "default")
+
+ if elem_type and is_std_type(elem_type):
+ elem_type = xsd_type_string(elem_type)
+
+ use = ""
+ default = ""
+
+ if elem_reqd == "1":
+ use = "use='required'"
+ elif elem_reqd == "0":
+ use = "use='optional'"
+ if elem_default is not None:
+ default = f"default='{elem_default}'"
+
+ lines.append(
+ f""
+ )
+ lines.extend(indent_lines(print_documentation(element), 2))
+ lines.append("")
+ return lines
+
+
+def print_xsd(element: ElementTree.Element, sdf_root_dir: str) -> List[str]:
+ """
+ Print xsd for top level SDF element
+ """
+ lines = []
+
+ elem_name = get_attribute(element, "name")
+ elem_type = get_attribute(element, "type")
+
+ elements = element.findall("element")
+ attributes = element.findall("attribute")
+ includes = element.findall("include")
+
+ lines.extend(print_documentation(element))
+ lines.append(
+ ""
+ )
+
+ # Reference any includes in the SDF file
+ for include in includes:
+ lines.extend(print_include(include))
- if elem_type and self._is_std_type(elem_type):
- elem_type = self._xsd_type_string(elem_type)
+ if len(elements) or len(attributes) or len(includes):
+ lines.append(f"")
+ lines.append(" ")
- use = ""
- default = ""
-
- if elem_reqd == "1":
- use = "use='required'"
- elif elem_reqd == "0":
- use = "use='optional'"
- if elem_default is not None:
- default = f"default='{elem_default}'"
-
- lines.append(f"")
- lines.extend(self._indent_lines(self._print_documentation(element), 2))
- lines.append(f"")
- return lines
-
- def _print_xsd(self, element: ElementTree.Element) -> List[str]:
- lines = []
-
- elem_name = self._get_attribute(element, 'name')
- elem_type = self._get_attribute(element, 'type')
+ if elem_name != "plugin" and (len(elements) or len(includes)):
+ lines.append(" ")
- elements = element.findall('element')
- attributes = element.findall('attribute')
- includes = element.findall('include')
+ for child_element in elements:
+ if "copy_data" in child_element.attrib:
+ element_lines = print_plugin_element(child_element)
+ lines.extend(indent_lines(element_lines, 4))
+ else:
+ element_lines = print_element(child_element)
+ lines.extend(indent_lines(element_lines, 6))
- lines.extend(self._print_documentation(element))
- lines.append("")
+ for include_element in includes:
+ element_lines = print_include_ref(include_element, sdf_root_dir)
+ lines.extend(indent_lines(element_lines, 6))
- # Reference any includes in the SDF file
- for include in includes:
- lines.extend(self._print_include(include))
+ if elem_name != "plugin" and (len(elements) or len(includes)):
+ lines.append(" ")
- if len(elements) or len(attributes) or len(includes):
- lines.append(f"")
- lines.append(" ")
+ for attribute_element in attributes:
+ lines.extend(indent_lines(print_attribute(attribute_element), 4))
- if elem_name != "plugin" and (len(elements) or len(includes)):
- lines.append(" ")
+ lines.append(" ")
+ lines.append("")
+ else:
+ if elem_type and is_std_type(elem_type):
+ elem_type = xsd_type_string(elem_type)
+ else:
+ elem_type = ""
- for child_element in elements:
- if 'copy_data' in child_element.attrib:
- lines.extend(self._indent_lines(self._print_plugin_element(child_element), 4))
- else:
- lines.extend(self._indent_lines(self._print_element(child_element), 6))
+ lines.append(f"")
+ return lines
- for include_element in includes:
- lines.extend(self._indent_lines(self._print_include_ref(include_element), 6))
- if elem_name != "plugin" and (len(elements) or len(includes)):
- lines.append(" ")
+def process(input_file_sdf: str, sdf_dir: str) -> List[str]:
+ '''
+ Produce an XSD file from an input SDF file
+ '''
+ lines = []
+ tree = ElementTree.parse(input_file_sdf)
+ root = tree.getroot()
+ lines.append("")
+ lines.append("")
+ lines.extend(indent_lines(print_xsd(root, sdf_dir), 2))
+ lines.append("")
+ return lines
- for attribute_element in attributes:
- lines.extend(self._indent_lines(self._print_attribute(attribute_element), 4))
- lines.append(" ")
- lines.append(f"")
- else:
- if elem_type and self._is_std_type(elem_type):
- elem_type = self._xsd_type_string(elem_type)
- else:
- elem_type = ""
-
- lines.append(f"")
- return lines
-
- def process(self, input_file: str) -> List[str]:
- lines = []
- tree = ElementTree.parse(input_file)
- root = tree.getroot()
- lines.append("")
- lines.append("")
- lines.extend(self._indent_lines(self._print_xsd(root), 2))
- lines.append('')
- return lines
-
-if __name__ == '__main__':
+if __name__ == "__main__":
import argparse
- parser = argparse.ArgumentParser('xmlschema.py')
- parser.add_argument('--input-file')
- parser.add_argument('--sdf-dir')
- parser.add_argument('--output-dir')
+ parser = argparse.ArgumentParser("xmlschema.py")
+ parser.add_argument("--input-file")
+ parser.add_argument("--sdf-dir")
+ parser.add_argument("--output-dir")
args = parser.parse_args()
input_file = os.path.abspath(args.input_file)
- lines = XmlSchema(args.sdf_dir).process(input_file)
+ output_lines = process(input_file, args.sdf_dir)
fname = os.path.splitext(os.path.basename(args.input_file))[0]
os.makedirs(args.output_dir, exist_ok=True)
- with open(os.path.join(args.output_dir, f'{fname}.xsd'), 'w') as f:
- f.write('\n'.join(lines))
- f.write('\n')
+ output_file = os.path.join(args.output_dir, f"{fname}.xsd")
+
+ with open(output_file, "w", encoding="utf8") as f:
+ f.write("\n".join(output_lines))
+ f.write("\n")