Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add jsonschema exporter for VSS. #296

Merged
merged 10 commits into from
Oct 19, 2023
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ repos:
exclude_types: ["csv", "proto"]
- id: end-of-file-fixer
exclude_types: ["json", "proto"]
exclude: '.*.fidl'
exclude: ".*\\.[fidl|jsonschema]"
- id: check-yaml
- id: check-added-large-files

Expand Down
30 changes: 30 additions & 0 deletions docs/vspec2x.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,36 @@ Lets the exporter generate _all_ extended metadata attributes found in the model
### --json-pretty
If the paramter is set it will pretty-print the JSON output, otherwise you will get a minimized version

## JSONSCHEMA exporter notes

### --jsonschema-all-extended-attributes
Lets the exporter generate _all_ extended metadata attributes found in the model. By default the exporter is generating only those given by the `-e`/`--extended-attributes` parameter. This will also add unconverted VSS standard attribtues into the schema using the following attributes

| VSS attribute | in schema |
|---------------|---------------|
| type | x-VSStype |
| datatype | x-datatype |
| deprecation | x-deprecation |
| aggregate | x-aggregate |
| comment | x-comment |
| uuid | x-uuid |

Not that strict JSON schema validators might not accept jsonschemas whoch such extra, non-standard entries.

### --jsonschema-disallow-additional-properties
Do not allow properties not defined in VSS tree, when elements are validated agains the schema, what this basically does is setting

```json
"additionalProperties": false
```
for all defined objects. See: https://json-schema.org/draft/2020-12/json-schema-core#additionalProperties

### --jsonschema-require-all-properties
Require all elements defined in VSS tree for a valid object, i.e. this populates the `required` list with all childs. See: https://json-schema.org/draft/2020-12/json-schema-validation#name-required

### --jsonschema-pretty
If the paramter is set it will pretty-print the JSON output, otherwise you will get a minimized version

## YAML exporter notes

### --yaml-all-extended-attributes
Expand Down
1 change: 1 addition & 0 deletions tests/vspec/test_comment/expected.jsonschema
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"$schema": "https://json-schema.org/draft/2020-12/schema", "title": "A", "type": "object", "description": "Branch A.", "properties": {"SingleLineNotQuoted": {"description": "A sensor.", "type": "number"}, "SingleLineInternalQuotes": {"description": "A sensor.", "type": "number"}, "SingleLineQuoted": {"description": "A sensor.", "type": "number"}, "SingleLineQuotedInternalQuotes": {"description": "A sensor.", "type": "number"}, "SingleLineComma": {"description": "A sensor.", "type": "number"}, "SingleLineCommaQuoted": {"description": "A sensor.", "type": "number"}, "MultiLineCommaNotQuoted": {"description": "A sensor.", "type": "number"}, "MultiLineCommaQuoted": {"description": "A sensor.", "type": "number"}, "MultiLineStyleInitialBreak": {"description": "A sensor.", "type": "number"}, "MultiLineLiteralStyleQuote": {"description": "A sensor.", "type": "number"}}}
1 change: 1 addition & 0 deletions tests/vspec/test_datatypes/expected.jsonschema
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"$schema": "https://json-schema.org/draft/2020-12/schema", "title": "A", "type": "object", "description": "Branch A.", "properties": {"UInt8": {"description": "A uint8.", "type": "integer"}, "Int8": {"description": "An int8.", "type": "integer"}, "UInt16": {"description": "A uint16.", "type": "integer"}, "Int16": {"description": "An int16.", "type": "integer"}, "UInt32": {"description": "A uint32.", "type": "integer"}, "Int32": {"description": "An int32", "type": "integer"}, "UInt64": {"description": "A uint64.", "type": "integer"}, "Int64": {"description": "An int64", "type": "integer"}, "IsBoolean": {"description": "A boolean", "type": "boolean"}, "Float": {"description": "A float.", "type": "number"}, "Double": {"description": "A double.", "type": "number"}, "String": {"description": "A string.", "type": "string"}}}
3 changes: 2 additions & 1 deletion tests/vspec/test_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def run_exporter(directory, exporter):
def test_exporters(directory, change_test_dir):
# Run all "supported" exporters, i.e. not those in contrib
# Exception is "binary", as it is assumed output may vary depending on target
exporters = ["json", "ddsidl", "csv", "yaml", "franca", "graphql"]
exporters = ["json", "jsonschema", "ddsidl", "csv", "yaml", "franca", "graphql"]

for exporter in exporters:
run_exporter(directory, exporter)
1 change: 1 addition & 0 deletions tests/vspec/test_instances/expected.jsonschema
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"$schema": "https://json-schema.org/draft/2020-12/schema", "title": "A", "type": "object", "description": "Branch A.", "properties": {"B": {"description": "This description will be duplicated.", "properties": {"Row1": {"description": "This description will be duplicated.", "properties": {"Left": {"description": "This description will be duplicated.", "properties": {"C": {"description": "This description will also exist multiple times.", "type": "integer"}}}, "Right": {"description": "This description will be duplicated.", "properties": {"C": {"description": "This description will also exist multiple times.", "type": "integer"}}}}}, "Row2": {"description": "This description will be duplicated.", "properties": {"Left": {"description": "This description will be duplicated.", "properties": {"C": {"description": "This description will also exist multiple times.", "type": "integer"}}}, "Right": {"description": "This description will be duplicated.", "properties": {"C": {"description": "This description will also exist multiple times.", "type": "integer"}}}}}}}}}
159 changes: 159 additions & 0 deletions vspec/vssexporters/vss2jsonschema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#!/usr/bin/env python3
#
# Copyright (c) 2023 Contributors to COVESA
#
# This program and the accompanying materials are made available under the
# terms of the Mozilla Public License 2.0 which is available at
# https://www.mozilla.org/en-US/MPL/2.0/
#
# SPDX-License-Identifier: MPL-2.0
#
# Convert vspec tree compatible JSON schema

import argparse
import json
import logging
from typing import Dict, Any
from vspec.model.vsstree import VSSNode
from vspec.loggingconfig import initLogging

type_map = {
"int8": "integer",
"uint8": "integer",
"int16": "integer",
"uint16": "integer",
"int32": "integer",
"uint32": "integer",
"int64": "integer",
"uint64": "integer",
"boolean": "boolean",
"float": "number",
"double": "number",
"string": "string",
"int8[]": "array",
"uint8[]": "array",
"int16[]": "array",
"uint16[]": "array",
"int32[]": "array",
"uint32[]": "array",
"int64[]": "array",
"uint64[]": "array",
"boolean[]": "array",
"float[]": "array",
"double[]": "array",
"string[]": "array"
}


def add_arguments(parser: argparse.ArgumentParser):
"""Check for input arguments."""
parser.add_argument('--jsonschema-all-extended-attributes', action='store_true',
help="Generate all extended attributes found in the model."
"Should not be used with strict mode JSON Schema validators.")
parser.add_argument("--jsonschema-disallow-additional-properties", action='store_true',
help="Do not allow properties not defined in VSS tree")
parser.add_argument("--jsonschema-require-all-properties", action='store_true',
help="Require all elements defined in VSS tree for a valid object")
parser.add_argument('--jsonschema-pretty', action='store_true',
help=" Pretty print JSON Schema output.")


def export_node(json_dict, node, config, print_uuid):
"""Preparing nodes for JSON schema output."""
# keyword with X- sign are left for extensions and they are not part of official JSON schema
json_dict[node.name] = {
"description": node.description,
}

if node.is_signal() or node.is_property():
json_dict[node.name]["type"] = type_map[node.data_type_str]

# many optional attributes are initialized to "" in vsstree.py
if node.min != "":
json_dict[node.name]["minimum"] = node.min
if node.max != "":
json_dict[node.name]["maximum"] = node.max
if node.allowed != "":
json_dict[node.name]["enum"] = node.allowed
if node.default != "":
json_dict[node.name]["default"] = node.default
if node.is_struct():
# change type to object
json_dict[node.type]["type"] = "object"

if config.jsonschema_all_extended_attributes:
if node.type != "":
json_dict[node.name]["x-VSStype"] = str(node.type.value)
if node.data_type_str != "":
json_dict[node.name]["x-datatype"] = node.data_type_str
if node.deprecation != "":
json_dict[node.name]["x-deprecation"] = node.deprecation

# in case of unit or aggregate, the attribute will be missing
try:
json_dict[node.name]["x-unit"] = str(node.unit.value)
except AttributeError:
pass
try:
json_dict[node.name]["x-aggregate"] = node.aggregate
if node.aggregate:
# change type to object
json_dict[node.type]["type"] = "object"
except AttributeError:
pass

if node.comment != "":
json_dict[node.name]["x-comment"] = node.comment

if print_uuid:
json_dict[node.name]["x-uuid"] = node.uuid

for k, v in node.extended_attributes.items():
json_dict[node.name][k] = v

# Generate child nodes
if node.is_branch() or node.is_struct():
if config.jsonschema_disallow_additional_properties:
json_dict[node.name]["additionalProperties"] = False
json_dict[node.name]["properties"] = {}
if config.jsonschema_require_all_properties:
json_dict[node.name]["required"] = [child.name for child in node.children]
for child in node.children:
export_node(json_dict[node.name]["properties"], child, config, print_uuid)


def export(config: argparse.Namespace, signal_root: VSSNode, print_uuid, data_type_root: VSSNode):
"""Export function for generating JSON schema file."""
logging.info("Generating JSON schema...")
indent = None
if config.jsonschema_pretty:
logging.info("Serializing pretty JSON schema...")
indent = 2

signals_json_schema: Dict[str, Any] = {}
export_node(signals_json_schema, signal_root, config, print_uuid)

# Add data types to the schema
if data_type_root is not None:
data_types_json_schema: Dict[str, Any] = {}
export_node(data_types_json_schema, data_type_root, config, print_uuid)
if config.jsonschema_all_extended_attributes:
signals_json_schema["x-ComplexDataTypes"] = data_types_json_schema

# VSS models only have one root, so there should only be one
# key in the dict
assert (len(signals_json_schema.keys()) == 1)
top_node_name = list(signals_json_schema.keys())[0]
signals_json_schema = signals_json_schema.pop(top_node_name)

json_schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": top_node_name,
"type": "object",
**signals_json_schema}
with open(config.output_file, 'w', encoding="utf-8") as output_file:
json.dump(json_schema, output_file, indent=indent, sort_keys=False)


if __name__ == "__main__":
initLogging()
11 changes: 11 additions & 0 deletions vspec2jsonschema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env python3
#
#
# Convert vspec2jsonschema wrapper for vspec2x
#

import sys
import vspec2x

if __name__ == "__main__":
vspec2x.main(["--format", "jsonschema"]+sys.argv[1:])
5 changes: 3 additions & 2 deletions vspec2x.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@


from vspec.vssexporters import vss2json, vss2csv, vss2yaml, \
vss2binary, vss2franca, vss2ddsidl, vss2graphql, vss2protobuf
vss2binary, vss2franca, vss2ddsidl, vss2graphql, vss2protobuf, vss2jsonschema

SUPPORTED_STRUCT_EXPORT_FORMATS = set(["json", "yaml", "csv", "protobuf"])
SUPPORTED_STRUCT_EXPORT_FORMATS = set(["json", "yaml", "csv", "protobuf", "jsonschema"])


class Exporter(Enum):
Expand All @@ -43,6 +43,7 @@ def export(config: argparse.Namespace, root: VSSNode):
idl = vss2ddsidl
graphql = vss2graphql
protobuf = vss2protobuf
jsonschema = vss2jsonschema

def __str__(self):
return self.name
Expand Down