Skip to content

Commit f5724d6

Browse files
Add x-tag-groups migration code
1 parent fd2a04c commit f5724d6

File tree

3 files changed

+541
-5
lines changed

3 files changed

+541
-5
lines changed

openapi/openapi_examples_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ func Example_walking() {
359359
// Found Info: Test OpenAPI Document (version 1.0.0)
360360
// Found Schema of type: string
361361
// Found Operation: test
362+
// Found Operation: copyTest
362363
// Found Schema of type: integer
363364
// Found Operation: updateUser
364365
// Walk terminated early

openapi/upgrade.go

Lines changed: 187 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"slices"
77

8+
"github.com/speakeasy-api/openapi/extensions"
89
"github.com/speakeasy-api/openapi/internal/version"
910
"github.com/speakeasy-api/openapi/jsonschema/oas3"
1011
"github.com/speakeasy-api/openapi/marshaller"
@@ -77,7 +78,9 @@ func Upgrade(ctx context.Context, doc *OpenAPI, opts ...Option[UpgradeOptions])
7778
// add logic to skip certain upgrades in certain situations in the future
7879
upgradeFrom30To31(ctx, doc, currentVersion, targetVersion)
7980
upgradeFrom310To312(ctx, doc, currentVersion, targetVersion)
80-
upgradeFrom31To32(ctx, doc, currentVersion, targetVersion)
81+
if err := upgradeFrom31To32(ctx, doc, currentVersion, targetVersion); err != nil {
82+
return false, err
83+
}
8184

8285
_, err = marshaller.Sync(ctx, doc)
8386
return true, err
@@ -113,25 +116,30 @@ func upgradeFrom310To312(_ context.Context, doc *OpenAPI, currentVersion *versio
113116
doc.OpenAPI = maxVersion.String()
114117
}
115118

116-
func upgradeFrom31To32(ctx context.Context, doc *OpenAPI, currentVersion *version.Version, targetVersion *version.Version) {
119+
func upgradeFrom31To32(ctx context.Context, doc *OpenAPI, currentVersion *version.Version, targetVersion *version.Version) error {
117120
if !targetVersion.GreaterThan(*currentVersion) {
118-
return
121+
return nil
119122
}
120123

121124
// Upgrade path additionalOperations for non-standard HTTP methods
122125
migrateAdditionalOperations31to32(ctx, doc)
123126

124-
// TODO: Upgrade tags such as x-displayName to summary, and x-tagGroups with parents, etc.
127+
// Upgrade tags from extensions to new 3.2 fields
128+
if err := migrateTags31to32(ctx, doc); err != nil {
129+
return err
130+
}
125131

126132
// Currently no breaking changes between 3.1.x and 3.2.x that need to be handled
127133
maxVersion, err := version.ParseVersion("3.2.0")
128134
if err != nil {
129-
panic("failed to parse hardcoded version 3.2.0")
135+
return err
130136
}
131137
if targetVersion.LessThan(*maxVersion) {
132138
maxVersion = targetVersion
133139
}
134140
doc.OpenAPI = maxVersion.String()
141+
142+
return nil
135143
}
136144

137145
// migrateAdditionalOperations31to32 migrates non-standard HTTP methods from the main operations map
@@ -174,6 +182,180 @@ func migrateAdditionalOperations31to32(_ context.Context, doc *OpenAPI) {
174182
}
175183
}
176184

185+
// migrateTags31to32 migrates tag extensions to new OpenAPI 3.2 tag fields
186+
func migrateTags31to32(_ context.Context, doc *OpenAPI) error {
187+
if doc == nil {
188+
return nil
189+
}
190+
191+
// First, migrate x-displayName to summary for individual tags
192+
if doc.Tags != nil {
193+
for _, tag := range doc.Tags {
194+
if err := migrateTagDisplayName(tag); err != nil {
195+
return err
196+
}
197+
}
198+
}
199+
200+
// Second, migrate x-tagGroups to parent relationships
201+
// This should always run to process extensions, even if no tags exist yet
202+
if err := migrateTagGroups(doc); err != nil {
203+
return err
204+
}
205+
206+
return nil
207+
}
208+
209+
// migrateTagDisplayName migrates x-displayName extension to summary field
210+
func migrateTagDisplayName(tag *Tag) error {
211+
if tag == nil || tag.Extensions == nil {
212+
return nil
213+
}
214+
215+
// Check if x-displayName extension exists and summary is not already set
216+
if displayNameExt, exists := tag.Extensions.Get("x-displayName"); exists {
217+
if tag.Summary != nil {
218+
// Error out if we can't migrate as summary is already set
219+
return fmt.Errorf("cannot migrate x-displayName to summary for tag %q as summary is already set", tag.Name)
220+
}
221+
// The extension value is stored as a string
222+
if displayNameExt.Value != "" {
223+
displayName := displayNameExt.Value
224+
tag.Summary = &displayName
225+
// Remove the extension after migration
226+
tag.Extensions.Delete("x-displayName")
227+
}
228+
}
229+
return nil
230+
}
231+
232+
// TagGroup represents a single tag group from x-tagGroups extension
233+
type TagGroup struct {
234+
Name string `yaml:"name"`
235+
Tags []string `yaml:"tags"`
236+
}
237+
238+
// migrateTagGroups migrates x-tagGroups extension to parent field relationships
239+
func migrateTagGroups(doc *OpenAPI) error {
240+
if doc.Extensions == nil {
241+
return nil
242+
}
243+
244+
// Check if x-tagGroups extension exists first
245+
_, exists := doc.Extensions.Get("x-tagGroups")
246+
if !exists {
247+
return nil // No x-tagGroups extension found
248+
}
249+
250+
// Parse x-tagGroups extension
251+
tagGroups, err := extensions.GetExtensionValue[[]TagGroup](doc.Extensions, "x-tagGroups")
252+
if err != nil {
253+
return fmt.Errorf("failed to parse x-tagGroups extension: %w", err)
254+
}
255+
256+
// Always remove the extension, even if empty or invalid
257+
defer doc.Extensions.Delete("x-tagGroups")
258+
259+
if tagGroups == nil || len(*tagGroups) == 0 {
260+
return nil // Nothing to migrate
261+
}
262+
263+
// Initialize tags slice if it doesn't exist
264+
if doc.Tags == nil {
265+
doc.Tags = []*Tag{}
266+
}
267+
268+
// Create a map for quick tag lookup
269+
tagMap := make(map[string]*Tag)
270+
for _, tag := range doc.Tags {
271+
if tag != nil {
272+
tagMap[tag.Name] = tag
273+
}
274+
}
275+
276+
// Process each tag group
277+
for _, group := range *tagGroups {
278+
if group.Name == "" {
279+
continue // Skip groups without names
280+
}
281+
282+
// Ensure parent tag exists for this group
283+
parentTag := ensureParentTagExists(doc, tagMap, group.Name)
284+
if parentTag == nil {
285+
return fmt.Errorf("failed to create parent tag for group: %s", group.Name)
286+
}
287+
288+
// Set parent relationships for all child tags in this group
289+
for _, childTagName := range group.Tags {
290+
if childTagName == "" {
291+
continue // Skip empty tag names
292+
}
293+
294+
if err := setTagParent(doc, tagMap, childTagName, group.Name); err != nil {
295+
return fmt.Errorf("failed to set parent for tag %s in group %s: %w", childTagName, group.Name, err)
296+
}
297+
}
298+
}
299+
300+
return nil
301+
}
302+
303+
// ensureParentTagExists creates a parent tag if it doesn't already exist
304+
func ensureParentTagExists(doc *OpenAPI, tagMap map[string]*Tag, groupName string) *Tag {
305+
// Check if parent tag already exists
306+
if existingTag, exists := tagMap[groupName]; exists {
307+
// Set kind to "nav" if not already set (common pattern for navigation groups)
308+
if existingTag.Kind == nil {
309+
kind := "nav"
310+
existingTag.Kind = &kind
311+
}
312+
return existingTag
313+
}
314+
315+
// Create new parent tag
316+
kind := "nav"
317+
parentTag := &Tag{
318+
Name: groupName,
319+
Summary: &groupName, // Use group name as summary for display
320+
Kind: &kind,
321+
}
322+
323+
// Add to document and map
324+
doc.Tags = append(doc.Tags, parentTag)
325+
tagMap[groupName] = parentTag
326+
327+
return parentTag
328+
}
329+
330+
// setTagParent sets the parent field for a child tag, creating the child tag if it doesn't exist
331+
func setTagParent(doc *OpenAPI, tagMap map[string]*Tag, childTagName, parentTagName string) error {
332+
// Prevent self-referencing (tag can't be its own parent)
333+
if childTagName == parentTagName {
334+
return fmt.Errorf("tag cannot be its own parent: %s", childTagName)
335+
}
336+
337+
// Check if child tag exists
338+
childTag, exists := tagMap[childTagName]
339+
if !exists {
340+
// Create child tag if it doesn't exist
341+
childTag = &Tag{
342+
Name: childTagName,
343+
}
344+
doc.Tags = append(doc.Tags, childTag)
345+
tagMap[childTagName] = childTag
346+
}
347+
348+
// Check if child tag already has a different parent
349+
if childTag.Parent != nil && *childTag.Parent != parentTagName {
350+
return fmt.Errorf("tag %s already has parent %s, cannot assign new parent %s", childTagName, *childTag.Parent, parentTagName)
351+
}
352+
353+
// Set the parent relationship
354+
childTag.Parent = &parentTagName
355+
356+
return nil
357+
}
358+
177359
func upgradeSchema30to31(js *oas3.JSONSchema[oas3.Referenceable]) {
178360
if js == nil || js.IsReference() || js.IsRight() {
179361
return

0 commit comments

Comments
 (0)