Skip to content

Commit 109f9a2

Browse files
committed
feat: Implement instance dimension enum sanitization and metadata annotation
Signed-off-by: JD Alvarez <8550265+jdacoello@users.noreply.github.com>
1 parent 5af0ba5 commit 109f9a2

File tree

4 files changed

+131
-1
lines changed

4 files changed

+131
-1
lines changed

src/vss_tools/exporters/s2dm/type_builders.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ def create_instance_types(
110110
Mapping of type names to GraphQL enum or object types
111111
"""
112112
types: dict[str, GraphQLEnumType | GraphQLObjectType] = {}
113+
vspec_comments.setdefault("instance_dimension_enums", {})
114+
113115
for fqn, row in branches_df[branches_df["instances"].notna()].iterrows():
114116
if instances := row.get("instances"):
115117
base_name = convert_name_for_graphql_schema(fqn, GraphQLElementType.TYPE, S2DM_CONVERSIONS)
@@ -119,11 +121,28 @@ def create_instance_types(
119121
fields = {}
120122
for i, values in enumerate(dimensions, 1):
121123
enum_name = f"{tag_name}_Dimension{i}"
124+
125+
# Sanitize enum values and track modifications
126+
enum_values = {}
127+
modified_values = {}
128+
129+
for v in values:
130+
sanitized, was_modified = _sanitize_enum_value_for_graphql(str(v))
131+
enum_values[sanitized] = GraphQLEnumValue(v)
132+
133+
if was_modified:
134+
modified_values[sanitized] = str(v)
135+
122136
types[enum_name] = GraphQLEnumType(
123137
enum_name,
124-
{v: GraphQLEnumValue(v) for v in values},
138+
enum_values,
125139
description=f"Dimensional enum for VSS instance dimension {i}.",
126140
)
141+
142+
# Store metadata for directive processor
143+
if modified_values:
144+
vspec_comments["instance_dimension_enums"][enum_name] = {"modified_values": modified_values}
145+
127146
fields[f"dimension{i}"] = GraphQLField(types[enum_name])
128147

129148
types[tag_name] = GraphQLObjectType(tag_name, fields)

src/vss_tools/utils/graphql_directive_processor.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ def process_schema(
5454

5555
lines = self._process_unit_enum_directives(lines, unit_enums_metadata, processed_enum_values)
5656
lines = self._process_allowed_enum_directives(lines, allowed_enums_metadata, processed_enum_values)
57+
lines = self._process_instance_dimension_enum_directives(
58+
lines, vspec_comments.get("instance_dimension_enums", {}), processed_enum_values
59+
)
5760
lines = self._process_field_directives(lines, vspec_comments)
5861
lines = self._process_deprecated_directives(lines, vspec_comments.get("field_deprecated", {}))
5962
lines = self._process_range_directives(lines, vspec_comments.get("field_ranges", {}))
@@ -167,6 +170,51 @@ def _process_allowed_enum_directives(
167170

168171
return lines
169172

173+
def _process_instance_dimension_enum_directives(
174+
self, lines: list[str], instance_dimension_enums: dict, processed_values: set
175+
) -> list[str]:
176+
"""
177+
Process instance dimension enum directives.
178+
179+
Annotates enum values that were modified during sanitization with their original names.
180+
"""
181+
for enum_name, enum_data in instance_dimension_enums.items():
182+
modified_values = enum_data.get("modified_values", {})
183+
if not modified_values:
184+
continue
185+
186+
in_target_enum = False
187+
188+
for i, line in enumerate(lines):
189+
# Detect enum start
190+
if line.strip().startswith(f"enum {enum_name}"):
191+
in_target_enum = True
192+
continue
193+
elif line.strip().startswith("enum ") and in_target_enum:
194+
in_target_enum = False
195+
continue
196+
elif line.strip() == "}" and in_target_enum:
197+
in_target_enum = False
198+
continue
199+
200+
# Process enum values within target enum
201+
if in_target_enum and line.strip() and not line.strip().startswith('"'):
202+
stripped_line = line.strip()
203+
204+
for enum_value_name, original_value in modified_values.items():
205+
enum_value_key = f"{enum_name}.{enum_value_name}"
206+
207+
if stripped_line.startswith(enum_value_name) and enum_value_key not in processed_values:
208+
if "@vspec" not in line:
209+
indent = line[: len(line) - len(line.lstrip())]
210+
directive = f'@vspec(metadata: [{{key: "originalName", value: "{original_value}"}}])'
211+
lines[i] = f"{indent}{enum_value_name} {directive}"
212+
213+
processed_values.add(enum_value_key)
214+
break
215+
216+
return lines
217+
170218
def _process_field_directives(self, lines: list[str], vspec_comments: dict) -> list[str]:
171219
"""Process consolidated field @vspec directives (element + fqn + optional metadata)."""
172220
# Process VSS type information (element + fqn + metadata)

tests/test_s2dm_exporter.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,3 +667,38 @@ def test_camelcase_enums_schema_generation(self):
667667
assert 'IO_ERROR @vspec(metadata: [{key: "originalName", value: "IOError"}])' in schema_str
668668
assert 'XML_PARSER @vspec(metadata: [{key: "originalName", value: "XMLParser"}])' in schema_str
669669
assert 'SOME_API_KEY @vspec(metadata: [{key: "originalName", value: "someAPIKey"}])' in schema_str
670+
671+
def test_instance_dimension_enum_sanitization(self):
672+
"""Test that instance dimension enum values are properly sanitized and annotated."""
673+
# Load the test vspec with instances that need sanitization
674+
tree, _ = get_trees(
675+
vspec=Path("tests/vspec/test_s2dm/test_instance_sanitization.vspec"),
676+
include_dirs=(),
677+
aborts=(),
678+
strict=False,
679+
extended_attributes=(),
680+
quantities=(Path("tests/vspec/test_s2dm/test_quantities.yaml"),),
681+
units=(Path("tests/vspec/test_s2dm/test_units.yaml"),),
682+
overlays=(),
683+
expand=False,
684+
)
685+
686+
schema, unit_metadata, allowed_metadata, vspec_comments = generate_s2dm_schema(tree)
687+
schema_str = print_schema_with_vspec_directives(schema, unit_metadata, allowed_metadata, vspec_comments)
688+
689+
# Check that Row instance enum is created and values are sanitized
690+
assert "enum Vehicle_Cabin_InstanceTag_Dimension1" in schema_str
691+
assert 'ROW1 @vspec(metadata: [{key: "originalName", value: "Row1"}])' in schema_str
692+
assert 'ROW2 @vspec(metadata: [{key: "originalName", value: "Row2"}])' in schema_str
693+
694+
# Check that DriverSide/PassengerSide instance enum is created and values are sanitized
695+
assert "enum Vehicle_Cabin_Seat_InstanceTag_Dimension1" in schema_str
696+
assert 'DRIVER_SIDE @vspec(metadata: [{key: "originalName", value: "DriverSide"}])' in schema_str
697+
assert 'PASSENGER_SIDE @vspec(metadata: [{key: "originalName", value: "PassengerSide"}])' in schema_str
698+
699+
# Check that FrontLeft/FrontRight/RearLeft/RearRight instance enum is created and values are sanitized
700+
assert "enum Vehicle_Cabin_Seat_Position_InstanceTag_Dimension1" in schema_str
701+
assert 'FRONT_LEFT @vspec(metadata: [{key: "originalName", value: "FrontLeft"}])' in schema_str
702+
assert 'FRONT_RIGHT @vspec(metadata: [{key: "originalName", value: "FrontRight"}])' in schema_str
703+
assert 'REAR_LEFT @vspec(metadata: [{key: "originalName", value: "RearLeft"}])' in schema_str
704+
assert 'REAR_RIGHT @vspec(metadata: [{key: "originalName", value: "RearRight"}])' in schema_str
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Test vspec for instance dimension enum sanitization
2+
3+
Vehicle:
4+
type: branch
5+
description: High-level vehicle data.
6+
7+
Vehicle.Cabin:
8+
type: branch
9+
description: Cabin information.
10+
instances:
11+
- Row[1,2]
12+
13+
Vehicle.Cabin.Seat:
14+
type: branch
15+
description: Seat information.
16+
instances:
17+
- ["DriverSide", "PassengerSide"]
18+
19+
Vehicle.Cabin.Seat.Position:
20+
type: branch
21+
description: Seat position.
22+
instances:
23+
- ["FrontLeft", "FrontRight", "RearLeft", "RearRight"]
24+
25+
Vehicle.Cabin.Seat.Position.IsOccupied:
26+
datatype: boolean
27+
type: actuator
28+
description: Is the seat position occupied.

0 commit comments

Comments
 (0)