Skip to content

Commit

Permalink
fix(openapi): better support 2.0
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
toumorokoshi committed Nov 2, 2024
1 parent 714673b commit 3fd2818
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 60 deletions.
57 changes: 55 additions & 2 deletions internal/openapi/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,70 @@ import (
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"net/url"
"os"
"strings"
)

const (
OAS2 = "2.0"
OAS3 = "3.0"
ContentType = "application/json"
)

type OpenAPI struct {
Openapi string `json:"openapi"`
// oas 2.0 has swagger in the root.k
Swagger string `json:"swagger,omitempty"`
Openapi string `json:"openapi,omitempty"`
Servers []Server `json:"servers,omitempty"`
Info Info `json:"info"`
Paths map[string]PathItem `json:"paths"`
Components Components `json:"components"`
Components Components `json:"components,omitempty"`
// oas 2.0 has definitions in the root.
Definitions map[string]Schema `json:"definitions,omitempty"`
}

func (o *OpenAPI) OASVersion() string {
if o.Swagger == "2.0" {
return OAS2
}
return OAS3
}

func (o *OpenAPI) DereferenceSchema(schema Schema) (*Schema, error) {
if schema.Ref != "" {
parts := strings.Split(schema.Ref, "/")
key := parts[len(parts)-1]
var childSchema Schema
var ok bool
switch o.OASVersion() {
case OAS2:
childSchema, ok = o.Definitions[key]
slog.Debug("oasv2.0", "key", key)
if !ok {
return nil, fmt.Errorf("schema %q not found", schema.Ref)
}
default:
childSchema, ok = o.Components.Schemas[key]
if !ok {
return nil, fmt.Errorf("schema %q not found", schema.Ref)
}
}
return o.DereferenceSchema(childSchema)
}
return &schema, nil
}

func (o *OpenAPI) GetSchemaFromResponse(r Response) *Schema {
switch o.OASVersion() {
case OAS2:
return r.Schema
default:
ct := r.Content[ContentType]
return ct.Schema
}
}

type Server struct {
Expand Down
75 changes: 26 additions & 49 deletions internal/service/service_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,13 @@ import (
"github.com/aep-dev/aepcli/internal/utils"
)

const contentType = "application/json"

type ServiceDefinition struct {
ServerURL string
Resources map[string]*Resource
}

func GetServiceDefinition(api *openapi.OpenAPI, serverURL, pathPrefix string) (*ServiceDefinition, error) {
slog.Debug("parsing openapi", "pathPrefix", pathPrefix)
oasVersion := api.Info.Version
resourceBySingular := make(map[string]*Resource)
// we try to parse the paths to find possible resources, since
// they may not always be annotated as such.
Expand All @@ -40,13 +37,13 @@ func GetServiceDefinition(api *openapi.OpenAPI, serverURL, pathPrefix string) (*
}
if pathItem.Get != nil {
if resp, ok := pathItem.Get.Responses["200"]; ok {
sRef = getSchemaFromResponse(resp, oasVersion)
sRef = api.GetSchemaFromResponse(resp)
r.GetMethod = &GetMethod{}
}
}
if pathItem.Patch != nil {
if resp, ok := pathItem.Patch.Responses["200"]; ok {
sRef = getSchemaFromResponse(resp, oasVersion)
sRef = api.GetSchemaFromResponse(resp)
r.UpdateMethod = &UpdateMethod{}
}
}
Expand All @@ -55,7 +52,7 @@ func GetServiceDefinition(api *openapi.OpenAPI, serverURL, pathPrefix string) (*
if pathItem.Post != nil {
// check if there is a query parameter "id"
if resp, ok := pathItem.Post.Responses["200"]; ok {
sRef = getSchemaFromResponse(resp, oasVersion)
sRef = api.GetSchemaFromResponse(resp)
supportsUserSettableCreate := false
for _, param := range pathItem.Post.Parameters {
if param.Name == "id" {
Expand All @@ -69,23 +66,26 @@ func GetServiceDefinition(api *openapi.OpenAPI, serverURL, pathPrefix string) (*
// list method
if pathItem.Get != nil {
if resp, ok := pathItem.Get.Responses["200"]; ok {
ct := resp.Content[contentType]
respSchema := getSchemaFromResponse(resp, oasVersion)
resolvedSchema, err := dereferencedSchema(*respSchema, api)
if err != nil {
return nil, fmt.Errorf("error dereferencing schema %q: %v", ct.Schema.Ref, err)
}
found := false
for _, property := range resolvedSchema.Properties {
if property.Type == "array" {
sRef = property.Items
r.ListMethod = &ListMethod{}
found = true
break
respSchema := api.GetSchemaFromResponse(resp)
if respSchema == nil {
slog.Warn(fmt.Sprintf("resource %q has a LIST method with a response schema, but the response schema is nil.", path))
} else {
resolvedSchema, err := api.DereferenceSchema(*respSchema)
if err != nil {
return nil, fmt.Errorf("error dereferencing schema %q: %v", respSchema.Ref, err)
}
found := false
for _, property := range resolvedSchema.Properties {
if property.Type == "array" {
sRef = property.Items
r.ListMethod = &ListMethod{}
found = true
break
}
}
if !found {
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))
}
}
if !found {
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))
}
}
}
Expand All @@ -94,17 +94,17 @@ func GetServiceDefinition(api *openapi.OpenAPI, serverURL, pathPrefix string) (*
// s should always be a reference to a schema in the components section.
parts := strings.Split(sRef.Ref, "/")
key := parts[len(parts)-1]
schema, ok := api.Components.Schemas[key]
if !ok {
return nil, fmt.Errorf("schema %q not found", key)
dereferencedSchema, err := api.DereferenceSchema(*sRef)
if err != nil {
return nil, fmt.Errorf("error dereferencing schema %q: %v", sRef.Ref, err)
}
singular := utils.PascalCaseToKebabCase(key)
pattern := strings.Split(path, "/")[1:]
// collection-level patterns don't include the singular, so we need to add it
if !p.IsResourcePattern {
pattern = append(pattern, fmt.Sprintf("{%s}", singular))
}
r2, err := getOrPopulateResource(singular, pattern, &schema, resourceBySingular, api)
r2, err := getOrPopulateResource(singular, pattern, dereferencedSchema, resourceBySingular, api)
if err != nil {
return nil, fmt.Errorf("error populating resource %q: %v", r.Singular, err)
}
Expand Down Expand Up @@ -224,26 +224,3 @@ func foldResourceMethods(from, into *Resource) {
into.DeleteMethod = from.DeleteMethod
}
}

func dereferencedSchema(schema openapi.Schema, api *openapi.OpenAPI) (*openapi.Schema, error) {
if schema.Ref != "" {
parts := strings.Split(schema.Ref, "/")
key := parts[len(parts)-1]
childSchema, ok := api.Components.Schemas[key]
if !ok {
return nil, fmt.Errorf("schema %q not found", key)
}
return dereferencedSchema(childSchema, api)
}
return &schema, nil
}

func getSchemaFromResponse(r openapi.Response, oasVersion string) *openapi.Schema {
switch oasVersion {
case "2.0":
return r.Schema
default:
ct := r.Content[contentType]
return ct.Schema
}
}
16 changes: 7 additions & 9 deletions internal/service/service_definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,28 +217,26 @@ func TestGetServiceDefinition(t *testing.T) {
{
name: "OAS 2.0 style schema in response",
api: &openapi.OpenAPI{
Info: openapi.Info{Version: "2.0"},
Swagger: "2.0",
Servers: []openapi.Server{{URL: "https://api.example.com"}},
Paths: map[string]openapi.PathItem{
"/widgets/{widget}": {
Get: &openapi.Operation{
Responses: map[string]openapi.Response{
"200": {
Schema: &openapi.Schema{
Ref: "#/components/schemas/Widget",
Ref: "#/definitions/Widget",
},
},
},
},
},
},
Components: openapi.Components{
Schemas: map[string]openapi.Schema{
"Widget": {
Type: "object",
Properties: map[string]openapi.Schema{
"name": {Type: "string"},
},
Definitions: map[string]openapi.Schema{
"Widget": {
Type: "object",
Properties: map[string]openapi.Schema{
"name": {Type: "string"},
},
},
},
Expand Down

0 comments on commit 3fd2818

Please sign in to comment.