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")