Skip to content

Commit 44c4783

Browse files
committed
feat: improve model merging, patches for allOf, anyOf simplification
1 parent c8bc49c commit 44c4783

13 files changed

Lines changed: 544 additions & 187 deletions

File tree

pkg/openapi/openapidocument/merge.go

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ package openapidocument
22

33
import (
44
"fmt"
5+
"log/slog"
6+
"reflect"
57
"slices"
8+
"strings"
69

710
"github.com/pb33f/libopenapi/datamodel/high/base"
811
"github.com/pb33f/libopenapi/orderedmap"
9-
"github.com/primelib/primecodegen/pkg/logging"
12+
"github.com/primelib/primecodegen/pkg/util"
13+
"go.yaml.in/yaml/v4"
1014
)
1115

1216
func MergeSchemaProxy(baseSP *base.SchemaProxy, overwriteSP *base.SchemaProxy) (*base.Schema, error) {
@@ -155,15 +159,20 @@ func MergeSchema(result *base.Schema, override *base.Schema) (*base.Schema, erro
155159
}
156160
}
157161
if override.Properties != nil {
158-
for op := override.Properties.Oldest(); op != nil; op = op.Next() {
159-
bytes, _ := op.Value.Render()
160-
logging.Trace("Properties: ", "key", op.Key, "value", string(bytes))
161-
}
162162
if result.Properties == nil {
163163
result.Properties = orderedmap.New[string, *base.SchemaProxy]()
164164
}
165+
165166
for op := override.Properties.Oldest(); op != nil; op = op.Next() {
166-
result.Properties.Set(op.Key, op.Value)
167+
propertyName := op.Key
168+
newProperty := op.Value
169+
170+
if existingProperty, exists := result.Properties.Get(propertyName); exists {
171+
resolvedProperty := resolvePropertyConflict(existingProperty, newProperty)
172+
result.Properties.Set(propertyName, resolvedProperty)
173+
} else {
174+
result.Properties.Set(propertyName, newProperty)
175+
}
167176
}
168177
}
169178
if override.Title != "" {
@@ -233,9 +242,9 @@ func MergeSchema(result *base.Schema, override *base.Schema) (*base.Schema, erro
233242
}
234243
if len(override.Required) > 0 {
235244
if result.Required == nil {
236-
result.Required = override.Required
245+
result.Required = append([]string(nil), override.Required...)
237246
} else {
238-
result.Required = append(result.Required, override.Required...)
247+
result.Required = util.AppendUnique(result.Required, override.Required)
239248
}
240249
}
241250
if len(override.Enum) > 0 {
@@ -254,7 +263,9 @@ func MergeSchema(result *base.Schema, override *base.Schema) (*base.Schema, erro
254263
if result.Description == "" {
255264
result.Description = override.Description
256265
} else {
257-
result.Description = result.Description + "\n" + override.Description
266+
if !strings.Contains(result.Description, override.Description) {
267+
result.Description = result.Description + "\n" + override.Description
268+
}
258269
}
259270
}
260271
if override.Default != nil {
@@ -302,8 +313,54 @@ func MergeSchema(result *base.Schema, override *base.Schema) (*base.Schema, erro
302313
result.Deprecated = override.Deprecated
303314
}
304315
}
305-
// TODO: Extensions
306-
// Skip: low, ParentProxy
316+
if override.Extensions != nil {
317+
if result.Extensions == nil {
318+
result.Extensions = orderedmap.New[string, *yaml.Node]()
319+
}
320+
321+
for ext := override.Extensions.Oldest(); ext != nil; ext = ext.Next() {
322+
result.Extensions.Set(ext.Key, ext.Value)
323+
}
324+
}
307325

308326
return result, nil
309327
}
328+
329+
func resolvePropertyConflict(existing, override *base.SchemaProxy) *base.SchemaProxy {
330+
existingSchema := existing.Schema()
331+
overrideSchema := override.Schema()
332+
333+
if existingSchema == nil || overrideSchema == nil {
334+
return existing
335+
}
336+
337+
// match
338+
if reflect.DeepEqual(existingSchema.Type, overrideSchema.Type) {
339+
return existing
340+
}
341+
342+
// array for content conflict
343+
itemSchema := &base.Schema{
344+
Type: []string{"object"},
345+
Description: "Generic object created due to item type conflict",
346+
}
347+
if slices.Contains(existingSchema.Type, "array") && slices.Contains(overrideSchema.Type, "array") {
348+
slog.Debug("Array type match but items diverge, creating array of objects", "property", existingSchema.Title)
349+
350+
arrayObject := &base.Schema{
351+
Type: []string{"array"},
352+
Items: &base.DynamicValue[*base.SchemaProxy, bool]{
353+
N: 0,
354+
A: base.CreateSchemaProxy(itemSchema),
355+
},
356+
}
357+
arrayObject.Extensions = orderedmap.New[string, *yaml.Node]()
358+
arrayObject.Extensions.Set("x-primecodegen-conflict", &yaml.Node{Value: "Array items diverged; defaulted to List<Object>"})
359+
return base.CreateSchemaProxy(arrayObject)
360+
}
361+
362+
// total conflict
363+
itemSchema.Extensions = orderedmap.New[string, *yaml.Node]()
364+
itemSchema.Extensions.Set("x-primecodegen-conflict", &yaml.Node{Value: "Types diverged on merge; defaulted to object"})
365+
return base.CreateSchemaProxy(itemSchema)
366+
}

pkg/openapi/openapidocument/references.go

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package openapidocument
22

33
import (
4+
"fmt"
5+
46
"github.com/pb33f/doctor/model"
57
"github.com/pb33f/libopenapi"
68
"github.com/pb33f/libopenapi/datamodel/high/base"
@@ -26,30 +28,35 @@ func WalkDocument(doc *libopenapi.DocumentModel[v3.Document]) WalkDocumentResult
2628
}
2729

2830
// CollectSchemas collects all schemas from the OpenAPI document (doesn't follow recursion, flatten should be used first).
29-
func CollectSchemas(doc *libopenapi.DocumentModel[v3.Document]) []*base.Schema {
30-
var schemas []*base.Schema
31+
func CollectSchemas(doc *libopenapi.DocumentModel[v3.Document]) map[string]*base.Schema {
32+
schemas := make(map[string]*base.Schema)
33+
34+
// component schemas
35+
for s := doc.Model.Components.Schemas.Oldest(); s != nil; s = s.Next() {
36+
ref := "#/components/schemas/" + s.Key
37+
schemas[ref] = s.Value.Schema()
38+
}
39+
40+
// component parameters
41+
for s := doc.Model.Components.Parameters.Oldest(); s != nil; s = s.Next() {
42+
if s.Value.Schema != nil && !s.Value.Schema.IsReference() {
43+
ref := "#/components/parameters/" + s.Key
44+
schemas[ref] = s.Value.Schema.Schema()
45+
}
46+
}
3147

3248
// requests parameters
3349
for path := doc.Model.Paths.PathItems.Oldest(); path != nil; path = path.Next() {
3450
for op := path.Value.GetOperations().Oldest(); op != nil; op = op.Next() {
3551
for _, param := range op.Value.Parameters {
3652
if param.Schema != nil && !param.Schema.IsReference() {
37-
schemas = append(schemas, collectFromSchema(param.Schema)...)
53+
ref := "#/paths/" + path.Key + "/" + op.Key + "/parameters/" + param.Name
54+
schemas[ref] = param.Schema.Schema()
3855
}
3956
}
4057
}
4158
}
4259

43-
// components
44-
for s := doc.Model.Components.Parameters.Oldest(); s != nil; s = s.Next() {
45-
if s.Value.Schema != nil && !s.Value.Schema.IsReference() {
46-
schemas = append(schemas, collectFromSchema(s.Value.Schema)...)
47-
}
48-
}
49-
for s := doc.Model.Components.Schemas.Oldest(); s != nil; s = s.Next() {
50-
schemas = append(schemas, collectFromSchema(s.Value)...)
51-
}
52-
5360
return schemas
5461
}
5562

@@ -66,15 +73,41 @@ func CollectOperations(doc *libopenapi.DocumentModel[v3.Document]) []*v3.Operati
6673
return operations
6774
}
6875

69-
func collectFromSchema(schema *base.SchemaProxy) []*base.Schema {
70-
schemas := []*base.Schema{schema.Schema()}
76+
// CollectSchemaProxies collects all schema proxies from the OpenAPI document.
77+
func CollectSchemaProxies(doc *libopenapi.DocumentModel[v3.Document]) map[string]*base.SchemaProxy {
78+
proxies := make(map[string]*base.SchemaProxy)
7179

72-
// properties
73-
if schema.Schema().Properties != nil {
74-
for p := schema.Schema().Properties.Oldest(); p != nil; p = p.Next() {
75-
schemas = append(schemas, p.Value.Schema())
80+
// Component Schemas
81+
if doc.Model.Components != nil && doc.Model.Components.Schemas != nil {
82+
for s := doc.Model.Components.Schemas.Oldest(); s != nil; s = s.Next() {
83+
ref := "#/components/schemas/" + s.Key
84+
proxies[ref] = s.Value
7685
}
7786
}
7887

79-
return schemas
88+
// Component Parameters
89+
if doc.Model.Components != nil && doc.Model.Components.Parameters != nil {
90+
for s := doc.Model.Components.Parameters.Oldest(); s != nil; s = s.Next() {
91+
if s.Value.Schema != nil {
92+
ref := "#/components/parameters/" + s.Key
93+
proxies[ref] = s.Value.Schema
94+
}
95+
}
96+
}
97+
98+
// Path/Operation Parameters
99+
if doc.Model.Paths != nil && doc.Model.Paths.PathItems != nil {
100+
for path := doc.Model.Paths.PathItems.Oldest(); path != nil; path = path.Next() {
101+
for op := path.Value.GetOperations().Oldest(); op != nil; op = op.Next() {
102+
for _, param := range op.Value.Parameters {
103+
if param.Schema != nil {
104+
ref := fmt.Sprintf("#/paths/%s/%s/parameters/%s", path.Key, op.Key, param.Name)
105+
proxies[ref] = param.Schema
106+
}
107+
}
108+
}
109+
}
110+
}
111+
112+
return proxies
80113
}

pkg/openapi/openapigenerator/model.go

Lines changed: 80 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -439,45 +439,48 @@ func BuildComponentModels(opts ModelOpts) ([]Model, error) {
439439
}
440440

441441
add := Model{
442-
Name: gen.ToClassName(s.Title),
443-
Description: s.Description,
442+
Name: gen.ToClassName(s.Title),
443+
Description: s.Description,
444+
SchemaReference: "#/components/schemas/" + schema.Key,
444445
}
445-
if slices.Contains(s.Type, "object") && s.Properties != nil {
446+
if slices.Contains(s.Type, "object") && (s.Properties != nil || s.OneOf != nil || s.AnyOf != nil || s.AllOf != nil) {
446447
var addedProperties []string
447448

448-
for p := s.Properties.Oldest(); p != nil; p = p.Next() {
449-
if slices.Contains(addedProperties, gen.ToPropertyName(p.Key)) {
450-
continue
451-
}
449+
if s.Properties != nil {
450+
for p := s.Properties.Oldest(); p != nil; p = p.Next() {
451+
if slices.Contains(addedProperties, gen.ToPropertyName(p.Key)) {
452+
continue
453+
}
452454

453-
pSchema, pErr := p.Value.BuildSchema()
454-
if pErr != nil {
455-
return models, fmt.Errorf("error building property schema: %w", err)
456-
}
455+
pSchema, pErr := p.Value.BuildSchema()
456+
if pErr != nil {
457+
return models, fmt.Errorf("error building property schema: %w", err)
458+
}
457459

458-
pType, err := gen.ToCodeType(pSchema, CodeTypeSchemaProperty, false)
459-
if err != nil {
460-
return models, fmt.Errorf("error converting type of [%s:object:%s]: %w", schema.Key, p.Key, err)
461-
}
462-
pType = gen.PostProcessType(pType)
460+
pType, err := gen.ToCodeType(pSchema, CodeTypeSchemaProperty, false)
461+
if err != nil {
462+
return models, fmt.Errorf("error converting type of [%s:object:%s]: %w", schema.Key, p.Key, err)
463+
}
464+
pType = gen.PostProcessType(pType)
463465

464-
allowedValues, err := openapidocument.EnumToAllowedValues(pSchema)
465-
if err != nil {
466-
return models, fmt.Errorf("error processing enum definitions: %w", err)
466+
allowedValues, err := openapidocument.EnumToAllowedValues(pSchema)
467+
if err != nil {
468+
return models, fmt.Errorf("error processing enum definitions: %w", err)
469+
}
470+
add.Properties = append(add.Properties, Property{
471+
Name: gen.ToPropertyName(p.Key),
472+
FieldName: p.Key,
473+
Description: pSchema.Description,
474+
Title: pSchema.Title,
475+
Type: pType,
476+
IsPrimitiveType: gen.IsPrimitiveType(pType.Name),
477+
Nullable: openapiutil.IsSchemaNullable(pSchema),
478+
AllowedValues: allowedValues,
479+
})
480+
add.Imports = append(add.Imports, gen.TypeToImport(pType))
481+
482+
addedProperties = append(addedProperties, gen.ToPropertyName(p.Key))
467483
}
468-
add.Properties = append(add.Properties, Property{
469-
Name: gen.ToPropertyName(p.Key),
470-
FieldName: p.Key,
471-
Description: pSchema.Description,
472-
Title: pSchema.Title,
473-
Type: pType,
474-
IsPrimitiveType: gen.IsPrimitiveType(pType.Name),
475-
Nullable: openapiutil.IsSchemaNullable(pSchema),
476-
AllowedValues: allowedValues,
477-
})
478-
add.Imports = append(add.Imports, gen.TypeToImport(pType))
479-
480-
addedProperties = append(addedProperties, gen.ToPropertyName(p.Key))
481484
}
482485
} else if slices.Contains(s.Type, "array") {
483486
mParent, err := gen.ToCodeType(s, CodeTypeSchemaArray, false)
@@ -501,6 +504,51 @@ func BuildComponentModels(opts ModelOpts) ([]Model, error) {
501504
add.IsTypeAlias = true
502505
}
503506

507+
// polymorphism
508+
if s.Discriminator != nil {
509+
add.DiscriminatorProperty = s.Discriminator.PropertyName
510+
}
511+
if s.OneOf != nil {
512+
for _, ps := range s.OneOf {
513+
pss := ps.Schema()
514+
pssRefPath := ps.GetReference()
515+
516+
discriminatorValue := ""
517+
if s.Discriminator != nil {
518+
for entry := s.Discriminator.Mapping.Oldest(); entry != nil; entry = entry.Next() {
519+
if entry.Value == pssRefPath {
520+
discriminatorValue = entry.Key
521+
break
522+
}
523+
}
524+
}
525+
526+
add.OneOf = append(add.OneOf, Model{
527+
Name: gen.ToClassName(pss.Title),
528+
Description: pss.Description,
529+
DiscriminatorValue: discriminatorValue,
530+
})
531+
}
532+
}
533+
if s.AllOf != nil {
534+
for _, ps := range s.AllOf {
535+
pss := ps.Schema()
536+
add.AllOf = append(add.AllOf, Model{
537+
Name: gen.ToClassName(pss.Title),
538+
Description: pss.Description,
539+
})
540+
}
541+
}
542+
if s.AnyOf != nil {
543+
for _, ps := range s.AnyOf {
544+
pss := ps.Schema()
545+
add.AnyOf = append(add.AnyOf, Model{
546+
Name: gen.ToClassName(pss.Title),
547+
Description: pss.Description,
548+
})
549+
}
550+
}
551+
504552
add.Imports = uniqueSortImports(add.Imports)
505553
models = append(models, add)
506554
}

pkg/openapi/openapigenerator/types.go

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -273,17 +273,20 @@ type Parameter struct {
273273
}
274274

275275
type Model struct {
276-
Name string `yaml:"name"`
277-
Description string `yaml:"description,omitempty"`
278-
Parent CodeType `yaml:"parent,omitempty"`
279-
Properties []Property `yaml:"properties,omitempty"`
280-
AnyOf []Model `yaml:"anyOf,omitempty"`
281-
AllOf []Model `yaml:"allOf,omitempty"`
282-
OneOf []Model `yaml:"oneOf,omitempty"`
283-
Imports []string `yaml:"imports,omitempty"`
284-
Deprecated bool `yaml:"deprecated,omitempty"`
285-
DeprecatedReason string `yaml:"deprecatedReason,omitempty"`
286-
IsTypeAlias bool `yaml:"isTypeAlias,omitempty"`
276+
Name string `yaml:"name"`
277+
Description string `yaml:"description,omitempty"`
278+
Parent CodeType `yaml:"parent,omitempty"`
279+
Properties []Property `yaml:"properties,omitempty"`
280+
AnyOf []Model `yaml:"anyOf,omitempty"`
281+
AllOf []Model `yaml:"allOf,omitempty"`
282+
OneOf []Model `yaml:"oneOf,omitempty"`
283+
DiscriminatorProperty string `yaml:"discriminatorProperty,omitempty"` // DiscriminatorProperty is the name of the discriminator property
284+
DiscriminatorValue string `yaml:"discriminatorValue,omitempty"` // DiscriminatorValue is the value of the discriminator property for this model
285+
Imports []string `yaml:"imports,omitempty"`
286+
Deprecated bool `yaml:"deprecated,omitempty"`
287+
DeprecatedReason string `yaml:"deprecatedReason,omitempty"`
288+
IsTypeAlias bool `yaml:"isTypeAlias,omitempty"`
289+
SchemaReference string `yaml:"schemaReference,omitempty"` // SchemaReference points to the schema in the spec this model is based on
287290
}
288291

289292
type Enum struct {

0 commit comments

Comments
 (0)