Skip to content

Commit c721c2d

Browse files
authored
Fix enum value annotations not being preserved (opensearch-project#353)
* Fix enum value annotations not being preserved Signed-off-by: xil <fridalu66@gmail.com> --------- Signed-off-by: xil <fridalu66@gmail.com>
1 parent 07d436c commit c721c2d

7 files changed

Lines changed: 151 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
99
- Add CompatibilityReporter for backward compatibility change tracking ([#349](https://github.com/opensearch-project/opensearch-protobufs/pull/349))
1010

1111
### Changed
12-
12+
- Fix enum value annotations not being preserved ([#353](https://github.com/opensearch-project/opensearch-protobufs/pull/353))
1313
### Removed
1414

1515
### Fixed

tools/proto-convert/src/postprocessing/CleanupUnusedMessages.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ export function cleanupUnusedMessages(opts: CleanupOptions): { removedMessages:
159159
// Find reachable types
160160
const reachable = findReachableTypes(roots, parsed.messages);
161161

162-
// Filter to keep only reachable
162+
// Filter to keep only reachable messages and enums
163163
const keptMessages = filterMessages(parsed.messages, reachable);
164164
const keptEnums = filterEnums(parsed.enums, reachable);
165165

tools/proto-convert/src/postprocessing/CompatibilityMerger.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,11 @@ export function mergeEnum(
257257
for (const sourceValue of sourceEnum.values) {
258258
maxValueNumber = Math.max(maxValueNumber, sourceValue.number);
259259

260+
if (isDeprecated(sourceValue)) {
261+
mergedValues.push(sourceValue);
262+
continue;
263+
}
264+
260265
const upcomingValue = upcomingValueMap.get(sourceValue.name);
261266

262267
if (upcomingValue) {

tools/proto-convert/src/postprocessing/parser.ts

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,59 @@ export function convertField(field: Field): ProtoField {
5454
};
5555
}
5656

57+
/**
58+
* Extract enum value annotations from raw proto file content.
59+
60+
*/
61+
export function extractEnumValueAnnotations(content: string): Map<string, Map<string, Annotation[]>> {
62+
const result = new Map<string, Map<string, Annotation[]>>();
63+
64+
// Match enum blocks
65+
const enumRegex = /enum\s+(\w+)\s*\{([^}]+)\}/g;
66+
let enumMatch;
67+
68+
while ((enumMatch = enumRegex.exec(content)) !== null) {
69+
const enumName = enumMatch[1];
70+
const enumBody = enumMatch[2];
71+
const valueAnnotations = new Map<string, Annotation[]>();
72+
73+
// Match enum values with options: VALUE_NAME = 123 [option = value, ...];
74+
const valueRegex = /(\w+)\s*=\s*\d+\s*\[([^\]]+)\]/g;
75+
let valueMatch;
76+
77+
while ((valueMatch = valueRegex.exec(enumBody)) !== null) {
78+
const valueName = valueMatch[1];
79+
const optionsStr = valueMatch[2];
80+
const annotations: Annotation[] = [];
81+
82+
// Parse options like "deprecated = true, custom = value"
83+
const optionParts = optionsStr.split(',');
84+
for (const part of optionParts) {
85+
const eqIndex = part.indexOf('=');
86+
if (eqIndex > 0) {
87+
const name = part.substring(0, eqIndex).trim();
88+
const value = part.substring(eqIndex + 1).trim();
89+
annotations.push({ name, value });
90+
}
91+
}
92+
93+
if (annotations.length > 0) {
94+
valueAnnotations.set(valueName, annotations);
95+
}
96+
}
97+
98+
if (valueAnnotations.size > 0) {
99+
result.set(enumName, valueAnnotations);
100+
}
101+
}
102+
103+
return result;
104+
}
105+
57106
/**
58107
* Convert a protobufjs Enum to internal ProtoEnum type.
59108
*/
60-
export function convertEnum(enumDef: Enum): ProtoEnum {
109+
export function convertEnum(enumDef: Enum, valueAnnotations?: Map<string, Annotation[]>): ProtoEnum {
61110
const values: ProtoEnumValue[] = [];
62111

63112
for (const [name, number] of Object.entries(enumDef.values)) {
@@ -66,12 +115,9 @@ export function convertEnum(enumDef: Enum): ProtoEnum {
66115
number: number as number
67116
};
68117

69-
const valuesOptions = (enumDef as any).valuesOptions;
70-
if (valuesOptions && valuesOptions[name]) {
71-
value.annotations = Object.entries(valuesOptions[name]).map(([k, v]) => ({
72-
name: k,
73-
value: String(v)
74-
}));
118+
// Get annotations from raw file parsing (protobufjs doesn't parse these)
119+
if (valueAnnotations && valueAnnotations.has(name)) {
120+
value.annotations = valueAnnotations.get(name);
75121
}
76122

77123
values.push(value);
@@ -188,6 +234,9 @@ export function parseProtoFile(filePath: string): ParsedProtoFile {
188234
const content = readFileSync(filePath, 'utf8');
189235
const parsed = parse(content, { keepCase: true, alternateCommentMode: true });
190236

237+
// Extract enum value annotations from raw content (protobufjs doesn't parse these)
238+
const enumValueAnnotations = extractEnumValueAnnotations(content);
239+
191240
const messages: ProtoMessage[] = [];
192241
const enums: ProtoEnum[] = [];
193242
const services: ProtoService[] = [];
@@ -198,7 +247,8 @@ export function parseProtoFile(filePath: string): ParsedProtoFile {
198247
if (nested instanceof Type) {
199248
messages.push(convertMessage(nested));
200249
} else if (nested instanceof Enum) {
201-
enums.push(convertEnum(nested));
250+
const annotations = enumValueAnnotations.get(nested.name);
251+
enums.push(convertEnum(nested, annotations));
202252
} else if (nested instanceof Service) {
203253
services.push(convertService(nested));
204254
} else if (nested instanceof Namespace) {

tools/proto-convert/test/fixtures/proto/test.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ enum Status {
102102
STATUS_UNSPECIFIED = 0;
103103
STATUS_ACTIVE = 1;
104104
STATUS_INACTIVE = 2;
105+
STATUS_DEPRECATED = 3 [deprecated = true];
105106
}
106107

107108
// ==================== UNUSED (for cleanup testing) ====================

tools/proto-convert/test/postprocessing/CompatibilityMerger.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,28 @@ describe('mergeEnum', () => {
741741
);
742742
expect(deprecatedOptions).toHaveLength(1);
743743
});
744+
745+
it('should not report already deprecated values', () => {
746+
const source: ProtoEnum = {
747+
name: 'Status',
748+
values: [{
749+
name: 'STATUS_OLD',
750+
number: 1,
751+
annotations: [{ name: 'deprecated', value: 'true' }]
752+
}]
753+
};
754+
const upcoming: ProtoEnum = {
755+
name: 'Status',
756+
values: []
757+
};
758+
759+
const reporter = new CompatibilityReporter();
760+
mergeEnum(source, upcoming, reporter);
761+
762+
// Reporter should have no enum changes since value was already deprecated
763+
const markdown = reporter.toMarkdown();
764+
expect(markdown).toContain('No changes detected');
765+
});
744766
});
745767

746768
describe('new values', () => {

tools/proto-convert/test/postprocessing/parser.test.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import * as path from 'path';
7-
import { parseProtoFile } from '../../src/postprocessing/parser';
7+
import { parseProtoFile, extractEnumValueAnnotations } from '../../src/postprocessing/parser';
88

99
const TEST_PROTO = path.join(__dirname, '../fixtures/proto/test.proto');
1010

@@ -96,4 +96,66 @@ describe('parseProtoFile', () => {
9696
const filterMsg = result.messages.find(m => m.name === 'FilterSettings');
9797
expect(filterMsg!.oneofs![0].comment).toBe('Filter value - can be different types');
9898
});
99+
100+
it('should parse enum value annotations', () => {
101+
const statusEnum = result.enums.find(e => e.name === 'Status');
102+
expect(statusEnum).toBeDefined();
103+
104+
// Regular values should not have annotations
105+
const activeValue = statusEnum!.values.find(v => v.name === 'STATUS_ACTIVE');
106+
expect(activeValue!.annotations).toBeUndefined();
107+
108+
// Deprecated value should have annotation
109+
const deprecatedValue = statusEnum!.values.find(v => v.name === 'STATUS_DEPRECATED');
110+
expect(deprecatedValue).toBeDefined();
111+
expect(deprecatedValue!.annotations).toContainEqual({ name: 'deprecated', value: 'true' });
112+
});
113+
});
114+
115+
describe('extractEnumValueAnnotations', () => {
116+
117+
it('should extract multiple annotations from enum values', () => {
118+
const content = `
119+
enum MultiAnnotation {
120+
VALUE_A = 0 [deprecated = true, custom = value];
121+
}
122+
`;
123+
const result = extractEnumValueAnnotations(content);
124+
125+
expect(result.has('MultiAnnotation')).toBe(true);
126+
const annotations = result.get('MultiAnnotation')!.get('VALUE_A')!;
127+
128+
expect(annotations).toHaveLength(2);
129+
expect(annotations).toContainEqual({ name: 'deprecated', value: 'true' });
130+
expect(annotations).toContainEqual({ name: 'custom', value: 'value' });
131+
});
132+
133+
it('should handle multiple enums', () => {
134+
const content = `
135+
enum EnumA {
136+
A_VALUE = 0 [deprecated = true];
137+
}
138+
enum EnumB {
139+
B_VALUE = 1 [deprecated = true];
140+
}
141+
`;
142+
const result = extractEnumValueAnnotations(content);
143+
144+
expect(result.has('EnumA')).toBe(true);
145+
expect(result.has('EnumB')).toBe(true);
146+
expect(result.get('EnumA')!.has('A_VALUE')).toBe(true);
147+
expect(result.get('EnumB')!.has('B_VALUE')).toBe(true);
148+
});
149+
150+
it('should return empty map when no annotations exist', () => {
151+
const content = `
152+
enum NoAnnotations {
153+
VALUE_A = 0;
154+
VALUE_B = 1;
155+
}
156+
`;
157+
const result = extractEnumValueAnnotations(content);
158+
159+
expect(result.size).toBe(0);
160+
});
99161
});

0 commit comments

Comments
 (0)