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
26 changes: 26 additions & 0 deletions docs/protobuf.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ This example assumes that you checked out the COVESA VSS repository next to the
```bash
--static-uid Expect staticUID attribute in the vspec input and use it as field number.
--add-optional Set each field to optional
--include-comments Include descriptions and metadata as comments in the generated proto files
```

## Field Numbers and Backwards Compatibility
Expand Down Expand Up @@ -95,3 +96,28 @@ For the incrementally assigned field numbers, we only need a few bits per number
In proto3, one can mark a field as `optional` to change the behavior of how to deal with values that are not present during the encoding. By default, the fields are not optional, and you can use the flag `--add-optional` to make all fields optional.

See the [Protocol Buffers Language Guide](https://protobuf.dev/programming-guides/proto3/#field-labels) for the implications of using `optional`.

## Include comments

By default, the generated proto files do not contain any comments. Use the `--include-comments` flag to include VSS descriptions and metadata as protobuf comments above each message and field.

The comment block includes the node's description followed by any additional metadata such as allowed values, min/max, default, unit, deprecation, and comment.

For example, given a vspec entry:

```yaml
Media.Played.Source:
datatype: string
type: actuator
allowed: ['UNKNOWN', 'SIRIUS_XM', 'AM', 'FM', 'DAB', 'TV', 'CD', 'DVD', 'AUX', 'USB', 'DISK', 'BLUETOOTH', 'INTERNET', 'VOICE', 'BEEP']
Copy link
Collaborator

Choose a reason for hiding this comment

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

Wouldn't the translation of allowed be using a protobuf enum?

Copy link
Author

Choose a reason for hiding this comment

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

This PR and these docs represents the current state of the protobuf generator.

However I agree, that would probably be the logical next step to use protobuf enums for this.

I've created an Issue for this: #493

description: Media selected for playback
```

The generated proto output with `--include-comments` would be:

```proto
// Media selected for playback
//
// Allowed: ['UNKNOWN', 'SIRIUS_XM', 'AM', 'FM', 'DAB', 'TV', 'CD', 'DVD', 'AUX', 'USB', 'DISK', 'BLUETOOTH', 'INTERNET', 'VOICE', 'BEEP']
string Source = 1;
```
63 changes: 56 additions & 7 deletions src/vss_tools/exporters/protobuf.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from vss_tools import log
from vss_tools.main import get_trees
from vss_tools.model import (
VSSData,
VSSDataBranch,
VSSDataDatatype,
VSSDataStruct,
Expand All @@ -45,7 +46,7 @@ def init_package_file(path: Path, package_name: str):
f.write(f"package {package_name};\n\n")


def traverse_data_type_tree(tree: VSSNode, static_uid: bool, add_optional: bool, out_dir: Path):
def traverse_data_type_tree(tree: VSSNode, static_uid: bool, add_optional: bool, include_comments: bool, out_dir: Path):
"""
All structs in a branch are written to a single .proto file.
The file's base name is same as the branch's name
Expand Down Expand Up @@ -86,13 +87,21 @@ def traverse_data_type_tree(tree: VSSNode, static_uid: bool, add_optional: bool,

write_imports(fd, imports)

if include_comments:
write_comment(fd, node, indent="")
fd.write(f"message {struct_path.name} {{" + "\n")
print_messages(node.children, fd, static_uid, add_optional)
print_messages(node.children, fd, static_uid, add_optional, include_comments)
fd.write("}\n\n")
log.info(f"Wrote {struct_path.name} to {out_file}")


def traverse_signal_tree(tree: VSSNode, fd: TextIOWrapper, static_uid: bool, add_optional: bool):
def traverse_signal_tree(
tree: VSSNode,
fd: TextIOWrapper,
static_uid: bool,
add_optional: bool,
include_comments: bool,
):
fd.write('syntax = "proto3";\n\n')

imports = []
Expand All @@ -106,8 +115,10 @@ def traverse_signal_tree(tree: VSSNode, fd: TextIOWrapper, static_uid: bool, add

# write proto messages to file
for node in findall(tree, filter_=lambda node: isinstance(node.data, VSSDataBranch)):
if include_comments:
write_comment(fd, node, indent="")
fd.write(f"message {node.get_fqn('')} {{" + "\n")
print_messages(node.children, fd, static_uid, add_optional)
print_messages(node.children, fd, static_uid, add_optional, include_comments)
fd.write("}\n\n")


Expand All @@ -119,7 +130,41 @@ def write_imports(fd: TextIOWrapper, imports: list[str]):
fd.write("\n")


def print_messages(nodes: tuple[VSSNode], fd: TextIOWrapper, static_uid: bool, add_optional: bool):
def write_comment(fd: TextIOWrapper, node: VSSNode, indent: str = " "):
"""Write a protobuf comment block with description and metadata."""
if not isinstance(node.data, VSSData):
return
lines: list[str] = []
if node.data.description:
lines.append(node.data.description)
if isinstance(node.data, VSSDataDatatype):
if node.data.allowed is not None:
lines.append(f"Allowed: {node.data.allowed}")
if node.data.min is not None:
lines.append(f"Min: {node.data.min}")
if node.data.max is not None:
lines.append(f"Max: {node.data.max}")
if node.data.default is not None:
lines.append(f"Default: {node.data.default}")
if node.data.unit is not None:
lines.append(f"Unit: {node.data.unit}")
if node.data.deprecation is not None:
lines.append(f"Deprecation: {node.data.deprecation}")
if node.data.comment is not None:
lines.append(f"Comment: {node.data.comment}")
if not lines:
return
fd.write(f"{indent}// {lines[0]}\n")
if len(lines) > 1:
if node.data.description:
fd.write(f"{indent}//\n")
for line in lines[1:]:
fd.write(f"{indent}// {line}\n")


def print_messages(
nodes: tuple[VSSNode], fd: TextIOWrapper, static_uid: bool, add_optional: bool, include_comments: bool
):
usedKeys: dict[int, str] = {}
for i, node in enumerate(nodes, 1):
if isinstance(node.data, VSSDataDatatype):
Expand Down Expand Up @@ -161,6 +206,8 @@ def print_messages(nodes: tuple[VSSNode], fd: TextIOWrapper, static_uid: bool, a
usedKeys[fieldNumber] = node.get_fqn()
else:
fieldNumber = i
if include_comments:
write_comment(fd, node)
fd.write(f" {data_type} {node.name} = {fieldNumber};" + "\n")


Expand All @@ -187,6 +234,7 @@ def print_messages(nodes: tuple[VSSNode], fd: TextIOWrapper, static_uid: bool, a
help="Expect staticUID attribute in the vspec input and use it as field number",
)
@click.option("--add-optional", is_flag=True, help="Set each field to 'optional'")
@click.option("--include-comments", is_flag=True, help="Include descriptions and metadata as comments")
def cli(
vspec: Path,
output: Path,
Expand All @@ -201,6 +249,7 @@ def cli(
types_out_dir: Path | None,
static_uid: bool,
add_optional: bool,
include_comments: bool,
strict_exceptions: Path | None,
):
"""
Expand All @@ -223,8 +272,8 @@ def cli(
if not types_out_dir:
types_out_dir = Path.cwd()
log.warning(f"No output directory given. Writing to: {types_out_dir.absolute()}")
traverse_data_type_tree(datatype_tree, static_uid, add_optional, types_out_dir)
traverse_data_type_tree(datatype_tree, static_uid, add_optional, include_comments, types_out_dir)

with open(output, "w") as f:
log.info(f"Writing to: {output}")
traverse_signal_tree(tree, f, static_uid, add_optional)
traverse_signal_tree(tree, f, static_uid, add_optional, include_comments)
11 changes: 11 additions & 0 deletions tests/vspec/test_protobuf_comments/expected_no_comments.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
syntax = "proto3";


message A {
string StringWithAllowed = 1;
uint32 UInt8WithMinMax = 2;
string DeprecatedString = 3;
float DescOnly = 4;
uint32 NoMetadata = 5;
}

27 changes: 27 additions & 0 deletions tests/vspec/test_protobuf_comments/expected_with_comments.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
syntax = "proto3";


// A is a test branch
message A {
// A string with allowed values
//
// Allowed: ['VALUE_A', 'VALUE_B', 'VALUE_C']
string StringWithAllowed = 1;
// A uint8 with min and max
//
// Min: 0
// Max: 100
// Unit: percent
uint32 UInt8WithMinMax = 2;
// A deprecated node
//
// Deprecation: Use A.StringWithAllowed instead
string DeprecatedString = 3;
// Only a description
//
// Unit: mm
float DescOnly = 4;
// A node with no extra metadata
uint32 NoMetadata = 5;
}

34 changes: 34 additions & 0 deletions tests/vspec/test_protobuf_comments/test.vspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
A:
type: branch
description: A is a test branch

A.StringWithAllowed:
datatype: string
type: actuator
allowed: ['VALUE_A', 'VALUE_B', 'VALUE_C']
description: A string with allowed values

A.UInt8WithMinMax:
datatype: uint8
type: sensor
unit: percent
min: 0
max: 100
description: A uint8 with min and max

A.DeprecatedString:
datatype: string
type: sensor
description: A deprecated node
deprecation: Use A.StringWithAllowed instead

A.DescOnly:
datatype: float
type: sensor
unit: mm
description: Only a description

A.NoMetadata:
datatype: uint32
type: sensor
description: A node with no extra metadata
42 changes: 42 additions & 0 deletions tests/vspec/test_protobuf_comments/test_protobuf_comments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Copyright (c) 2026 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

import filecmp
import subprocess
from pathlib import Path

import pytest

HERE = Path(__file__).resolve().parent
TEST_UNITS = HERE / ".." / "test_units.yaml"
TEST_QUANT = HERE / ".." / "test_quantities.yaml"


@pytest.mark.parametrize(
"include_comments, expected_file",
[
(False, "expected_no_comments.proto"),
(True, "expected_with_comments.proto"),
],
)
def test_protobuf_comments(include_comments, expected_file, tmp_path):
"""
Test that --include-comments adds description and metadata
as comments to the protobuf output, and that without the flag
no comments are present.
"""
vspec = HERE / "test.vspec"
output = tmp_path / "out.proto"
cmd = f"vspec export protobuf -u {TEST_UNITS} -q {TEST_QUANT} --vspec {vspec} --output {output}"
if include_comments:
cmd += " --include-comments"
subprocess.run(cmd.split(), check=True)
expected = HERE / expected_file
assert filecmp.cmp(output, expected), (
f"Output differs from expected.\n" f"Got:\n{output.read_text()}\n" f"Expected:\n{expected.read_text()}"
)
Loading