Skip to content

Commit 843213e

Browse files
committed
address issue #757
Address 3.0 description workaround
1 parent 8bbd1fa commit 843213e

File tree

2 files changed

+100
-9
lines changed

2 files changed

+100
-9
lines changed

functions/openapi/unnecessary_combinator.go

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,15 @@ func (uc UnnecessaryCombinator) RunRule(_ []*yaml.Node, context model.RuleFuncti
3939
return results
4040
}
4141

42-
// cache to prevent checking the same schema twice
42+
// in OAS 3.0.x, $ref siblings are ignored, so allOf is the only way to add properties
43+
isOAS30 := false
44+
if context.SpecInfo != nil {
45+
isOAS30 = context.SpecInfo.VersionNumeric >= 3.0 && context.SpecInfo.VersionNumeric < 3.1
46+
}
47+
4348
seen := make(map[string]bool)
4449

4550
buildResult := func(message, path string, node *yaml.Node, component v3.AcceptsRuleResults) model.RuleFunctionResult {
46-
// try to find all paths for this node if it's a schema
4751
var allPaths []string
4852
if schema, ok := component.(*v3.Schema); ok {
4953
_, allPaths = vacuumUtils.LocateSchemaPropertyPaths(context, schema, node, node)
@@ -57,7 +61,6 @@ func (uc UnnecessaryCombinator) RunRule(_ []*yaml.Node, context model.RuleFuncti
5761
Rule: context.Rule,
5862
}
5963

60-
// set the Paths array if we found multiple locations
6164
if len(allPaths) > 1 {
6265
result.Paths = allPaths
6366
}
@@ -69,6 +72,12 @@ func (uc UnnecessaryCombinator) RunRule(_ []*yaml.Node, context model.RuleFuncti
6972
checkCombinator := func(schema *v3.Schema, combinatorName string, combinatorSlice []*libopenapi_base.SchemaProxy,
7073
keyNode *yaml.Node) {
7174
if len(combinatorSlice) == 1 {
75+
// OAS 3.0.x: allOf with single $ref + sibling properties is legitimate
76+
if isOAS30 && combinatorName == "allOf" &&
77+
hasSiblingProperties(schema) && hasRefInCombinator(combinatorSlice) {
78+
return
79+
}
80+
7281
path := fmt.Sprintf("%s.%s", schema.GenerateJSONPath(), combinatorName)
7382
message := fmt.Sprintf("schema with `%s` combinator containing only one item "+
7483
"should be replaced with the item directly", combinatorName)
@@ -82,7 +91,6 @@ func (uc UnnecessaryCombinator) RunRule(_ []*yaml.Node, context model.RuleFuncti
8291
return
8392
}
8493

85-
// create cache key to prevent duplicate processing
8694
var cacheKey strings.Builder
8795
cacheKey.WriteString(schema.GenerateJSONPath())
8896
key := cacheKey.String()
@@ -92,23 +100,19 @@ func (uc UnnecessaryCombinator) RunRule(_ []*yaml.Node, context model.RuleFuncti
92100
}
93101
seen[key] = true
94102

95-
// check allOf combinator
96103
if schema.Value.AllOf != nil && len(schema.Value.AllOf) > 0 {
97104
checkCombinator(schema, "allOf", schema.Value.AllOf, schema.Value.GoLow().AllOf.GetKeyNode())
98105
}
99106

100-
// check anyOf combinator
101107
if schema.Value.AnyOf != nil && len(schema.Value.AnyOf) > 0 {
102108
checkCombinator(schema, "anyOf", schema.Value.AnyOf, schema.Value.GoLow().AnyOf.GetKeyNode())
103109
}
104110

105-
// check oneOf combinator
106111
if schema.Value.OneOf != nil && len(schema.Value.OneOf) > 0 {
107112
checkCombinator(schema, "oneOf", schema.Value.OneOf, schema.Value.GoLow().OneOf.GetKeyNode())
108113
}
109114
}
110115

111-
// check all schemas in the document
112116
if context.DrDocument.Schemas != nil {
113117
for i := range context.DrDocument.Schemas {
114118
checkSchema(context.DrDocument.Schemas[i])
@@ -117,3 +121,39 @@ func (uc UnnecessaryCombinator) RunRule(_ []*yaml.Node, context model.RuleFuncti
117121

118122
return results
119123
}
124+
125+
func hasSiblingProperties(schema *v3.Schema) bool {
126+
if schema == nil || schema.Value == nil {
127+
return false
128+
}
129+
v := schema.Value
130+
131+
if v.Description != "" || v.Title != "" {
132+
return true
133+
}
134+
if v.Default != nil || v.Example != nil || v.ExternalDocs != nil {
135+
return true
136+
}
137+
if v.Nullable != nil || v.ReadOnly != nil || v.WriteOnly != nil || v.Deprecated != nil {
138+
return true
139+
}
140+
if v.XML != nil {
141+
return true
142+
}
143+
if len(v.Enum) > 0 {
144+
return true
145+
}
146+
lowSchema := v.GoLow()
147+
if lowSchema != nil && lowSchema.Extensions != nil && lowSchema.Extensions.Len() > 0 {
148+
return true
149+
}
150+
return false
151+
}
152+
153+
func hasRefInCombinator(combinatorSlice []*libopenapi_base.SchemaProxy) bool {
154+
if len(combinatorSlice) != 1 || combinatorSlice[0] == nil {
155+
return false
156+
}
157+
lowProxy := combinatorSlice[0].GoLow()
158+
return lowProxy != nil && lowProxy.GetReference() != ""
159+
}

functions/openapi/unnecessary_combinator_test.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,57 @@ components:
247247
assert.Len(t, res, 0) // Schemas without combinators should not trigger
248248
}
249249

250-
// Helper function to build test context with DrDocument
250+
// OAS 3.0.x: single allOf with $ref and sibling description is legitimate workaround
251+
func TestUnnecessaryCombinator_RunRule_OAS30_AllOfWithRefAndDescription(t *testing.T) {
252+
def := UnnecessaryCombinator{}
253+
yml := `openapi: 3.0.3
254+
info:
255+
title: Test
256+
version: 1.0.0
257+
components:
258+
schemas:
259+
BaseModel:
260+
type: object
261+
properties:
262+
id:
263+
type: string
264+
ExtendedModel:
265+
description: "Extended description that overrides the ref"
266+
allOf:
267+
- $ref: '#/components/schemas/BaseModel'
268+
`
269+
ctx := buildTestContext(yml, t)
270+
271+
res := def.RunRule(nil, ctx)
272+
assert.Len(t, res, 0, "OAS 3.0.x allOf with $ref and sibling description should not trigger")
273+
}
274+
275+
// OAS 3.1: single allOf with $ref and sibling description should still trigger
276+
func TestUnnecessaryCombinator_RunRule_OAS31_AllOfWithRefAndDescription(t *testing.T) {
277+
def := UnnecessaryCombinator{}
278+
yml := `openapi: 3.1.0
279+
info:
280+
title: Test
281+
version: 1.0.0
282+
components:
283+
schemas:
284+
BaseModel:
285+
type: object
286+
properties:
287+
id:
288+
type: string
289+
ExtendedModel:
290+
description: "Extended description"
291+
allOf:
292+
- $ref: '#/components/schemas/BaseModel'
293+
`
294+
ctx := buildTestContext(yml, t)
295+
296+
res := def.RunRule(nil, ctx)
297+
assert.Len(t, res, 1, "OAS 3.1 should trigger because $ref siblings are supported natively")
298+
assert.Contains(t, res[0].Message, "allOf")
299+
}
300+
251301
func buildTestContext(yamlContent string, t *testing.T) model.RuleFunctionContext {
252302
document, err := libopenapi.NewDocument([]byte(yamlContent))
253303
if err != nil {
@@ -259,6 +309,7 @@ func buildTestContext(yamlContent string, t *testing.T) model.RuleFunctionContex
259309

260310
return model.RuleFunctionContext{
261311
DrDocument: drDoc,
312+
SpecInfo: document.GetSpecInfo(),
262313
Rule: &model.Rule{},
263314
}
264315
}

0 commit comments

Comments
 (0)