Skip to content

Commit 651bdf9

Browse files
committed
Python script to replace ruby xmlschema generator
Signed-off-by: Michael Carroll <[email protected]>
1 parent 8a26cb4 commit 651bdf9

File tree

9 files changed

+325
-314
lines changed

9 files changed

+325
-314
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ build
33
build_*
44
*.*.sw?
55
.vscode
6+
__pycache__

sdf/1.10/CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ foreach(FIL ${sdfs})
7373

7474
add_custom_command(
7575
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${FIL_WE}.xsd"
76-
COMMAND ${RUBY} ${CMAKE_SOURCE_DIR}/tools/xmlschema.rb
77-
ARGS -s ${CMAKE_CURRENT_SOURCE_DIR} -i ${ABS_FIL} -o ${CMAKE_CURRENT_BINARY_DIR}
76+
COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tools/xmlschema.py
77+
ARGS --sdf-dir ${CMAKE_CURRENT_SOURCE_DIR} --input-file ${ABS_FIL} --output-dir ${CMAKE_CURRENT_BINARY_DIR}
7878
DEPENDS ${ABS_FIL}
7979
COMMENT "Running xml schema compiler on ${FIL}"
8080
VERBATIM)

sdf/1.5/CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ foreach(FIL ${sdfs})
6767

6868
add_custom_command(
6969
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${FIL_WE}.xsd"
70-
COMMAND ${RUBY} ${CMAKE_SOURCE_DIR}/tools/xmlschema.rb
71-
ARGS -s ${CMAKE_CURRENT_SOURCE_DIR} -i ${ABS_FIL} -o ${CMAKE_CURRENT_BINARY_DIR}
70+
COMMAND ${Pyhton3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tools/xmlschema.py
71+
ARGS --sdf-dir ${CMAKE_CURRENT_SOURCE_DIR} --input-file ${ABS_FIL} --output-dir ${CMAKE_CURRENT_BINARY_DIR}
7272
DEPENDS ${ABS_FIL}
7373
COMMENT "Running xml schema compiler on ${FIL}"
7474
VERBATIM)

sdf/1.6/CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ foreach(FIL ${sdfs})
7171

7272
add_custom_command(
7373
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${FIL_WE}.xsd"
74-
COMMAND ${RUBY} ${CMAKE_SOURCE_DIR}/tools/xmlschema.rb
75-
ARGS -s ${CMAKE_CURRENT_SOURCE_DIR} -i ${ABS_FIL} -o ${CMAKE_CURRENT_BINARY_DIR}
74+
COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tools/xmlschema.py
75+
ARGS --sdf-dir ${CMAKE_CURRENT_SOURCE_DIR} --input-file ${ABS_FIL} --output-dir ${CMAKE_CURRENT_BINARY_DIR}
7676
DEPENDS ${ABS_FIL}
7777
COMMENT "Running xml schema compiler on ${FIL}"
7878
VERBATIM)

sdf/1.7/CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ foreach(FIL ${sdfs})
7272

7373
add_custom_command(
7474
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${FIL_WE}.xsd"
75-
COMMAND ${RUBY} ${CMAKE_SOURCE_DIR}/tools/xmlschema.rb
76-
ARGS -s ${CMAKE_CURRENT_SOURCE_DIR} -i ${ABS_FIL} -o ${CMAKE_CURRENT_BINARY_DIR}
75+
COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tools/xmlschema.py
76+
ARGS --sdf-dir ${CMAKE_CURRENT_SOURCE_DIR} --input-file ${ABS_FIL} --output-dir ${CMAKE_CURRENT_BINARY_DIR}
7777
DEPENDS ${ABS_FIL}
7878
COMMENT "Running xml schema compiler on ${FIL}"
7979
VERBATIM)

sdf/1.8/CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ foreach(FIL ${sdfs})
7474

7575
add_custom_command(
7676
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${FIL_WE}.xsd"
77-
COMMAND ${RUBY} ${CMAKE_SOURCE_DIR}/tools/xmlschema.rb
78-
ARGS -s ${CMAKE_CURRENT_SOURCE_DIR} -i ${ABS_FIL} -o ${CMAKE_CURRENT_BINARY_DIR}
77+
COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tools/xmlschema.py
78+
ARGS --sdf-dir ${CMAKE_CURRENT_SOURCE_DIR} --input-file ${ABS_FIL} --output-dir ${CMAKE_CURRENT_BINARY_DIR}
7979
DEPENDS ${ABS_FIL}
8080
COMMENT "Running xml schema compiler on ${FIL}"
8181
VERBATIM)

sdf/1.9/CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ foreach(FIL ${sdfs})
7474

7575
add_custom_command(
7676
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${FIL_WE}.xsd"
77-
COMMAND ${RUBY} ${CMAKE_SOURCE_DIR}/tools/xmlschema.rb
78-
ARGS -s ${CMAKE_CURRENT_SOURCE_DIR} -i ${ABS_FIL} -o ${CMAKE_CURRENT_BINARY_DIR}
77+
COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tools/xmlschema.py
78+
ARGS --sdf-dir ${CMAKE_CURRENT_SOURCE_DIR} --input-file ${ABS_FIL} --output-dir ${CMAKE_CURRENT_BINARY_DIR}
7979
DEPENDS ${ABS_FIL}
8080
COMMENT "Running xml schema compiler on ${FIL}"
8181
VERBATIM)

tools/xmlschema.py

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2023 Open Source Robotics Foundation, Inc.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
"""
17+
Conversion script for SDF definitions to XML XSD files
18+
"""
19+
20+
from xml.etree import ElementTree
21+
22+
import os
23+
24+
from typing import List, Dict, Tuple, Optional
25+
26+
27+
# Mapping between "type" values found in SDF files to the corresponding
28+
# XSD standard datatypes as defined by https://www.w3.org/TR/xmlschema11-2/
29+
SDF_TYPES_TO_XSD_STD_TYPES = {
30+
"bool": "boolean",
31+
"char": "char",
32+
"int": "int",
33+
"double": "double",
34+
"float": "float",
35+
"string": "string",
36+
"unsigned int": "unsignedInt",
37+
"unsigned long": "unsignedLong",
38+
}
39+
40+
# Mapping between "required" values found in SDF files to the corresponding
41+
# minOccurs and maxOccurs found in XSD
42+
SDF_REQUIRED_TO_MIN_MAX_OCCURS: Dict[str, Tuple[str, str]] = {
43+
"0": ("0", "1"), # Required: 0, (minOccurs: 0, maxOccurs: 1)
44+
"1": ("1", "1"), # Required: 1, (minOccurs: 1, maxOccurs: 1)
45+
"+": ("1", "unbounded"), # Required: +, (minOccurs: 1, maxOccurs: inf)
46+
"*": ("0", "unbounded"), # Required: *, (minOccurs: 0, maxOccurs: inf)
47+
"-1": ("0", "unbounded"), # Required: -1, (minOccurs: 0, maxOccurs: inf)
48+
}
49+
50+
51+
def indent_lines(lines: List[str], indent: int) -> List[str]:
52+
"""
53+
Indent a list of xml lines group of lines by a number (indent) of spaces
54+
55+
"""
56+
return [" " * indent + line for line in lines]
57+
58+
59+
def get_attribute(element: ElementTree.Element, attrib: str) -> Optional[str]:
60+
"""
61+
Retrieve XML attribute from an element
62+
"""
63+
return element.attrib[attrib] if attrib in element.attrib else None
64+
65+
66+
def is_std_type(sdf_type: str) -> bool:
67+
"""
68+
Check if sdf_type is a known XSD standard type.
69+
Return true if the sdf_type is in the set of known types, false otherwise.
70+
"""
71+
return sdf_type in SDF_TYPES_TO_XSD_STD_TYPES
72+
73+
74+
def xsd_type_string(sdf_type: str) -> Optional[str]:
75+
"""
76+
Check if xsd_type is a known XSD standard type.
77+
If it is, return 'xsd:' + type, None otherwise.
78+
"""
79+
if is_std_type(sdf_type):
80+
xsd_type = SDF_TYPES_TO_XSD_STD_TYPES[sdf_type]
81+
return "xsd:" + xsd_type
82+
return None
83+
84+
85+
def print_documentation(element: ElementTree.Element) -> List[str]:
86+
"""
87+
Print the documentation associated with an element
88+
"""
89+
lines = []
90+
description = element.find("description")
91+
if (
92+
description is not None
93+
and description.text is not None
94+
and len(description.text)
95+
):
96+
lines.append("<xsd:annotation>")
97+
lines.append(" <xsd:documentation xml:lang='en'>")
98+
lines.append(f" <![CDATA[{description.text}]]>")
99+
lines.append(" </xsd:documentation>")
100+
lines.append("</xsd:annotation>")
101+
return lines
102+
103+
104+
def print_include(element: ElementTree.Element) -> List[str]:
105+
"""
106+
Print include tag information
107+
"""
108+
lines = []
109+
filename = get_attribute(element, "filename")
110+
if filename is not None:
111+
loc = "http://sdformat.org/schemas/"
112+
loc += filename.replace(".sdf", ".xsd")
113+
lines.append(f"<xsd:include schemaLocation='{loc}'/>")
114+
return lines
115+
116+
117+
def print_include_ref(element: ElementTree.Element, sdf_root_dir: str) -> List[str]:
118+
"""
119+
Print include tag reference information
120+
"""
121+
lines = []
122+
filename = get_attribute(element, "filename")
123+
if filename is not None:
124+
sdf_path = os.path.join(sdf_root_dir, filename)
125+
if os.path.exists(sdf_path):
126+
include_tree = ElementTree.parse(sdf_path)
127+
root = include_tree.getroot()
128+
include_element_name = root.attrib["name"]
129+
lines.append(f"<xsd:element ref='{include_element_name}'/>")
130+
return lines
131+
132+
133+
def print_plugin_element(element: ElementTree.Element) -> List[str]:
134+
"""
135+
Separate handling of the 'plugin' element
136+
"""
137+
lines = []
138+
# Short circuit for plugin.sdf copy_data element
139+
if "copy_data" in element.attrib:
140+
lines.append("<xsd:sequence>")
141+
lines.append(
142+
" <xsd:any minOccurs='0' maxOccurs='unbounded' processContents='lax'/>"
143+
)
144+
lines.append("</xsd:sequence>")
145+
return lines
146+
147+
148+
def print_element(element: ElementTree.Element) -> List[str]:
149+
"""
150+
Print a child element of the sdf definition
151+
"""
152+
lines = []
153+
154+
elem_name = get_attribute(element, "name")
155+
elem_type = get_attribute(element, "type")
156+
elem_reqd = get_attribute(element, "required")
157+
158+
if elem_type and is_std_type(elem_type):
159+
elem_type = xsd_type_string(elem_type)
160+
161+
if elem_reqd:
162+
min_occurs, max_occurs = SDF_REQUIRED_TO_MIN_MAX_OCCURS[elem_reqd]
163+
lines.append(f"<xsd:choice minOccurs='{min_occurs}' maxOccurs='{max_occurs}'>")
164+
165+
if elem_type is None:
166+
lines.append(f"<xsd:element name='{elem_name}'>")
167+
lines.extend(indent_lines(print_documentation(element), 2))
168+
lines.append(" <xsd:complexType>")
169+
lines.append(" <xsd:choice maxOccurs='unbounded'>")
170+
171+
for child_element in element.findall("element"):
172+
lines.extend(indent_lines(print_element(child_element), 6))
173+
174+
lines.append(" </xsd:choice>")
175+
176+
for attribute in element.findall("attribute"):
177+
lines.extend(indent_lines(print_attribute(attribute), 4))
178+
179+
lines.append(" </xsd:complexType>")
180+
else:
181+
lines.append(f"<xsd:element name='{elem_name}' type='{elem_type}'>")
182+
lines.extend(indent_lines(print_documentation(element), 2))
183+
184+
lines.append("</xsd:element>")
185+
lines.append("</xsd:choice>")
186+
return lines
187+
188+
189+
def print_attribute(element: ElementTree.Element) -> List[str]:
190+
"""
191+
Print an attribute of the sdf definition
192+
"""
193+
lines = []
194+
195+
elem_name = get_attribute(element, "name")
196+
elem_type = get_attribute(element, "type")
197+
elem_reqd = get_attribute(element, "required")
198+
elem_default = get_attribute(element, "default")
199+
200+
if elem_type and is_std_type(elem_type):
201+
elem_type = xsd_type_string(elem_type)
202+
203+
use = ""
204+
default = ""
205+
206+
if elem_reqd == "1":
207+
use = "use='required'"
208+
elif elem_reqd == "0":
209+
use = "use='optional'"
210+
if elem_default is not None:
211+
default = f"default='{elem_default}'"
212+
213+
lines.append(
214+
f"<xsd:attribute name='{elem_name}' type='{elem_type}' {use} {default}>"
215+
)
216+
lines.extend(indent_lines(print_documentation(element), 2))
217+
lines.append("</xsd:attribute>")
218+
return lines
219+
220+
221+
def print_xsd(element: ElementTree.Element, sdf_root_dir: str) -> List[str]:
222+
"""
223+
Print xsd for top level SDF element
224+
"""
225+
lines = []
226+
227+
elem_name = get_attribute(element, "name")
228+
elem_type = get_attribute(element, "type")
229+
230+
elements = element.findall("element")
231+
attributes = element.findall("attribute")
232+
includes = element.findall("include")
233+
234+
lines.extend(print_documentation(element))
235+
lines.append(
236+
"<xsd:include schemaLocation='http://sdformat.org/schemas/types.xsd'/>"
237+
)
238+
239+
# Reference any includes in the SDF file
240+
for include in includes:
241+
lines.extend(print_include(include))
242+
243+
if len(elements) or len(attributes) or len(includes):
244+
lines.append(f"<xsd:element name='{elem_name}'>")
245+
lines.append(" <xsd:complexType>")
246+
247+
if elem_name != "plugin" and (len(elements) or len(includes)):
248+
lines.append(" <xsd:choice maxOccurs='unbounded'>")
249+
250+
for child_element in elements:
251+
if "copy_data" in child_element.attrib:
252+
element_lines = print_plugin_element(child_element)
253+
lines.extend(indent_lines(element_lines, 4))
254+
else:
255+
element_lines = print_element(child_element)
256+
lines.extend(indent_lines(element_lines, 6))
257+
258+
for include_element in includes:
259+
element_lines = print_include_ref(include_element, sdf_root_dir)
260+
lines.extend(indent_lines(element_lines, 6))
261+
262+
if elem_name != "plugin" and (len(elements) or len(includes)):
263+
lines.append(" </xsd:choice>")
264+
265+
for attribute_element in attributes:
266+
lines.extend(indent_lines(print_attribute(attribute_element), 4))
267+
268+
lines.append(" </xsd:complexType>")
269+
lines.append("</xsd:element>")
270+
else:
271+
if elem_type and is_std_type(elem_type):
272+
elem_type = xsd_type_string(elem_type)
273+
else:
274+
elem_type = ""
275+
276+
lines.append(f"<xsd:element name='{elem_name}'{elem_type} />")
277+
return lines
278+
279+
280+
def process(input_file_sdf: str, sdf_dir: str) -> List[str]:
281+
"""
282+
Produce an XSD file from an input SDF file
283+
"""
284+
lines = []
285+
tree = ElementTree.parse(input_file_sdf)
286+
root = tree.getroot()
287+
lines.append("<?xml version='1.0' encoding='UTF-8'?>")
288+
lines.append("<xsd:schema xmlns:xsd='http://www.w3.org/2001/XMLSchema'>")
289+
lines.extend(indent_lines(print_xsd(root, sdf_dir), 2))
290+
lines.append("</xsd:schema>")
291+
return lines
292+
293+
294+
if __name__ == "__main__":
295+
import argparse
296+
297+
parser = argparse.ArgumentParser("xmlschema.py")
298+
parser.add_argument("--input-file")
299+
parser.add_argument("--sdf-dir")
300+
parser.add_argument("--output-dir")
301+
args = parser.parse_args()
302+
303+
input_file = os.path.abspath(args.input_file)
304+
output_lines = process(input_file, args.sdf_dir)
305+
fname = os.path.splitext(os.path.basename(args.input_file))[0]
306+
os.makedirs(args.output_dir, exist_ok=True)
307+
308+
output_file = os.path.join(args.output_dir, f"{fname}.xsd")
309+
310+
with open(output_file, "w", encoding="utf8") as f:
311+
f.write("\n".join(output_lines))
312+
f.write("\n")

0 commit comments

Comments
 (0)