Skip to content

Commit 26b1022

Browse files
committed
support type change
Signed-off-by: xil <fridalu66@gmail.com>
1 parent 4a1ad53 commit 26b1022

8 files changed

Lines changed: 156 additions & 165 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export class BackwardCompatibleWriter {
111111
const incomingEnum = this.incomingEnumMap.get(existingEnum.name);
112112

113113
if (incomingEnum) {
114-
const mergedEnum = mergeEnum(existingEnum, incomingEnum, this.errors);
114+
const mergedEnum = mergeEnum(existingEnum, incomingEnum);
115115
outputParts.push(generateEnum(mergedEnum));
116116
this.incomingEnumMap.delete(existingEnum.name);
117117
} else {

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

Lines changed: 93 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
/**
22
* Merger module: Compare and merge messages/enums for backward compatibility.
3-
*
43
*/
54

65
import {
76
ProtoField,
87
ProtoMessage,
98
ProtoEnum,
109
ProtoEnumValue,
11-
ProtoOneof
10+
ProtoOneof,
11+
Annotation
1212
} from './types';
1313

14+
const DEPRECATED: Annotation = { name: 'deprecated', value: 'true' };
15+
1416
/**
1517
* Extract base name
1618
*/
@@ -27,61 +29,114 @@ function getFieldVersion(fieldName: string): number {
2729
return match ? parseInt(match[1], 10) : 0;
2830
}
2931

32+
/** Type with annotations array */
33+
type HasAnnotations = { annotations?: Annotation[] };
34+
3035
/**
31-
* Check if a field is already deprecated.
36+
* Check if an item is already deprecated.
3237
*/
33-
function isDeprecated(field: ProtoField): boolean {
34-
return field.options?.some(opt => opt.name === 'deprecated' && opt.value === 'true') ?? false;
38+
function isDeprecated(item: HasAnnotations): boolean {
39+
return item.annotations?.some(a =>
40+
a.name === DEPRECATED.name && a.value === DEPRECATED.value
41+
) ?? false;
3542
}
3643

3744
/**
38-
* Check if two fields are compatible (same type and modifier).
45+
* Add deprecated annotation to an item if not already deprecated.
46+
*/
47+
function addDeprecated<T extends HasAnnotations>(item: T): T {
48+
if (isDeprecated(item)) {
49+
return item;
50+
}
51+
return {
52+
...item,
53+
annotations: [...(item.annotations || []), DEPRECATED]
54+
};
55+
}
56+
57+
/**
58+
* Check if optional added or removed. If so, push error and return true
59+
*/
60+
function hasOptionalError(
61+
source: ProtoField,
62+
upcoming: ProtoField,
63+
msgName: string,
64+
errors: string[]
65+
): boolean {
66+
const sourceOptional = source.modifier === 'optional';
67+
const upcomingOptional = upcoming.modifier === 'optional';
68+
69+
if (sourceOptional !== upcomingOptional) {
70+
const change = sourceOptional ? 'removed' : 'added';
71+
errors.push(`${msgName}.${source.name}: optional ${change}`);
72+
return true;
73+
}
74+
return false;
75+
}
76+
77+
/**
78+
* Check if two fields are compatible (same type and compatible modifiers).
3979
*/
4080
function fieldsMatch(a: ProtoField, b: ProtoField): boolean {
4181
return a.type === b.type && (a.modifier || '') === (b.modifier || '');
4282
}
4383

84+
/**
85+
* Merge a source field with upcoming map.
86+
*/
87+
function mergeField(
88+
sourceField: ProtoField,
89+
upcomingMap: Map<string, ProtoField>,
90+
msgName: string,
91+
errors: string[]
92+
): ProtoField {
93+
if (isDeprecated(sourceField)) {
94+
return sourceField;
95+
}
96+
97+
const baseName = getBaseName(sourceField.name);
98+
const upcomingField = upcomingMap.get(baseName);
99+
100+
if (upcomingField) {
101+
upcomingMap.delete(baseName);
102+
103+
if (hasOptionalError(sourceField, upcomingField, msgName, errors)) {
104+
return sourceField;
105+
}
106+
107+
if (fieldsMatch(sourceField, upcomingField)) {
108+
return sourceField;
109+
} else {
110+
// Type or repeated change - deprecate and version
111+
const newName = `${baseName}_${getFieldVersion(sourceField.name) + 1}`;
112+
upcomingMap.set(newName, { ...upcomingField, name: newName });
113+
return addDeprecated(sourceField);
114+
}
115+
} else {
116+
return addDeprecated(sourceField);
117+
}
118+
}
119+
44120
/**
45121
* Merge a source message with an upcoming message.
46-
* Errors are pushed to the errors array (currently unused but kept for future).
47122
*/
48123
export function mergeMessage(
49124
sourceMsg: ProtoMessage,
50125
upcomingMsg: ProtoMessage,
51-
_errors: string[]
126+
errors: string[]
52127
): ProtoMessage {
53128
const upcomingByName = new Map(upcomingMsg.fields.map(f => [f.name, f]));
54129

55130
let maxFieldNumber = 0;
56131
const mergedFields: ProtoField[] = [];
57132

133+
// Process regular fields
58134
for (const sourceField of sourceMsg.fields) {
59135
maxFieldNumber = Math.max(maxFieldNumber, sourceField.number);
60-
61-
// Skip deprecated fields
62-
if (isDeprecated(sourceField)) {
63-
mergedFields.push(sourceField);
64-
continue;
65-
}
66-
67-
const baseName = getBaseName(sourceField.name);
68-
const upcomingField = upcomingByName.get(baseName);
69-
70-
if (upcomingField) {
71-
upcomingByName.delete(baseName);
72-
if (fieldsMatch(sourceField, upcomingField)) {
73-
mergedFields.push(sourceField);
74-
} else {
75-
mergedFields.push(markDeprecated(sourceMsg.name, sourceField));
76-
const currentVersion = getFieldVersion(sourceField.name);
77-
const newName = `${baseName}_${currentVersion + 1}`;
78-
upcomingByName.set(newName, { ...upcomingField, name: newName });
79-
}
80-
} else {
81-
mergedFields.push(markDeprecated(sourceMsg.name, sourceField));
82-
}
136+
mergedFields.push(mergeField(sourceField, upcomingByName, sourceMsg.name, errors));
83137
}
84138

139+
// Process oneofs
85140
let mergedOneofs: ProtoOneof[] | undefined;
86141
const oneofMaps: Map<string, Map<string, ProtoField>> = new Map();
87142

@@ -98,45 +153,19 @@ export function mergeMessage(
98153
);
99154

100155
const mergedOneofFields: ProtoField[] = [];
101-
102156
for (const sourceField of sourceOneof.fields) {
103157
maxFieldNumber = Math.max(maxFieldNumber, sourceField.number);
104-
// Skip deprecated fields
105-
if (isDeprecated(sourceField)) {
106-
mergedOneofFields.push(sourceField);
107-
continue;
108-
}
109-
110-
const baseName = getBaseName(sourceField.name);
111-
const upcomingField = upcomingOneofByName.get(baseName);
112-
113-
if (upcomingField) {
114-
upcomingOneofByName.delete(baseName);
115-
if (fieldsMatch(sourceField, upcomingField)) {
116-
mergedOneofFields.push(sourceField);
117-
} else {
118-
mergedOneofFields.push(markDeprecated(sourceMsg.name, sourceField));
119-
const currentVersion = getFieldVersion(sourceField.name);
120-
const newName = `${baseName}_${currentVersion + 1}`;
121-
upcomingOneofByName.set(newName, { ...upcomingField, name: newName });
122-
}
123-
} else {
124-
mergedOneofFields.push(markDeprecated(sourceMsg.name, sourceField));
125-
}
158+
mergedOneofFields.push(mergeField(sourceField, upcomingOneofByName, sourceMsg.name, errors));
126159
}
127160

128-
mergedOneofs.push({
129-
...sourceOneof,
130-
fields: mergedOneofFields
131-
});
161+
mergedOneofs.push({ ...sourceOneof, fields: mergedOneofFields });
132162
oneofMaps.set(sourceOneof.name, upcomingOneofByName);
133163
}
134164
}
135165

136166
// Assign field max number to remaining fields.
137167
for (const field of upcomingByName.values()) {
138-
maxFieldNumber++;
139-
mergedFields.push({ ...field, number: maxFieldNumber });
168+
mergedFields.push({ ...field, number: ++maxFieldNumber });
140169
}
141170

142171
// Assign field max number to remaining oneof fields.
@@ -145,8 +174,7 @@ export function mergeMessage(
145174
const remaining = oneofMaps.get(oneof.name);
146175
if (remaining) {
147176
for (const field of remaining.values()) {
148-
maxFieldNumber++;
149-
oneof.fields.push({ ...field, number: maxFieldNumber });
177+
oneof.fields.push({ ...field, number: ++maxFieldNumber });
150178
}
151179
}
152180
}
@@ -159,31 +187,12 @@ export function mergeMessage(
159187
};
160188
}
161189

162-
/**
163-
* Mark a field as deprecated if not already.
164-
*/
165-
function markDeprecated(msgName: string, field: ProtoField): ProtoField {
166-
const isAlreadyDeprecated = field.options?.some(
167-
opt => opt.name === 'deprecated' && opt.value === 'true'
168-
);
169-
170-
if (!isAlreadyDeprecated) {
171-
return {
172-
...field,
173-
options: [...(field.options || []), { name: 'deprecated', value: 'true' }]
174-
};
175-
}
176-
return field;
177-
}
178-
179190
/**
180191
* Merge a source enum with an upcoming enum.
181-
* Errors are pushed to the errors array.
182192
*/
183193
export function mergeEnum(
184194
sourceEnum: ProtoEnum,
185-
upcomingEnum: ProtoEnum,
186-
errors: string[]
195+
upcomingEnum: ProtoEnum
187196
): ProtoEnum {
188197
const sourceValueMap = new Map(sourceEnum.values.map(v => [v.name, v]));
189198
const upcomingValueMap = new Map(upcomingEnum.values.map(v => [v.name, v]));
@@ -199,27 +208,15 @@ export function mergeEnum(
199208
if (upcomingValue) {
200209
mergedValues.push(sourceValue);
201210
} else {
202-
const isAlreadyDeprecated = sourceValue.options?.some(
203-
opt => opt.name === 'deprecated' && opt.value === 'true'
204-
);
205-
206-
if (!isAlreadyDeprecated) {
207-
mergedValues.push({
208-
...sourceValue,
209-
options: [...(sourceValue.options || []), { name: 'deprecated', value: 'true' }]
210-
});
211-
} else {
212-
mergedValues.push(sourceValue);
213-
}
211+
mergedValues.push(addDeprecated(sourceValue));
214212
}
215213
}
216214

217215
for (const [valueName, upcomingValue] of upcomingValueMap) {
218216
if (!sourceValueMap.has(valueName)) {
219-
maxValueNumber++;
220217
mergedValues.push({
221218
...upcomingValue,
222-
number: maxValueNumber
219+
number: ++maxValueNumber
223220
});
224221
}
225222
}

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
ProtoEnumValue,
1212
ProtoOneof,
1313
ParsedProtoFile,
14-
FieldOption
14+
Annotation
1515
} from './types';
1616

1717
/**
@@ -34,11 +34,11 @@ export function convertField(field: Field): ProtoField {
3434
}
3535
}
3636

37-
const options: FieldOption[] = [];
37+
const annotations: Annotation[] = [];
3838
if (field.options) {
3939
for (const [key, value] of Object.entries(field.options)) {
4040
if (key === 'proto3_optional') continue;
41-
options.push({ name: key, value: String(value) });
41+
annotations.push({ name: key, value: String(value) });
4242
}
4343
}
4444

@@ -48,7 +48,7 @@ export function convertField(field: Field): ProtoField {
4848
number: field.id,
4949
modifier,
5050
comment: field.comment || undefined,
51-
options: options.length > 0 ? options : undefined
51+
annotations: annotations.length > 0 ? annotations : undefined
5252
};
5353
}
5454

@@ -66,7 +66,7 @@ export function convertEnum(enumDef: Enum): ProtoEnum {
6666

6767
const valuesOptions = (enumDef as any).valuesOptions;
6868
if (valuesOptions && valuesOptions[name]) {
69-
value.options = Object.entries(valuesOptions[name]).map(([k, v]) => ({
69+
value.annotations = Object.entries(valuesOptions[name]).map(([k, v]) => ({
7070
name: k,
7171
value: String(v)
7272
}));
@@ -112,7 +112,7 @@ export function convertMessage(msgDef: Type): ProtoMessage {
112112
type: field.type,
113113
number: field.id,
114114
comment: field.comment || undefined,
115-
options: field.options
115+
annotations: field.options
116116
? Object.entries(field.options).map(([k, v]) => ({ name: k, value: String(v) }))
117117
: undefined
118118
});

tools/proto-convert/src/postprocessing/templates/proto.mustache

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ message {{name}} {
99
{{#commentLines}}
1010
// {{{.}}}
1111
{{/commentLines}}
12-
{{#modifier}}{{modifier}} {{/modifier}}{{{type}}} {{name}} = {{number}}{{#hasOptions}} [{{{options}}}]{{/hasOptions}};
12+
{{#modifier}}{{modifier}} {{/modifier}}{{{type}}} {{name}} = {{number}}{{#hasAnnotations}} [{{{annotations}}}]{{/hasAnnotations}};
1313
{{/fields}}
1414
{{#oneofs}}
1515
{{#commentLines}}
@@ -20,7 +20,7 @@ message {{name}} {
2020
{{#commentLines}}
2121
// {{{.}}}
2222
{{/commentLines}}
23-
{{{type}}} {{name}} = {{number}}{{#hasOptions}} [{{{options}}}]{{/hasOptions}};
23+
{{{type}}} {{name}} = {{number}}{{#hasAnnotations}} [{{{annotations}}}]{{/hasAnnotations}};
2424

2525
{{/fields}}
2626
}
@@ -30,7 +30,7 @@ message {{name}} {
3030
{{#isEnum}}
3131
enum {{name}} {
3232
{{#values}}
33-
{{name}} = {{number}}{{#hasOptions}} [{{{options}}}]{{/hasOptions}};
33+
{{name}} = {{number}}{{#hasAnnotations}} [{{{annotations}}}]{{/hasAnnotations}};
3434
{{/values}}
3535
}
3636
{{/isEnum}}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Internal types for proto processing.
33
*/
44

5-
export interface FieldOption {
5+
export interface Annotation {
66
name: string;
77
value: string;
88
}
@@ -13,7 +13,7 @@ export interface ProtoField {
1313
number: number;
1414
modifier?: string; // 'optional' | 'repeated'
1515
comment?: string;
16-
options?: FieldOption[];
16+
annotations?: Annotation[];
1717
}
1818

1919
export interface ProtoOneof {
@@ -26,7 +26,7 @@ export interface ProtoEnumValue {
2626
name: string;
2727
number: number;
2828
comment?: string;
29-
options?: FieldOption[];
29+
annotations?: Annotation[];
3030
}
3131

3232
export interface ProtoEnum {

0 commit comments

Comments
 (0)