Skip to content

Commit dc8f16a

Browse files
committed
Added 3.2 support for QUERY operation.
model and what-changed now support OpenAPI 3.2 QUERY operations.
1 parent 58c609a commit dc8f16a

File tree

6 files changed

+600
-1
lines changed

6 files changed

+600
-1
lines changed

datamodel/high/v3/path_item.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const (
2323
head
2424
patch
2525
trace
26+
query
2627
)
2728

2829
// PathItem represents a high-level OpenAPI 3+ PathItem object backed by a low-level one.
@@ -42,6 +43,7 @@ type PathItem struct {
4243
Head *Operation `json:"head,omitempty" yaml:"head,omitempty"`
4344
Patch *Operation `json:"patch,omitempty" yaml:"patch,omitempty"`
4445
Trace *Operation `json:"trace,omitempty" yaml:"trace,omitempty"`
46+
Query *Operation `json:"query,omitempty" yaml:"query,omitempty"`
4547
Servers []*Server `json:"servers,omitempty" yaml:"servers,omitempty"`
4648
Parameters []*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"`
4749
Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"`
@@ -83,6 +85,7 @@ func NewPathItem(pathItem *lowV3.PathItem) *PathItem {
8385
go buildOperation(head, pathItem.Head.Value, opChan)
8486
go buildOperation(patch, pathItem.Patch.Value, opChan)
8587
go buildOperation(trace, pathItem.Trace.Value, opChan)
88+
go buildOperation(query, pathItem.Query.Value, opChan)
8689

8790
if !pathItem.Parameters.IsEmpty() {
8891
params := make([]*Parameter, len(pathItem.Parameters.Value))
@@ -113,10 +116,12 @@ func NewPathItem(pathItem *lowV3.PathItem) *PathItem {
113116
pi.Patch = opRes.op
114117
case trace:
115118
pi.Trace = opRes.op
119+
case query:
120+
pi.Query = opRes.op
116121
}
117122

118123
opCount++
119-
if opCount == 8 {
124+
if opCount == 9 {
120125
complete = true
121126
}
122127
}
@@ -183,6 +188,9 @@ func (p *PathItem) GetOperations() *orderedmap.Map[string, *Operation] {
183188
if p.Trace != nil {
184189
ops = append(ops, op{name: lowV3.TraceLabel, op: p.Trace, line: getLine("Trace", -1)})
185190
}
191+
if p.Query != nil {
192+
ops = append(ops, op{name: lowV3.QueryLabel, op: p.Query, line: getLine("Query", 0)})
193+
}
186194

187195
slices.SortStableFunc(ops, func(a op, b op) int {
188196
return a.line - b.line
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
// Copyright 2024 Princess B33f Heavy Industries / Dave Shanley
2+
// SPDX-License-Identifier: MIT
3+
4+
package v3
5+
6+
import (
7+
"context"
8+
"testing"
9+
10+
"github.com/pb33f/libopenapi/datamodel/low"
11+
lowV3 "github.com/pb33f/libopenapi/datamodel/low/v3"
12+
"github.com/pb33f/libopenapi/index"
13+
"github.com/stretchr/testify/assert"
14+
"gopkg.in/yaml.v3"
15+
)
16+
17+
func TestNewPathItem_WithQuery(t *testing.T) {
18+
yml := `query:
19+
summary: Query resources with complex criteria
20+
operationId: queryResources
21+
requestBody:
22+
required: true
23+
content:
24+
application/json:
25+
schema:
26+
type: object
27+
properties:
28+
filters:
29+
type: array
30+
responses:
31+
'200':
32+
description: Query results`
33+
34+
var idxNode yaml.Node
35+
_ = yaml.Unmarshal([]byte(yml), &idxNode)
36+
37+
idx := index.NewSpecIndexWithConfig(&idxNode, index.CreateOpenAPIIndexConfig())
38+
ctx := context.Background()
39+
40+
var n lowV3.PathItem
41+
err := low.BuildModel(&idxNode, &n)
42+
assert.NoError(t, err)
43+
44+
err = n.Build(ctx, nil, idxNode.Content[0], idx)
45+
assert.NoError(t, err)
46+
47+
// Create high-level PathItem
48+
highPath := NewPathItem(&n)
49+
50+
assert.NotNil(t, highPath.Query)
51+
assert.Equal(t, "Query resources with complex criteria", highPath.Query.Summary)
52+
assert.Equal(t, "queryResources", highPath.Query.OperationId)
53+
assert.NotNil(t, highPath.Query.RequestBody)
54+
assert.NotNil(t, highPath.Query.RequestBody.Required)
55+
assert.True(t, *highPath.Query.RequestBody.Required)
56+
}
57+
58+
func TestPathItem_GetOperations_WithQuery(t *testing.T) {
59+
yml := `get:
60+
summary: Get resource
61+
operationId: getResource
62+
post:
63+
summary: Create resource
64+
operationId: createResource
65+
query:
66+
summary: Query resources
67+
operationId: queryResources
68+
requestBody:
69+
required: true
70+
content:
71+
application/json:
72+
schema:
73+
type: object`
74+
75+
var idxNode yaml.Node
76+
_ = yaml.Unmarshal([]byte(yml), &idxNode)
77+
78+
idx := index.NewSpecIndexWithConfig(&idxNode, index.CreateOpenAPIIndexConfig())
79+
ctx := context.Background()
80+
81+
var n lowV3.PathItem
82+
err := low.BuildModel(&idxNode, &n)
83+
assert.NoError(t, err)
84+
85+
err = n.Build(ctx, nil, idxNode.Content[0], idx)
86+
assert.NoError(t, err)
87+
88+
// Create high-level PathItem
89+
highPath := NewPathItem(&n)
90+
91+
// Get operations map
92+
ops := highPath.GetOperations()
93+
94+
// Should have 3 operations
95+
assert.Equal(t, 3, ops.Len())
96+
97+
// Check that query operation is included
98+
queryOp, ok := ops.Get(lowV3.QueryLabel)
99+
assert.True(t, ok)
100+
assert.NotNil(t, queryOp)
101+
assert.Equal(t, "Query resources", queryOp.Summary)
102+
assert.Equal(t, "queryResources", queryOp.OperationId)
103+
104+
// Check other operations
105+
getOp, ok := ops.Get(lowV3.GetLabel)
106+
assert.True(t, ok)
107+
assert.NotNil(t, getOp)
108+
assert.Equal(t, "Get resource", getOp.Summary)
109+
110+
postOp, ok := ops.Get(lowV3.PostLabel)
111+
assert.True(t, ok)
112+
assert.NotNil(t, postOp)
113+
assert.Equal(t, "Create resource", postOp.Summary)
114+
}
115+
116+
func TestPathItem_MarshalYAML_WithQuery(t *testing.T) {
117+
yml := `description: Resource operations
118+
get:
119+
summary: Get resource
120+
query:
121+
summary: Query resources
122+
requestBody:
123+
required: true
124+
content:
125+
application/json:
126+
schema:
127+
type: object`
128+
129+
var idxNode yaml.Node
130+
_ = yaml.Unmarshal([]byte(yml), &idxNode)
131+
132+
idx := index.NewSpecIndexWithConfig(&idxNode, index.CreateOpenAPIIndexConfig())
133+
ctx := context.Background()
134+
135+
var n lowV3.PathItem
136+
err := low.BuildModel(&idxNode, &n)
137+
assert.NoError(t, err)
138+
139+
err = n.Build(ctx, nil, idxNode.Content[0], idx)
140+
assert.NoError(t, err)
141+
142+
// Create high-level PathItem
143+
highPath := NewPathItem(&n)
144+
145+
// Render to YAML
146+
rendered, err := highPath.Render()
147+
assert.NoError(t, err)
148+
149+
// Parse the rendered YAML
150+
var parsed map[string]interface{}
151+
err = yaml.Unmarshal(rendered, &parsed)
152+
assert.NoError(t, err)
153+
154+
// Verify query operation is present
155+
queryOp, ok := parsed["query"].(map[string]interface{})
156+
assert.True(t, ok)
157+
assert.Equal(t, "Query resources", queryOp["summary"])
158+
159+
// Verify requestBody is present in query operation
160+
reqBody, ok := queryOp["requestBody"].(map[string]interface{})
161+
assert.True(t, ok)
162+
assert.Equal(t, true, reqBody["required"])
163+
}

datamodel/low/v3/path_item.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type PathItem struct {
3636
Head low.NodeReference[*Operation]
3737
Patch low.NodeReference[*Operation]
3838
Trace low.NodeReference[*Operation]
39+
Query low.NodeReference[*Operation]
3940
Servers low.NodeReference[[]low.ValueReference[*Server]]
4041
Parameters low.NodeReference[[]low.ValueReference[*Parameter]]
4142
Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]]
@@ -103,6 +104,10 @@ func (p *PathItem) Hash() [32]byte {
103104
sb.WriteString(fmt.Sprintf("%s-%s", TraceLabel, low.GenerateHashString(p.Trace.Value)))
104105
sb.WriteByte('|')
105106
}
107+
if !p.Query.IsEmpty() {
108+
sb.WriteString(fmt.Sprintf("%s-%s", QueryLabel, low.GenerateHashString(p.Query.Value)))
109+
sb.WriteByte('|')
110+
}
106111

107112
// Process Parameters with pre-allocation and sorting
108113
if len(p.Parameters.Value) > 0 {
@@ -255,6 +260,7 @@ func (p *PathItem) Build(ctx context.Context, keyNode, root *yaml.Node, idx *ind
255260
case HeadLabel:
256261
case OptionsLabel:
257262
case TraceLabel:
263+
case QueryLabel:
258264
default:
259265
continue // ignore everything else.
260266
}
@@ -335,6 +341,8 @@ func (p *PathItem) Build(ctx context.Context, keyNode, root *yaml.Node, idx *ind
335341
p.Options = opRef
336342
case TraceLabel:
337343
p.Trace = opRef
344+
case QueryLabel:
345+
p.Query = opRef
338346
}
339347
}
340348

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// Copyright 2024 Princess B33f Heavy Industries / Dave Shanley
2+
// SPDX-License-Identifier: MIT
3+
4+
package v3
5+
6+
import (
7+
"context"
8+
"testing"
9+
10+
"github.com/pb33f/libopenapi/datamodel/low"
11+
"github.com/pb33f/libopenapi/index"
12+
"github.com/stretchr/testify/assert"
13+
"gopkg.in/yaml.v3"
14+
)
15+
16+
func TestPathItem_BuildQuery(t *testing.T) {
17+
yml := `query:
18+
summary: Query resources
19+
description: Query resources with complex criteria
20+
operationId: queryResources
21+
requestBody:
22+
required: true
23+
content:
24+
application/json:
25+
schema:
26+
type: object
27+
properties:
28+
filters:
29+
type: array
30+
items:
31+
type: string
32+
responses:
33+
'200':
34+
description: Query results
35+
content:
36+
application/json:
37+
schema:
38+
type: array
39+
items:
40+
type: object`
41+
42+
var idxNode yaml.Node
43+
_ = yaml.Unmarshal([]byte(yml), &idxNode)
44+
45+
idx := index.NewSpecIndexWithConfig(&idxNode, index.CreateOpenAPIIndexConfig())
46+
ctx := context.Background()
47+
48+
var n PathItem
49+
err := low.BuildModel(&idxNode, &n)
50+
assert.NoError(t, err)
51+
52+
err = n.Build(ctx, nil, idxNode.Content[0], idx)
53+
assert.NoError(t, err)
54+
55+
assert.NotNil(t, n.Query.Value)
56+
assert.Equal(t, "Query resources", n.Query.Value.Summary.Value)
57+
assert.Equal(t, "Query resources with complex criteria", n.Query.Value.Description.Value)
58+
assert.Equal(t, "queryResources", n.Query.Value.OperationId.Value)
59+
assert.NotNil(t, n.Query.Value.RequestBody.Value)
60+
assert.True(t, n.Query.Value.RequestBody.Value.Required.Value)
61+
}
62+
63+
func TestPathItem_HashWithQuery(t *testing.T) {
64+
yml1 := `query:
65+
summary: Query resources
66+
operationId: queryResources
67+
responses:
68+
'200':
69+
description: OK`
70+
71+
yml2 := `query:
72+
summary: Query different resources
73+
operationId: queryResources
74+
responses:
75+
'200':
76+
description: OK`
77+
78+
var idxNode1, idxNode2 yaml.Node
79+
_ = yaml.Unmarshal([]byte(yml1), &idxNode1)
80+
_ = yaml.Unmarshal([]byte(yml2), &idxNode2)
81+
82+
idx1 := index.NewSpecIndexWithConfig(&idxNode1, index.CreateOpenAPIIndexConfig())
83+
idx2 := index.NewSpecIndexWithConfig(&idxNode2, index.CreateOpenAPIIndexConfig())
84+
ctx := context.Background()
85+
86+
var n1, n2 PathItem
87+
_ = low.BuildModel(&idxNode1, &n1)
88+
_ = low.BuildModel(&idxNode2, &n2)
89+
90+
_ = n1.Build(ctx, nil, idxNode1.Content[0], idx1)
91+
_ = n2.Build(ctx, nil, idxNode2.Content[0], idx2)
92+
93+
// Different summaries should produce different hashes
94+
hash1 := n1.Hash()
95+
hash2 := n2.Hash()
96+
assert.NotEqual(t, hash1, hash2)
97+
}
98+
99+
func TestPathItem_MultipleOperationsIncludingQuery(t *testing.T) {
100+
yml := `get:
101+
summary: Get resource
102+
operationId: getResource
103+
responses:
104+
'200':
105+
description: OK
106+
post:
107+
summary: Create resource
108+
operationId: createResource
109+
responses:
110+
'201':
111+
description: Created
112+
query:
113+
summary: Query resources
114+
operationId: queryResources
115+
requestBody:
116+
required: true
117+
content:
118+
application/json:
119+
schema:
120+
type: object
121+
responses:
122+
'200':
123+
description: OK`
124+
125+
var idxNode yaml.Node
126+
_ = yaml.Unmarshal([]byte(yml), &idxNode)
127+
128+
idx := index.NewSpecIndexWithConfig(&idxNode, index.CreateOpenAPIIndexConfig())
129+
ctx := context.Background()
130+
131+
var n PathItem
132+
err := low.BuildModel(&idxNode, &n)
133+
assert.NoError(t, err)
134+
135+
err = n.Build(ctx, nil, idxNode.Content[0], idx)
136+
assert.NoError(t, err)
137+
138+
// Verify all operations are present
139+
assert.NotNil(t, n.Get.Value)
140+
assert.Equal(t, "Get resource", n.Get.Value.Summary.Value)
141+
142+
assert.NotNil(t, n.Post.Value)
143+
assert.Equal(t, "Create resource", n.Post.Value.Summary.Value)
144+
145+
assert.NotNil(t, n.Query.Value)
146+
assert.Equal(t, "Query resources", n.Query.Value.Summary.Value)
147+
assert.NotNil(t, n.Query.Value.RequestBody.Value)
148+
}

0 commit comments

Comments
 (0)