Skip to content

Commit

Permalink
validate keyboard data with jsonschema
Browse files Browse the repository at this point in the history
  • Loading branch information
skullydazed committed Jan 8, 2021
1 parent 95cbcef commit ededff8
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 12 deletions.
2 changes: 1 addition & 1 deletion lib/python/qmk/cli/generate/info_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def generate_info_json(cli):
pared_down_json[key] = kb_info_json[key]

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():
pared_down_json['layouts'][layout_name] = {}
pared_down_json['layouts'][layout_name]['key_count'] = layout.get('key_count', len(layout['layout']))
Expand Down
13 changes: 13 additions & 0 deletions lib/python/qmk/cli/generate/rules_mk.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
from qmk.info import info_json
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('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
Expand All @@ -30,13 +34,22 @@ def generate_rules_mk(cli):
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.', '']

# 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
if 'features' in kb_info_json:
for feature, enabled in kb_info_json['features'].items():
feature = feature.upper()
enabled = 'yes' if enabled else 'no'
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
if 'community_layouts' in kb_info_json:
rules_mk_lines.append(f'LAYOUTS = {" ".join(kb_info_json["community_layouts"])}')
Expand Down
151 changes: 140 additions & 11 deletions lib/python/qmk/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from glob import glob
from pathlib import Path

import jsonschema
from milc import cli

from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS, LED_INDICATORS
Expand All @@ -13,6 +14,17 @@
from qmk.makefile import parse_rules_mk_file
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 = {
'led_count': 'RGBLED_NUM',
'pin': 'RGB_DI_PIN',
Expand Down Expand Up @@ -80,6 +92,15 @@ def info_json(keyboard):
info_data = _extract_config_h(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
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.')
Expand All @@ -102,14 +123,58 @@ def info_json(keyboard):
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):
"""Handle debounce.
"""
if 'debounce' in info_data and 'DEBOUNCE' in config_c:
_log_warning(info_data, 'Debounce is specified in both info.json and config.h, the config.h value wins.')

if 'DEBOUNCE' in config_c:
info_data['debounce'] = config_c.get('DEBOUNCE')
info_data['debounce'] = int(config_c['DEBOUNCE'])

return info_data

Expand Down Expand Up @@ -181,8 +246,36 @@ def _extract_features(info_data, rules):
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):
"""Handle the rgblight configuration
"""Handle the rgblight configuration.
"""
rgblight = info_data.get('rgblight', {})
animations = rgblight.get('animations', {})
Expand Down Expand Up @@ -303,6 +396,7 @@ def _extract_config_h(info_data):
_extract_indicators(info_data, config_c)
_extract_matrix_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)

return info_data
Expand All @@ -326,6 +420,7 @@ def _extract_rules_mk(info_data):

_extract_community_layouts(info_data, rules)
_extract_features(info_data, rules)
_extract_led_drivers(info_data, rules)

return info_data

Expand Down Expand Up @@ -412,13 +507,28 @@ def arm_processor_rules(info_data, rules):
"""Setup the default info for an ARM board.
"""
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'

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']:
info_data['bootloader'] = 'stm32-dfu'
else:
info_data['bootloader'] = 'unknown'

if 'STM32' in info_data['processor']:
info_data['platform'] = 'STM32'
Expand All @@ -436,9 +546,25 @@ def avr_processor_rules(info_data, rules):
info_data['processor_type'] = 'avr'
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['processor'] = rules['MCU'] if 'MCU' in rules else 'unknown'
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:
# info_data['protocol'] = 'V-USB' if rules.get('PROTOCOL') == 'VUSB' else 'LUFA'

Expand All @@ -463,10 +589,13 @@ def merge_info_jsons(keyboard, info_data):
for info_file in find_info_json(keyboard):
# Load and validate the JSON data
try:
new_info_data = json.load(info_file.open('r'))
except Exception as e:
_log_error(info_data, "Invalid JSON in file %s: %s: %s" % (str(info_file), e.__class__.__name__, e))
new_info_data = {}
new_info_data = _json_load(info_file)
keyboard_validate(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):
_log_error(info_data, "Invalid file %s, root object should be a dictionary." % (str(info_file),))
Expand All @@ -479,7 +608,7 @@ def merge_info_jsons(keyboard, info_data):

# 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.
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 not in info_data:
info_data[key] = {}
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ appdirs
argcomplete
colorama
hjson
jsonschema
milc
pygments

0 comments on commit ededff8

Please sign in to comment.