diff --git a/docs/s2dm.md b/docs/s2dm.md index 22b30b24..78d22e1f 100644 --- a/docs/s2dm.md +++ b/docs/s2dm.md @@ -44,6 +44,8 @@ type Vehicle_Cabin @vspec(element: BRANCH, fqn: "Vehicle.Cabin") { In this example, the metadata shows that the `Vehicle_Cabin` type was derived from the Fully-Qualified Name (FQN) `Vehicle.Cabin`, and that is was a `BRANCH`. Likewise, the `driverPosition` was derived from `Vehicle.Cabin.DriverPosition` and it was an `ATTRIBUTE`. +The `@vspec` directive is also used to annotate original names when they have been modified for GraphQL compliance (for example, see [Enum Value Sanitization](#enum-value-sanitization)). + ### VSS Data Types Support The exporter handles all `vspec` data types as follows: - **Strings** → GraphQL String @@ -52,6 +54,34 @@ The exporter handles all `vspec` data types as follows: - **Arrays** → GraphQL Lists - **Allowed values** → GraphQL Enums +#### Enum Value Sanitization + +GraphQL enum values must follow strict naming rules (alphanumeric + underscore only, cannot start with a digit). The S2DM exporter automatically sanitizes VSS enum values to comply with GraphQL requirements: + +- **Spaces & special characters** → Converted to underscores (`"some value"` → `SOME_VALUE`) +- **CamelCase** → Converted to SCREAMING_SNAKE_CASE (`"HTTPSProtocol"` → `HTTPS_PROTOCOL`) +- **Leading digits** → Prefixed with underscore (`"123abc"` → `_123ABC`) + +When enum values are modified, the original VSS value is preserved using `@vspec` metadata for complete traceability. This applies to both **allowed value enums** and **instance dimension enums**. + +**Allowed Value Enum Example:** +```graphql +enum Vehicle_Connection_Protocol_Enum @vspec(element: SENSOR, fqn: "Vehicle.Connection.Protocol", metadata: [{key: "allowed", value: "['HTTPSProtocol', 'TCPProtocol']"}]) { + HTTPS_PROTOCOL @vspec(metadata: [{key: "originalName", value: "HTTPSProtocol"}]) + TCP_PROTOCOL @vspec(metadata: [{key: "originalName", value: "TCPProtocol"}]) +} +``` + +**Instance Dimension Enum Example:** +```graphql +enum Vehicle_Cabin_Seat_InstanceTag_Dimension2 { + DRIVER_SIDE @vspec(metadata: [{key: "originalName", value: "DriverSide"}]) + PASSENGER_SIDE @vspec(metadata: [{key: "originalName", value: "PassengerSide"}]) +} +``` + +This ensures complete traceability between the VSS source and the generated GraphQL schema. + ### VSS Instances Become GraphQL Structures When your `vspec` has instances (like multiple seats), the exporter creates proper GraphQL types: @@ -63,13 +93,13 @@ type Vehicle_Cabin_Seat_InstanceTag @instanceTag { } enum Vehicle_Cabin_Seat_InstanceTag_Dimension1 { - Row1 - Row2 + ROW1 @vspec(metadata: [{key: "originalName", value: "Row1"}]) + ROW2 @vspec(metadata: [{key: "originalName", value: "Row2"}]) } enum Vehicle_Cabin_Seat_InstanceTag_Dimension2 { - DriverSide - PassengerSide + DRIVER_SIDE @vspec(metadata: [{key: "originalName", value: "DriverSide"}]) + PASSENGER_SIDE @vspec(metadata: [{key: "originalName", value: "PassengerSide"}]) } ``` Such an structure is then usable by any other type like: diff --git a/src/vss_tools/exporters/s2dm/predefined_elements/directives.graphql b/src/vss_tools/exporters/s2dm/predefined_elements/directives.graphql index ebcdb866..bfd20e36 100644 --- a/src/vss_tools/exporters/s2dm/predefined_elements/directives.graphql +++ b/src/vss_tools/exporters/s2dm/predefined_elements/directives.graphql @@ -1,7 +1,7 @@ """Annotation directive for Vspec mapping information.""" directive @vspec( """The element of the Vspec language to which the item in the GraphQL schema maps.""" - element: VspecElement!, + element: VspecElement, """The Fully Qualified Name (FQN) of the related element in Vspec (aka., path). Example: Vehicle.Cabin.Door.Window.position""" fqn: String """Additional metadata associated with the mapping.""" diff --git a/src/vss_tools/exporters/s2dm/type_builders.py b/src/vss_tools/exporters/s2dm/type_builders.py index 300c6f0a..148f6763 100644 --- a/src/vss_tools/exporters/s2dm/type_builders.py +++ b/src/vss_tools/exporters/s2dm/type_builders.py @@ -18,8 +18,10 @@ from __future__ import annotations +import re from typing import Any +import caseconverter import pandas as pd from graphql import ( GraphQLArgument, @@ -108,6 +110,8 @@ def create_instance_types( Mapping of type names to GraphQL enum or object types """ types: dict[str, GraphQLEnumType | GraphQLObjectType] = {} + vspec_comments.setdefault("instance_dimension_enums", {}) + for fqn, row in branches_df[branches_df["instances"].notna()].iterrows(): if instances := row.get("instances"): base_name = convert_name_for_graphql_schema(fqn, GraphQLElementType.TYPE, S2DM_CONVERSIONS) @@ -117,11 +121,28 @@ def create_instance_types( fields = {} for i, values in enumerate(dimensions, 1): enum_name = f"{tag_name}_Dimension{i}" + + # Sanitize enum values and track modifications + enum_values = {} + modified_values = {} + + for v in values: + sanitized, was_modified = _sanitize_enum_value_for_graphql(str(v)) + enum_values[sanitized] = GraphQLEnumValue(v) + + if was_modified: + modified_values[sanitized] = str(v) + types[enum_name] = GraphQLEnumType( enum_name, - {v: GraphQLEnumValue(v) for v in values}, + enum_values, description=f"Dimensional enum for VSS instance dimension {i}.", ) + + # Store metadata for directive processor + if modified_values: + vspec_comments["instance_dimension_enums"][enum_name] = {"modified_values": modified_values} + fields[f"dimension{i}"] = GraphQLField(types[enum_name]) types[tag_name] = GraphQLObjectType(tag_name, fields) @@ -171,29 +192,85 @@ def create_allowed_enums( for fqn, row in leaves_df[leaves_df["allowed"].notna()].iterrows(): if allowed := row.get("allowed"): enum_name = f"{convert_name_for_graphql_schema(fqn, GraphQLElementType.TYPE, S2DM_CONVERSIONS)}_Enum" - values = {_clean_enum_name(str(v)): GraphQLEnumValue(v) for v in allowed} + + # Track values and their modifications + values = {} + modified_values = {} + + for v in allowed: + sanitized, was_modified = _sanitize_enum_value_for_graphql(str(v)) + values[sanitized] = GraphQLEnumValue(v) + + # Track if value was modified for metadata annotation + if was_modified: + modified_values[sanitized] = str(v) + enums[enum_name] = GraphQLEnumType(enum_name, values, description=f"Allowed values for {fqn}.") vss_type = row.get("type", "").upper() if vss_type not in VSS_LEAF_TYPES: vss_type = "ATTRIBUTE" - allowed_values_graphql = {_clean_enum_name(str(v)): str(v).replace('"', "'") for v in allowed} + allowed_values_graphql = { + _sanitize_enum_value_for_graphql(str(v))[0]: str(v).replace('"', "'") for v in allowed + } metadata[enum_name] = { "fqn": fqn, "vss_type": vss_type, "allowed_values": allowed_values_graphql, + "modified_values": modified_values, # Store modified values for directive annotations } return enums, metadata -def _clean_enum_name(value: str) -> str: - """Sanitize enum value names for GraphQL.""" - if value[0].isdigit(): - value = f"_{value}" - return value.replace(".", "_DOT_").replace("-", "_DASH_") +def _sanitize_enum_value_for_graphql(original_value: str) -> tuple[str, bool]: + """ + Sanitize enum value for GraphQL schema compliance. + + Converts values with spaces, camelCase, or other invalid characters to valid GraphQL enum values. + Uses caseconverter to properly handle camelCase word boundaries. + + Examples: + "some value" -> "SOME_VALUE" + "SOME VALUE" -> "SOME_VALUE" + "PbCa" -> "PB_CA" + "HTTPSConnection" -> "HTTPS_CONNECTION" + "value-with-dash" -> "VALUE_WITH_DASH" + + Args: + original_value: The original enum value from VSS + + Returns: + Tuple of (sanitized_value, was_modified) + - sanitized_value: Valid GraphQL enum value name + - was_modified: True if the value was changed, False otherwise + """ + + # Handle empty or whitespace-only strings + if not original_value or not original_value.strip(): + raise ValueError(f"Cannot create GraphQL enum value from empty or whitespace-only string: {original_value!r}") + + # Replace with underscore all the special characters that are not allowed in GraphQL enum names + sanitized = re.sub(r"[^a-zA-Z0-9_]", "_", original_value) + + # Convert to caseconverter MACRO_CASE (i.e., SCREAMING_SNAKE_CASE) + if re.search(r"[A-Z]{2,}", sanitized) and re.search(r"[a-z]", sanitized): + words: list[str] = [] + for segment in sanitized.split("_"): + words.extend(re.findall(r"[A-Z]+(?![a-z])|[A-Z]?[a-z]+|[0-9]+", segment)) + sanitized = "_".join(caseconverter.macrocase(word, strip_punctuation=False) for word in words if word) + else: + sanitized = caseconverter.macrocase(sanitized, strip_punctuation=False) + + # Handle enum values starting with digits + sanitized = f"_{sanitized}" if sanitized[0].isdigit() else sanitized + + # Check if modification occurred + was_modified = sanitized != original_value + + return sanitized, was_modified def create_struct_types( diff --git a/src/vss_tools/utils/graphql_directive_processor.py b/src/vss_tools/utils/graphql_directive_processor.py index 053d536d..74ef4922 100644 --- a/src/vss_tools/utils/graphql_directive_processor.py +++ b/src/vss_tools/utils/graphql_directive_processor.py @@ -54,6 +54,9 @@ def process_schema( lines = self._process_unit_enum_directives(lines, unit_enums_metadata, processed_enum_values) lines = self._process_allowed_enum_directives(lines, allowed_enums_metadata, processed_enum_values) + lines = self._process_instance_dimension_enum_directives( + lines, vspec_comments.get("instance_dimension_enums", {}), processed_enum_values + ) lines = self._process_field_directives(lines, vspec_comments) lines = self._process_deprecated_directives(lines, vspec_comments.get("field_deprecated", {})) lines = self._process_range_directives(lines, vspec_comments.get("field_ranges", {})) @@ -117,28 +120,98 @@ def _process_allowed_enum_directives( Process allowed value enum directives. Annotates the enum type itself with @vspec(element, fqn, metadata), - but does NOT annotate individual enum values. + and annotates individual enum values that were modified with @vspec(metadata). """ for enum_name, enum_data in allowed_enums_metadata.items(): fqn = enum_data.get("fqn", "") vss_type = enum_data.get("vss_type", "ATTRIBUTE") allowed_values_dict = enum_data.get("allowed_values", {}) + modified_values = enum_data.get("modified_values", {}) # Build the allowed values list for metadata # GraphQL requires: value: "['val1', 'val2']" (double quotes outside, single quotes inside) allowed_values_list = list(allowed_values_dict.values()) allowed_str = ", ".join([f"'{v}'" for v in allowed_values_list]) + in_target_enum = False for i, line in enumerate(lines): - if line.strip().startswith(f"enum {enum_name}") and "@vspec" not in line: - # Annotate the enum type (not individual values) - # Format: @vspec(element: ATTRIBUTE, fqn: "...", metadata: [{key: "allowed", value: "[...]"}]) - directive = ( - f'@vspec(element: {vss_type}, fqn: "{fqn}", ' - f'metadata: [{{key: "allowed", value: "[{allowed_str}]"}}])' - ) - lines[i] = line.replace(" {", f" {directive} {{") - break + if line.strip().startswith(f"enum {enum_name}"): + if "@vspec" not in line: + # Annotate the enum type + directive = ( + f'@vspec(element: {vss_type}, fqn: "{fqn}", ' + f'metadata: [{{key: "allowed", value: "[{allowed_str}]"}}])' + ) + lines[i] = line.replace(" {", f" {directive} {{") + in_target_enum = True + continue + elif line.strip().startswith("enum ") and in_target_enum: + in_target_enum = False + continue + elif line.strip() == "}" and in_target_enum: + in_target_enum = False + continue + + # Process individual enum values that were modified + if in_target_enum and line.strip() and not line.strip().startswith('"'): + stripped_line = line.strip() + + for enum_value_name, original_value in modified_values.items(): + enum_value_key = f"{enum_name}.{enum_value_name}" + if stripped_line.startswith(enum_value_name) and enum_value_key not in processed_values: + if "@vspec" not in line: + indent = line[: len(line) - len(line.lstrip())] + # Annotate modified enum value with original value in metadata + directive = f'@vspec(metadata: [{{key: "originalName", value: "{original_value}"}}])' + lines[i] = f"{indent}{enum_value_name} {directive}" + + processed_values.add(enum_value_key) + break + + return lines + + def _process_instance_dimension_enum_directives( + self, lines: list[str], instance_dimension_enums: dict, processed_values: set + ) -> list[str]: + """ + Process instance dimension enum directives. + + Annotates enum values that were modified during sanitization with their original names. + """ + for enum_name, enum_data in instance_dimension_enums.items(): + modified_values = enum_data.get("modified_values", {}) + if not modified_values: + continue + + in_target_enum = False + + for i, line in enumerate(lines): + # Detect enum start + if line.strip().startswith(f"enum {enum_name}"): + in_target_enum = True + continue + elif line.strip().startswith("enum ") and in_target_enum: + in_target_enum = False + continue + elif line.strip() == "}" and in_target_enum: + in_target_enum = False + continue + + # Process enum values within target enum + if in_target_enum and line.strip() and not line.strip().startswith('"'): + stripped_line = line.strip() + + for enum_value_name, original_value in modified_values.items(): + enum_value_key = f"{enum_name}.{enum_value_name}" + + if stripped_line.startswith(enum_value_name) and enum_value_key not in processed_values: + if "@vspec" not in line: + indent = line[: len(line) - len(line.lstrip())] + directive = f'@vspec(metadata: [{{key: "originalName", value: "{original_value}"}}])' + lines[i] = f"{indent}{enum_value_name} {directive}" + + processed_values.add(enum_value_key) + break return lines diff --git a/tests/test_s2dm_exporter.py b/tests/test_s2dm_exporter.py index 62cc3ea5..cdba1282 100644 --- a/tests/test_s2dm_exporter.py +++ b/tests/test_s2dm_exporter.py @@ -8,6 +8,7 @@ from pathlib import Path +import pytest from graphql import build_schema, print_schema from vss_tools.exporters.s2dm import ( S2DM_CONVERSIONS, @@ -347,8 +348,8 @@ def test_allowed_value_enums_generation(self): # Check for float field with allowed values [1.0, 2.5, 4.0, 5.0] assert "Vehicle_Performance_Rating_Enum" in schema_sdl - assert "_1" in schema_sdl # 1.0 becomes _1 - assert "_2_DOT_5" in schema_sdl # 2.5 should use _DOT_ + assert "_1_0" in schema_sdl # 1.0 becomes _1_0 + assert "_2_5" in schema_sdl # 2.5 becomes _2_5 assert "_4" in schema_sdl assert "_5" in schema_sdl @@ -500,3 +501,204 @@ def test_non_instantiated_property_hoisting(self, tmp_path: Path): assert 'metadata: [{key: "instantiate", value: "false"}]' in cabin_type_content # And door_s array field should also be there assert "door_s" in cabin_type_content + + def test_enum_value_sanitization_with_spaces(self): + """Test that enum values with spaces are properly sanitized and annotated.""" + from vss_tools.exporters.s2dm.type_builders import _sanitize_enum_value_for_graphql + + # Test the sanitization function + assert _sanitize_enum_value_for_graphql("some value") == ("SOME_VALUE", True) + assert _sanitize_enum_value_for_graphql("SOME VALUE") == ("SOME_VALUE", True) + assert _sanitize_enum_value_for_graphql("another-value") == ("ANOTHER_VALUE", True) + assert _sanitize_enum_value_for_graphql("YET_ANOTHER") == ("YET_ANOTHER", False) + assert _sanitize_enum_value_for_graphql("front left") == ("FRONT_LEFT", True) + assert _sanitize_enum_value_for_graphql("1value") == ("_1VALUE", True) + + def test_enum_sanitization_in_schema_generation(self): + """Test that enum values with spaces generate proper schema with metadata.""" + # Load the test vspec with spaces in enum values + tree, _ = get_trees( + vspec=Path("tests/vspec/test_s2dm/test_enum_sanitization.vspec"), + include_dirs=(), + aborts=(), + strict=False, + extended_attributes=(), + quantities=(Path("tests/vspec/test_s2dm/test_quantities.yaml"),), + units=(Path("tests/vspec/test_s2dm/test_units.yaml"),), + overlays=(), + expand=False, + ) + + schema, _, allowed_metadata, _ = generate_s2dm_schema(tree) + + # Check that schema is valid + assert schema is not None + + # Check that enum type for LightMode exists + light_mode_enum_name = "Vehicle_Cabin_LightMode_Enum" + assert light_mode_enum_name in schema.type_map + + # Check metadata includes modified values + assert light_mode_enum_name in allowed_metadata + metadata = allowed_metadata[light_mode_enum_name] + assert "modified_values" in metadata + + # Verify specific modifications + modified = metadata["modified_values"] + assert "SOME_VALUE" in modified + assert modified["SOME_VALUE"] in ["some value", "SOME VALUE"] # One of them + assert "ANOTHER_VALUE" in modified + assert modified["ANOTHER_VALUE"] == "another-value" + + # YET_ANOTHER should not be in modified (no modification needed) + assert "YET_ANOTHER" not in modified + + def test_enum_sanitization_schema_output_with_directives(self): + """Test that the schema output includes proper @vspec directives for modified enum values.""" + # Load the test vspec with spaces in enum values + tree, _ = get_trees( + vspec=Path("tests/vspec/test_s2dm/test_enum_sanitization.vspec"), + include_dirs=(), + aborts=(), + strict=False, + extended_attributes=(), + quantities=(Path("tests/vspec/test_s2dm/test_quantities.yaml"),), + units=(Path("tests/vspec/test_s2dm/test_units.yaml"),), + overlays=(), + expand=False, + ) + + schema, unit_metadata, allowed_metadata, vspec_comments = generate_s2dm_schema(tree) + schema_str = print_schema_with_vspec_directives(schema, unit_metadata, allowed_metadata, vspec_comments) + + # Check that enum type has @vspec directive with element + assert "enum Vehicle_Cabin_LightMode_Enum @vspec" in schema_str + + # Check that modified enum values have @vspec directives with originalName metadata + # "some value" -> SOME_VALUE + assert ( + 'SOME_VALUE @vspec(metadata: [{key: "originalName", value: "some value"}])' in schema_str + or 'SOME_VALUE @vspec(metadata: [{key: "originalName", value: "SOME VALUE"}])' in schema_str + ) + + # "another-value" -> ANOTHER_VALUE + assert 'ANOTHER_VALUE @vspec(metadata: [{key: "originalName", value: "another-value"}])' in schema_str + + # YET_ANOTHER should not have originalName metadata (wasn't modified) + assert 'YET_ANOTHER @vspec(metadata: [{key: "originalName"' not in schema_str + + # Check SeatPosition enum + assert "enum Vehicle_Cabin_SeatPosition_Enum @vspec" in schema_str + assert 'FRONT_LEFT @vspec(metadata: [{key: "originalName", value: "front left"}])' in schema_str + assert 'FRONT_RIGHT @vspec(metadata: [{key: "originalName", value: "front right"}])' in schema_str + + def test_enum_camelcase_sanitization(self): + """Test that camelCase enum values are properly converted using caseconverter.""" + from vss_tools.exporters.s2dm.type_builders import _sanitize_enum_value_for_graphql + + # Test camelCase word boundary detection + test_cases = [ + ("PbCa", "PB_CA", True), # Mixed case acronyms + ("HTTPSConnection", "HTTPS_CONNECTION", True), # Acronym + word + ("someAPIKey", "SOME_API_KEY", True), # word + acronym + word + ("XMLParser", "XML_PARSER", True), # Acronym + word + ("myValue", "MY_VALUE", True), # Simple camelCase + ("IOError", "IO_ERROR", True), # Two-letter acronym + ("AGM", "AGM", False), # Already uppercase, no change + ("EFB", "EFB", False), # Already uppercase, no change + ("already_snake", "ALREADY_SNAKE", True), # snake_case to SCREAMING_SNAKE_CASE + ("ALREADY_SCREAMING", "ALREADY_SCREAMING", False), # No change needed + ("mixed-Case", "MIXED_CASE", True), # Mixed with hyphen + ("some value", "SOME_VALUE", True), # Spaces + ("value.with.dots", "VALUE_WITH_DOTS", True), # Dots + ("123value", "_123VALUE", True), # Starts with number + ] + + error_cases = [ + "", # Empty string + " ", # Whitespace only + ] + + for input_val, expected_output, expected_modified in test_cases: + result, was_modified = _sanitize_enum_value_for_graphql(input_val) + assert result == expected_output, f"Failed for '{input_val}': expected '{expected_output}', got '{result}'" + assert was_modified == expected_modified, f"Failed modification flag for '{input_val}'" + + for input_val in error_cases: + with pytest.raises(ValueError): + _sanitize_enum_value_for_graphql(input_val) + + def test_camelcase_enums_schema_generation(self): + """Test that camelCase enum values in vspec generate proper schema with directives.""" + # Load the test vspec with camelCase enum values + tree, _ = get_trees( + vspec=Path("tests/vspec/test_s2dm/test_camelcase_enums.vspec"), + include_dirs=(), + aborts=(), + strict=False, + extended_attributes=(), + quantities=(Path("tests/vspec/test_s2dm/test_quantities.yaml"),), + units=(Path("tests/vspec/test_s2dm/test_units.yaml"),), + overlays=(), + expand=False, + ) + + schema, unit_metadata, allowed_metadata, vspec_comments = generate_s2dm_schema(tree) + schema_str = print_schema_with_vspec_directives(schema, unit_metadata, allowed_metadata, vspec_comments) + + # Check Component.Type enum with AbCd + assert "enum Vehicle_Component_Type_Enum @vspec" in schema_str + + # AbCd should be converted to AB_CD with originalName annotation + assert 'AB_CD @vspec(metadata: [{key: "originalName", value: "AbCd"}])' in schema_str + + # AAA, BBB, CCC, DDD should not have originalName (no change needed) + assert 'AAA @vspec(metadata: [{key: "originalName"' not in schema_str + assert 'BBB @vspec(metadata: [{key: "originalName"' not in schema_str + + # Check Connection.Protocol enum + assert "enum Vehicle_Connection_Protocol_Enum @vspec" in schema_str + assert 'HTTPS_PROTOCOL @vspec(metadata: [{key: "originalName", value: "HTTPSProtocol"}])' in schema_str + assert 'TCP_PROTOCOL @vspec(metadata: [{key: "originalName", value: "TCPProtocol"}])' in schema_str + assert 'UDP_PROTOCOL @vspec(metadata: [{key: "originalName", value: "UDPProtocol"}])' in schema_str + + # Check Status.Code enum + assert "enum Vehicle_Status_Code_Enum @vspec" in schema_str + assert 'IO_ERROR @vspec(metadata: [{key: "originalName", value: "IOError"}])' in schema_str + assert 'XML_PARSER @vspec(metadata: [{key: "originalName", value: "XMLParser"}])' in schema_str + assert 'SOME_API_KEY @vspec(metadata: [{key: "originalName", value: "someAPIKey"}])' in schema_str + + def test_instance_dimension_enum_sanitization(self): + """Test that instance dimension enum values are properly sanitized and annotated.""" + # Load the test vspec with instances that need sanitization + tree, _ = get_trees( + vspec=Path("tests/vspec/test_s2dm/test_instance_sanitization.vspec"), + include_dirs=(), + aborts=(), + strict=False, + extended_attributes=(), + quantities=(Path("tests/vspec/test_s2dm/test_quantities.yaml"),), + units=(Path("tests/vspec/test_s2dm/test_units.yaml"),), + overlays=(), + expand=False, + ) + + schema, unit_metadata, allowed_metadata, vspec_comments = generate_s2dm_schema(tree) + schema_str = print_schema_with_vspec_directives(schema, unit_metadata, allowed_metadata, vspec_comments) + + # Check that Row instance enum is created and values are sanitized + assert "enum Vehicle_Cabin_InstanceTag_Dimension1" in schema_str + assert 'ROW1 @vspec(metadata: [{key: "originalName", value: "Row1"}])' in schema_str + assert 'ROW2 @vspec(metadata: [{key: "originalName", value: "Row2"}])' in schema_str + + # Check that DriverSide/PassengerSide instance enum is created and values are sanitized + assert "enum Vehicle_Cabin_Seat_InstanceTag_Dimension1" in schema_str + assert 'DRIVER_SIDE @vspec(metadata: [{key: "originalName", value: "DriverSide"}])' in schema_str + assert 'PASSENGER_SIDE @vspec(metadata: [{key: "originalName", value: "PassengerSide"}])' in schema_str + + # Check that FrontLeft/FrontRight/RearLeft/RearRight instance enum is created and values are sanitized + assert "enum Vehicle_Cabin_Seat_Position_InstanceTag_Dimension1" in schema_str + assert 'FRONT_LEFT @vspec(metadata: [{key: "originalName", value: "FrontLeft"}])' in schema_str + assert 'FRONT_RIGHT @vspec(metadata: [{key: "originalName", value: "FrontRight"}])' in schema_str + assert 'REAR_LEFT @vspec(metadata: [{key: "originalName", value: "RearLeft"}])' in schema_str + assert 'REAR_RIGHT @vspec(metadata: [{key: "originalName", value: "RearRight"}])' in schema_str diff --git a/tests/vspec/test_s2dm/test_camelcase_enums.vspec b/tests/vspec/test_s2dm/test_camelcase_enums.vspec new file mode 100644 index 00000000..7426a9e2 --- /dev/null +++ b/tests/vspec/test_s2dm/test_camelcase_enums.vspec @@ -0,0 +1,36 @@ +# +# S2DM test file for camelCase enum value sanitization +# +Vehicle: + type: branch + description: High-level vehicle data. + +Vehicle.Component: + type: branch + description: Component related signals. + +Vehicle.Component.Type: + datatype: string + type: attribute + allowed: ['AAA', 'BBB', 'CCC', 'DDD', 'AbCd'] + description: Component type with mixed case values including AbCd. + +Vehicle.Connection: + type: branch + description: Connection related signals. + +Vehicle.Connection.Protocol: + datatype: string + type: sensor + allowed: ['HTTPSProtocol', 'TCPProtocol', 'UDPProtocol'] + description: Connection protocol with camelCase values. + +Vehicle.Status: + type: branch + description: Status related signals. + +Vehicle.Status.Code: + datatype: string + type: attribute + allowed: ['IOError', 'XMLParser', 'someAPIKey'] + description: Status codes with various camelCase patterns. diff --git a/tests/vspec/test_s2dm/test_enum_sanitization.vspec b/tests/vspec/test_s2dm/test_enum_sanitization.vspec new file mode 100644 index 00000000..349fc23e --- /dev/null +++ b/tests/vspec/test_s2dm/test_enum_sanitization.vspec @@ -0,0 +1,38 @@ +# +# S2DM test file for enum value sanitization with spaces +# +Vehicle: + type: branch + description: High-level vehicle data. + +Vehicle.Cabin: + type: branch + description: Cabin related signals. + +Vehicle.Cabin.LightMode: + datatype: string + type: actuator + allowed: ['some value', 'SOME VALUE', 'another-value', 'YET_ANOTHER'] + description: Light mode with values containing spaces. + +Vehicle.Cabin.SeatPosition: + datatype: string + type: sensor + allowed: ['front left', 'front right', 'rear left', 'rear right'] + description: Seat position descriptors with spaces. + +Vehicle.Cabin.AirflowDirection: + datatype: string + type: actuator + allowed: ['up and down', 'left-right', 'CIRCULAR'] + description: Airflow direction with mixed case and spaces. + +Vehicle.Status: + type: branch + description: Status related signals. + +Vehicle.Status.Priority: + datatype: uint8 + type: attribute + allowed: [1, 2, 3] + description: Priority level without spaces (should not be modified). diff --git a/tests/vspec/test_s2dm/test_instance_sanitization.vspec b/tests/vspec/test_s2dm/test_instance_sanitization.vspec new file mode 100644 index 00000000..74424462 --- /dev/null +++ b/tests/vspec/test_s2dm/test_instance_sanitization.vspec @@ -0,0 +1,28 @@ +# Test vspec for instance dimension enum sanitization + +Vehicle: + type: branch + description: High-level vehicle data. + +Vehicle.Cabin: + type: branch + description: Cabin information. + instances: + - Row[1,2] + +Vehicle.Cabin.Seat: + type: branch + description: Seat information. + instances: + - ["DriverSide", "PassengerSide"] + +Vehicle.Cabin.Seat.Position: + type: branch + description: Seat position. + instances: + - ["FrontLeft", "FrontRight", "RearLeft", "RearRight"] + +Vehicle.Cabin.Seat.Position.IsOccupied: + datatype: boolean + type: actuator + description: Is the seat position occupied.