1
- import { HttpMethod , OpenAPI2Document , OpenAPI2Operation , OpenAPI2Response , Refable } from "@azure-tools/typespec-autorest" ;
1
+ import { HttpMethod , OpenAPI2Document , OpenAPI2Operation , OpenAPI2Response , OpenAPI2Schema , OpenAPI2SchemaProperty , Ref , Refable } from "@azure-tools/typespec-autorest" ;
2
+ import { getCommonTypeDefinition } from "./commonType.js" ;
2
3
3
4
interface Diff {
4
5
before : any ;
@@ -21,16 +22,188 @@ interface PathDiff extends Diff {
21
22
type : "path" | "parameters" | "pageable" | "longrunning" | "finalstate" | "finalresult" | "responses" | "response" ;
22
23
}
23
24
24
- export function printPathDiff ( diff : PathDiff ) : string {
25
- return `| ${ diff . type } | ${ diff . level } | The ${ diff . type } of operation "${ diff . operationId } " changed: ${ JSON . stringify ( diff . before ) } -> ${ JSON . stringify ( diff . after ) } |\n` ;
25
+ /**
26
+ * properties: the property names of the definition should be the same
27
+ * property: the property should have same schema
28
+ * required: the required properties of the definition should be the same
29
+ */
30
+ interface DefinitionDiff extends Diff {
31
+ name : string ;
32
+ propertyName ?: string ;
33
+ changeType ?: string ;
34
+ type : "properties" | "property" | "required" ;
35
+ }
36
+
37
+ export function printDiff ( diff : PathDiff | DefinitionDiff ) : string {
38
+ if ( "operationId" in diff ) {
39
+ return `| ${ diff . type } | ${ diff . level } | ${ getPathDiffMessage ( diff ) } ${ JSON . stringify ( diff . before ) } -> ${ JSON . stringify ( diff . after ) } |\n` ;
40
+ } else {
41
+ return `| ${ diff . type } | ${ diff . level } | ${ getDefinitionDiffMessage ( diff ) } ${ JSON . stringify ( diff . before ) } -> ${ JSON . stringify ( diff . after ) } |\n` ;
42
+ }
26
43
}
27
44
28
- export function compareDocuments ( oldDocument : OpenAPI2Document , newDocument : OpenAPI2Document ) {
29
- const diffs : PathDiff [ ] = [ ] ;
45
+ function getPathDiffMessage ( diff : PathDiff ) : string {
46
+ switch ( diff . type ) {
47
+ case "path" :
48
+ return `The path for operation "${ diff . operationId } " changed:` ;
49
+ case "parameters" :
50
+ return `The number of parameters for operation "${ diff . operationId } " changed:` ;
51
+ case "pageable" :
52
+ return `The pageable for operation "${ diff . operationId } " changed:` ;
53
+ case "longrunning" :
54
+ return `The long-running status for operation "${ diff . operationId } " changed:` ;
55
+ case "finalstate" :
56
+ return `The final state for operation "${ diff . operationId } " changed:` ;
57
+ case "finalresult" :
58
+ return `The final result schema for operation "${ diff . operationId } " changed:` ;
59
+ case "responses" :
60
+ return `The response codes for operation "${ diff . operationId } " changed:` ;
61
+ case "response" :
62
+ return `The response schema for operation "${ diff . operationId } " changed:` ;
63
+ default :
64
+ return `The ${ diff . type } for operation "${ diff . operationId } " changed:` ;
65
+ }
66
+ }
67
+
68
+ function getDefinitionDiffMessage ( diff : DefinitionDiff ) : string {
69
+ switch ( diff . type ) {
70
+ case "properties" :
71
+ return `The property names of definition "${ diff . name } " changed:` ;
72
+ case "property" :
73
+ return `The ${ diff . changeType } of property "${ diff . propertyName } " in definition "${ diff . name } " changed:` ;
74
+ case "required" :
75
+ return `The required properties of definition "${ diff . name } " changed:` ;
76
+ }
77
+ }
78
+
79
+ export function compareDocuments ( oldDocument : OpenAPI2Document , newDocument : OpenAPI2Document ) : ( PathDiff | DefinitionDiff ) [ ] {
80
+ const diffs : ( PathDiff | DefinitionDiff ) [ ] = [ ] ;
30
81
diffs . push ( ...comparePaths ( oldDocument , newDocument ) ) ;
82
+
83
+ // If not exists in the new document, it might be an orphaned definition previously
84
+ for ( const definition in newDocument . definitions ?? { } ) {
85
+ // If not exists in old document, it might be an anonymous definition previously
86
+ if ( oldDocument . definitions ?. [ definition ] ) {
87
+ diffs . push ( ...compareNamedDefinition ( oldDocument . definitions [ definition ] , oldDocument , newDocument . definitions ! [ definition ] , newDocument , definition ) ) ;
88
+ }
89
+ }
90
+ return diffs ;
91
+ }
92
+
93
+ const definitionNameCache : Set < string > = new Set ( ) ;
94
+ function compareNamedDefinition ( oldDefinition : OpenAPI2Schema , oldDocument : OpenAPI2Document , newDefinition : OpenAPI2Schema , newDocument : OpenAPI2Document , name : string ) : DefinitionDiff [ ] {
95
+ if ( definitionNameCache . has ( name ) ) {
96
+ console . warn ( `Definition "${ name } " has been compared before, skipping.` ) ;
97
+ return [ ] ;
98
+ }
99
+ definitionNameCache . add ( name ) ;
100
+
101
+ const diffs : DefinitionDiff [ ] = [ ] ;
102
+
103
+ const oldRequired = oldDefinition . required || [ ] ;
104
+ const newRequired = newDefinition . required || [ ] ;
105
+
106
+ if ( oldRequired . length !== newRequired . length ||
107
+ ! oldRequired . every ( item => newRequired . includes ( item ) ) ||
108
+ ! newRequired . every ( item => oldRequired . includes ( item ) ) ) {
109
+ diffs . push ( {
110
+ before : oldRequired ,
111
+ after : newRequired ,
112
+ name,
113
+ type : "required" ,
114
+ level : "warning" ,
115
+ } ) ;
116
+ }
117
+
118
+ const oldProperties = getAllProperties ( oldDefinition , oldDocument ) ;
119
+ const sortedOldProperties = Object . keys ( oldProperties ) . sort ( ) . reduce ( ( obj : Record < string , OpenAPI2SchemaProperty > , key ) => {
120
+ obj [ key ] = oldProperties [ key ] ;
121
+ return obj ;
122
+ } , { } ) ;
123
+
124
+ const newProperties = getAllProperties ( newDefinition , newDocument ) ;
125
+ const sortedNewProperties = Object . keys ( newProperties ) . sort ( ) . reduce ( ( obj : Record < string , OpenAPI2SchemaProperty > , key ) => {
126
+ obj [ key ] = newProperties [ key ] ;
127
+ return obj ;
128
+ } , { } ) ;
129
+
130
+ // First check if the properties are the same
131
+ const oldKeys = Object . keys ( sortedOldProperties ) ;
132
+ const newKeys = Object . keys ( sortedNewProperties ) ;
133
+ if ( oldKeys . length !== newKeys . length ) {
134
+ // Check if newKeys has exactly one more key and it's systemData
135
+ const isSystemDataOnlyDifference = newKeys . length === oldKeys . length + 1 &&
136
+ newKeys . filter ( key => ! oldKeys . includes ( key ) ) . length === 1 &&
137
+ newKeys . filter ( key => ! oldKeys . includes ( key ) ) [ 0 ] === 'systemData' ;
138
+
139
+ diffs . push ( {
140
+ before : oldKeys ,
141
+ after : newKeys ,
142
+ name,
143
+ type : "properties" ,
144
+ level : isSystemDataOnlyDifference ? "warning" : "error"
145
+ } ) ;
146
+ }
147
+ for ( const key of oldKeys ) {
148
+ if ( ! newKeys . includes ( key ) ) {
149
+ diffs . push ( {
150
+ before : oldKeys ,
151
+ after : newKeys ,
152
+ name,
153
+ type : "properties" ,
154
+ level : "error"
155
+ } ) ;
156
+ }
157
+ }
158
+ for ( const key of newKeys ) {
159
+ if ( ! oldKeys . includes ( key ) ) {
160
+ // Check if the only additional key is systemData
161
+ const additionalKeys = newKeys . filter ( k => ! oldKeys . includes ( k ) ) ;
162
+ const isOnlySystemData = additionalKeys . length === 1 && additionalKeys [ 0 ] === 'systemData' ;
163
+
164
+ diffs . push ( {
165
+ before : oldKeys ,
166
+ after : newKeys ,
167
+ name,
168
+ type : "properties" ,
169
+ level : isOnlySystemData ? "warning" : "error"
170
+ } ) ;
171
+ }
172
+ }
173
+
174
+ // Then check if the property types are the same
175
+ for ( const key of oldKeys ) {
176
+ const oldProperty = sortedOldProperties [ key ] ;
177
+ const newProperty = sortedNewProperties [ key ] ;
178
+ diffs . push ( ...compareDefinitionProperty ( oldProperty , newProperty , oldDocument , newDocument , key , name ) ) ;
179
+ }
180
+
31
181
return diffs ;
32
182
}
33
183
184
+ function compareDefinitionProperty ( oldProperty : OpenAPI2SchemaProperty , newProperty : OpenAPI2SchemaProperty , oldDocument : OpenAPI2Document , newDocument : OpenAPI2Document , propertyName : string , modelName : string ) : DefinitionDiff [ ] {
185
+ const oldPropertySchema = resolveCommonType ( oldProperty ) ;
186
+ const newPropertySchema = resolveCommonType ( newProperty ) ;
187
+ if ( isRef ( oldPropertySchema ) && isRef ( newPropertySchema ) ) {
188
+ if ( oldPropertySchema . $ref !== newPropertySchema . $ref ) {
189
+ return [ {
190
+ before : oldPropertySchema . $ref ,
191
+ after : newPropertySchema . $ref ,
192
+ name : modelName ,
193
+ propertyName,
194
+ type : "property" ,
195
+ changeType : "reference" ,
196
+ level : "warning"
197
+ } ] ;
198
+ }
199
+ }
200
+ else if ( ! isRef ( oldPropertySchema ) && ! isRef ( newPropertySchema ) ) {
201
+ return compareNamedDefinition ( oldPropertySchema , oldDocument , newPropertySchema , newDocument , `${ modelName } .${ propertyName } ` ) ;
202
+ }
203
+
204
+ return [ ] ;
205
+ }
206
+
34
207
function comparePaths ( oldDocument : OpenAPI2Document , newDocument : OpenAPI2Document ) : PathDiff [ ] {
35
208
const oldOperations = organizeOperationById ( oldDocument ) ;
36
209
const newOperations = organizeOperationById ( newDocument ) ;
@@ -48,7 +221,7 @@ function comparePaths(oldDocument: OpenAPI2Document, newDocument: OpenAPI2Docume
48
221
}
49
222
else {
50
223
pathDiffs . push ( ...compareOperation ( oldOperations [ operationId ] [ 1 ] , newOperations [ operationId ] [ 1 ] , operationId ) ) ;
51
- }
224
+ }
52
225
}
53
226
for ( const operationId in newOperations ) {
54
227
if ( ! oldOperations [ operationId ] ) {
@@ -191,7 +364,7 @@ function compareLongRunning(oldOperation: OpenAPI2Operation, newOperation: OpenA
191
364
level : "error"
192
365
} ) ;
193
366
}
194
- }
367
+ }
195
368
196
369
return pathDiffs ;
197
370
}
@@ -204,7 +377,7 @@ function getResponseSchema(response: Refable<OpenAPI2Response> | undefined): str
204
377
205
378
return undefined ;
206
379
}
207
-
380
+
208
381
209
382
function organizeOperationById ( document : OpenAPI2Document ) : Record < string , [ string , OpenAPI2Operation ] > {
210
383
function isHttpMethod ( key : string ) : key is HttpMethod {
@@ -220,14 +393,14 @@ function organizeOperationById(document: OpenAPI2Document): Record<string, [stri
220
393
] ;
221
394
return httpMethods . includes ( key as HttpMethod ) ;
222
395
}
223
-
224
- const operationMap : Record < string , [ string , OpenAPI2Operation ] > = { } ;
396
+
397
+ const operationMap : Record < string , [ string , OpenAPI2Operation ] > = { } ;
225
398
if ( ! document . paths ) {
226
399
return operationMap ;
227
400
}
228
401
229
402
for ( const route in document . paths ) {
230
- const pathItem = document . paths [ route ] ;
403
+ const pathItem = document . paths [ route ] ;
231
404
for ( const verb in pathItem ) {
232
405
if ( isHttpMethod ( verb ) ) {
233
406
const operation = pathItem [ verb ] ;
@@ -237,7 +410,62 @@ function organizeOperationById(document: OpenAPI2Document): Record<string, [stri
237
410
}
238
411
}
239
412
}
240
-
413
+
241
414
return operationMap ;
242
415
}
243
416
417
+ function getAllProperties ( schema : OpenAPI2Schema , document : OpenAPI2Document , properties : Record < string , OpenAPI2SchemaProperty > = { } ) : Record < string , OpenAPI2SchemaProperty > {
418
+ for ( const baseSchema of schema . allOf ?? [ ] ) {
419
+ if ( isRef ( baseSchema ) ) {
420
+ const refPath = baseSchema . $ref ;
421
+ const commonTypeDefinition = getCommonTypeDefinition ( refPath ) ;
422
+ if ( commonTypeDefinition ) {
423
+ getAllProperties ( commonTypeDefinition [ 0 ] , commonTypeDefinition [ 1 ] , properties ) ;
424
+ }
425
+ else {
426
+ const baseDefinition = getLocalDefinition ( refPath , document ) ;
427
+ if ( baseDefinition ) {
428
+ getAllProperties ( baseDefinition , document , properties ) ;
429
+ }
430
+ }
431
+ }
432
+ else {
433
+ if ( baseSchema . properties ) {
434
+ for ( const key in baseSchema . properties ) {
435
+ properties [ key ] = baseSchema . properties [ key ] ;
436
+ }
437
+ }
438
+ }
439
+ }
440
+
441
+ if ( schema . properties ) {
442
+ for ( const key in schema . properties ) {
443
+ properties [ key ] = schema . properties [ key ] ;
444
+ }
445
+ }
446
+ return properties ;
447
+ }
448
+
449
+ function resolveCommonType ( property : OpenAPI2SchemaProperty ) : OpenAPI2SchemaProperty {
450
+ if ( isRef ( property ) ) {
451
+ const { $ref, ...propertyWithoutRef } = property ;
452
+ const commonTypeDefinition = getCommonTypeDefinition ( $ref ) ;
453
+ if ( commonTypeDefinition ) {
454
+ return { ...propertyWithoutRef , ...commonTypeDefinition [ 0 ] } ;
455
+ }
456
+ }
457
+
458
+ return property ;
459
+ }
460
+
461
+ function getLocalDefinition ( refPath : string , document : OpenAPI2Document ) : OpenAPI2Schema | undefined {
462
+ const definitionName = refPath . split ( "/" ) . pop ( ) ;
463
+ if ( ! document . definitions || ! document . definitions [ definitionName ! ] ) {
464
+ console . warn ( `Reference to ${ definitionName } cannot be found, skipping.` ) ;
465
+ }
466
+ return document . definitions ?. [ definitionName ! ] ;
467
+ }
468
+
469
+ export function isRef < T > ( value : Refable < T > ) : value is Ref < T > {
470
+ return ( value as Ref < T > ) . $ref !== undefined ;
471
+ }
0 commit comments