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
232 changes: 148 additions & 84 deletions codegen/pkg/generator/collect.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
)

type schemaUsage struct {
name string
schema *base.SchemaProxy
tags map[string]struct{}
}
Expand All @@ -23,79 +24,28 @@ func (g *Generator) collectSchemaUsage() map[string]*schemaUsage {

for path, pathItem := range g.spec.Paths.PathItems.FromOldest() {
for method, op := range pathItem.GetOperations().FromOldest() {
c := make(schemaProxyCollection, 0, 16)
c.collectSchemasInResponse(op)
c.collectSchemasInParams(op)
c.collectSchemasInRequest(op)

for _, schema := range c {
if schema == nil {
continue
}

if schema.GetReference() == "" {
continue
}

name := schemaClassName(schema)
entry, ok := usage[name]
if !ok {
entry = &schemaUsage{
schema: schema,
tags: make(map[string]struct{}),
}
usage[name] = entry
}

if len(op.Tags) == 0 {
slog.Warn("operation without tags; skipping schema assignment",
slog.String("path", path),
slog.String("method", method),
slog.String("schema", name),
)
continue
}

for _, tag := range op.Tags {
entry.tags[normalizeTagKey(tag)] = struct{}{}
}
opTags := make([]string, 0, len(op.Tags))
for _, tag := range op.Tags {
opTags = append(opTags, normalizeTagKey(tag))
}
}
}

return usage
}

func (g *Generator) assignSchemasToTags(usage map[string]*schemaUsage) (map[string][]*base.SchemaProxy, map[string]string) {
result := make(map[string][]*base.SchemaProxy)
namespaceBySchema := make(map[string]string)

for schemaName, info := range usage {
targetTag := typesTagKey

// Skip schemas that are additionalProperties-only (they'll be treated as arrays)
if schemaIsAdditionalPropertiesOnly(info.schema) {
continue
}
g.collectSchemaUsageInResponse(op, opTags, usage)
g.collectSchemaUsageInParams(op, opTags, usage)
g.collectSchemaUsageInRequest(op, opTags, usage)

if schemaIsObject(info.schema) {
result[targetTag] = append(result[targetTag], info.schema)
namespaceBySchema[schemaName] = g.namespaceForTag(targetTag)
if len(op.Tags) == 0 {
slog.Warn("operation without tags; skipping schema assignment",
slog.String("path", path),
slog.String("method", method),
)
}
}
}

for tag := range result {
slices.SortFunc(result[tag], func(a, b *base.SchemaProxy) int {
return strings.Compare(schemaClassName(a), schemaClassName(b))
})
}

return result, namespaceBySchema
return usage
}

type schemaProxyCollection []*base.SchemaProxy

func (c *schemaProxyCollection) collectSchemasInResponse(op *v3.Operation) {
func (g *Generator) collectSchemaUsageInResponse(op *v3.Operation, tags []string, usage map[string]*schemaUsage) {
if op == nil || op.Responses == nil || op.Responses.Codes.Len() == 0 {
return
}
Expand All @@ -106,64 +56,178 @@ func (c *schemaProxyCollection) collectSchemasInResponse(op *v3.Operation) {
}

for _, mediaType := range response.Content.FromOldest() {
c.collectReferencedSchemas(mediaType.Schema)
g.collectSchemaUsageFromSchema(mediaType.Schema, tags, usage, make(map[*base.SchemaProxy]struct{}), "")
}
}
}

func (c *schemaProxyCollection) collectSchemasInParams(op *v3.Operation) {
func (g *Generator) collectSchemaUsageInParams(op *v3.Operation, tags []string, usage map[string]*schemaUsage) {
if op == nil {
return
}

for _, param := range op.Parameters {
c.collectReferencedSchemas(param.Schema)
g.collectSchemaUsageFromSchema(param.Schema, tags, usage, make(map[*base.SchemaProxy]struct{}), "")
}
}

func (c *schemaProxyCollection) collectSchemasInRequest(op *v3.Operation) {
func (g *Generator) collectSchemaUsageInRequest(op *v3.Operation, tags []string, usage map[string]*schemaUsage) {
if op == nil || op.RequestBody == nil {
return
}

for _, mediaType := range op.RequestBody.Content.FromOldest() {
c.collectReferencedSchemas(mediaType.Schema)
g.collectSchemaUsageFromSchema(mediaType.Schema, tags, usage, make(map[*base.SchemaProxy]struct{}), "")
}
}

func (c *schemaProxyCollection) collectReferencedSchemas(schema *base.SchemaProxy) {
func (g *Generator) collectSchemaUsageFromSchema(schema *base.SchemaProxy, tags []string, usage map[string]*schemaUsage, stack map[*base.SchemaProxy]struct{}, suggestedName string) {
if schema == nil {
return
}

if _, ok := stack[schema]; ok {
return
}
stack[schema] = struct{}{}
defer delete(stack, schema)

spec := schema.Schema()
if spec == nil {
return
}

if slices.Contains(spec.Type, "object") {
for _, prop := range spec.Properties.FromOldest() {
c.collectReferencedSchemas(prop)
name := g.registerSchemaUsage(schema, suggestedName, tags, usage)
parentName := name
if parentName == "" {
parentName = suggestedName
}

if spec.Properties != nil {
for propName, propSchema := range spec.Properties.FromOldest() {
childName := g.inlinePropertyClassName(parentName, propName, propSchema)
g.collectSchemaUsageFromSchema(propSchema, tags, usage, stack, childName)
}
}

if slices.Contains(spec.Type, "array") && spec.Items != nil {
c.collectReferencedSchemas(spec.Items.A)
if hasSchemaType(spec, "array") && spec.Items != nil && spec.Items.A != nil {
itemName := g.inlineArrayItemClassName(parentName, spec.Items.A)
g.collectSchemaUsageFromSchema(spec.Items.A, tags, usage, stack, itemName)
}

for _, one := range spec.AnyOf {
c.collectReferencedSchemas(one)
for _, composite := range spec.AllOf {
g.collectSchemaUsageFromSchema(composite, tags, usage, stack, parentName)
}
for _, composite := range spec.AnyOf {
g.collectSchemaUsageFromSchema(composite, tags, usage, stack, parentName)
}
for _, composite := range spec.OneOf {
g.collectSchemaUsageFromSchema(composite, tags, usage, stack, parentName)
}
}

for _, one := range spec.AllOf {
c.collectReferencedSchemas(one)
func (g *Generator) registerSchemaUsage(schema *base.SchemaProxy, suggestedName string, tags []string, usage map[string]*schemaUsage) string {
if schema == nil {
return ""
}

name := g.classNameForSchema(schema)
if name == "" {
if suggestedName == "" || !schemaIsObject(schema) || schemaIsAdditionalPropertiesOnly(schema) {
return ""
}
name = suggestedName
g.inlineSchemaNames[schema] = name
}

for _, one := range spec.OneOf {
c.collectReferencedSchemas(one)
entry, ok := usage[name]
if !ok {
entry = &schemaUsage{
name: name,
schema: schema,
tags: make(map[string]struct{}),
}
usage[name] = entry
}

if !slices.Contains(*c, schema) {
*c = append(*c, schema)
for _, tag := range tags {
entry.tags[tag] = struct{}{}
}

return name
}

func (g *Generator) assignSchemasToTags(usage map[string]*schemaUsage) (map[string][]*base.SchemaProxy, map[string]string) {
result := make(map[string][]*base.SchemaProxy)
namespaceBySchema := make(map[string]string)

for schemaName, info := range usage {
targetTag := typesTagKey

// Skip schemas that are additionalProperties-only (they'll be treated as arrays)
if schemaIsAdditionalPropertiesOnly(info.schema) {
continue
}

if schemaIsObject(info.schema) {
result[targetTag] = append(result[targetTag], info.schema)
namespaceBySchema[schemaName] = g.namespaceForTag(targetTag)
}
}

for tag := range result {
slices.SortFunc(result[tag], func(a, b *base.SchemaProxy) int {
return strings.Compare(g.classNameForSchema(a), g.classNameForSchema(b))
})
}

return result, namespaceBySchema
}

func (g *Generator) classNameForSchema(schema *base.SchemaProxy) string {
if schema == nil {
return ""
}

if ref := schema.GetReference(); ref != "" {
return schemaClassName(schema)
}

if name, ok := g.inlineSchemaNames[schema]; ok {
return name
}

return ""
}

func (g *Generator) inlinePropertyClassName(parentName string, propertyName string, schema *base.SchemaProxy) string {
if parentName == "" || schema == nil {
return ""
}

if schema.GetReference() != "" {
return ""
}

if !schemaIsObject(schema) || schemaIsAdditionalPropertiesOnly(schema) {
return ""
}

return phpInlineObjectName(parentName, propertyName)
}

func (g *Generator) inlineArrayItemClassName(parentName string, schema *base.SchemaProxy) string {
if parentName == "" || schema == nil {
return ""
}

if schema.GetReference() != "" {
return ""
}

if !schemaIsObject(schema) || schemaIsAdditionalPropertiesOnly(schema) {
return ""
}

return parentName + "Item"
}
14 changes: 11 additions & 3 deletions codegen/pkg/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ type Generator struct {

tagLookup map[string]*base.Tag

// inlineSchemaNames tracks deterministic generated names for inline object schemas.
inlineSchemaNames map[*base.SchemaProxy]string

// schemasByTag maps normalized tag names to schemas they own.
schemasByTag map[string][]*base.SchemaProxy

Expand All @@ -62,7 +65,8 @@ type enumDefinition struct {
// New creates a new Generator instance.
func New(cfg Config) *Generator {
return &Generator{
cfg: cfg,
cfg: cfg,
inlineSchemaNames: make(map[*base.SchemaProxy]string),
}
}

Expand Down Expand Up @@ -265,7 +269,7 @@ func (g *Generator) buildPHPClass(name string, schema *base.SchemaProxy, current

fmt.Fprintf(&buf, "class %s\n{\n", name)

properties := g.schemaProperties(schema, currentNamespace)
properties := g.schemaProperties(schema, currentNamespace, name)
if len(properties) == 0 {
buf.WriteString("}\n")
return buf.String()
Expand Down Expand Up @@ -430,7 +434,11 @@ func (g *Generator) collectEnumsFromSchema(schema *base.SchemaProxy, tagKey stri
}

if len(propSpec.Enum) > 0 {
enumName := phpEnumName(schemaClassName(schema), propName)
schemaName := g.classNameForSchema(schema)
if schemaName == "" {
continue
}
enumName := phpEnumName(schemaName, propName)
if _, seen := enumsSeen[enumName]; seen {
continue
}
Expand Down
9 changes: 9 additions & 0 deletions codegen/pkg/generator/names.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ func phpEnumName(schemaName, propertyName string) string {
return schemaName + baseName
}

func phpInlineObjectName(schemaName, propertyName string) string {
propertyName = strings.TrimSpace(propertyName)
propertyName = strings.ReplaceAll(propertyName, "-", "_")
propertyName = strings.ReplaceAll(propertyName, ".", "_")

baseName := strcase.ToCamel(propertyName)
return schemaName + baseName
}

func phpEnumCaseName(value string) string {
value = strings.TrimSpace(value)

Expand Down
Loading
Loading