Skip to content

Commit 9f61df2

Browse files
committed
Address issue #457
1 parent 98804ed commit 9f61df2

File tree

2 files changed

+284
-2
lines changed

2 files changed

+284
-2
lines changed

index/extract_refs.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ import (
77
"context"
88
"errors"
99
"fmt"
10-
"github.com/pb33f/libopenapi/utils"
11-
"gopkg.in/yaml.v3"
1210
"net/url"
1311
"os"
1412
"path/filepath"
1513
"slices"
1614
"strconv"
1715
"strings"
16+
17+
"github.com/pb33f/libopenapi/utils"
18+
"gopkg.in/yaml.v3"
1819
)
1920

2021
// ExtractRefs will return a deduplicated slice of references for every unique ref found in the document.
@@ -472,6 +473,15 @@ func (index *SpecIndex) ExtractRefs(ctx context.Context, node, parent *yaml.Node
472473
if utils.IsNodeArray(node) {
473474
continue
474475
}
476+
// Skip if "description" is a property name inside schema properties
477+
// We check if the previous element in seenPath is "properties" and this is at an even index
478+
// (property names are at even indices, values at odd)
479+
if len(seenPath) > 0 && (seenPath[len(seenPath)-1] == "properties" || seenPath[len(seenPath)-1] == "patternProperties") {
480+
// This means "description" is a property name, not a description field, skip extraction
481+
seenPath = append(seenPath, strings.ReplaceAll(n.Value, "/", "~1"))
482+
prev = n.Value
483+
continue
484+
}
475485
if !slices.Contains(seenPath, "example") && !slices.Contains(seenPath, "examples") {
476486
ref := &DescriptionReference{
477487
ParentNode: parent,
@@ -491,6 +501,16 @@ func (index *SpecIndex) ExtractRefs(ctx context.Context, node, parent *yaml.Node
491501

492502
if n.Value == "summary" {
493503

504+
// Skip if "summary" is a property name inside schema properties
505+
// We check if the previous element in seenPath is "properties" and this is at an even index
506+
// (property names are at even indices, values at odd)
507+
if len(seenPath) > 0 && (seenPath[len(seenPath)-1] == "properties" || seenPath[len(seenPath)-1] == "patternProperties") {
508+
// This means "summary" is a property name, not a summary field, skip extraction
509+
seenPath = append(seenPath, strings.ReplaceAll(n.Value, "/", "~1"))
510+
prev = n.Value
511+
continue
512+
}
513+
494514
if slices.Contains(seenPath, "example") || slices.Contains(seenPath, "examples") {
495515
continue
496516
}

index/extract_refs_test.go

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,268 @@ func TestSpecIndex_ExtractRefs_CheckSummarySummary(t *testing.T) {
4949
assert.Equal(t, 3, idx.summaryCount)
5050
}
5151

52+
// https://github.com/pb33f/libopenapi/issues/457
53+
func TestSpecIndex_ExtractRefs_SkipSummaryInSchemaProperties(t *testing.T) {
54+
// Test case for issue #457
55+
// When a schema has a property named "summary", it should NOT be extracted as a summary description
56+
yml := `openapi: 3.1.1
57+
info:
58+
title: Test API
59+
version: 1.0.0
60+
summary: This is an API summary
61+
paths:
62+
/tasks:
63+
get:
64+
summary: Get all tasks
65+
description: Returns all tasks
66+
responses:
67+
200:
68+
description: Successful response
69+
content:
70+
application/json:
71+
schema:
72+
$ref: '#/components/schemas/Task'
73+
components:
74+
schemas:
75+
Task:
76+
type: object
77+
description: A task object
78+
properties:
79+
id:
80+
type: string
81+
description: Task ID
82+
summary:
83+
type: boolean
84+
description: Whether this is a summary task
85+
name:
86+
type: string
87+
description: Task name
88+
Project:
89+
type: object
90+
properties:
91+
summary:
92+
type: boolean
93+
description: Project summary flag
94+
description:
95+
type: string
96+
description: Project description text`
97+
98+
var rootNode yaml.Node
99+
_ = yaml.Unmarshal([]byte(yml), &rootNode)
100+
c := CreateOpenAPIIndexConfig()
101+
idx := NewSpecIndexWithConfig(&rootNode, c)
102+
103+
// Should only capture summaries from info and operations, NOT from schema properties
104+
assert.Equal(t, 2, idx.summaryCount, "Should only have 2 summaries (info.summary and path operation summary)")
105+
106+
// Verify that the captured summaries are the correct ones
107+
summaryContents := []string{}
108+
for _, summary := range idx.allSummaries {
109+
summaryContents = append(summaryContents, summary.Content)
110+
}
111+
assert.Contains(t, summaryContents, "This is an API summary", "Should contain info.summary")
112+
assert.Contains(t, summaryContents, "Get all tasks", "Should contain operation summary")
113+
114+
// Should not contain the boolean property names as summaries
115+
for _, summary := range idx.allSummaries {
116+
assert.NotEqual(t, "boolean", summary.Content, "Should not extract schema property type as summary")
117+
}
118+
119+
// Check descriptions - should have proper descriptions but not property "description" fields
120+
descriptionCount := idx.descriptionCount
121+
assert.Greater(t, descriptionCount, 0, "Should have some descriptions")
122+
123+
// Verify descriptions are from the right places (API descriptions, not property names)
124+
descriptionContents := []string{}
125+
for _, desc := range idx.allDescriptions {
126+
descriptionContents = append(descriptionContents, desc.Content)
127+
}
128+
assert.Contains(t, descriptionContents, "Returns all tasks", "Should contain operation description")
129+
assert.Contains(t, descriptionContents, "A task object", "Should contain schema description")
130+
}
131+
132+
// https://github.com/pb33f/libopenapi/issues/457
133+
func TestSpecIndex_ExtractRefs_SkipDescriptionInSchemaProperties(t *testing.T) {
134+
// Test that description properties in schemas are not extracted as API descriptions
135+
yml := `openapi: 3.1.0
136+
info:
137+
title: Test API
138+
version: 1.0.0
139+
description: Main API description
140+
paths:
141+
/items:
142+
get:
143+
description: Get items operation description
144+
responses:
145+
200:
146+
description: Success response description
147+
content:
148+
application/json:
149+
schema:
150+
type: object
151+
properties:
152+
description:
153+
type: string
154+
description: The item's description field
155+
title:
156+
type: string
157+
description: The item's title`
158+
159+
var rootNode yaml.Node
160+
_ = yaml.Unmarshal([]byte(yml), &rootNode)
161+
c := CreateOpenAPIIndexConfig()
162+
idx := NewSpecIndexWithConfig(&rootNode, c)
163+
164+
// Count descriptions - should not include the "description" property name
165+
expectedDescriptions := []string{
166+
"Main API description",
167+
"Get items operation description",
168+
"Success response description",
169+
"The item's description field",
170+
"The item's title",
171+
}
172+
173+
assert.Equal(t, len(expectedDescriptions), idx.descriptionCount,
174+
"Should only count actual descriptions, not property names")
175+
176+
// Verify the content
177+
actualContents := []string{}
178+
for _, desc := range idx.allDescriptions {
179+
actualContents = append(actualContents, desc.Content)
180+
}
181+
182+
for _, expected := range expectedDescriptions {
183+
assert.Contains(t, actualContents, expected,
184+
"Should contain description: %s", expected)
185+
}
186+
}
187+
188+
// https://github.com/pb33f/libopenapi/issues/457
189+
func TestSpecIndex_ExtractRefs_Issue457_SummaryPropertyConfusion(t *testing.T) {
190+
// Direct test for GitHub issue #457
191+
// Schema properties named "summary" should not be confused with API summary fields
192+
yml := `openapi: 3.1.1
193+
info:
194+
title: Issue 457 Test
195+
version: 1.0.0
196+
paths:
197+
/items:
198+
get:
199+
summary: List items
200+
responses:
201+
200:
202+
description: Success
203+
content:
204+
application/json:
205+
examples:
206+
taskExample:
207+
value:
208+
id: task-1
209+
summary: true
210+
name: Important task
211+
projectExample:
212+
value:
213+
id: project-1
214+
summary: false
215+
description: Project description
216+
schema:
217+
type: object
218+
properties:
219+
items:
220+
type: array
221+
items:
222+
oneOf:
223+
- $ref: '#/components/schemas/Task'
224+
- $ref: '#/components/schemas/Project'
225+
components:
226+
schemas:
227+
Task:
228+
type: object
229+
required:
230+
- id
231+
- summary
232+
properties:
233+
id:
234+
type: string
235+
summary:
236+
type: boolean
237+
description: Is this a summary task
238+
name:
239+
type: string
240+
Project:
241+
type: object
242+
required:
243+
- id
244+
- summary
245+
properties:
246+
id:
247+
type: string
248+
summary:
249+
type: boolean
250+
description: Is this a summary project
251+
description:
252+
type: string
253+
description: The project description`
254+
255+
var rootNode yaml.Node
256+
_ = yaml.Unmarshal([]byte(yml), &rootNode)
257+
c := CreateOpenAPIIndexConfig()
258+
idx := NewSpecIndexWithConfig(&rootNode, c)
259+
260+
// The key assertion: should only have 1 summary (from the operation)
261+
// NOT from the schema properties named "summary"
262+
assert.Equal(t, 1, idx.summaryCount, "Should only extract operation summary, not schema property names")
263+
264+
if idx.summaryCount > 0 {
265+
assert.Equal(t, "List items", idx.allSummaries[0].Content, "The only summary should be 'List items'")
266+
}
267+
268+
// Check that descriptions are properly counted
269+
// Should have: "Success", "Is this a summary task", "Is this a summary project", "The project description"
270+
assert.Equal(t, 4, idx.descriptionCount, "Should have 4 descriptions total")
271+
}
272+
273+
// https://github.com/pb33f/libopenapi/issues/457
274+
func TestSpecIndex_ExtractRefs_SkipSummaryInPatternProperties(t *testing.T) {
275+
// Test that summary/description in patternProperties are also skipped
276+
yml := `openapi: 3.1.0
277+
info:
278+
title: Test API
279+
version: 1.0.0
280+
paths:
281+
/items:
282+
get:
283+
summary: Get items
284+
responses:
285+
200:
286+
description: Success
287+
content:
288+
application/json:
289+
schema:
290+
type: object
291+
patternProperties:
292+
"^S_":
293+
type: string
294+
summary:
295+
type: boolean
296+
description: Pattern property named summary
297+
description:
298+
type: string
299+
description: Pattern property named description`
300+
301+
var rootNode yaml.Node
302+
_ = yaml.Unmarshal([]byte(yml), &rootNode)
303+
c := CreateOpenAPIIndexConfig()
304+
idx := NewSpecIndexWithConfig(&rootNode, c)
305+
306+
// Should only have 1 summary from the operation
307+
assert.Equal(t, 1, idx.summaryCount, "Should only have operation summary, not patternProperties property names")
308+
assert.Equal(t, "Get items", idx.allSummaries[0].Content)
309+
310+
// Should have 3 descriptions: "Success", plus the two pattern property descriptions
311+
assert.Equal(t, 3, idx.descriptionCount, "Should have 3 descriptions")
312+
}
313+
52314
func TestSpecIndex_ExtractRefs_CheckPropertiesForInlineSchema(t *testing.T) {
53315
yml := `openapi: 3.1.0
54316
servers:

0 commit comments

Comments
 (0)