Skip to content

Commit ededff8

Browse files
committed
validate keyboard data with jsonschema
1 parent 95cbcef commit ededff8

File tree

4 files changed

+155
-12
lines changed

4 files changed

+155
-12
lines changed

lib/python/qmk/cli/generate/info_json.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def generate_info_json(cli):
3939
pared_down_json[key] = kb_info_json[key]
4040

4141
pared_down_json['layouts'] = {}
42-
if 'layouts' in pared_down_json:
42+
if 'layouts' in kb_info_json:
4343
for layout_name, layout in kb_info_json['layouts'].items():
4444
pared_down_json['layouts'][layout_name] = {}
4545
pared_down_json['layouts'][layout_name]['key_count'] = layout.get('key_count', len(layout['layout']))

lib/python/qmk/cli/generate/rules_mk.py

+13
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
from qmk.info import info_json
77
from qmk.path import is_keyboard, normpath
88

9+
info_to_rules = {
10+
'bootloader': 'BOOTLOADER',
11+
'processor': 'MCU'
12+
}
913

1014
@cli.argument('-o', '--output', arg_only=True, type=normpath, help='File to write to')
1115
@cli.argument('-q', '--quiet', arg_only=True, action='store_true', help="Quiet mode, only output error messages")
@@ -30,13 +34,22 @@ def generate_rules_mk(cli):
3034
kb_info_json = info_json(cli.config.generate_rules_mk.keyboard)
3135
rules_mk_lines = ['# This file was generated by `qmk generate-rules-mk`. Do not edit or copy.', '']
3236

37+
# Bring in settings
38+
for info_key, rule_key in info_to_rules.items():
39+
rules_mk_lines.append(f'{rule_key} := {kb_info_json[info_key]}')
40+
3341
# Find features that should be enabled
3442
if 'features' in kb_info_json:
3543
for feature, enabled in kb_info_json['features'].items():
3644
feature = feature.upper()
3745
enabled = 'yes' if enabled else 'no'
3846
rules_mk_lines.append(f'{feature}_ENABLE := {enabled}')
3947

48+
# Set the LED driver
49+
if 'led_matrix' in kb_info_json and 'driver' in kb_info_json['led_matrix']:
50+
driver = kb_info_json['led_matrix']['driver']
51+
rules_mk_lines.append(f'LED_MATRIX_DRIVER = {driver}')
52+
4053
# Add community layouts
4154
if 'community_layouts' in kb_info_json:
4255
rules_mk_lines.append(f'LAYOUTS = {" ".join(kb_info_json["community_layouts"])}')

lib/python/qmk/info.py

+140-11
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from glob import glob
55
from pathlib import Path
66

7+
import jsonschema
78
from milc import cli
89

910
from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS, LED_INDICATORS
@@ -13,6 +14,17 @@
1314
from qmk.makefile import parse_rules_mk_file
1415
from qmk.math import compute
1516

17+
led_matrix_properties = {
18+
'driver_count': 'LED_DRIVER_COUNT',
19+
'driver_addr1': 'LED_DRIVER_ADDR_1',
20+
'driver_addr2': 'LED_DRIVER_ADDR_2',
21+
'driver_addr3': 'LED_DRIVER_ADDR_3',
22+
'driver_addr4': 'LED_DRIVER_ADDR_4',
23+
'led_count': 'LED_DRIVER_LED_COUNT',
24+
'timeout': 'ISSI_TIMEOUT',
25+
'persistence': 'ISSI_PERSISTENCE'
26+
}
27+
1628
rgblight_properties = {
1729
'led_count': 'RGBLED_NUM',
1830
'pin': 'RGB_DI_PIN',
@@ -80,6 +92,15 @@ def info_json(keyboard):
8092
info_data = _extract_config_h(info_data)
8193
info_data = _extract_rules_mk(info_data)
8294

95+
# Validate against the jsonschema
96+
try:
97+
keyboard_api_validate(info_data)
98+
99+
except jsonschema.ValidationError as e:
100+
cli.log.error('Invalid info.json data: %s', e.message)
101+
print(dir(e))
102+
exit()
103+
83104
# Make sure we have at least one layout
84105
if not info_data.get('layouts'):
85106
_log_error(info_data, 'No LAYOUTs defined! Need at least one layout defined in the keyboard.h or info.json.')
@@ -102,14 +123,58 @@ def info_json(keyboard):
102123
return info_data
103124

104125

126+
def _json_load(json_file):
127+
"""Load a json file from disk.
128+
129+
Note: file must be a Path object.
130+
"""
131+
try:
132+
return json.load(json_file.open())
133+
134+
except json.decoder.JSONDecodeError as e:
135+
cli.log.error('Invalid JSON encountered attempting to load {fg_cyan}%s{fg_reset}:\n\t{fg_red}%s', json_file, e)
136+
exit(1)
137+
138+
139+
def _jsonschema(schema_name):
140+
"""Read a jsonschema file from disk.
141+
"""
142+
schema_path = Path(f'data/schemas/{schema_name}.jsonschema')
143+
144+
if not schema_path.exists():
145+
schema_path = Path('data/schemas/false.jsonschema')
146+
147+
return _json_load(schema_path)
148+
149+
150+
def keyboard_validate(data):
151+
"""Validates data against the keyboard jsonschema.
152+
"""
153+
schema = _jsonschema('keyboard')
154+
validator = jsonschema.Draft7Validator(schema).validate
155+
156+
return validator(data)
157+
158+
159+
def keyboard_api_validate(data):
160+
"""Validates data against the api_keyboard jsonschema.
161+
"""
162+
base = _jsonschema('keyboard')
163+
relative = _jsonschema('api_keyboard')
164+
resolver = jsonschema.RefResolver.from_schema(base)
165+
validator = jsonschema.Draft7Validator(relative, resolver=resolver).validate
166+
167+
return validator(data)
168+
169+
105170
def _extract_debounce(info_data, config_c):
106171
"""Handle debounce.
107172
"""
108173
if 'debounce' in info_data and 'DEBOUNCE' in config_c:
109174
_log_warning(info_data, 'Debounce is specified in both info.json and config.h, the config.h value wins.')
110175

111176
if 'DEBOUNCE' in config_c:
112-
info_data['debounce'] = config_c.get('DEBOUNCE')
177+
info_data['debounce'] = int(config_c['DEBOUNCE'])
113178

114179
return info_data
115180

@@ -181,8 +246,36 @@ def _extract_features(info_data, rules):
181246
return info_data
182247

183248

249+
def _extract_led_drivers(info_data, rules):
250+
"""Find all the LED drivers set in rules.mk.
251+
"""
252+
if 'LED_MATRIX_DRIVER' in rules:
253+
if 'led_matrix' not in info_data:
254+
info_data['led_matrix'] = {}
255+
256+
if info_data['led_matrix'].get('driver'):
257+
_log_warning(info_data, 'LED Matrix driver is specified in both info.json and rules.mk, the rules.mk value wins.')
258+
259+
info_data['led_matrix']['driver'] = rules['LED_MATRIX_DRIVER']
260+
261+
return info_data
262+
263+
264+
def _extract_led_matrix(info_data, config_c):
265+
"""Handle the led_matrix configuration.
266+
"""
267+
led_matrix = info_data.get('led_matrix', {})
268+
269+
for json_key, config_key in led_matrix_properties.items():
270+
if config_key in config_c:
271+
if json_key in led_matrix:
272+
_log_warning(info_data, 'LED Matrix: %s is specified in both info.json and config.h, the config.h value wins.' % (json_key,))
273+
274+
led_matrix[json_key] = config_c[config_key]
275+
276+
184277
def _extract_rgblight(info_data, config_c):
185-
"""Handle the rgblight configuration
278+
"""Handle the rgblight configuration.
186279
"""
187280
rgblight = info_data.get('rgblight', {})
188281
animations = rgblight.get('animations', {})
@@ -303,6 +396,7 @@ def _extract_config_h(info_data):
303396
_extract_indicators(info_data, config_c)
304397
_extract_matrix_info(info_data, config_c)
305398
_extract_usb_info(info_data, config_c)
399+
_extract_led_matrix(info_data, config_c)
306400
_extract_rgblight(info_data, config_c)
307401

308402
return info_data
@@ -326,6 +420,7 @@ def _extract_rules_mk(info_data):
326420

327421
_extract_community_layouts(info_data, rules)
328422
_extract_features(info_data, rules)
423+
_extract_led_drivers(info_data, rules)
329424

330425
return info_data
331426

@@ -412,13 +507,28 @@ def arm_processor_rules(info_data, rules):
412507
"""Setup the default info for an ARM board.
413508
"""
414509
info_data['processor_type'] = 'arm'
415-
info_data['bootloader'] = rules['BOOTLOADER'] if 'BOOTLOADER' in rules else 'unknown'
416-
info_data['processor'] = rules['MCU'] if 'MCU' in rules else 'unknown'
417510
info_data['protocol'] = 'ChibiOS'
418511

419-
if info_data['bootloader'] == 'unknown':
512+
if 'MCU' in rules:
513+
if 'processor' in info_data:
514+
_log_warning(info_data, 'Processor/MCU is specified in both info.json and rules.mk, the rules.mk value wins.')
515+
516+
info_data['processor'] = rules['MCU']
517+
518+
elif 'processor' not in info_data:
519+
info_data['processor'] = 'unknown'
520+
521+
if 'BOOTLOADER' in rules:
522+
if 'bootloader' in info_data:
523+
_log_warning(info_data, 'Bootloader is specified in both info.json and rules.mk, the rules.mk value wins.')
524+
525+
info_data['bootloader'] = rules['BOOTLOADER']
526+
527+
else:
420528
if 'STM32' in info_data['processor']:
421529
info_data['bootloader'] = 'stm32-dfu'
530+
else:
531+
info_data['bootloader'] = 'unknown'
422532

423533
if 'STM32' in info_data['processor']:
424534
info_data['platform'] = 'STM32'
@@ -436,9 +546,25 @@ def avr_processor_rules(info_data, rules):
436546
info_data['processor_type'] = 'avr'
437547
info_data['bootloader'] = rules['BOOTLOADER'] if 'BOOTLOADER' in rules else 'atmel-dfu'
438548
info_data['platform'] = rules['ARCH'] if 'ARCH' in rules else 'unknown'
439-
info_data['processor'] = rules['MCU'] if 'MCU' in rules else 'unknown'
440549
info_data['protocol'] = 'V-USB' if rules.get('MCU') in VUSB_PROCESSORS else 'LUFA'
441550

551+
if 'MCU' in rules:
552+
if 'processor' in info_data:
553+
_log_warning(info_data, 'Processor/MCU is specified in both info.json and rules.mk, the rules.mk value wins.')
554+
555+
info_data['processor'] = rules['MCU']
556+
557+
elif 'processor' not in info_data:
558+
info_data['processor'] = 'unknown'
559+
560+
if 'BOOTLOADER' in rules:
561+
if 'bootloader' in info_data:
562+
_log_warning(info_data, 'Bootloader is specified in both info.json and rules.mk, the rules.mk value wins.')
563+
564+
info_data['bootloader'] = rules['BOOTLOADER']
565+
else:
566+
info_data['bootloader'] = 'atmel-dfu'
567+
442568
# FIXME(fauxpark/anyone): Eventually we should detect the protocol by looking at PROTOCOL inherited from mcu_selection.mk:
443569
# info_data['protocol'] = 'V-USB' if rules.get('PROTOCOL') == 'VUSB' else 'LUFA'
444570

@@ -463,10 +589,13 @@ def merge_info_jsons(keyboard, info_data):
463589
for info_file in find_info_json(keyboard):
464590
# Load and validate the JSON data
465591
try:
466-
new_info_data = json.load(info_file.open('r'))
467-
except Exception as e:
468-
_log_error(info_data, "Invalid JSON in file %s: %s: %s" % (str(info_file), e.__class__.__name__, e))
469-
new_info_data = {}
592+
new_info_data = _json_load(info_file)
593+
keyboard_validate(new_info_data)
594+
595+
except jsonschema.ValidationError as e:
596+
cli.log.error('Invalid info.json data: %s', e.message)
597+
cli.log.error('Not including file %s', info_file)
598+
continue
470599

471600
if not isinstance(new_info_data, dict):
472601
_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):
479608

480609
# Deep merge certain keys
481610
# 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.
482-
for key in ('features', 'layout_aliases', 'matrix_pins', 'rgblight', 'usb'):
611+
for key in ('features', 'layout_aliases', 'led_matrix', 'matrix_pins', 'rgblight', 'usb'):
483612
if key in new_info_data:
484613
if key not in info_data:
485614
info_data[key] = {}

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ appdirs
33
argcomplete
44
colorama
55
hjson
6+
jsonschema
67
milc
78
pygments

0 commit comments

Comments
 (0)