Skip to content

Commit 860b66a

Browse files
authored
Merge pull request #5022 from scartmell-arm/mbed-config-json-schema
Add JSON schema based validation to mbed config script
2 parents 670c62c + b1ea388 commit 860b66a

File tree

8 files changed

+190
-40
lines changed

8 files changed

+190
-40
lines changed

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ mbed-greentea>=0.2.24
1212
beautifulsoup4>=4
1313
fuzzywuzzy>=0.11
1414
pyelftools>=0.24
15+
jsonschema>=2.6

tools/config/__init__.py

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
"""
1717

1818
from copy import deepcopy
19+
from six import moves
20+
import json
21+
import six
1922
import os
2023
from os.path import dirname, abspath, exists, join, isabs
2124
import sys
@@ -24,6 +27,7 @@
2427
from intelhex import IntelHex
2528
from jinja2 import FileSystemLoader, StrictUndefined
2629
from jinja2.environment import Environment
30+
from jsonschema import Draft4Validator, RefResolver
2731
# Implementation of mbed configuration mechanism
2832
from tools.utils import json_file_to_dict, intelhex_offset
2933
from tools.arm_pack_manager import Cache
@@ -341,12 +345,6 @@ def _process_macros(mlist, macros, unit_name, unit_kind):
341345
macros[macro.macro_name] = macro
342346

343347

344-
def check_dict_types(dict, type_dict, dict_loc):
345-
for key, value in dict.iteritems():
346-
if not isinstance(value, type_dict[key]):
347-
raise ConfigException("The value of %s.%s is not of type %s" %
348-
(dict_loc, key, type_dict[key].__name__))
349-
350348
Region = namedtuple("Region", "name start size active filename")
351349

352350
class Config(object):
@@ -357,17 +355,8 @@ class Config(object):
357355
__mbed_app_config_name = "mbed_app.json"
358356
__mbed_lib_config_name = "mbed_lib.json"
359357

360-
# Allowed keys in configuration dictionaries, and their types
361-
# (targets can have any kind of keys, so this validation is not applicable
362-
# to them)
363-
__allowed_keys = {
364-
"library": {"name": str, "config": dict, "target_overrides": dict,
365-
"macros": list, "__config_path": str},
366-
"application": {"config": dict, "target_overrides": dict,
367-
"macros": list, "__config_path": str,
368-
"artifact_name": str}
369-
}
370-
358+
__unused_overrides = set(["target.bootloader_img", "target.restrict_size",
359+
"target.mbed_app_start", "target.mbed_app_size"])
371360

372361
# Allowed features in configurations
373362
__allowed_features = [
@@ -420,15 +409,24 @@ def __init__(self, tgt, top_level_dirs=None, app_config=None):
420409
ConfigException("Could not parse mbed app configuration from %s"
421410
% self.app_config_location))
422411

423-
# Check the keys in the application configuration data
424-
unknown_keys = set(self.app_config_data.keys()) - \
425-
set(self.__allowed_keys["application"].keys())
426-
if unknown_keys:
427-
raise ConfigException("Unknown key(s) '%s' in %s" %
428-
(",".join(unknown_keys),
429-
self.__mbed_app_config_name))
430-
check_dict_types(self.app_config_data, self.__allowed_keys["application"],
431-
"app-config")
412+
413+
if self.app_config_location is not None:
414+
# Validate the format of the JSON file based on schema_app.json
415+
schema_root = os.path.dirname(os.path.abspath(__file__))
416+
schema_path = os.path.join(schema_root, "schema_app.json")
417+
schema = json_file_to_dict(schema_path)
418+
419+
url = moves.urllib.request.pathname2url(schema_path)
420+
uri = moves.urllib_parse.urljoin("file://", url)
421+
422+
resolver = RefResolver(uri, schema)
423+
validator = Draft4Validator(schema, resolver=resolver)
424+
425+
errors = sorted(validator.iter_errors(self.app_config_data))
426+
427+
if errors:
428+
raise ConfigException(",".join(x.message for x in errors))
429+
432430
# Update the list of targets with the ones defined in the application
433431
# config, if applicable
434432
self.lib_config_data = {}
@@ -478,11 +476,24 @@ def add_config_files(self, flist):
478476
sys.stderr.write(str(exc) + "\n")
479477
continue
480478

479+
# Validate the format of the JSON file based on the schema_lib.json
480+
schema_root = os.path.dirname(os.path.abspath(__file__))
481+
schema_path = os.path.join(schema_root, "schema_lib.json")
482+
schema_file = json_file_to_dict(schema_path)
483+
484+
url = moves.urllib.request.pathname2url(schema_path)
485+
uri = moves.urllib_parse.urljoin("file://", url)
486+
487+
resolver = RefResolver(uri, schema_file)
488+
validator = Draft4Validator(schema_file, resolver=resolver)
489+
490+
errors = sorted(validator.iter_errors(cfg))
491+
492+
if errors:
493+
raise ConfigException(",".join(x.message for x in errors))
494+
481495
cfg["__config_path"] = full_path
482496

483-
if "name" not in cfg:
484-
raise ConfigException(
485-
"Library configured at %s has no name field." % full_path)
486497
# If there's already a configuration for a module with the same
487498
# name, exit with error
488499
if self.lib_config_data.has_key(cfg["name"]):
@@ -781,12 +792,6 @@ def get_lib_config_data(self):
781792
"""
782793
all_params, macros = {}, {}
783794
for lib_name, lib_data in self.lib_config_data.items():
784-
unknown_keys = (set(lib_data.keys()) -
785-
set(self.__allowed_keys["library"].keys()))
786-
if unknown_keys:
787-
raise ConfigException("Unknown key(s) '%s' in %s" %
788-
(",".join(unknown_keys), lib_name))
789-
check_dict_types(lib_data, self.__allowed_keys["library"], lib_name)
790795
all_params.update(self._process_config_and_overrides(lib_data, {},
791796
lib_name,
792797
"library"))

tools/config/definitions.json

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
{
2+
"name_definition": {
3+
"description": "Name of the library",
4+
"type": "string",
5+
"items": {
6+
"type": "string"
7+
}
8+
},
9+
"macro_definition": {
10+
"description": "A list of extra macros that will be defined when compiling a project that includes this library.",
11+
"type": "array",
12+
"items": {
13+
"type": "string",
14+
"pattern": "(^[\\w]+$|^[\\w]+=.+$)"
15+
}
16+
},
17+
"config_definition": {
18+
"description": "List of configuration parameters",
19+
"type": "object",
20+
"patternProperties": {
21+
"^[^ ]+$": {
22+
"$ref": "#/config_parameter_base"
23+
}
24+
},
25+
"additionalProperties": false
26+
},
27+
"target_overrides_definition": {
28+
"description": "List of overrides for specific targets",
29+
"type": "object",
30+
"patternProperties": {
31+
"\\*": {
32+
"type": "object",
33+
"patternProperties": {
34+
".*\\..*": {}
35+
},
36+
"additionalProperties": false
37+
},
38+
"^\\S+$": {
39+
"$ref": "#/target_override_entry"
40+
}
41+
},
42+
"additionalProperties": false
43+
},
44+
"config_parameter_long": {
45+
"type": "object",
46+
"properties": {
47+
"help": {
48+
"description": "An optional help message that describes the purpose of the parameter",
49+
"type": "string"
50+
},
51+
"value": {
52+
"description": "An optional field that defines the value of the parameter",
53+
"type": [
54+
"integer",
55+
"string",
56+
"boolean",
57+
"null"
58+
]
59+
},
60+
"required": {
61+
"description": "An optional field that specifies whether the parameter must be given a value before compiling the code. (False by default)",
62+
"type": "boolean"
63+
},
64+
"macro_name": {
65+
"description": "An optional field for the macro defined at compile time for this configuration parameter. The system will automatically figure out the macro name from the configuration parameter, but this field will override it",
66+
"type": "string"
67+
}
68+
}
69+
},
70+
"config_parameter_short": {
71+
"type": [
72+
"string",
73+
"integer",
74+
"boolean",
75+
"null"
76+
]
77+
},
78+
"config_parameter_base": {
79+
"oneOf": [
80+
{
81+
"$ref": "#/config_parameter_long"
82+
},
83+
{
84+
"$ref": "#/config_parameter_short"
85+
}
86+
]
87+
},
88+
"target_override_entry": {
89+
"type": "object",
90+
"patternProperties": {
91+
"^\\S+$": {}
92+
},
93+
"additionalProperties": false
94+
}
95+
}

tools/config/schema_app.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-06/schema#",
3+
"title": "Mbed Library Schema",
4+
"description": "Configuration file for an mbed application",
5+
"type": "object",
6+
"properties": {
7+
"name": {
8+
"$ref": "definitions.json#/name_definition"
9+
},
10+
"config": {
11+
"$ref": "definitions.json#/config_definition"
12+
},
13+
"target_overrides": {
14+
"$ref": "definitions.json#/target_overrides_definition"
15+
},
16+
"macros": {
17+
"$ref": "definitions.json#/macro_definition"
18+
},
19+
"artifact_name": {
20+
"type": "string"
21+
}
22+
},
23+
"additionalProperties": false
24+
}

tools/config/schema_lib.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-06/schema#",
3+
"title": "Mbed Library Schema",
4+
"description": "Configuration file for an mbed library",
5+
"type": "object",
6+
"properties": {
7+
"name": {
8+
"$ref": "definitions.json#/name_definition"
9+
},
10+
"config": {
11+
"$ref": "definitions.json#/config_definition"
12+
},
13+
"target_overrides": {
14+
"$ref": "definitions.json#/target_overrides_definition"
15+
},
16+
"macros": {
17+
"$ref": "definitions.json#/macro_definition"
18+
}
19+
},
20+
"required": [
21+
"name"
22+
],
23+
"additionalProperties": false
24+
}

tools/test/config/config_test.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@ def test_init_app_config(target):
108108

109109
config = Config(target, app_config=app_config)
110110

111-
mock_json_file_to_dict.assert_called_with(app_config)
111+
mock_json_file_to_dict.assert_any_call("app_config")
112+
112113
assert config.app_config_data == mock_return
113114

114115

@@ -149,7 +150,7 @@ def test_init_no_app_config_with_dir(target):
149150
config = Config(target, [directory])
150151

151152
mock_isfile.assert_called_with(path)
152-
mock_json_file_to_dict.assert_called_once_with(path)
153+
mock_json_file_to_dict.assert_any_call(path)
153154
assert config.app_config_data == mock_return
154155

155156

@@ -171,5 +172,5 @@ def test_init_override_app_config(target):
171172

172173
config = Config(target, [directory], app_config=app_config)
173174

174-
mock_json_file_to_dict.assert_called_once_with(app_config)
175+
mock_json_file_to_dict.assert_any_call(app_config)
175176
assert config.app_config_data == mock_return
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"K64F": {
3-
"exception_msg": "Unknown key(s)"
3+
"exception_msg": "Additional properties are not allowed ('unknown_key' was unexpected)"
44
}
55
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"K64F": {
3-
"exception_msg": "Unknown key(s)"
3+
"exception_msg": "Additional properties are not allowed ('unknown_key' was unexpected)"
44
}
55
}

0 commit comments

Comments
 (0)