Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Osvr json to c py #483

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
117 changes: 117 additions & 0 deletions devtools/osvr_json_to_c.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"""
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.
"""

import argparse
import sys
import re


# http://stackoverflow.com/questions/2319019/using-regex-to-remove-comments-from-source-files
# http://stackoverflow.com/a/18381470
def remove_comments(string):
"""

:param string:
:return: string
"""
pattern = r"(\".*?\"|\'.*?\')|(/\*.*?\*/|//[^\r\n]*$)"
# first group captures quoted strings (double or single)
# second group captures comments (//single-line or /* multi-line */)
regex = re.compile(pattern, re.MULTILINE | re.DOTALL)

def _replacer(match):
# if the 2nd group (capturing comments) is not None,
# it means we have captured a non-quoted (real) comment string.
if match.group(2) is not None:
return ""
else: # otherwise, we will return the 1st group
return match.group(1) # captured quoted-string
return regex.sub(_replacer, string)


# http://stackoverflow.com/questions/3609596/python-regular-expression-must-strip-whitespace-except-between-quotes
# http://stackoverflow.com/a/3609802
def stripwhite(text):
"""

:param text:
:return: string
"""
# Remove whitespace characters not found in between double-quotes
lst = text.split('"')
for i, item in enumerate(lst):
if not i % 2:
lst[i] = re.sub("\s+", "", item)
return '"'.join(lst)


def migrate_file_data(json_filename, variable_name='json', output_filename=None):
"""
Provide a JSON filename, an (optional) variable name, and an (optional) output filename.
It reads in the JSON file (discarding comments, turns it back into a string in "compressed" format - that is,
without unneeded whitespaces, etc. It then writes out a file that basically is a C/C++ source file defining a
string array by converting every character to hex.
:return:
"""
try:
# Generate a default output filename based on the input filename if output filename is not provided
if output_filename is None:
filename_parts = json_filename.split('.')[:-1]
filename_parts.append('cpp')
output_filename = '.'.join(filename_parts)

# 'with' keyword will handle the closing of files
with open(json_filename, 'rb') as json_input:
with open(output_filename, 'w') as cpp_output:
end_of_file = ''
cpp_output.write('static const char {}[] = {{'.format(variable_name))
line = json_input.readline()
while line != end_of_file:
# TODO: Are comments allowed in JSON outside of data?
# http://www.json.org/
# https://plus.google.com/+DouglasCrockfordEsq/posts/RK8qyGVaGSr
# https://groups.yahoo.com/neo/groups/json/conversations/topics/156
uncommented_line = remove_comments(line)
stripped_line = stripwhite(uncommented_line)
if stripped_line != '':
# Don't write (now) empty line contents to the file
json_file_contents = list(stripped_line)
cpp_file_contents = ["0x{}".format(char.encode("hex")) for char in json_file_contents]
write_to_file = ", ".join(cpp_file_contents)
# Add the extra trailing comma to connect data from multiple lines
write_to_file = "{}, ".format(write_to_file)
cpp_output.write(write_to_file)
line = json_input.readline()
# The End of File newline would have been removed in the while loop above. Re-add it.
cpp_output.write("0x{}".format('\n'.encode("hex")))
cpp_output.write('};\n')
except IOError:
return "Could not read file: {}".format(json_filename)


def main(argv):
parser = argparse.ArgumentParser()
parser.add_argument('input', help="input file")
parser.add_argument('-o', '--output', help="output file (defaults to standard out)")
parser.add_argument('-s', '--symbol', default='json', help="symbol/variable name to create in generated file")
args = parser.parse_args(argv)
input_filename = args.input
output_filename = args.output
symbol = args.symbol

migrate_file_data(json_filename=input_filename, variable_name=symbol, output_filename=output_filename)

if __name__ == "__main__":
main(sys.argv[1:])

112 changes: 112 additions & 0 deletions devtools/test_osvr_json_to_c.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""
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.
"""

from os import path
import shutil
import tempfile
import unittest
from osvr_json_to_c import migrate_file_data


class OSVRJsonToCTestCase(unittest.TestCase):
"""Tests for `osvr_json_to_c.py`."""

def setUp(self):
# Create a temporary directory
self.test_dir = tempfile.mkdtemp()
self.test_json_filename = 'test.json'
self.test_json_filepath = path.join(self.test_dir, self.test_json_filename)
# 'with' keyword will handle the closing of files
with open(self.test_json_filepath, 'w') as json_file:
json_file_contents = """{
"hello": "world"
}"""
json_file.write(json_file_contents)

def tearDown(self):
# Remove the directory after the test
shutil.rmtree(self.test_dir)

def test_set_variable_name_in_output(self):
"""
Does the variable name set as expected?
:return:
"""
symbol = 'variable_name'
temp_output_filepath = path.join(self.test_dir, 'test.cpp')
migrate_file_data(json_filename=self.test_json_filepath, variable_name=symbol)
# 'with' keyword will handle the closing of files
with open(temp_output_filepath, 'rb') as cpp_file:
# Reopen the file and check if what we read back is the same
line = cpp_file.readline()
self.assertTrue('static const char {}[]'.format(symbol) in line)

def test_default_variable_name_in_output(self):
"""
Does the variable name default as expected?
:return:
"""
symbol = 'json'
output_filepath = path.join(self.test_dir, 'test.cpp')
migrate_file_data(json_filename=self.test_json_filepath)
# 'with' keyword will handle the closing of files
with open(output_filepath, 'rb') as cpp_file:
# Reopen the file and check if what we read back is the same
line = cpp_file.readline()
self.assertTrue('static const char {}[]'.format(symbol) in line)

def test_default_output_filename(self):
"""
Does the output filename default as expected?
:return:
"""
migrate_file_data(json_filename=self.test_json_filepath)
output_filepath = path.join(self.test_dir, 'test.cpp')
self.assertTrue(path.isfile(output_filepath))

def test_set_output_filename(self):
"""
Does the output filename set as expected?
:return:
"""
output_filename = 'test_output'
output_filepath = path.join(self.test_dir, '{}.cpp'.format(output_filename))
migrate_file_data(json_filename=self.test_json_filepath, output_filename=output_filepath)
self.assertTrue(path.isfile(output_filepath))

def test_file_read_error(self):
"""
Does migrate_file_data() return an error message as expected if the input file is unreadable?
:return:
"""
error_message = migrate_file_data(json_filename='')
self.assertTrue('Could not read file:' in error_message)

def test_hello_world_json(self):
"""
Does migrate_file_data() create the proper cpp file contents given the hello world test data?
:return:
"""
expected_cpp_contents = "static const char json[] = {0x7b, 0x22, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x22, 0x3a, 0x22, 0x77, 0x6f," \
" 0x72, 0x6c, 0x64, 0x22, 0x7d, 0x0a};\n"
output_filepath = path.join(self.test_dir, 'test.cpp')
migrate_file_data(json_filename=self.test_json_filepath)
# 'with' keyword will handle the closing of files
with open(output_filepath, 'rb') as cpp_file:
# Reopen the file and check if what we read back is the same
cpp_contents = cpp_file.read()
self.assertEqual(cpp_contents, expected_cpp_contents)

if __name__ == '__main__':
unittest.main()