Skip to content

Commit b79f07f

Browse files
Added support for 3.2 tags
1 parent 79a5a47 commit b79f07f

File tree

8 files changed

+437
-0
lines changed

8 files changed

+437
-0
lines changed

openapi/bootstrap.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,27 @@ func createBootstrapTags() []*Tag {
6565
return []*Tag{
6666
{
6767
Name: "users",
68+
Summary: pointer.From("Users"),
6869
Description: pointer.From("User management operations"),
70+
Kind: pointer.From("nav"),
6971
ExternalDocs: &oas3.ExternalDocumentation{
7072
Description: pointer.From("User API documentation"),
7173
URL: "https://docs.example.com/users",
7274
},
7375
},
76+
{
77+
Name: "admin",
78+
Summary: pointer.From("Admin"),
79+
Description: pointer.From("Administrative operations"),
80+
Parent: pointer.From("users"),
81+
Kind: pointer.From("nav"),
82+
},
83+
{
84+
Name: "beta-features",
85+
Summary: pointer.From("Beta"),
86+
Description: pointer.From("Experimental features"),
87+
Kind: pointer.From("badge"),
88+
},
7489
}
7590
}
7691

openapi/core/tag.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ type Tag struct {
1010
marshaller.CoreModel `model:"tag"`
1111

1212
Name marshaller.Node[string] `key:"name"`
13+
Summary marshaller.Node[*string] `key:"summary"`
1314
Description marshaller.Node[*string] `key:"description"`
1415
ExternalDocs marshaller.Node[*oas3core.ExternalDocumentation] `key:"externalDocs"`
16+
Parent marshaller.Node[*string] `key:"parent"`
17+
Kind marshaller.Node[*string] `key:"kind"`
1518
Extensions core.Extensions `key:"extensions"`
1619
}

openapi/tag.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,16 @@ type Tag struct {
1717

1818
// The name of the tag.
1919
Name string
20+
// A short summary of the tag, used for display purposes.
21+
Summary *string
2022
// A description for the tag. May contain CommonMark syntax.
2123
Description *string
2224
// External documentation for this tag.
2325
ExternalDocs *oas3.ExternalDocumentation
26+
// The name of a tag that this tag is nested under. The named tag must exist in the API description.
27+
Parent *string
28+
// A machine-readable string to categorize what sort of tag it is.
29+
Kind *string
2430

2531
// Extensions provides a list of extensions to the Tag object.
2632
Extensions *extensions.Extensions
@@ -52,6 +58,30 @@ func (t *Tag) GetExternalDocs() *oas3.ExternalDocumentation {
5258
return t.ExternalDocs
5359
}
5460

61+
// GetSummary returns the value of the Summary field. Returns empty string if not set.
62+
func (t *Tag) GetSummary() string {
63+
if t == nil || t.Summary == nil {
64+
return ""
65+
}
66+
return *t.Summary
67+
}
68+
69+
// GetParent returns the value of the Parent field. Returns empty string if not set.
70+
func (t *Tag) GetParent() string {
71+
if t == nil || t.Parent == nil {
72+
return ""
73+
}
74+
return *t.Parent
75+
}
76+
77+
// GetKind returns the value of the Kind field. Returns empty string if not set.
78+
func (t *Tag) GetKind() string {
79+
if t == nil || t.Kind == nil {
80+
return ""
81+
}
82+
return *t.Kind
83+
}
84+
5585
// GetExtensions returns the value of the Extensions field. Returns an empty extensions map if not set.
5686
func (t *Tag) GetExtensions() *extensions.Extensions {
5787
if t == nil || t.Extensions == nil {
@@ -77,3 +107,61 @@ func (t *Tag) Validate(ctx context.Context, opts ...validation.Option) []error {
77107

78108
return errs
79109
}
110+
111+
// ValidateWithTags validates the Tag object in the context of all tags to check for parent relationships.
112+
// This should be called during document-level validation where all tags are available.
113+
func (t *Tag) ValidateWithTags(ctx context.Context, allTags []*Tag, opts ...validation.Option) []error {
114+
errs := t.Validate(ctx, opts...)
115+
116+
if t.Parent != nil && *t.Parent != "" {
117+
// Check if parent tag exists
118+
parentExists := false
119+
for _, tag := range allTags {
120+
if tag != nil && tag.Name == *t.Parent {
121+
parentExists = true
122+
break
123+
}
124+
}
125+
126+
if !parentExists {
127+
core := t.GetCore()
128+
errs = append(errs, validation.NewValueError(
129+
validation.NewMissingValueError("parent tag '%s' does not exist", *t.Parent),
130+
core, core.Parent))
131+
}
132+
133+
// Check for circular references
134+
if t.hasCircularParentReference(allTags, make(map[string]bool)) {
135+
core := t.GetCore()
136+
errs = append(errs, validation.NewValueError(
137+
validation.NewValueValidationError("circular parent reference detected for tag '%s'", t.Name),
138+
core, core.Parent))
139+
}
140+
}
141+
142+
return errs
143+
}
144+
145+
// hasCircularParentReference checks if this tag has a circular parent reference
146+
func (t *Tag) hasCircularParentReference(allTags []*Tag, visited map[string]bool) bool {
147+
if t == nil || t.Parent == nil || *t.Parent == "" {
148+
return false
149+
}
150+
151+
// If we've already visited this tag, we have a circular reference
152+
if visited[t.Name] {
153+
return true
154+
}
155+
156+
// Mark this tag as visited
157+
visited[t.Name] = true
158+
159+
// Find the parent tag and recursively check
160+
for _, tag := range allTags {
161+
if tag != nil && tag.Name == *t.Parent {
162+
return tag.hasCircularParentReference(allTags, visited)
163+
}
164+
}
165+
166+
return false
167+
}

openapi/tag_kind_registry.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package openapi
2+
3+
// TagKind represents commonly used values for the Tag.Kind field.
4+
// These values are registered in the OpenAPI Initiative's Tag Kind Registry
5+
// at https://spec.openapis.org/registry/tag-kind/
6+
type TagKind string
7+
8+
// Officially registered Tag Kind values from the OpenAPI Initiative registry
9+
const (
10+
// TagKindNav represents tags used for navigation purposes
11+
TagKindNav TagKind = "nav"
12+
13+
// TagKindBadge represents tags used for visible badges or labels
14+
TagKindBadge TagKind = "badge"
15+
16+
// TagKindAudience represents tags that categorize operations by target audience
17+
TagKindAudience TagKind = "audience"
18+
)
19+
20+
// String returns the string representation of the TagKind
21+
func (tk TagKind) String() string {
22+
return string(tk)
23+
}
24+
25+
// IsRegistered checks if the TagKind value is one of the officially registered values
26+
func (tk TagKind) IsRegistered() bool {
27+
switch tk {
28+
case TagKindNav, TagKindBadge, TagKindAudience:
29+
return true
30+
default:
31+
return false
32+
}
33+
}
34+
35+
// GetRegisteredTagKinds returns all officially registered tag kind values
36+
func GetRegisteredTagKinds() []TagKind {
37+
return []TagKind{
38+
TagKindNav,
39+
TagKindBadge,
40+
TagKindAudience,
41+
}
42+
}
43+
44+
// TagKindDescriptions provides human-readable descriptions for each registered tag kind
45+
var TagKindDescriptions = map[TagKind]string{
46+
TagKindNav: "Navigation - Used for structuring API documentation navigation",
47+
TagKindBadge: "Badge - Used for visible badges or labels in documentation",
48+
TagKindAudience: "Audience - Used to categorize operations by target audience",
49+
}
50+
51+
// GetTagKindDescription returns a human-readable description for a tag kind
52+
func GetTagKindDescription(kind TagKind) string {
53+
if desc, exists := TagKindDescriptions[kind]; exists {
54+
return desc
55+
}
56+
return "Custom tag kind - not in the official registry (any string value is allowed)"
57+
}

openapi/tag_unmarshal_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,95 @@ x-test: some-value
3939
require.True(t, ok)
4040
require.Equal(t, "some-value", ext.Value)
4141
}
42+
43+
func TestTag_Unmarshal_WithNewFields_Success(t *testing.T) {
44+
t.Parallel()
45+
46+
yml := `
47+
name: products
48+
summary: Products
49+
description: All product-related operations
50+
parent: catalog
51+
kind: nav
52+
externalDocs:
53+
description: Product API documentation
54+
url: https://example.com/products
55+
x-custom: custom-value
56+
`
57+
58+
var tag openapi.Tag
59+
60+
validationErrs, err := marshaller.Unmarshal(t.Context(), bytes.NewBufferString(yml), &tag)
61+
require.NoError(t, err)
62+
require.Empty(t, validationErrs)
63+
64+
require.Equal(t, "products", tag.GetName())
65+
require.Equal(t, "Products", tag.GetSummary())
66+
require.Equal(t, "All product-related operations", tag.GetDescription())
67+
require.Equal(t, "catalog", tag.GetParent())
68+
require.Equal(t, "nav", tag.GetKind())
69+
70+
extDocs := tag.GetExternalDocs()
71+
require.NotNil(t, extDocs)
72+
require.Equal(t, "Product API documentation", extDocs.GetDescription())
73+
require.Equal(t, "https://example.com/products", extDocs.GetURL())
74+
75+
ext, ok := tag.GetExtensions().Get("x-custom")
76+
require.True(t, ok)
77+
require.Equal(t, "custom-value", ext.Value)
78+
}
79+
80+
func TestTag_Unmarshal_MinimalNewFields_Success(t *testing.T) {
81+
t.Parallel()
82+
83+
yml := `
84+
name: minimal
85+
summary: Minimal Tag
86+
`
87+
88+
var tag openapi.Tag
89+
90+
validationErrs, err := marshaller.Unmarshal(t.Context(), bytes.NewBufferString(yml), &tag)
91+
require.NoError(t, err)
92+
require.Empty(t, validationErrs)
93+
94+
require.Equal(t, "minimal", tag.GetName())
95+
require.Equal(t, "Minimal Tag", tag.GetSummary())
96+
require.Equal(t, "", tag.GetDescription())
97+
require.Equal(t, "", tag.GetParent())
98+
require.Equal(t, "", tag.GetKind())
99+
}
100+
101+
func TestTag_Unmarshal_KindValues_Success(t *testing.T) {
102+
t.Parallel()
103+
104+
tests := []struct {
105+
name string
106+
kind string
107+
expected string
108+
}{
109+
{"nav kind", "nav", "nav"},
110+
{"badge kind", "badge", "badge"},
111+
{"audience kind", "audience", "audience"},
112+
{"custom kind", "custom-value", "custom-value"},
113+
}
114+
115+
for _, tt := range tests {
116+
t.Run(tt.name, func(t *testing.T) {
117+
t.Parallel()
118+
119+
yml := `
120+
name: test
121+
kind: ` + tt.kind
122+
123+
var tag openapi.Tag
124+
125+
validationErrs, err := marshaller.Unmarshal(t.Context(), bytes.NewBufferString(yml), &tag)
126+
require.NoError(t, err)
127+
require.Empty(t, validationErrs)
128+
129+
require.Equal(t, "test", tag.GetName())
130+
require.Equal(t, tt.expected, tag.GetKind())
131+
})
132+
}
133+
}

0 commit comments

Comments
 (0)