diff --git a/epoch/openapi/generator.go b/epoch/openapi/generator.go index 7f18453..22f793a 100644 --- a/epoch/openapi/generator.go +++ b/epoch/openapi/generator.go @@ -191,6 +191,12 @@ func (sg *SchemaGenerator) processTypeForVersion( typ reflect.Type, version *epoch.Version, ) error { + // Skip slice/array types - they don't become named schemas + // Their element types are handled separately in getRegisteredTypes() + if typ.Kind() == reflect.Slice || typ.Kind() == reflect.Array { + return nil + } + goTypeName := typ.Name() // Map to schema name in spec (e.g., "versionedapi.UpdateExampleRequest") @@ -271,19 +277,51 @@ func (sg *SchemaGenerator) getRegisteredTypes() []reflect.Type { for _, endpoint := range endpoints { if endpoint.RequestType != nil { - if !typeMap[endpoint.RequestType] { - typeMap[endpoint.RequestType] = true - types = append(types, endpoint.RequestType) + reqType := endpoint.RequestType + // For slice/array types, register the element type instead + if reqType.Kind() == reflect.Slice || reqType.Kind() == reflect.Array { + elemType := reqType.Elem() + if elemType.Kind() == reflect.Ptr { + elemType = elemType.Elem() + } + // Only add element type if it's a struct (not primitives) + if elemType.Kind() == reflect.Struct && !typeMap[elemType] { + typeMap[elemType] = true + types = append(types, elemType) + } + sg.collectNestedTypes(elemType, typeMap, &types) + } else { + // Existing logic for non-array types + if !typeMap[reqType] { + typeMap[reqType] = true + types = append(types, reqType) + } + sg.collectNestedTypes(reqType, typeMap, &types) } - sg.collectNestedTypes(endpoint.RequestType, typeMap, &types) } if endpoint.ResponseType != nil { - if !typeMap[endpoint.ResponseType] { - typeMap[endpoint.ResponseType] = true - types = append(types, endpoint.ResponseType) + respType := endpoint.ResponseType + // For slice/array types, register the element type instead + if respType.Kind() == reflect.Slice || respType.Kind() == reflect.Array { + elemType := respType.Elem() + if elemType.Kind() == reflect.Ptr { + elemType = elemType.Elem() + } + // Only add element type if it's a struct (not primitives) + if elemType.Kind() == reflect.Struct && !typeMap[elemType] { + typeMap[elemType] = true + types = append(types, elemType) + } + sg.collectNestedTypes(elemType, typeMap, &types) + } else { + // Existing logic for non-array types + if !typeMap[respType] { + typeMap[respType] = true + types = append(types, respType) + } + sg.collectNestedTypes(respType, typeMap, &types) } - sg.collectNestedTypes(endpoint.ResponseType, typeMap, &types) } for _, itemType := range endpoint.ResponseNestedArrays { diff --git a/epoch/openapi/generator_test.go b/epoch/openapi/generator_test.go index 9f12519..c5d143d 100644 --- a/epoch/openapi/generator_test.go +++ b/epoch/openapi/generator_test.go @@ -488,8 +488,8 @@ var _ = Describe("SchemaGenerator", func() { v1Spec, err := generator.GenerateSpecForVersion(baseSpec, v1) Expect(err).NotTo(HaveOccurred()) - // Should generate schema from scratch with versioned name - generatedSchema := v1Spec.Components.Schemas["UpdateExampleRequestV10"] + // Should generate schema from scratch using Go type name + generatedSchema := v1Spec.Components.Schemas["UpdateExampleRequest"] Expect(generatedSchema).NotTo(BeNil()) Expect(generatedSchema.Value).NotTo(BeNil()) }) @@ -546,8 +546,8 @@ var _ = Describe("SchemaGenerator", func() { // ExistingRequest: should be transformed in place Expect(v1Spec.Components.Schemas["versionedapi.ExistingRequest"]).NotTo(BeNil()) - // MissingRequest: should be generated with versioned name - Expect(v1Spec.Components.Schemas["MissingRequestV10"]).NotTo(BeNil()) + // MissingRequest: should be generated using Go type name + Expect(v1Spec.Components.Schemas["MissingRequest"]).NotTo(BeNil()) }) }) @@ -1262,6 +1262,162 @@ var _ = Describe("SchemaGenerator", func() { }) }) + Describe("Array Return Types", func() { + var ( + generator *SchemaGenerator + registry *epoch.EndpointRegistry + v1 *epoch.Version + ) + + BeforeEach(func() { + v1, _ = epoch.NewDateVersion("2024-01-01") + versionBundle, _ := epoch.NewVersionBundle([]*epoch.Version{v1}) + registry = epoch.NewEndpointRegistry() + + config := SchemaGeneratorConfig{ + VersionBundle: versionBundle, + TypeRegistry: registry, + } + generator = NewSchemaGenerator(config) + }) + + It("should not create empty-named schemas for array response types", func() { + // Register endpoint that returns an array + arrayType := reflect.TypeOf([]TestUserResponse{}) + registry.Register("GET", "/users", &epoch.EndpointDefinition{ + Method: "GET", + PathPattern: "/users", + ResponseType: arrayType, + }) + + baseSpec := &openapi3.T{ + OpenAPI: "3.0.3", + Info: &openapi3.Info{Title: "Test", Version: "1.0"}, + Components: &openapi3.Components{ + Schemas: openapi3.Schemas{}, + }, + } + + spec, err := generator.GenerateSpecForVersion(baseSpec, v1) + Expect(err).NotTo(HaveOccurred()) + + // Should NOT have a schema with empty name + _, hasEmptySchema := spec.Components.Schemas[""] + Expect(hasEmptySchema).To(BeFalse()) + }) + + It("should register element type as component for array responses", func() { + // Register endpoint that returns an array + arrayType := reflect.TypeOf([]TestUserResponse{}) + registry.Register("GET", "/users", &epoch.EndpointDefinition{ + Method: "GET", + PathPattern: "/users", + ResponseType: arrayType, + }) + + baseSpec := &openapi3.T{ + OpenAPI: "3.0.3", + Info: &openapi3.Info{Title: "Test", Version: "1.0"}, + Components: &openapi3.Components{ + Schemas: openapi3.Schemas{}, + }, + } + + spec, err := generator.GenerateSpecForVersion(baseSpec, v1) + Expect(err).NotTo(HaveOccurred()) + + // Should have TestUserResponse as a component + userResponseSchema := spec.Components.Schemas["TestUserResponse"] + Expect(userResponseSchema).NotTo(BeNil()) + Expect(userResponseSchema.Value).NotTo(BeNil()) + Expect(userResponseSchema.Value.Type).NotTo(BeNil()) + Expect((*userResponseSchema.Value.Type)[0]).To(Equal("object")) + }) + + It("should handle array request types", func() { + // Register endpoint that accepts an array + arrayType := reflect.TypeOf([]TestUserRequest{}) + registry.Register("POST", "/users/bulk", &epoch.EndpointDefinition{ + Method: "POST", + PathPattern: "/users/bulk", + RequestType: arrayType, + }) + + baseSpec := &openapi3.T{ + OpenAPI: "3.0.3", + Info: &openapi3.Info{Title: "Test", Version: "1.0"}, + Components: &openapi3.Components{ + Schemas: openapi3.Schemas{}, + }, + } + + spec, err := generator.GenerateSpecForVersion(baseSpec, v1) + Expect(err).NotTo(HaveOccurred()) + + // Should NOT have a schema with empty name + _, hasEmptySchema := spec.Components.Schemas[""] + Expect(hasEmptySchema).To(BeFalse()) + + // Should have TestUserRequest as a component + userRequestSchema := spec.Components.Schemas["TestUserRequest"] + Expect(userRequestSchema).NotTo(BeNil()) + }) + + It("should handle nested arrays with custom types", func() { + // Register endpoint that returns an array of nested items + arrayType := reflect.TypeOf([]NestedItem{}) + registry.Register("GET", "/items", &epoch.EndpointDefinition{ + Method: "GET", + PathPattern: "/items", + ResponseType: arrayType, + }) + + baseSpec := &openapi3.T{ + OpenAPI: "3.0.3", + Info: &openapi3.Info{Title: "Test", Version: "1.0"}, + Components: &openapi3.Components{ + Schemas: openapi3.Schemas{}, + }, + } + + spec, err := generator.GenerateSpecForVersion(baseSpec, v1) + Expect(err).NotTo(HaveOccurred()) + + // Should NOT have a schema with empty name + _, hasEmptySchema := spec.Components.Schemas[""] + Expect(hasEmptySchema).To(BeFalse()) + + // Should have NestedItem and all its nested types + Expect(spec.Components.Schemas["NestedItem"]).NotTo(BeNil()) + Expect(spec.Components.Schemas["NestedSubItem"]).NotTo(BeNil()) + Expect(spec.Components.Schemas["NestedMetadata"]).NotTo(BeNil()) + }) + + It("should not register primitive array elements as schemas", func() { + // Register endpoint that returns an array of strings + arrayType := reflect.TypeOf([]string{}) + registry.Register("GET", "/tags", &epoch.EndpointDefinition{ + Method: "GET", + PathPattern: "/tags", + ResponseType: arrayType, + }) + + baseSpec := &openapi3.T{ + OpenAPI: "3.0.3", + Info: &openapi3.Info{Title: "Test", Version: "1.0"}, + Components: &openapi3.Components{ + Schemas: openapi3.Schemas{}, + }, + } + + spec, err := generator.GenerateSpecForVersion(baseSpec, v1) + Expect(err).NotTo(HaveOccurred()) + + // Should NOT have any schemas since string is primitive + Expect(len(spec.Components.Schemas)).To(Equal(0)) + }) + }) + Describe("Direction Detection", func() { var ( generator *SchemaGenerator