Skip to content

Commit e213bec

Browse files
committed
feat(s2dm): simplify vspec metadata annotation with sidecar lookup spec file
Signed-off-by: JD Alvarez <8550265+jdacoello@users.noreply.github.com>
1 parent f63becd commit e213bec

File tree

4 files changed

+37
-136
lines changed

4 files changed

+37
-136
lines changed

src/vss_tools/exporters/s2dm/graphql_directive_processor.py

Lines changed: 15 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ def _process_unit_enum_directives(
7070
"""
7171
Process unit enum directives.
7272
73-
Annotates enum type with @vspec(element: QUANTITY_KIND, metadata: [{key: "quantity", value: "..."}])
74-
and individual enum values with @vspec(element: UNIT, metadata: [{key: "unit", value: "..."}])
73+
Annotates enum type with @vspec(element: QUANTITY_KIND)
74+
and individual enum values with @vspec(element: UNIT).
7575
"""
7676
for quantity, units_data in unit_enums_metadata.items():
7777
enum_name = f"{convert_name_for_graphql_schema(quantity, GraphQLElementType.TYPE)}UnitEnum"
@@ -80,11 +80,7 @@ def _process_unit_enum_directives(
8080
for i, line in enumerate(lines):
8181
if line.strip().startswith(f"enum {enum_name}"):
8282
if "@vspec" not in line:
83-
# Annotate enum type with QUANTITY_KIND
84-
directive = (
85-
f"@vspec(element: QUANTITY_KIND, " f'metadata: [{{key: "quantity", value: "{quantity}"}}])'
86-
)
87-
lines[i] = line.replace(" {", f" {directive} {{")
83+
lines[i] = line.replace(" {", " @vspec(element: QUANTITY_KIND) {")
8884
in_target_enum = True
8985
continue
9086
elif line.strip().startswith("enum ") and in_target_enum:
@@ -105,9 +101,7 @@ def _process_unit_enum_directives(
105101
if stripped_line.startswith(enum_value_name) and enum_value_key not in processed_values:
106102
if "@vspec" not in line:
107103
indent = line[: len(line) - len(line.lstrip())]
108-
# Annotate enum value with UNIT
109-
directive = f'@vspec(element: UNIT, metadata: [{{key: "unit", value: "{unit_key}"}}])'
110-
lines[i] = f"{indent}{enum_value_name} {directive}"
104+
lines[i] = f"{indent}{enum_value_name} @vspec(element: UNIT)"
111105

112106
processed_values.add(enum_value_key)
113107
break
@@ -119,29 +113,19 @@ def _process_allowed_enum_directives(
119113
"""
120114
Process allowed value enum directives.
121115
122-
Annotates the enum type itself with @vspec(element, fqn, metadata),
123-
and annotates individual enum values that were modified with @vspec(metadata).
116+
Annotates the enum type itself with @vspec(element, fqn).
117+
Annotates individual enum values that were sanitized with @vspec(metadata: originalName).
124118
"""
125119
for enum_name, enum_data in allowed_enums_metadata.items():
126120
fqn = enum_data.get("fqn", "")
127121
vss_type = enum_data.get("vss_type", "ATTRIBUTE")
128-
allowed_values_dict = enum_data.get("allowed_values", {})
129122
modified_values = enum_data.get("modified_values", {})
130123

131-
# Build the allowed values list for metadata
132-
# GraphQL requires: value: "['val1', 'val2']" (double quotes outside, single quotes inside)
133-
allowed_values_list = list(allowed_values_dict.values())
134-
allowed_str = ", ".join([f"'{v}'" for v in allowed_values_list])
135-
136124
in_target_enum = False
137125
for i, line in enumerate(lines):
138126
if line.strip().startswith(f"enum {enum_name}"):
139127
if "@vspec" not in line:
140-
# Annotate the enum type
141-
directive = (
142-
f'@vspec(element: {vss_type}, fqn: "{fqn}", '
143-
f'metadata: [{{key: "allowed", value: "[{allowed_str}]"}}])'
144-
)
128+
directive = f'@vspec(element: {vss_type}, fqn: "{fqn}")'
145129
lines[i] = line.replace(" {", f" {directive} {{")
146130
in_target_enum = True
147131
continue
@@ -216,13 +200,12 @@ def _process_instance_dimension_enum_directives(
216200
return lines
217201

218202
def _process_field_directives(self, lines: list[str], vspec_comments: dict) -> list[str]:
219-
"""Process consolidated field @vspec directives (element + fqn + optional metadata)."""
220-
# Process VSS type information (element + fqn + metadata)
203+
"""Process consolidated field @vspec directives (element + fqn + optional instantiate metadata)."""
221204
for field_path, vss_info in vspec_comments.get("field_vss_types", {}).items():
222205
type_name, field_name = field_path.split(".", 1) # Use maxsplit=1 to handle field names with dots
223206
element = vss_info["element"]
224207
fqn = vss_info["fqn"]
225-
instantiate = vss_info.get("instantiate") # Check if this is a hoisted non-instantiated field
208+
instantiate = vss_info.get("instantiate") # Only False for hoisted non-instantiated fields
226209

227210
in_type = False
228211
for i, line in enumerate(lines):
@@ -237,26 +220,12 @@ def _process_field_directives(self, lines: list[str], vspec_comments: dict) -> l
237220
continue
238221

239222
if in_type and line.strip().startswith(f"{field_name}") and "@vspec" not in line:
240-
# Build metadata array from extended attributes
241-
metadata_entries = []
242-
243-
# Add instantiate metadata if present
244223
if instantiate is False:
245-
metadata_entries.append('{key: "instantiate", value: "false"}')
246-
247-
# Add extended attributes metadata
248-
for key, value in vss_info.items():
249-
if key not in ["element", "fqn", "instantiate"]:
250-
# Escape quotes in value
251-
escaped_value = str(value).replace('"', '\\\\"')
252-
metadata_entries.append(f'{{key: "{key}", value: "{escaped_value}"}}')
253-
254-
# Build directive
255-
if metadata_entries:
256-
metadata_str = ", ".join(metadata_entries)
257-
directive = f'@vspec(element: {element}, fqn: "{fqn}", metadata: [{metadata_str}])'
224+
directive = (
225+
f'@vspec(element: {element}, fqn: "{fqn}", '
226+
+ 'metadata: [{key: "instantiate", value: "false"}])'
227+
)
258228
else:
259-
# Standard directive without metadata
260229
directive = f'@vspec(element: {element}, fqn: "{fqn}")'
261230

262231
lines[i] = line.rstrip() + f" {directive}"
@@ -376,36 +345,14 @@ def _process_type_directives(self, lines: list[str], vspec_comments: dict) -> li
376345

377346
# Add @vspec directive
378347
if needs_instance_tag and "@vspec" not in line:
379-
# Instance tag types get special metadata with instances
380348
element = instance_tag_info["element"]
381349
fqn = instance_tag_info["fqn"]
382-
instances = instance_tag_info["instances"]
383-
directive = (
384-
f'@vspec(element: {element}, fqn: "{fqn}", '
385-
f'metadata: [{{key: "instances", value: "{instances}"}}])'
386-
)
387-
new_line += f" {directive}"
350+
new_line += f' @vspec(element: {element}, fqn: "{fqn}")'
388351
elif needs_vss_type and "@vspec" not in line:
389-
# Regular types get element + fqn + extended attributes metadata
390352
vss_info = vspec_comments["vss_types"][type_name]
391353
element = vss_info["element"]
392354
fqn = vss_info["fqn"]
393-
394-
# Build metadata array from extended attributes
395-
metadata_entries = []
396-
for key, value in vss_info.items():
397-
if key not in ["element", "fqn"]:
398-
# Escape quotes in value
399-
escaped_value = str(value).replace('"', '\\\\"')
400-
metadata_entries.append(f'{{key: "{key}", value: "{escaped_value}"}}')
401-
402-
# Build directive
403-
if metadata_entries:
404-
metadata_str = ", ".join(metadata_entries)
405-
directive = f'@vspec(element: {element}, fqn: "{fqn}", metadata: [{metadata_str}])'
406-
else:
407-
directive = f'@vspec(element: {element}, fqn: "{fqn}")'
408-
new_line += f" {directive}"
355+
new_line += f' @vspec(element: {element}, fqn: "{fqn}")'
409356

410357
new_line += " {"
411358
lines[i] = new_line # No extra indentation

src/vss_tools/exporters/s2dm/predefined_elements/directives.graphql

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,6 @@ enum VspecElement {
3535
}
3636

3737

38-
#enum FuelTypeEnum @vspec(element:"ATTRIBUTE", fqn:"Vehicle.Powertrain.FuelType", metadata:[{key:"allowed", value:"[GASOLINE, DIESEL, ELECTRIC, HYBRID]"}]) {
39-
# GASOLINE
40-
# DIESEL
41-
# ELECTRIC
42-
# HYBRID
43-
#}
44-
4538
directive @range(min: Float, max: Float) on FIELD_DEFINITION
4639

4740
directive @instanceTag on OBJECT

src/vss_tools/exporters/s2dm/type_builders.py

Lines changed: 8 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
GraphQLEnumType,
3030
GraphQLEnumValue,
3131
GraphQLField,
32-
GraphQLID,
3332
GraphQLList,
3433
GraphQLNonNull,
3534
GraphQLObjectType,
@@ -46,25 +45,6 @@
4645
from .graphql_utils import GraphQLElementType, convert_name_for_graphql_schema
4746
from .metadata_tracker import build_field_path
4847

49-
50-
def _extract_extended_attributes(row: pd.Series, extended_attributes: tuple[str, ...]) -> dict[str, Any]:
51-
"""
52-
Extract extended attributes from a DataFrame row.
53-
54-
Args:
55-
row: pandas Series (DataFrame row) containing VSS node data
56-
extended_attributes: Tuple of extended attribute names to extract
57-
58-
Returns:
59-
Dictionary containing only the extended attributes that exist and are not NA
60-
"""
61-
extracted = {}
62-
for ext_attr in extended_attributes:
63-
if ext_attr in row.index and pd.notna(row.get(ext_attr)):
64-
extracted[ext_attr] = row[ext_attr]
65-
return extracted
66-
67-
6848
# Initialize inflect engine for pluralization (singleton)
6949
_inflect_engine = inflect.engine()
7050

@@ -375,10 +355,6 @@ def create_struct_types(
375355

376356
field_path = build_field_path(type_name, field_name)
377357
field_metadata = {"element": "STRUCT_PROPERTY", "fqn": prop_fqn}
378-
379-
# Capture extended attributes if present
380-
field_metadata.update(_extract_extended_attributes(prop_row, extended_attributes))
381-
382358
vspec_comments["field_vss_types"][field_path] = field_metadata
383359

384360
if pd.notna(prop_row.get("min")) or pd.notna(prop_row.get("max")):
@@ -394,10 +370,7 @@ def create_struct_types(
394370
name=type_name, fields=fields, description=struct_row.get("description", "")
395371
)
396372

397-
# Store type-level metadata including extended attributes
398-
type_metadata = {"element": "STRUCT", "fqn": fqn}
399-
type_metadata.update(_extract_extended_attributes(struct_row, extended_attributes))
400-
vspec_comments["vss_types"][type_name] = type_metadata
373+
vspec_comments["vss_types"][type_name] = {"element": "STRUCT", "fqn": fqn}
401374

402375
return struct_types
403376

@@ -513,8 +486,6 @@ def get_fields() -> dict[str, GraphQLField]:
513486
fields = {}
514487

515488
# System fields
516-
if type_name == "Vehicle" or branch_row.get("instances"):
517-
fields["id"] = GraphQLField(GraphQLNonNull(GraphQLID))
518489
if instance_tag_type := vspec_comments.get("instance_tag_types", {}).get(type_name):
519490
if instance_tag_type in types_registry:
520491
fields["instanceTag"] = GraphQLField(types_registry[instance_tag_type])
@@ -536,12 +507,7 @@ def get_fields() -> dict[str, GraphQLField]:
536507
field_path = build_field_path(type_name, field_name)
537508

538509
if leaf_type := _get_vss_type_if_valid(leaf_row):
539-
field_metadata = {"element": leaf_type, "fqn": child_fqn}
540-
541-
# Capture extended attributes if present
542-
field_metadata.update(_extract_extended_attributes(leaf_row, extended_attributes))
543-
544-
vspec_comments["field_vss_types"][field_path] = field_metadata
510+
vspec_comments["field_vss_types"][field_path] = {"element": leaf_type, "fqn": child_fqn}
545511

546512
if pd.notna(leaf_row.get("min")) or pd.notna(leaf_row.get("max")):
547513
vspec_comments["field_ranges"][field_path] = {
@@ -597,14 +563,15 @@ def get_fields() -> dict[str, GraphQLField]:
597563
{"fqn": child_fqn, "plural_field_name": plural_field_name, "path_in_graphql_model": field_path}
598564
)
599565
else:
566+
field_path = build_field_path(type_name, field_name)
600567
fields[field_name] = GraphQLField(child_type)
601568

569+
# Annotate field with @vspec directive (shared for both cases)
570+
vspec_comments["field_vss_types"][field_path] = {"element": "BRANCH", "fqn": child_fqn}
571+
602572
return fields
603573

604-
# Store type-level metadata including extended attributes
605-
type_metadata = {"element": "BRANCH", "fqn": fqn}
606-
type_metadata.update(_extract_extended_attributes(branch_row, extended_attributes))
607-
vspec_comments["vss_types"][type_name] = type_metadata
574+
vspec_comments["vss_types"][type_name] = {"element": "BRANCH", "fqn": fqn}
608575

609576
return GraphQLObjectType(name=type_name, fields=get_fields, description=branch_row.get("description", ""))
610577

@@ -647,17 +614,12 @@ def get_hoisted_fields(
647614
field_path = build_field_path(parent_type_name, hoisted_field_name)
648615

649616
if leaf_type := _get_vss_type_if_valid(leaf_row):
650-
field_metadata = {
617+
vspec_comments["field_vss_types"][field_path] = {
651618
"element": leaf_type,
652619
"fqn": leaf_fqn,
653620
"instantiate": False,
654621
}
655622

656-
# Capture extended attributes if present
657-
field_metadata.update(_extract_extended_attributes(leaf_row, extended_attributes))
658-
659-
vspec_comments["field_vss_types"][field_path] = field_metadata
660-
661623
if pd.notna(leaf_row.get("min")) or pd.notna(leaf_row.get("max")):
662624
vspec_comments["field_ranges"][field_path] = {
663625
"min": leaf_row.get("min") if pd.notna(leaf_row.get("min")) else None,

tests/test_s2dm_exporter.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -751,8 +751,12 @@ def test_instance_dimension_enum_sanitization(self):
751751
assert 'REAR_LEFT @vspec(metadata: [{key: "originalName", value: "RearLeft"}])' in schema_str
752752
assert 'REAR_RIGHT @vspec(metadata: [{key: "originalName", value: "RearRight"}])' in schema_str
753753

754-
def test_extended_attributes_in_metadata(self):
755-
"""Test that extended attributes are captured and added to @vspec metadata."""
754+
def test_extended_attributes_not_in_schema_metadata(self):
755+
"""Test that extended attributes are not annotated in @vspec schema metadata.
756+
757+
Extended attributes are available via the sidecar vspec lookup, so they are
758+
intentionally omitted from the schema to keep it minimal.
759+
"""
756760
# Load the test vspec with extended attributes
757761
tree, _ = get_trees(
758762
vspec=Path("tests/vspec/test_s2dm/test_extended_attributes.vspec"),
@@ -771,24 +775,19 @@ def test_extended_attributes_in_metadata(self):
771775
)
772776
schema_str = print_schema_with_vspec_directives(schema, unit_metadata, allowed_metadata, vspec_comments)
773777

774-
# Check Vehicle.Speed has source and quality in metadata
778+
# Fields are still annotated with element + fqn
775779
assert "speed(unit: RelationUnitEnum = PERCENT): Float" in schema_str
776780
assert '@vspec(element: SENSOR, fqn: "Vehicle.Speed"' in schema_str
777-
assert '{key: "source", value: "ecu0xAA"}' in schema_str
778-
assert '{key: "quality", value: "100"}' in schema_str
779-
780-
# Check Vehicle.Temperature has source, quality, and calibration
781-
assert "temperature(unit: AngleUnitEnum = DEGREE): Int16" in schema_str
782781
assert '@vspec(element: SENSOR, fqn: "Vehicle.Temperature"' in schema_str
783-
assert '{key: "source", value: "ecu0xBB"}' in schema_str
784-
assert '{key: "quality", value: "95"}' in schema_str
785-
assert '{key: "calibration", value: "factory"}' in schema_str
786-
787-
# Check Vehicle.Info.Model has customMetadata and anotherAttribute
788782
assert "model: String" in schema_str
789783
assert '@vspec(element: ATTRIBUTE, fqn: "Vehicle.Info.Model"' in schema_str
790-
assert '{key: "customMetadata", value: "test_value"}' in schema_str
791-
assert '{key: "anotherAttribute", value: "42"}' in schema_str
784+
785+
# Extended attributes are NOT annotated in @vspec metadata (available via sidecar instead)
786+
assert '{key: "source"' not in schema_str
787+
assert '{key: "quality"' not in schema_str
788+
assert '{key: "calibration"' not in schema_str
789+
assert '{key: "customMetadata"' not in schema_str
790+
assert '{key: "anotherAttribute"' not in schema_str
792791

793792
def test_empty_branches_are_skipped(self, caplog):
794793
"""Test that empty branches (branches with no children) are skipped during export."""

0 commit comments

Comments
 (0)