validate keyboard data with jsonschema
This commit is contained in:
parent
95cbcef34f
commit
ededff8556
4 changed files with 155 additions and 12 deletions
|
@ -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']))
|
||||||
|
|
|
@ -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"])}')
|
||||||
|
|
|
@ -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] = {}
|
||||||
|
|
|
@ -3,5 +3,6 @@ appdirs
|
||||||
argcomplete
|
argcomplete
|
||||||
colorama
|
colorama
|
||||||
hjson
|
hjson
|
||||||
|
jsonschema
|
||||||
milc
|
milc
|
||||||
pygments
|
pygments
|
||||||
|
|
Loading…
Reference in a new issue