Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 46 additions & 8 deletions epoch/openapi/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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 {
Expand Down
164 changes: 160 additions & 4 deletions epoch/openapi/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
})
Expand Down Expand Up @@ -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())
})
})

Expand Down Expand Up @@ -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
Expand Down