Files
OpenCellular/firmware/utilities/schema/schemautils.py
2018-11-19 22:08:04 -05:00

429 lines
14 KiB
Python

#!/usr/bin/env python3
#
# Copyright (c) 2018-present, Facebook, Inc.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree. An additional grant
# of patent rights can be found in the PATENTS file in the same directory.
#
import re
import json
import jsonschema as js
class SchemaUtils:
draftFiles = {'d0' : 'meta_sys_schema_d0.json',
'd4' : 'meta_sys_schema_d4.json',
'd6' : 'meta_sys_schema_d6.json'}
def __init__(self, md, json_fname, c_fname, draft):
"""
Load meta schema and configuration schema
"""
self.draft = draft
self.valid = False
self.json_fname = json_fname
self.c_fname = c_fname
self.sys_schema = ''
self.md = md
# Meta schema: this shouldn't change except when either a new JSON
# draft requires it, or when the hardware is redesigned
self.metaschema = json.load(open(SchemaUtils.draftFiles[draft], 'r'))
if md != 'g':
# JSON definition file exists and we want to produce the
# corresponding C-file
# Configuration schema: this can change any time device
# configuration changes
self.cfgschema = json.load(open(self.json_fname, 'r'))
else:
# Want to create JSON definition file from a C-file
# containing a schema declared as a structure
self.cfgschema = []
def generate(self):
new_schema = []
with open(self.c_fname, 'r') as f:
in_schema = False
# Find start of schema declaration and extract
for line in f:
# find start of schema declaration
if (not in_schema) and ('sys_schema' in line):
in_schema = True
# Skip first line
new_schema.append('[\n')
continue
if in_schema:
new_schema.append(line)
schema = ''.join(str(line) for line in new_schema)
# Remove all C language features
schema = SchemaUtils.stripC(schema)
# Find terminations to array declarations
schema = SchemaUtils.fixarray(schema)
# JSON line formatting
schema = SchemaUtils.parse(schema, 4)
# JSON punctuation
self.sys_schema = SchemaUtils.punct(schema)
def auto_file(self):
"""
Write C-file from validated schema
"""
# Remove all JSON format features
schema, decls = SchemaUtils.strip(self.sys_schema)
# Format values as required for each data type
schema = SchemaUtils.typeformat(schema)
# Write C-file
SchemaUtils.writeSchema(decls, schema, self.c_fname)
def dump(self):
"""
Save the validated schema
"""
if self.md != 'g':
# convert the data dictionary to a string
if self.valid:
self.sys_schema = json.dumps(self.cfgschema, indent=4)
#
v_fname = 'valid_schema.json'
msg = 'Saved validated schema to ' + v_fname
else:
v_fname = self.json_fname
msg = 'Saved auto generated schema to ' + v_fname
with open(v_fname, 'w') as f:
f.write(self.sys_schema)
f.close()
print(msg)
def validate(self):
"""
Check for errors in either instance or schema
"""
# Sample errors:
# self.metaschema['definitions']['Subcomponent']['required'] = []
# self.cfgschema[0]['name'] = []
try:
try:
if (self.draft == 'd4') or (self.draft == 'd0'):
js.Draft4Validator.check_schema(self.metaschema)
else:
# not supported until jsonschema 3.0
js.Draft6Validator.check_schema(self.metaschema)
print('Meta-schema OK')
except js.SchemaError as e:
print('Error in meta-schema:' + str(e))
except Exception():
raise
try:
js.validate(self.cfgschema, self.metaschema)
print('Configuration schema validated')
except js.ValidationError as e:
print('Error in schema configuration:' + str(e))
except Exception():
raise
except Exception() as e:
print(e)
self.valid = False
else:
self.valid = True
return self.valid
@staticmethod
def declares(text):
"""
Select fields for which either a declaration or a prototype is necessary
"""
setA = set()
setC = set()
setP = set()
for line in text.split('\n'):
words = line.split()
if 'driver_cfg' in line:
setA.add(words[-1])
if 'factory_config' in line:
setC.add(words[-1])
if 'null' not in line.lower():
if 'preInitFxn' in line:
setP.add(words[-1])
if 'postInitFxn' in line:
setP.add(words[-1])
if 'cb_cmd' in line:
setP.add(words[-1])
qual1 = 'SCHEMA_IMPORT DriverStruct '
lines1 = [qual1 + value + ';' for value in iter(setA)]
lines1.sort()
str1 = '\n'.join(str(line) for line in lines1)
qual2 = 'SCHEMA_IMPORT const DriverStruct '
lines2 = [qual2 + value + ';' for value in iter(setC)]
lines2.sort()
str2 = '\n'.join(str(line) for line in lines2)
qual3 = 'SCHEMA_IMPORT bool '
qual4 = '(void *, void *)'
lines3 = [qual3 + value + qual4 + ';' for value in iter(setP)]
lines3.sort(key=str.lower)
str3 = '\n'.join(str(line) for line in lines3)
decls = str1 + '\n\n' + str2 + '\n\n' + str3 + '\n\n'
return decls
@staticmethod
def strip(text):
"""
Remove JSON format features
"""
# Find and remove all double quotes
p = re.compile(r'\"')
result = p.sub('', text)
# Find and replace all colons
result = re.sub(r':', ' =', result)
# Find and replace all commas
result = re.sub(r',', '', result)
# Find values for which a declaration is required
decls = SchemaUtils.declares(result)
# Format structure elements
result = re.sub(r'name', '.name', result)
result = re.sub(r'components', '.components', result)
result = re.sub(r'\bdriver\b', '.driver', result)
result = re.sub(r'\bdriver_cfg\b', '.driver_cfg', result)
result = re.sub(r'factory_config', '.factory_config', result)
result = re.sub(r'commands', '.commands', result)
result = re.sub(r'ssHookSet', '.ssHookSet', result)
result = re.sub(r'postDisabled', '.postDisabled', result)
result = re.sub(r'cb_cmd', '.cb_cmd', result)
result = re.sub(r'preInitFxn', '.preInitFxn', result)
result = re.sub(r'postInitFxn', '.postInitFxn', result)
return result, decls
@staticmethod
def typeformat(text):
"""
Format each element type as required
"""
newschema = []
for line in text.split('\n'):
words = line.split()
if '.name' in line:
newline = line.replace(words[-1], '"' + words[-1] + '"')
elif '.driver' in line:
newline = line.replace(words[-1], '&' + words[-1])
elif '.factory_config' in line:
newline = line.replace(words[-1], '&' + words[-1])
elif '.components' in line:
newline = line.replace('[', '(Component[]) {')
elif '.commands' in line:
newline = line.replace('[', '(Command[]) {')
elif '.ssHookSet' in line:
newline = line.replace('{', '&(SSHookSet) {')
elif '.preInitFxn' in line and 'NULL' not in line:
newline = line.replace(words[-1], '(ssHook_Cb)' + words[-1])
elif '.postInitFxn' in line and 'NULL' not in line:
newline = line.replace(words[-1], '(ssHook_Cb)' + words[-1])
elif ']' in line:
n = line.find(']') + 4
newschema.append(' ' * n + '{}')
newline = line.replace(']', '}')
else:
newline = line
if '{' in newline:
newschema.append(newline)
else:
newschema.append(newline + ',')
# Set first line correctly
newschema[0] = 'const Component sys_schema[] = {'
# Set last line correctly
newschema[-1] = '};'
return '\n'.join(str(line) for line in newschema)
@staticmethod
def writeSchema(decls, schema, c_fname):
"""
Save schema to .c file
"""
f = open(c_fname, 'w')
cpw = []
cpw.append('/**')
cpw.append('* Copyright (c) 2018-present, Facebook, Inc.')
cpw.append('* All rights reserved.')
cpw.append('*')
cpw.append('* This source code is licensed under the BSD-style license')
cpw.append('* found in the LICENSE file in the root directory of this')
cpw.append('* source tree. An additional grant of patent rights can be')
cpw.append('* found in the PATENTS file in the same directory.')
cpw.append('*')
cpw.append('* WARNING: Do not modify this file by hand. It is auto')
cpw.append('* generated from the json schema definition.')
cpw.append('* Refer to sdtester.py')
cpw.append('*/\n')
cpw.append('#include "auto_schema.h"\n\n')
f.write('\n'.join(line for line in cpw))
f.write(decls)
f.write('\n')
f.write(schema)
f.write('\n')
f.close()
print('Saved schema to ' + c_fname)
@staticmethod
def stripC(text):
"""
Using regular expressions, strip schema struct from
C-languange features
"""
# Find and remove C-style comments, single line and multiline
p = re.compile(r'/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/')
result = p.sub('', text)
# Find and remove all dots
result = re.sub(r'\.', '', result)
# Find and remove all double quotes
result = re.sub(r'\"', '', result)
# Find and remove all commas
result = re.sub(r',', '', result)
# Find and replace all =
result = re.sub(r'=', ' : ', result)
# Find and remove all ampersands
result = re.sub(r'&', '', result)
# Find and remove all {}
result = re.sub(r'{}', '', result)
# Find and remove all semicolons
result = re.sub(r';', '', result)
# Find and remove all empty lines
result = re.sub(r'\n\s*\n', '\n', result)
# Find and remove all array declarations
p = re.compile(r'\(\w*\[\]\)\s?\{')
result = p.sub('[', result)
# Find and remove all type casts
p = re.compile(r'\(\w*\)')
result = p.sub('', result)
#
return result
@staticmethod
def fixarray(text):
"""
Use a stack to find where array declarations should end
"""
opers = []
schema = []
for line in text.split('\n'):
modline = line
if '{' in line:
opers.append('{')
if '[' in line:
opers.append('[')
if '}' in line:
if opers.pop() == '[':
modline = line.replace('}', ']')
schema.append(modline)
return '\n'.join(str(line) for line in schema)
@staticmethod
def parse(text, ident):
"""
Apply indentation levels by parsing through schema
"""
tab = ident * ' '
depth = 0
schema = []
for line in text.split('\n'):
indent, depth = SchemaUtils.stack(line, depth)
modline = SchemaUtils.fields(line)
newline = indent * tab + modline
schema.append(newline)
return '\n'.join(str(line) for line in schema)
@staticmethod
def punct(text):
"""
JSON punctuation
"""
schema = []
lines = text.split('\n')
n = len(lines) - 1
for i in range(n):
# No comma in starting bracket or brace
if ('{' in lines[i]) or ('[' in lines[i]):
modline = lines[i]
# Comma in objects, except last in a group
elif (':' in lines[i]) and ('}' not in lines[i + 1]):
modline = lines[i] + ','
# No comma in a last array member
elif ('}' in lines[i]) and \
(('}' in lines[i + 1]) or (']' in lines[i + 1])):
modline = lines[i]
# Comma after object member of an array
# Comma after array member of an object
elif (('}' in lines[i]) or (']' in lines[i])) and \
(('{' in lines[i + 1]) or ('[' in lines[i + 1]) or
(':' in lines[i + 1])):
modline = lines[i] + ','
else:
modline = lines[i]
schema.append(modline)
schema.append(lines[-1])
return '\n'.join(str(line) for line in schema)
@staticmethod
def stack(line, depth):
"""
Compute indentation level
"""
if ('{' in line) or ('[' in line):
indent = depth
depth += 1
elif ('}' in line) or (']' in line):
depth -= 1
indent = depth
else:
depth = depth
indent = depth
return indent, depth
@staticmethod
def fields(line):
"""
Apply JSON formatting to the keys and values
"""
modline = ''
for word in line.split():
modline = modline + SchemaUtils.compose(word)
return modline
@staticmethod
def compose(word):
"""
Correct use of quotes and spaces
"""
if SchemaUtils.wordOK(word):
newword = '"' + word + '"'
elif word == ':':
newword = ' : '
else:
newword = word
return newword
@staticmethod
def wordOK(word):
"""
Check for non-words
"""
return word != '{' and word != '}' and \
word != '[' and word != ']' and word != ':'