validate keyboard data with jsonschema

This commit is contained in:
Zach White 2020-12-01 12:52:02 -08:00 committed by Zach White
parent 95cbcef34f
commit ededff8556
4 changed files with 155 additions and 12 deletions

View File

@ -39,7 +39,7 @@ def generate_info_json(cli):
pared_down_json[key] = kb_info_json[key] pared_down_json[key] = kb_info_json[key]
pared_down_json['layouts'] = {} pared_down_json['layouts'] = {}
if 'layouts' in pared_down_json: if 'layouts' in kb_info_json:
for layout_name, layout in kb_info_json['layouts'].items(): for layout_name, layout in kb_info_json['layouts'].items():
pared_down_json['layouts'][layout_name] = {} pared_down_json['layouts'][layout_name] = {}
pared_down_json['layouts'][layout_name]['key_count'] = layout.get('key_count', len(layout['layout'])) pared_down_json['layouts'][layout_name]['key_count'] = layout.get('key_count', len(layout['layout']))

View File

@ -6,6 +6,10 @@ from qmk.decorators import automagic_keyboard, automagic_keymap
from qmk.info import info_json from qmk.info import info_json
from qmk.path import is_keyboard, normpath from qmk.path import is_keyboard, normpath
info_to_rules = {
'bootloader': 'BOOTLOADER',
'processor': 'MCU'
}
@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to') @cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages") @cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
@ -30,6 +34,10 @@ def generate_rules_mk(cli):
kb_info_json = info_json(cli.config.generate_rules_mk.keyboard) kb_info_json = info_json(cli.config.generate_rules_mk.keyboard)
rules_mk_lines = ['# This file was generated by `qmk generate-rules-mk`. Do not edit or copy.', ''] rules_mk_lines = ['# This file was generated by `qmk generate-rules-mk`. Do not edit or copy.', '']
# Bring in settings
for info_key, rule_key in info_to_rules.items():
rules_mk_lines.append(f'{rule_key} := {kb_info_json[info_key]}')
# Find features that should be enabled # Find features that should be enabled
if 'features' in kb_info_json: if 'features' in kb_info_json:
for feature, enabled in kb_info_json['features'].items(): for feature, enabled in kb_info_json['features'].items():
@ -37,6 +45,11 @@ def generate_rules_mk(cli):
enabled = 'yes' if enabled else 'no' enabled = 'yes' if enabled else 'no'
rules_mk_lines.append(f'{feature}_ENABLE := {enabled}') rules_mk_lines.append(f'{feature}_ENABLE := {enabled}')
# Set the LED driver
if 'led_matrix' in kb_info_json and 'driver' in kb_info_json['led_matrix']:
driver = kb_info_json['led_matrix']['driver']
rules_mk_lines.append(f'LED_MATRIX_DRIVER = {driver}')
# Add community layouts # Add community layouts
if 'community_layouts' in kb_info_json: if 'community_layouts' in kb_info_json:
rules_mk_lines.append(f'LAYOUTS = {" ".join(kb_info_json["community_layouts"])}') rules_mk_lines.append(f'LAYOUTS = {" ".join(kb_info_json["community_layouts"])}')

View File

@ -4,6 +4,7 @@ import json
from glob import glob from glob import glob
from pathlib import Path from pathlib import Path
import jsonschema
from milc import cli from milc import cli
from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS, LED_INDICATORS from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS, LED_INDICATORS
@ -13,6 +14,17 @@ from qmk.keymap import list_keymaps
from qmk.makefile import parse_rules_mk_file from qmk.makefile import parse_rules_mk_file
from qmk.math import compute from qmk.math import compute
led_matrix_properties = {
'driver_count': 'LED_DRIVER_COUNT',
'driver_addr1': 'LED_DRIVER_ADDR_1',
'driver_addr2': 'LED_DRIVER_ADDR_2',
'driver_addr3': 'LED_DRIVER_ADDR_3',
'driver_addr4': 'LED_DRIVER_ADDR_4',
'led_count': 'LED_DRIVER_LED_COUNT',
'timeout': 'ISSI_TIMEOUT',
'persistence': 'ISSI_PERSISTENCE'
}
rgblight_properties = { rgblight_properties = {
'led_count': 'RGBLED_NUM', 'led_count': 'RGBLED_NUM',
'pin': 'RGB_DI_PIN', 'pin': 'RGB_DI_PIN',
@ -80,6 +92,15 @@ def info_json(keyboard):
info_data = _extract_config_h(info_data) info_data = _extract_config_h(info_data)
info_data = _extract_rules_mk(info_data) info_data = _extract_rules_mk(info_data)
# Validate against the jsonschema
try:
keyboard_api_validate(info_data)
except jsonschema.ValidationError as e:
cli.log.error('Invalid info.json data: %s', e.message)
print(dir(e))
exit()
# Make sure we have at least one layout # Make sure we have at least one layout
if not info_data.get('layouts'): if not info_data.get('layouts'):
_log_error(info_data, 'No LAYOUTs defined! Need at least one layout defined in the keyboard.h or info.json.') _log_error(info_data, 'No LAYOUTs defined! Need at least one layout defined in the keyboard.h or info.json.')
@ -102,6 +123,50 @@ def info_json(keyboard):
return info_data return info_data
def _json_load(json_file):
"""Load a json file from disk.
Note: file must be a Path object.
"""
try:
return json.load(json_file.open())
except json.decoder.JSONDecodeError as e:
cli.log.error('Invalid JSON encountered attempting to load {fg_cyan}%s{fg_reset}:\n\t{fg_red}%s', json_file, e)
exit(1)
def _jsonschema(schema_name):
"""Read a jsonschema file from disk.
"""
schema_path = Path(f'data/schemas/{schema_name}.jsonschema')
if not schema_path.exists():
schema_path = Path('data/schemas/false.jsonschema')
return _json_load(schema_path)
def keyboard_validate(data):
"""Validates data against the keyboard jsonschema.
"""
schema = _jsonschema('keyboard')
validator = jsonschema.Draft7Validator(schema).validate
return validator(data)
def keyboard_api_validate(data):
"""Validates data against the api_keyboard jsonschema.
"""
base = _jsonschema('keyboard')
relative = _jsonschema('api_keyboard')
resolver = jsonschema.RefResolver.from_schema(base)
validator = jsonschema.Draft7Validator(relative, resolver=resolver).validate
return validator(data)
def _extract_debounce(info_data, config_c): def _extract_debounce(info_data, config_c):
"""Handle debounce. """Handle debounce.
""" """
@ -109,7 +174,7 @@ def _extract_debounce(info_data, config_c):
_log_warning(info_data, 'Debounce is specified in both info.json and config.h, the config.h value wins.') _log_warning(info_data, 'Debounce is specified in both info.json and config.h, the config.h value wins.')
if 'DEBOUNCE' in config_c: if 'DEBOUNCE' in config_c:
info_data['debounce'] = config_c.get('DEBOUNCE') info_data['debounce'] = int(config_c['DEBOUNCE'])
return info_data return info_data
@ -181,8 +246,36 @@ def _extract_features(info_data, rules):
return info_data return info_data
def _extract_led_drivers(info_data, rules):
"""Find all the LED drivers set in rules.mk.
"""
if 'LED_MATRIX_DRIVER' in rules:
if 'led_matrix' not in info_data:
info_data['led_matrix'] = {}
if info_data['led_matrix'].get('driver'):
_log_warning(info_data, 'LED Matrix driver is specified in both info.json and rules.mk, the rules.mk value wins.')
info_data['led_matrix']['driver'] = rules['LED_MATRIX_DRIVER']
return info_data
def _extract_led_matrix(info_data, config_c):
"""Handle the led_matrix configuration.
"""
led_matrix = info_data.get('led_matrix', {})
for json_key, config_key in led_matrix_properties.items():
if config_key in config_c:
if json_key in led_matrix:
_log_warning(info_data, 'LED Matrix: %s is specified in both info.json and config.h, the config.h value wins.' % (json_key,))
led_matrix[json_key] = config_c[config_key]
def _extract_rgblight(info_data, config_c): def _extract_rgblight(info_data, config_c):
"""Handle the rgblight configuration """Handle the rgblight configuration.
""" """
rgblight = info_data.get('rgblight', {}) rgblight = info_data.get('rgblight', {})
animations = rgblight.get('animations', {}) animations = rgblight.get('animations', {})
@ -303,6 +396,7 @@ def _extract_config_h(info_data):
_extract_indicators(info_data, config_c) _extract_indicators(info_data, config_c)
_extract_matrix_info(info_data, config_c) _extract_matrix_info(info_data, config_c)
_extract_usb_info(info_data, config_c) _extract_usb_info(info_data, config_c)
_extract_led_matrix(info_data, config_c)
_extract_rgblight(info_data, config_c) _extract_rgblight(info_data, config_c)
return info_data return info_data
@ -326,6 +420,7 @@ def _extract_rules_mk(info_data):
_extract_community_layouts(info_data, rules) _extract_community_layouts(info_data, rules)
_extract_features(info_data, rules) _extract_features(info_data, rules)
_extract_led_drivers(info_data, rules)
return info_data return info_data
@ -412,13 +507,28 @@ def arm_processor_rules(info_data, rules):
"""Setup the default info for an ARM board. """Setup the default info for an ARM board.
""" """
info_data['processor_type'] = 'arm' info_data['processor_type'] = 'arm'
info_data['bootloader'] = rules['BOOTLOADER'] if 'BOOTLOADER' in rules else 'unknown'
info_data['processor'] = rules['MCU'] if 'MCU' in rules else 'unknown'
info_data['protocol'] = 'ChibiOS' info_data['protocol'] = 'ChibiOS'
if info_data['bootloader'] == 'unknown': if 'MCU' in rules:
if 'processor' in info_data:
_log_warning(info_data, 'Processor/MCU is specified in both info.json and rules.mk, the rules.mk value wins.')
info_data['processor'] = rules['MCU']
elif 'processor' not in info_data:
info_data['processor'] = 'unknown'
if 'BOOTLOADER' in rules:
if 'bootloader' in info_data:
_log_warning(info_data, 'Bootloader is specified in both info.json and rules.mk, the rules.mk value wins.')
info_data['bootloader'] = rules['BOOTLOADER']
else:
if 'STM32' in info_data['processor']: if 'STM32' in info_data['processor']:
info_data['bootloader'] = 'stm32-dfu' info_data['bootloader'] = 'stm32-dfu'
else:
info_data['bootloader'] = 'unknown'
if 'STM32' in info_data['processor']: if 'STM32' in info_data['processor']:
info_data['platform'] = 'STM32' info_data['platform'] = 'STM32'
@ -436,9 +546,25 @@ def avr_processor_rules(info_data, rules):
info_data['processor_type'] = 'avr' info_data['processor_type'] = 'avr'
info_data['bootloader'] = rules['BOOTLOADER'] if 'BOOTLOADER' in rules else 'atmel-dfu' info_data['bootloader'] = rules['BOOTLOADER'] if 'BOOTLOADER' in rules else 'atmel-dfu'
info_data['platform'] = rules['ARCH'] if 'ARCH' in rules else 'unknown' info_data['platform'] = rules['ARCH'] if 'ARCH' in rules else 'unknown'
info_data['processor'] = rules['MCU'] if 'MCU' in rules else 'unknown'
info_data['protocol'] = 'V-USB' if rules.get('MCU') in VUSB_PROCESSORS else 'LUFA' info_data['protocol'] = 'V-USB' if rules.get('MCU') in VUSB_PROCESSORS else 'LUFA'
if 'MCU' in rules:
if 'processor' in info_data:
_log_warning(info_data, 'Processor/MCU is specified in both info.json and rules.mk, the rules.mk value wins.')
info_data['processor'] = rules['MCU']
elif 'processor' not in info_data:
info_data['processor'] = 'unknown'
if 'BOOTLOADER' in rules:
if 'bootloader' in info_data:
_log_warning(info_data, 'Bootloader is specified in both info.json and rules.mk, the rules.mk value wins.')
info_data['bootloader'] = rules['BOOTLOADER']
else:
info_data['bootloader'] = 'atmel-dfu'
# FIXME(fauxpark/anyone): Eventually we should detect the protocol by looking at PROTOCOL inherited from mcu_selection.mk: # FIXME(fauxpark/anyone): Eventually we should detect the protocol by looking at PROTOCOL inherited from mcu_selection.mk:
# info_data['protocol'] = 'V-USB' if rules.get('PROTOCOL') == 'VUSB' else 'LUFA' # info_data['protocol'] = 'V-USB' if rules.get('PROTOCOL') == 'VUSB' else 'LUFA'
@ -463,10 +589,13 @@ def merge_info_jsons(keyboard, info_data):
for info_file in find_info_json(keyboard): for info_file in find_info_json(keyboard):
# Load and validate the JSON data # Load and validate the JSON data
try: try:
new_info_data = json.load(info_file.open('r')) new_info_data = _json_load(info_file)
except Exception as e: keyboard_validate(new_info_data)
_log_error(info_data, "Invalid JSON in file %s: %s: %s" % (str(info_file), e.__class__.__name__, e))
new_info_data = {} except jsonschema.ValidationError as e:
cli.log.error('Invalid info.json data: %s', e.message)
cli.log.error('Not including file %s', info_file)
continue
if not isinstance(new_info_data, dict): if not isinstance(new_info_data, dict):
_log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),)) _log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),))
@ -479,7 +608,7 @@ def merge_info_jsons(keyboard, info_data):
# Deep merge certain keys # Deep merge certain keys
# FIXME(skullydazed/anyone): this should be generalized more so that we can inteligently merge more than one level deep. It would be nice if we could filter on valid keys too. That may have to wait for a future where we use openapi or something. # FIXME(skullydazed/anyone): this should be generalized more so that we can inteligently merge more than one level deep. It would be nice if we could filter on valid keys too. That may have to wait for a future where we use openapi or something.
for key in ('features', 'layout_aliases', 'matrix_pins', 'rgblight', 'usb'): for key in ('features', 'layout_aliases', 'led_matrix', 'matrix_pins', 'rgblight', 'usb'):
if key in new_info_data: if key in new_info_data:
if key not in info_data: if key not in info_data:
info_data[key] = {} info_data[key] = {}

View File

@ -3,5 +3,6 @@ appdirs
argcomplete argcomplete
colorama colorama
hjson hjson
jsonschema
milc milc
pygments pygments