Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ download_archive(
)

bazel_dep(name = "nlohmann_json", version = "3.11.3")
bazel_dep(name = "flatbuffers", version = "25.2.10")
bazel_dep(name = "bazel_skylib", version = "1.8.1")
bazel_dep(name = "rules_doxygen", version = "2.5.0")
bazel_dep(name = "score_baselibs", version = "0.2.2")
Expand Down
6 changes: 6 additions & 0 deletions bazel/tools/BUILD
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
load(":json_schema_validator_test.bzl", "json_schema_validator_tests")

json_schema_validator_tests(name = "json_validator_tests")

py_binary(
name = "json_to_flatbuffer_json",
srcs = ["json_to_flatbuffer_json.py"],
visibility = ["//visibility:public"],
)
100 changes: 100 additions & 0 deletions bazel/tools/generate_com_config.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""
Bazel rule to generate FlatBuffer binary configuration from JSON files.

This rule converts existing communication JSON files to a FlatBuffer friendly format.
- Convert '-' to '_' in keys (required)
- Convert keys from camelCase to snake_case (avoids warnings)
"""

def generate_com_config(name, json, convert, visibility = None):
"""
Generate a FlatBuffer binary configuration file from a JSON input.

This rule can optionally convert the input JSON to FlatBuffer friendly format
before compiling to FlatBuffer binary format.

The schema is hardcoded to the COM FlatBuffer schema at:
//score/mw/com/impl/configuration:ara_com_config.fbs

The output .bin file will be generated in the same directory path as the
input JSON file to match the filegroup behavior for JSON files.

Args:
name: Name of the rule (will be used as the target name)
json: Input JSON configuration file
convert: Boolean - if True, converts JSON to FlatBuffer friendly format
(dash to underscore, camelCase to snake_case). If False, uses
JSON as-is (assumes it's already FlatBuffer conform)
visibility: Visibility for the generated target (optional)

Outputs:
A .bin file in the same directory as the input JSON
Example: "example/ara_com_config.json" -> "example/ara_com_config.bin"

Example:
generate_com_config(
name = "my_config.bin",
json = "example/config.json",
convert = True,
visibility = ["//visibility:public"],
)
"""

# Always use the COM FlatBuffer schema
schema = "//score/mw/com/impl/configuration:ara_com_config.fbs"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Thomas-Mikhael: Please check all references in *.bzl, which is used outside of this module, we have to specify the full bazel path e.g.:

Suggested change
schema = "//score/mw/com/impl/configuration:ara_com_config.fbs"
schema = "@score_communication//score/mw/com/impl/configuration:ara_com_config.fbs"


# Extract base filepath and filename (remove .json extension if present)
if json.endswith(".json"):
filepath_base = json[:-5]
filename_base = json.split("/")[-1][:-5]
else:
filepath_base = json
filename_base = json.split("/")[-1]

# Preserve the full path of the JSON file, just change extension
output_path = filepath_base + ".bin"

if convert:
# Intermediate converted JSON file (keep in same directory)
converted_json = filepath_base + "_converted.json"

# Intermediate flatc output filename
flatc_output = filename_base + "_converted.bin"

# Step 1: Convert JSON to FlatBuffer friendly format
native.genrule(
name = name + "_convert",
srcs = [json],
outs = [converted_json],
tools = ["//bazel/tools:json_to_flatbuffer_json"],
cmd = "$(location //bazel/tools:json_to_flatbuffer_json) $(SRCS) $@",
visibility = ["//visibility:private"],
)

# Step 2: Compile converted JSON to FlatBuffer binary
native.genrule(
name = name,
srcs = [converted_json, schema],
outs = [output_path],
tools = ["@flatbuffers//:flatc"],
cmd = "$(location @flatbuffers//:flatc) --binary -o $(@D) $(location " + schema + ") $(location " + converted_json + ") && mv $(@D)/" + flatc_output + " $@",
visibility = visibility,
)
else:
# Intermediate flatc output filename
flatc_output = filename_base + ".bin"

# Only move if the output paths differ
if flatc_output == output_path.split("/")[-1]:
cmd = "$(location @flatbuffers//:flatc) --binary -o $(@D) $(location " + schema + ") $(location " + json + ")"
else:
cmd = "$(location @flatbuffers//:flatc) --binary -o $(@D) $(location " + schema + ") $(location " + json + ") && mv $(@D)/" + flatc_output + " $@"

native.genrule(
name = name,
srcs = [json, schema],
outs = [output_path],
tools = ["@flatbuffers//:flatc"],
cmd = cmd,
visibility = visibility,
)
124 changes: 124 additions & 0 deletions bazel/tools/json_to_flatbuffer_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""
Convert JSON with kebab-case keys to FlatBuffer-compatible JSON with snake_case keys.

This script recursively processes JSON objects and converts all kebab-case keys
(e.g., "asil-level") to snake_case (e.g., "asil_level") to match FlatBuffer field
naming conventions.
"""

import json
import sys
import re
from pathlib import Path
from typing import Any


def to_snake_case(text: str) -> str:
"""
Convert kebab-case or camelCase string to snake_case

Args:
text: String in kebab-case or camelCase format

Returns:
String in snake_case format
"""
result = text.replace('-', '_')

# Preserve consecutive uppercase letters (like ID, QM, B)
# Insert underscore before uppercase letter that follows lowercase letter
result = re.sub(r'([a-z])([A-Z])', r'\1_\2', result)
# Insert underscore before uppercase letter that is followed by lowercase
result = re.sub(r'([A-Z]+)([A-Z][a-z])', r'\1_\2', result)

return result.lower()


def convert_json(obj: Any) -> Any:
"""
Recursively converts all dictionary keys to snake_case and applies custom conversion
for specific string values which are implemented as enum in FlatBuffers.

Args:
obj: JSON object (dict, list, or primitive type)

Returns:
Converted JSON object
"""
if isinstance(obj, dict):
converted = {}
for key, value in obj.items():
new_key = to_snake_case(key)
new_value = convert_enum_value(new_key, value)
converted[new_key] = convert_json(new_value)
return converted
elif isinstance(obj, list):
return [convert_json(item) for item in obj]
else:
return obj


def convert_enum_value(key: str, value: Any) -> Any:
"""
Convert communication JSON enum string values to FlatBuffer friendly format.

Args:
key: The field name
value: The field value

Returns:
Converted value
"""
if not isinstance(value, str):
return value

# Binding type: "SOME/IP" -> "SOME_IP", "SHM" -> "SHM"
if key == 'binding':
return value.replace('/', '_')

# Permission checks: "file-permissions-on-empty" -> "FILE_PERMISSIONS_ON_EMPTY", "strict" -> "STRICT"
if key == 'permission_checks':
return value.replace('-', '_').upper()

return value


def main():
"""Main entry point for the script."""
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <input.json> <output.json>", file=sys.stderr)
sys.exit(1)

input_file = Path(sys.argv[1])
output_file = Path(sys.argv[2])

# Validate input file exists
if not input_file.exists():
print(f"Error: Input file '{input_file}' does not exist", file=sys.stderr)
sys.exit(1)

try:
with open(input_file, 'r', encoding='utf-8') as f:
data = json.load(f)

converted_data = convert_json(data)

with open(output_file, 'w', encoding='utf-8') as f:
json.dump(converted_data, f, indent=4, ensure_ascii=False)
f.write('\n')
print(f"Successfully converted {input_file} -> {output_file}")

except json.JSONDecodeError as e:
print(f"Error: Failed to parse JSON from '{input_file}': {e}", file=sys.stderr)
sys.exit(1)
except IOError as e:
print(f"Error: I/O error: {e}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error: Unexpected error: {e}", file=sys.stderr)
sys.exit(1)


if __name__ == "__main__":
main()
128 changes: 128 additions & 0 deletions bazel/tools/validate_flatbuffer_json.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""
Validate a JSON file against a FlatBuffer schema.

This rule converts existing communication JSON files to a FlatBuffer friendly format.
- Convert '-' to '_' in keys (required)
- Convert keys from camelCase to snake_case (avoids warnings)
"""

def _validate_json_flatbuffer_test_impl(ctx):
"""Implementation of the validation test rule."""

# Create a script that performs the validation
script = """#!/bin/bash
set -euo pipefail

readonly expected_failure={expected_failure}
readonly converter='{converter}'
readonly flatc='{flatc}'
readonly schema='{schema}'
readonly json='{json}'
readonly tmpdir=$(mktemp -d)

cleanup() {{
rm -rf "$tmpdir"
}}
trap cleanup EXIT

# Step 1: Convert JSON to FlatBuffer friendly format
"$converter" "$json" "$tmpdir/converted.json"

# Step 2: Validate with flatc by compiling to binary (validates structure)
# Capture both stdout and stderr, suppress output unless there's an error
set +e
output=$("$flatc" --binary -o "$tmpdir" "$schema" "$tmpdir/converted.json" 2>&1)
ret=$?
set -e

if test "$expected_failure" = true && test "$ret" -ne 0; then
echo "Expected validation to fail, and it did (exit code $ret)."
echo ""
echo "FlatBuffer validation errors:"
echo "$output"
echo ""
echo "Test PASSED."
exit 0
elif test "$expected_failure" = false && test "$ret" -eq 0; then
echo "Expected validation to succeed, and it did. Test PASSED."
exit 0
fi

# Test failed - show what went wrong
if test "$ret" -ne 0; then
echo "FlatBuffer validation errors:"
echo "$output"
fi

echo ""
echo "Test FAILED: FlatBuffer validation of '$json' against '$schema' exited with code $ret, but expected_failure={expected_failure}"
exit 1
""".format(
expected_failure = "true" if ctx.attr.expected_failure else "false",
converter = ctx.executable._converter.short_path,
flatc = ctx.executable._flatc.short_path,
schema = ctx.file.schema.short_path,
json = ctx.file.json.short_path,
)

ctx.actions.write(
output = ctx.outputs.executable,
content = script,
is_executable = True,
)

runfiles = ctx.runfiles(
files = [ctx.file.json, ctx.file.schema],
).merge(ctx.attr._converter[DefaultInfo].default_runfiles)
runfiles = runfiles.merge(ctx.attr._flatc[DefaultInfo].default_runfiles)

return [DefaultInfo(runfiles = runfiles)]

validate_json_flatbuffer_test = rule(
implementation = _validate_json_flatbuffer_test_impl,
attrs = {
"json": attr.label(
allow_single_file = [".json"],
mandatory = True,
doc = "Input JSON file to validate",
),
"schema": attr.label(
allow_single_file = [".fbs"],
mandatory = True,
doc = "FlatBuffer schema file (.fbs) to validate against",
),
"expected_failure": attr.bool(
default = False,
doc = "If True, test passes when validation fails (for testing invalid inputs)",
),
"_converter": attr.label(
default = Label("//bazel/tools:json_to_flatbuffer_json"),
executable = True,
cfg = "exec",
),
"_flatc": attr.label(
default = Label("@flatbuffers//:flatc"),
executable = True,
cfg = "exec",
),
},
test = True,
doc = """
Validates a JSON file against a FlatBuffer schema.
This rule converts existing communication JSON files to a FlatBuffer friendly format.

Example:
validate_json_flatbuffer_test(
name = "valid_config_test",
json = "valid_config.json",
schema = "ara_com_config.fbs",
)

validate_json_flatbuffer_test(
name = "invalid_config_test",
json = "invalid_config.json",
schema = "ara_com_config.fbs",
expected_failure = True,
)
""",
)
Loading