diff --git a/internal/openapi/openapi.go b/internal/openapi/openapi.go index 2bddd7b..a10528c 100644 --- a/internal/openapi/openapi.go +++ b/internal/openapi/openapi.go @@ -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 { diff --git a/internal/service/service_definition.go b/internal/service/service_definition.go index 6cd838d..9b2966b 100644 --- a/internal/service/service_definition.go +++ b/internal/service/service_definition.go @@ -9,8 +9,6 @@ import ( "github.com/aep-dev/aepcli/internal/utils" ) -const contentType = "application/json" - type ServiceDefinition struct { ServerURL string Resources map[string]*Resource @@ -18,7 +16,6 @@ type ServiceDefinition struct { 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. @@ -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{} } } @@ -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" { @@ -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)) } } } @@ -94,9 +94,9 @@ 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:] @@ -104,7 +104,7 @@ func GetServiceDefinition(api *openapi.OpenAPI, serverURL, pathPrefix string) (* 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) } @@ -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 - } -} diff --git a/internal/service/service_definition_test.go b/internal/service/service_definition_test.go index 1a63109..64dcb3e 100644 --- a/internal/service/service_definition_test.go +++ b/internal/service/service_definition_test.go @@ -217,7 +217,7 @@ 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}": { @@ -225,20 +225,18 @@ func TestGetServiceDefinition(t *testing.T) { 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"}, }, }, },