Skip to content

Commit 3fd2818

Browse files
committed
fix(openapi): better support 2.0
Some missing components in how to evaluate swagger 2.0 schemas were added, enabling more robust handling for these apis. Some of the functions were refactored to the openapi struct, which helps separate concerns between openapi schema instrospection and services which utilize them.
1 parent 714673b commit 3fd2818

File tree

3 files changed

+88
-60
lines changed

3 files changed

+88
-60
lines changed

internal/openapi/openapi.go

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,70 @@ import (
44
"encoding/json"
55
"fmt"
66
"io"
7+
"log/slog"
78
"net/http"
89
"net/url"
910
"os"
11+
"strings"
12+
)
13+
14+
const (
15+
OAS2 = "2.0"
16+
OAS3 = "3.0"
17+
ContentType = "application/json"
1018
)
1119

1220
type OpenAPI struct {
13-
Openapi string `json:"openapi"`
21+
// oas 2.0 has swagger in the root.k
22+
Swagger string `json:"swagger,omitempty"`
23+
Openapi string `json:"openapi,omitempty"`
1424
Servers []Server `json:"servers,omitempty"`
1525
Info Info `json:"info"`
1626
Paths map[string]PathItem `json:"paths"`
17-
Components Components `json:"components"`
27+
Components Components `json:"components,omitempty"`
28+
// oas 2.0 has definitions in the root.
29+
Definitions map[string]Schema `json:"definitions,omitempty"`
30+
}
31+
32+
func (o *OpenAPI) OASVersion() string {
33+
if o.Swagger == "2.0" {
34+
return OAS2
35+
}
36+
return OAS3
37+
}
38+
39+
func (o *OpenAPI) DereferenceSchema(schema Schema) (*Schema, error) {
40+
if schema.Ref != "" {
41+
parts := strings.Split(schema.Ref, "/")
42+
key := parts[len(parts)-1]
43+
var childSchema Schema
44+
var ok bool
45+
switch o.OASVersion() {
46+
case OAS2:
47+
childSchema, ok = o.Definitions[key]
48+
slog.Debug("oasv2.0", "key", key)
49+
if !ok {
50+
return nil, fmt.Errorf("schema %q not found", schema.Ref)
51+
}
52+
default:
53+
childSchema, ok = o.Components.Schemas[key]
54+
if !ok {
55+
return nil, fmt.Errorf("schema %q not found", schema.Ref)
56+
}
57+
}
58+
return o.DereferenceSchema(childSchema)
59+
}
60+
return &schema, nil
61+
}
62+
63+
func (o *OpenAPI) GetSchemaFromResponse(r Response) *Schema {
64+
switch o.OASVersion() {
65+
case OAS2:
66+
return r.Schema
67+
default:
68+
ct := r.Content[ContentType]
69+
return ct.Schema
70+
}
1871
}
1972

2073
type Server struct {

internal/service/service_definition.go

Lines changed: 26 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,13 @@ import (
99
"github.com/aep-dev/aepcli/internal/utils"
1010
)
1111

12-
const contentType = "application/json"
13-
1412
type ServiceDefinition struct {
1513
ServerURL string
1614
Resources map[string]*Resource
1715
}
1816

1917
func GetServiceDefinition(api *openapi.OpenAPI, serverURL, pathPrefix string) (*ServiceDefinition, error) {
2018
slog.Debug("parsing openapi", "pathPrefix", pathPrefix)
21-
oasVersion := api.Info.Version
2219
resourceBySingular := make(map[string]*Resource)
2320
// we try to parse the paths to find possible resources, since
2421
// they may not always be annotated as such.
@@ -40,13 +37,13 @@ func GetServiceDefinition(api *openapi.OpenAPI, serverURL, pathPrefix string) (*
4037
}
4138
if pathItem.Get != nil {
4239
if resp, ok := pathItem.Get.Responses["200"]; ok {
43-
sRef = getSchemaFromResponse(resp, oasVersion)
40+
sRef = api.GetSchemaFromResponse(resp)
4441
r.GetMethod = &GetMethod{}
4542
}
4643
}
4744
if pathItem.Patch != nil {
4845
if resp, ok := pathItem.Patch.Responses["200"]; ok {
49-
sRef = getSchemaFromResponse(resp, oasVersion)
46+
sRef = api.GetSchemaFromResponse(resp)
5047
r.UpdateMethod = &UpdateMethod{}
5148
}
5249
}
@@ -55,7 +52,7 @@ func GetServiceDefinition(api *openapi.OpenAPI, serverURL, pathPrefix string) (*
5552
if pathItem.Post != nil {
5653
// check if there is a query parameter "id"
5754
if resp, ok := pathItem.Post.Responses["200"]; ok {
58-
sRef = getSchemaFromResponse(resp, oasVersion)
55+
sRef = api.GetSchemaFromResponse(resp)
5956
supportsUserSettableCreate := false
6057
for _, param := range pathItem.Post.Parameters {
6158
if param.Name == "id" {
@@ -69,23 +66,26 @@ func GetServiceDefinition(api *openapi.OpenAPI, serverURL, pathPrefix string) (*
6966
// list method
7067
if pathItem.Get != nil {
7168
if resp, ok := pathItem.Get.Responses["200"]; ok {
72-
ct := resp.Content[contentType]
73-
respSchema := getSchemaFromResponse(resp, oasVersion)
74-
resolvedSchema, err := dereferencedSchema(*respSchema, api)
75-
if err != nil {
76-
return nil, fmt.Errorf("error dereferencing schema %q: %v", ct.Schema.Ref, err)
77-
}
78-
found := false
79-
for _, property := range resolvedSchema.Properties {
80-
if property.Type == "array" {
81-
sRef = property.Items
82-
r.ListMethod = &ListMethod{}
83-
found = true
84-
break
69+
respSchema := api.GetSchemaFromResponse(resp)
70+
if respSchema == nil {
71+
slog.Warn(fmt.Sprintf("resource %q has a LIST method with a response schema, but the response schema is nil.", path))
72+
} else {
73+
resolvedSchema, err := api.DereferenceSchema(*respSchema)
74+
if err != nil {
75+
return nil, fmt.Errorf("error dereferencing schema %q: %v", respSchema.Ref, err)
76+
}
77+
found := false
78+
for _, property := range resolvedSchema.Properties {
79+
if property.Type == "array" {
80+
sRef = property.Items
81+
r.ListMethod = &ListMethod{}
82+
found = true
83+
break
84+
}
85+
}
86+
if !found {
87+
slog.Warn(fmt.Sprintf("resource %q has a LIST method with a response schema, but the items field is not present or is not an array.", path))
8588
}
86-
}
87-
if !found {
88-
slog.Warn(fmt.Sprintf("resource %q has a LIST method with a response schema, but the items field is not present or is not an array.", path))
8989
}
9090
}
9191
}
@@ -94,17 +94,17 @@ func GetServiceDefinition(api *openapi.OpenAPI, serverURL, pathPrefix string) (*
9494
// s should always be a reference to a schema in the components section.
9595
parts := strings.Split(sRef.Ref, "/")
9696
key := parts[len(parts)-1]
97-
schema, ok := api.Components.Schemas[key]
98-
if !ok {
99-
return nil, fmt.Errorf("schema %q not found", key)
97+
dereferencedSchema, err := api.DereferenceSchema(*sRef)
98+
if err != nil {
99+
return nil, fmt.Errorf("error dereferencing schema %q: %v", sRef.Ref, err)
100100
}
101101
singular := utils.PascalCaseToKebabCase(key)
102102
pattern := strings.Split(path, "/")[1:]
103103
// collection-level patterns don't include the singular, so we need to add it
104104
if !p.IsResourcePattern {
105105
pattern = append(pattern, fmt.Sprintf("{%s}", singular))
106106
}
107-
r2, err := getOrPopulateResource(singular, pattern, &schema, resourceBySingular, api)
107+
r2, err := getOrPopulateResource(singular, pattern, dereferencedSchema, resourceBySingular, api)
108108
if err != nil {
109109
return nil, fmt.Errorf("error populating resource %q: %v", r.Singular, err)
110110
}
@@ -224,26 +224,3 @@ func foldResourceMethods(from, into *Resource) {
224224
into.DeleteMethod = from.DeleteMethod
225225
}
226226
}
227-
228-
func dereferencedSchema(schema openapi.Schema, api *openapi.OpenAPI) (*openapi.Schema, error) {
229-
if schema.Ref != "" {
230-
parts := strings.Split(schema.Ref, "/")
231-
key := parts[len(parts)-1]
232-
childSchema, ok := api.Components.Schemas[key]
233-
if !ok {
234-
return nil, fmt.Errorf("schema %q not found", key)
235-
}
236-
return dereferencedSchema(childSchema, api)
237-
}
238-
return &schema, nil
239-
}
240-
241-
func getSchemaFromResponse(r openapi.Response, oasVersion string) *openapi.Schema {
242-
switch oasVersion {
243-
case "2.0":
244-
return r.Schema
245-
default:
246-
ct := r.Content[contentType]
247-
return ct.Schema
248-
}
249-
}

internal/service/service_definition_test.go

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -217,28 +217,26 @@ func TestGetServiceDefinition(t *testing.T) {
217217
{
218218
name: "OAS 2.0 style schema in response",
219219
api: &openapi.OpenAPI{
220-
Info: openapi.Info{Version: "2.0"},
220+
Swagger: "2.0",
221221
Servers: []openapi.Server{{URL: "https://api.example.com"}},
222222
Paths: map[string]openapi.PathItem{
223223
"/widgets/{widget}": {
224224
Get: &openapi.Operation{
225225
Responses: map[string]openapi.Response{
226226
"200": {
227227
Schema: &openapi.Schema{
228-
Ref: "#/components/schemas/Widget",
228+
Ref: "#/definitions/Widget",
229229
},
230230
},
231231
},
232232
},
233233
},
234234
},
235-
Components: openapi.Components{
236-
Schemas: map[string]openapi.Schema{
237-
"Widget": {
238-
Type: "object",
239-
Properties: map[string]openapi.Schema{
240-
"name": {Type: "string"},
241-
},
235+
Definitions: map[string]openapi.Schema{
236+
"Widget": {
237+
Type: "object",
238+
Properties: map[string]openapi.Schema{
239+
"name": {Type: "string"},
242240
},
243241
},
244242
},

0 commit comments

Comments
 (0)