-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathconvert-openapi-31-to-30.go
More file actions
342 lines (299 loc) · 8.6 KB
/
convert-openapi-31-to-30.go
File metadata and controls
342 lines (299 loc) · 8.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
//go:build ignore
// convert-openapi-31-to-30.go converts an OpenAPI 3.1 spec to 3.0.3 compatible format.
// This is needed because oapi-codegen does not fully support OpenAPI 3.1.
//
// Main transformations:
// 1. Changes openapi version from 3.1.x to 3.0.3
// 2. Converts anyOf[{type:X},{type:null}] to type:X with nullable:true
// 3. Converts anyOf[{$ref:X},{type:null}] to $ref:X with nullable:true
// 4. Converts anyOf with multiple types + null to {} (any type) with nullable:true
// 5. Removes standalone {type:null} entries from anyOf arrays
// 6. Adds missing path parameter declarations to operations (oapi-codegen requires all path params declared)
//
// Usage: go run scripts/convert-openapi-31-to-30.go < input.json > output.json
package main
import (
"encoding/json"
"fmt"
"os"
"regexp"
)
func main() {
// Read input
var spec map[string]interface{}
if err := json.NewDecoder(os.Stdin).Decode(&spec); err != nil {
fmt.Fprintf(os.Stderr, "Error reading JSON: %v\n", err)
os.Exit(1)
}
// Transform the spec
transformSpec(spec)
// Write output
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
if err := encoder.Encode(spec); err != nil {
fmt.Fprintf(os.Stderr, "Error writing JSON: %v\n", err)
os.Exit(1)
}
}
// conflictingSchemas maps schema names that conflict with enum value names
// to their renamed versions. oapi-codegen converts "issue_type" to "IssueType"
// which conflicts with the "IssueType" schema.
var conflictingSchemas = map[string]string{
"IssueType": "IssueTypeEnum",
"ReviewDecision": "ReviewDecisionEnum",
}
func transformSpec(spec map[string]interface{}) {
// Change OpenAPI version from 3.1.x to 3.0.3
if version, ok := spec["openapi"].(string); ok {
if len(version) >= 3 && version[:3] == "3.1" {
spec["openapi"] = "3.0.3"
}
}
// Rename conflicting schemas
renameConflictingSchemas(spec)
// Fix operations missing declared path parameters
fixMissingPathParams(spec)
// Recursively transform the entire spec
transformValue(spec)
}
// renameConflictingSchemas renames schemas that conflict with enum value names
func renameConflictingSchemas(spec map[string]interface{}) {
// Get the components/schemas section
components, ok := spec["components"].(map[string]interface{})
if !ok {
return
}
schemas, ok := components["schemas"].(map[string]interface{})
if !ok {
return
}
// Rename conflicting schemas
for oldName, newName := range conflictingSchemas {
if schema, exists := schemas[oldName]; exists {
delete(schemas, oldName)
schemas[newName] = schema
}
}
// Now update all $ref references throughout the spec
updateRefs(spec)
}
// updateRefs updates all $ref references to use renamed schema names
func updateRefs(v interface{}) {
switch val := v.(type) {
case map[string]interface{}:
// Check if this object has a $ref
if ref, ok := val["$ref"].(string); ok {
for oldName, newName := range conflictingSchemas {
oldRef := "#/components/schemas/" + oldName
newRef := "#/components/schemas/" + newName
if ref == oldRef {
val["$ref"] = newRef
}
}
}
// Recurse into all values
for _, v := range val {
updateRefs(v)
}
case []interface{}:
for _, item := range val {
updateRefs(item)
}
}
}
// fixMissingPathParams ensures every operation declares all path parameters
// present in the URL template. oapi-codegen requires this.
func fixMissingPathParams(spec map[string]interface{}) {
paths, ok := spec["paths"].(map[string]interface{})
if !ok {
return
}
pathParamRe := regexp.MustCompile(`\{(\w+)\}`)
for pathStr, pathObj := range paths {
matches := pathParamRe.FindAllStringSubmatch(pathStr, -1)
if len(matches) == 0 {
continue
}
urlParams := make(map[string]bool)
for _, m := range matches {
urlParams[m[1]] = true
}
methods, ok := pathObj.(map[string]interface{})
if !ok {
continue
}
for method, details := range methods {
op, ok := details.(map[string]interface{})
if !ok {
continue
}
params, _ := op["parameters"].([]interface{})
declared := make(map[string]bool)
for _, p := range params {
pm, ok := p.(map[string]interface{})
if !ok {
continue
}
if pm["in"] == "path" {
if name, ok := pm["name"].(string); ok {
declared[name] = true
}
}
}
// Find a sibling operation that declares the missing param so we can copy its schema
for paramName := range urlParams {
if declared[paramName] {
continue
}
var paramDef map[string]interface{}
// Look in sibling operations for the parameter definition
for otherMethod, otherDetails := range methods {
if otherMethod == method {
continue
}
otherOp, ok := otherDetails.(map[string]interface{})
if !ok {
continue
}
otherParams, _ := otherOp["parameters"].([]interface{})
for _, p := range otherParams {
pm, ok := p.(map[string]interface{})
if !ok {
continue
}
if pm["in"] == "path" {
if name, ok := pm["name"].(string); ok && name == paramName {
paramDef = pm
break
}
}
}
if paramDef != nil {
break
}
}
if paramDef != nil {
// Copy the parameter definition
newParam := make(map[string]interface{})
for k, v := range paramDef {
newParam[k] = v
}
params = append(params, newParam)
} else {
// Fallback: synthesize a string path parameter
params = append(params, map[string]interface{}{
"in": "path",
"name": paramName,
"required": true,
"schema": map[string]interface{}{"type": "string"},
})
}
fmt.Fprintf(os.Stderr, "Fixed missing path param %q on %s %s\n", paramName, method, pathStr)
}
op["parameters"] = params
}
}
}
func transformValue(v interface{}) {
switch val := v.(type) {
case map[string]interface{}:
transformObject(val)
case []interface{}:
for _, item := range val {
transformValue(item)
}
}
}
func transformObject(obj map[string]interface{}) {
// Check if this is a schema with anyOf containing null type
if anyOf, ok := obj["anyOf"].([]interface{}); ok {
transformed := transformAnyOfWithNull(anyOf)
if transformed != nil {
// Replace the anyOf with the transformed schema
delete(obj, "anyOf")
for k, v := range transformed {
obj[k] = v
}
} else {
// Remove null type entries from anyOf arrays that couldn't be fully transformed
obj["anyOf"] = removeNullFromAnyOf(anyOf)
}
}
// Handle oneOf similarly (less common but possible)
if oneOf, ok := obj["oneOf"].([]interface{}); ok {
transformed := transformAnyOfWithNull(oneOf)
if transformed != nil {
delete(obj, "oneOf")
for k, v := range transformed {
obj[k] = v
}
} else {
obj["oneOf"] = removeNullFromAnyOf(oneOf)
}
}
// Recurse into all nested values
for _, v := range obj {
transformValue(v)
}
}
// isNullType checks if a schema represents the null type
func isNullType(schema map[string]interface{}) bool {
typeVal, ok := schema["type"].(string)
return ok && typeVal == "null"
}
// removeNullFromAnyOf removes {type:null} entries from anyOf arrays
// and adds nullable:true to the remaining schemas if null was present
func removeNullFromAnyOf(anyOf []interface{}) []interface{} {
var result []interface{}
hasNull := false
for _, item := range anyOf {
schema, ok := item.(map[string]interface{})
if !ok {
result = append(result, item)
continue
}
if isNullType(schema) {
hasNull = true
continue
}
result = append(result, item)
}
// If we found and removed null, we should ideally mark the anyOf as nullable
// but OpenAPI 3.0 doesn't support nullable on anyOf directly
// We'll leave it as is - oapi-codegen should handle the non-null types
_ = hasNull
return result
}
// transformAnyOfWithNull checks if an anyOf/oneOf array contains a null type
// and can be simplified to a nullable schema
func transformAnyOfWithNull(anyOf []interface{}) map[string]interface{} {
var nonNullSchemas []map[string]interface{}
hasNull := false
for _, item := range anyOf {
schema, ok := item.(map[string]interface{})
if !ok {
return nil
}
// Check if this is the null type
if isNullType(schema) {
hasNull = true
continue
}
nonNullSchemas = append(nonNullSchemas, schema)
}
if !hasNull {
return nil
}
// Case 1: Single non-null schema -> make it nullable
if len(nonNullSchemas) == 1 {
result := make(map[string]interface{})
for k, v := range nonNullSchemas[0] {
result[k] = v
}
result["nullable"] = true
return result
}
// Case 2: Multiple non-null schemas with $refs -> keep anyOf without null
// but return nil to let removeNullFromAnyOf handle it
return nil
}