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: 25 additions & 1 deletion src/vss_tools/exporters/protobuf.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import vss_tools.cli_options as clo
from vss_tools import log
from vss_tools.datatypes import Datatypes
from vss_tools.main import get_trees
from vss_tools.model import (
VSSData,
Expand Down Expand Up @@ -162,14 +163,37 @@ def write_comment(fd: TextIOWrapper, node: VSSNode, indent: str = " "):
fd.write(f"{indent}// {line}\n")


def _enum_type_name(field_name: str) -> str:
"""Return the nested enum type name for a string field with allowed values."""
return f"{field_name}Enum"


def _write_nested_enum(fd: TextIOWrapper, field_name: str, allowed: list, indent: str = " ") -> None:
"""Write a nested proto3 enum for a string field constrained to allowed values.

Proto3 requires the first enum value to equal 0. We assign values in the
order they appear in the VSS `allowed` list, starting from 0.
"""
enum_name = _enum_type_name(field_name)
fd.write(f"{indent}enum {enum_name} {{\n")
for idx, val in enumerate(allowed):
fd.write(f"{indent} {val} = {idx};\n")
fd.write(f"{indent}}}\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):
dt_val = node.data.datatype
data_type = mapped.get(dt_val.strip("[]"), dt_val.strip("[]"))
base = dt_val.strip("[]")
if base == Datatypes.STRING[0] and node.data.allowed:
_write_nested_enum(fd, node.name, node.data.allowed)
data_type = _enum_type_name(node.name)
else:
data_type = mapped.get(base, base)
if dt_val.endswith("[]"):
data_type = "repeated " + data_type
elif add_optional:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ syntax = "proto3";


message A {
string StringWithAllowed = 1;
enum StringWithAllowedEnum {
VALUE_A = 0;
VALUE_B = 1;
VALUE_C = 2;
}
StringWithAllowedEnum StringWithAllowed = 1;
uint32 UInt8WithMinMax = 2;
string DeprecatedString = 3;
float DescOnly = 4;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ syntax = "proto3";

// A is a test branch
message A {
enum StringWithAllowedEnum {
VALUE_A = 0;
VALUE_B = 1;
VALUE_C = 2;
}
// A string with allowed values
//
// Allowed: ['VALUE_A', 'VALUE_B', 'VALUE_C']
string StringWithAllowed = 1;
StringWithAllowedEnum StringWithAllowed = 1;
// A uint8 with min and max
//
// Min: 0
Expand Down
21 changes: 21 additions & 0 deletions tests/vspec/test_protobuf_enum_allowed/expected.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
syntax = "proto3";


message A {
enum SourceEnum {
UNKNOWN = 0;
AM = 1;
FM = 2;
BLUETOOTH = 3;
}
SourceEnum Source = 1;
enum SurfaceEnum {
DRY = 0;
WET = 1;
SNOW = 2;
ICE = 3;
}
repeated SurfaceEnum Surface = 2;
float Speed = 3;
}

20 changes: 20 additions & 0 deletions tests/vspec/test_protobuf_enum_allowed/test.vspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
A:
type: branch
description: Test branch for enum allowed values

A.Source:
datatype: string
type: actuator
allowed: ['UNKNOWN', 'AM', 'FM', 'BLUETOOTH']
description: Media source with allowed values

A.Surface:
datatype: string[]
type: sensor
allowed: ['DRY', 'WET', 'SNOW', 'ICE']
description: Road surface condition list (array with allowed values)

A.Speed:
datatype: float
type: sensor
description: Speed without allowed values — must remain float
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

"""
Test that the protobuf exporter generates nested enum types for
`datatype: string` signals that have an `allowed` attribute, instead
of emitting a plain `string` field.

Related issue: https://github.com/COVESA/vss-tools/issues/493
"""

import filecmp
import subprocess
from pathlib import Path

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


def test_protobuf_enum_for_allowed_string(tmp_path):
"""
A `string` field with `allowed` values must produce a nested enum and
use the enum type as the field type, not plain `string`.

A `string[]` field with `allowed` must produce `repeated <EnumType>`.

A numeric field without `allowed` must remain unchanged (regression check).
"""
vspec = HERE / "test.vspec"
output = tmp_path / "out.proto"
cmd = f"vspec export protobuf -u {TEST_UNITS} -q {TEST_QUANT} --vspec {vspec} --output {output}"
subprocess.run(cmd.split(), check=True)
expected = HERE / "expected.proto"
assert filecmp.cmp(
output, expected
), f"Output differs from expected.\nGot:\n{output.read_text()}\nExpected:\n{expected.read_text()}"
Loading