11/**
22 * Merger module: Compare and merge messages/enums for backward compatibility.
3- *
43 */
54
65import {
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 */
4080function 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 */
48123export 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 */
183193export 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 }
0 commit comments