Skip to content

feat(protobuf): generate nested enum for string fields with allowed values - #502

Merged
erikbosch merged 5 commits into
COVESA:masterfrom
aki1770-del:feat/protobuf-enum-for-allowed-string-493
Apr 30, 2026
Merged

feat(protobuf): generate nested enum for string fields with allowed values#502
erikbosch merged 5 commits into
COVESA:masterfrom
aki1770-del:feat/protobuf-enum-for-allowed-string-493

Conversation

@aki1770-del

Copy link
Copy Markdown
Contributor

Closes #493

Problem

When a VSS signal has datatype: string and an allowed attribute, the protobuf exporter emits a plain string field. This silently accepts any value at the protobuf layer — the constraint declared in the VSS spec is invisible to protobuf consumers.

# Before — allowed values ['DRY', 'WET', 'SNOW', 'ICE'] are lost
string Surface = 2;

Fix

print_messages() now uses a two-pass approach in src/vss_tools/exporters/protobuf.py:

  • Pass 1: for each string + allowed field, emit a nested enum <FieldName>Enum with allowed values assigned indices starting at 0 (satisfying the proto3 zero-value requirement).
  • Pass 2: emit the field using the enum type instead of string.

repeated string[] fields with allowed become repeated <EnumType>. String fields without allowed and all non-string fields are unchanged.

# After
message A {
  enum SurfaceEnum {
    DRY = 0;
    WET = 1;
    SNOW = 2;
    ICE = 3;
  }
  repeated SurfaceEnum Surface = 2;
}

Note on proto3 zero-value semantics: proto3 uses the first enum entry as the default for unset fields. This implementation assigns index 0 to the first entry in the VSS allowed list. For signals that follow the convention of listing UNKNOWN first (e.g., Vehicle.Exterior.RoadSurfaceCondition), this aligns naturally. For signals without a natural "unset" first value, the first allowed entry becomes the proto default. Happy to discuss if a different index assignment strategy is preferred.

Changes

File Change
src/vss_tools/exporters/protobuf.py +33 lines — _enum_type_name(), _write_nested_enum(), two-pass logic in print_messages()
tests/vspec/test_protobuf_enum_allowed/ New test: scalar, repeated, and unchanged-float cases
tests/vspec/test_protobuf_comments/expected_*.proto Updated expected output to reflect enum generation for StringWithAllowed fixture

Tests

$ python3 -m pytest tests/ -k "proto" -v
13 passed in 5.36s

All 13 protobuf-related tests pass, including the two existing test_protobuf_comments parametrized cases (no-comments / with-comments) which already contained a string + allowed field in their fixture.

AI-assisted — authored with Claude, reviewed by Komada.

Comment thread src/vss_tools/exporters/protobuf.py Outdated
nodes: tuple[VSSNode], fd: TextIOWrapper, static_uid: bool, add_optional: bool, include_comments: bool
):
# Pass 1: write nested enum definitions for every string field with allowed values.
for node in nodes:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why do we need to iterate twice?

Comment thread src/vss_tools/exporters/protobuf.py Outdated
for node in nodes:
if isinstance(node.data, VSSDataDatatype):
base = node.data.datatype.strip("[]")
if base == "string" and node.data.allowed:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

"string" literal will probably never change. Still use the the one from datatypes.Datatypes

…alues

When a VSS signal has `datatype: string` and an `allowed` attribute,
the protobuf exporter previously emitted a plain `string` field,
allowing any value without enforcement. This silently accepted values
outside the declared allowed set at the protobuf layer.

Fix: `print_messages()` now uses a two-pass approach:
- Pass 1: emit a nested `enum <FieldName>Enum` for each string+allowed
  field, with allowed values assigned indices starting at 0 (satisfying
  the proto3 zero-value requirement).
- Pass 2: emit fields using the enum type instead of `string`.

`repeated string[]` fields with `allowed` become `repeated <EnumType>`.
Non-string fields and string fields without `allowed` are unchanged.

Closes COVESA#493

Co-Authored-By: Claude and aki1770-del <aki1770@gmail.com>
Signed-off-by: Akihiko Komada <aki1770@gmail.com>
Co-Authored-By: Claude and aki1770-del <aki1770@gmail.com>
Signed-off-by: Akihiko Komada <aki1770@gmail.com>
ruff-format reformatted the multi-line assert to put the condition
arguments on their own line instead of wrapping in parentheses.
Keeps pre-commit CI green.

Co-Authored-By: Claude and aki1770-del <aki1770@gmail.com>
Signed-off-by: Akihiko Komada <aki1770@gmail.com>
- Collapse two-pass loop into one: _write_nested_enum is called
  immediately before the field declaration, removing the redundant
  pre-iteration.
- Replace "string" literal with Datatypes.STRING[0] from datatypes module.
- Update expected.proto: enum definitions now appear inline before their
  respective fields rather than grouped at the top of the message.

Co-Authored-By: Claude and aki1770-del <aki1770@gmail.com>
Signed-off-by: Akihiko Komada <aki1770@gmail.com>
Move `from vss_tools.datatypes import Datatypes` before
`from vss_tools.main import get_trees` — alphabetical within
the vss_tools.* group as required by ruff/isort.

Co-Authored-By: Claude and aki1770-del <aki1770@gmail.com>
Signed-off-by: Akihiko Komada <aki1770@gmail.com>
@aki1770-del
aki1770-del force-pushed the feat/protobuf-enum-for-allowed-string-493 branch from 3e69703 to c151e10 Compare April 11, 2026 10:02
Comment thread src/vss_tools/exporters/protobuf.py
@aki1770-del

Copy link
Copy Markdown
Contributor Author

Thanks @erikbosch — sharp catch on the proto3 zero-default ↔ VSS-default alignment. You're right that ordering allowed with default first would make the proto-implicit-default carry semantic meaning instead of being "whatever happened to be first in the list."

To respect the scope of this PR (which you and @sschleemilch have already reviewed), I've filed it as a follow-up: #505 — happy to take it as a separate PR if the direction is confirmed there.

AI-assisted — authored with Claude, reviewed by Komada.

@erikbosch

Copy link
Copy Markdown
Collaborator

MoM:

  • Please review

@erikbosch

Copy link
Copy Markdown
Collaborator

MoM:

erikbosch pushed a commit that referenced this pull request Apr 30, 2026
…lowed[0] (#512)

* feat(strict): add DEFAULT_MATCHES_FIRST_ALLOWED check for default==allowed[0]

Closes #507.

Follow-up from #502 (proto3 nested-enum generation for string fields with
allowed values). Proto3 uses allowed[0] as the implicit wire-unset default,
so VSS default must match allowed[0] for the intent to survive
serialization. Canonical VSS pattern per erikbosch + sschleemilch
(see #507 comments): allowed: ['UNKNOWN', ...] + default: 'UNKNOWN'.

Implementation mirrors existing NAME_STYLE / UNKNOWN_ATTRIBUTE strict
checks:
  - StrictOption.DEFAULT_MATCHES_FIRST_ALLOWED enum member
  - StrictExceptions.defaults exemption set
  - load_strict_exceptions dispatch (+ null-options fallback)
  - VSSNode.get_default_first_allowed_violations tree walk
  - check_default_first_allowed_violations strict gate in main.py
  - DefaultFirstAllowedException signal class
  - aborts CLI choice extended

Gated behind --strict or --aborts default-matches-first-allowed; exempt
per-FQN via the existing --strict-exceptions file (new option value or
null for all-exceptions). No behavior change on default run.

Tests: 5 new (1 strict.py enum + dispatch, 4 tree.py violation collector
covering clean/mismatch/no-allowed/no-default), plus existing strict.py
tests extended with .defaults-set assertions for consistency. 132/132
pass on tests/ (pre-existing test_binary failure unrelated).

Chain A: #502 -> #507.

Signed-off-by: Akihiko Komada <aki1770@gmail.com>

* fix(strict): skip array datatypes in default-matches-first-allowed + ruff-format

Array datatypes (`string[]` etc.) don't have a proto3 nested-enum analog
for the default-equals-allowed[0] rule — per-element allowed-membership
is already enforced at the pydantic VSSDataDatatype layer. Previous
implementation incorrectly flagged
`tests/vspec/test_datatypes_pattern/test_pattern_ok.vspec` (A.Colors:
string[] with allowed=[white,...,blue] and default=[white,green,red])
as a violation because default (a list) != allowed[0] (a scalar).

Adds early-continue on `node.data.datatype.endswith("[]")` and a
regression test covering the exact fixture shape.

Also applies ruff-format on main.py and cli_options.py to satisfy the
pre-commit ruff-format hook (whitespace-only changes).

Signed-off-by: Akihiko Komada <aki1770@gmail.com>

* fix(strict): match upstream ruff 0.5.7 import style

Signed-off-by: Akihiko Komada <aki1770@gmail.com>

---------

Signed-off-by: Akihiko Komada <aki1770@gmail.com>

@erikbosch erikbosch left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@erikbosch
erikbosch merged commit 1f44f17 into COVESA:master Apr 30, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generate protobuf enums for datatype string with allowed option

3 participants