diff --git a/internal/test/all_of/doc.go b/internal/test/all_of/doc.go index b154cb2245..e3eaf6eebf 100644 --- a/internal/test/all_of/doc.go +++ b/internal/test/all_of/doc.go @@ -2,3 +2,4 @@ package allof //go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config1.yaml openapi.yaml //go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config2.yaml openapi.yaml +//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=embed-config.yaml embed-openapi.yaml diff --git a/internal/test/all_of/embed-config.yaml b/internal/test/all_of/embed-config.yaml new file mode 100644 index 0000000000..736b7b12ed --- /dev/null +++ b/internal/test/all_of/embed-config.yaml @@ -0,0 +1,7 @@ +package: v2 +generate: + models: true + embedded-spec: true +output-options: + skip-prune: true +output: embed/openapi.gen.go diff --git a/internal/test/all_of/embed-openapi.yaml b/internal/test/all_of/embed-openapi.yaml new file mode 100644 index 0000000000..9672cb5a17 --- /dev/null +++ b/internal/test/all_of/embed-openapi.yaml @@ -0,0 +1,44 @@ +openapi: "3.0.1" +info: + version: 1.0.0 + title: Tests AllOf composition +paths: {} +components: + schemas: + PersonProperties: + type: object + description: | + These are fields that specify a person. They are all optional, and + would be used by an `Edit` style API endpoint, where each is optional. + properties: + FirstName: + type: string + LastName: + type: string + GovernmentIDNumber: + type: integer + format: int64 + FirstName: + type: object + properties: + FirstName: + type: string + Person: + type: object + x-go-allof-embed-refs: true + description: | + This is a person, with mandatory first and last name, but optional ID + number. This would be returned by a `Get` style API. We merge the person + properties with another Schema which only provides required fields. + allOf: + - $ref: "#/components/schemas/PersonProperties" + - required: [ FirstName, LastName ] + - $ref: "#/components/schemas/FirstName" + PersonAllOfSingle: + type: object + x-go-allof-embed-refs: true + description: | + This is a person record as returned from a Create endpoint. It contains + all the fields of a Person, with an additional resource UUID. + allOf: + - $ref: "#/components/schemas/Person" diff --git a/internal/test/all_of/embed/openapi.gen.go b/internal/test/all_of/embed/openapi.gen.go new file mode 100644 index 0000000000..e6bdb5ee7f --- /dev/null +++ b/internal/test/all_of/embed/openapi.gen.go @@ -0,0 +1,124 @@ +// Package v2 provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen version v0.0.0-00010101000000-000000000000 DO NOT EDIT. +package v2 + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "fmt" + "net/url" + "path" + "strings" + + "github.com/getkin/kin-openapi/openapi3" +) + +// Person defines model for Person. +type Person struct { + // Embedded struct due to allOf(#/components/schemas/PersonProperties) + PersonProperties `yaml:",inline"` + // Embedded fields due to inline allOf schema +} + +// PersonAllOfSingle defines model for PersonAllOfSingle. +type PersonAllOfSingle struct { + // Embedded struct due to allOf(#/components/schemas/Person) + Person `yaml:",inline"` +} + +// PersonProperties These are fields that specify a person. They are all optional, and +// would be used by an `Edit` style API endpoint, where each is optional. +type PersonProperties struct { + FirstName *string `json:"FirstName,omitempty"` + GovernmentIDNumber *int64 `json:"GovernmentIDNumber,omitempty"` + LastName *string `json:"LastName,omitempty"` +} + +// Base64 encoded, gzipped, json marshaled Swagger object +var swaggerSpec = []string{ + + "H4sIAAAAAAAC/5yTT2/UQAzFv4plOGbTViAOuVUUqpVQWamtOHQr1ZtxNoMST/A4XaJVvjua7L9CDwhu", + "o8h+9nv+ZYtlaLsgLBax2GIsa25pei5YY5D0oqb5WmHxsMW3yhUW+Obs1HW2bznb1S80dKzmOeKYbVH5", + "R++VHRYP+NlrtBtqGTP8Qvvn4/iYoeNYqu/Mp3l4V/sIPgJBN0lmsPFWQ0viyIIOUCUhIHHQUDQQajmD", + "VW8QJglqYH61FOnbFWsOk9wm9I2DFYOy9SrsYDUAwdM12xNEGxqGy8U8h28MLeuawWrej19Kd/S024Qk", + "WM0Kt5Nz2NS+rCFIM0Cn4dk7jnDwDZXnxsV8KZihDR1jgWH1nUvDDH/O1mFGTROqGbcrdjPlKmJh2vOY", + "7fO/TNnfelk3/K+nwL9HC8plUAcUT8FUGlog+KhMxsDiuuDFcpgblEGMvMSlUNNMEe3sQaiAYPHyWiRA", + "zvn9PZRj6LVkuL+fX/1/GC/gKravrHFkID3uZDUZxI5LXw1HwwkHHqayZOEATJZoWsqRkj7uCRF4+uT8", + "S0aOiWSwqVkZmMo6ZXrQ2tnrflv1hH6xPTiPpl7WOGZ4HZ5ZpWWx+dXNRG0qq4K2ZFigF/vw/pSYF+M1", + "a2o8/kWvVcc/Ex7TJy9VmIq9JZ7wjqNFmBiDiaM4XQwzfGaNu2Av8vP8PE0LHQt1Hgt8l5/nF8kkWZ38", + "jeOvAAAA//+5KSqZSAQAAA==", +} + +// GetSwagger returns the content of the embedded swagger specification file +// or error if failed to decode +func decodeSpec() ([]byte, error) { + zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, "")) + if err != nil { + return nil, fmt.Errorf("error base64 decoding spec: %w", err) + } + zr, err := gzip.NewReader(bytes.NewReader(zipped)) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + var buf bytes.Buffer + _, err = buf.ReadFrom(zr) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + + return buf.Bytes(), nil +} + +var rawSpec = decodeSpecCached() + +// a naive cached of a decoded swagger spec +func decodeSpecCached() func() ([]byte, error) { + data, err := decodeSpec() + return func() ([]byte, error) { + return data, err + } +} + +// Constructs a synthetic filesystem for resolving external references when loading openapi specifications. +func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { + res := make(map[string]func() ([]byte, error)) + if len(pathToFile) > 0 { + res[pathToFile] = rawSpec + } + + return res +} + +// GetSwagger returns the Swagger specification corresponding to the generated code +// in this file. The external references of Swagger specification are resolved. +// The logic of resolving external references is tightly connected to "import-mapping" feature. +// Externally referenced files must be embedded in the corresponding golang packages. +// Urls can be supported but this task was out of the scope. +func GetSwagger() (swagger *openapi3.T, err error) { + resolvePath := PathToRawSpec("") + + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) { + pathToFile := url.String() + pathToFile = path.Clean(pathToFile) + getSpec, ok := resolvePath[pathToFile] + if !ok { + err1 := fmt.Errorf("path not found: %s", pathToFile) + return nil, err1 + } + return getSpec() + } + var specData []byte + specData, err = rawSpec() + if err != nil { + return + } + swagger, err = loader.LoadFromData(specData) + if err != nil { + return + } + return +} diff --git a/internal/test/all_of/openapi.yaml b/internal/test/all_of/openapi.yaml index f73a8e5f1b..eae89ee8cc 100644 --- a/internal/test/all_of/openapi.yaml +++ b/internal/test/all_of/openapi.yaml @@ -51,3 +51,10 @@ components: type: integer format: int64 required: [ ID ] + PersonAllOfSingle: + type: object + description: | + This is a person record as returned from a Create endpoint. It contains + all the fields of a Person, with an additional resource UUID. + allOf: + - $ref: "#/components/schemas/Person" diff --git a/internal/test/all_of/v1/openapi.gen.go b/internal/test/all_of/v1/openapi.gen.go index db5fd302cd..c79610f790 100644 --- a/internal/test/all_of/v1/openapi.gen.go +++ b/internal/test/all_of/v1/openapi.gen.go @@ -22,6 +22,12 @@ type Person struct { // Embedded fields due to inline allOf schema } +// PersonAllOfSingle defines model for PersonAllOfSingle. +type PersonAllOfSingle struct { + // Embedded struct due to allOf(#/components/schemas/Person) + Person `yaml:",inline"` +} + // PersonProperties These are fields that specify a person. They are all optional, and // would be used by an `Edit` style API endpoint, where each is optional. type PersonProperties struct { @@ -41,17 +47,18 @@ type PersonWithID struct { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/5SUT2/bOBDFv8qAu0dBTrCLPegWrNtAQJEaaNIc4gAZSyOLKTVkyVEMwfB3L0jJ/+AW", - "aX0awOTjvDe/0VZVtnOWiSWoYqtC1VKHqVyQD5ZjhcZ8blTxtFV/e2pUof6aHW/Npiuz8fzCW0deNAW1", - "y7bK0/dee6pV8aQ+ah/kDjtSmfqEU/m8e85UTaHy2omO76n7VgfQARBcksxgo6WFDrlGsX6AJgoBcg0G", - "gwBjRxmsegGbJNBAOV8y992KfA5JbmN7U8OKwJP0nqmG1QAIL7ckLxBkMAQ3izKHR4KO/JpAWpqeX7I7", - "eBo7QbbSkocvyTlsWl21YNkM4Lx90zUF2PuGRpOpQ75klSkZHKlC2dUrVaJ2mbqIrNheZEGBAD1NQiAt", - "CgRHlW6GQ0LRJA3pGBpziCGLGS354L0Pk2+Glw+1PnUOxLWzmiWDTUuegLBq4xD2WqMDd9bqcaDFdm8u", - "iNe8juZu7Rt57oilnN+lWcRjjfUdiiqUZvnv32MomoXW5OPFAxuXqrtfhviopS3nf0prYvTc1Cjybpu7", - "7Iztcv47JIOnyvoaMBw5bLztAOF/Tyh0GEMOpUBlWVBzWHKcaiRygsA2gLA4XQ5kwLrWE/6egu19RfDw", - "UM5/yl7sX3NjU8ZaTPzvnoIEuInxQUosJD2VqTfyYXR0nV/lVzF164jRaVWof/Kr/DqygdKmBGfOYEWt", - "NfU48jXJJdhf0ei0zgE2yAIoYChus2WCKJVBsCAxwA6/UQSfOmjRuWE0FGeGUaysVaEWJ0/GyQRnOewX", - "qsHepBZioMSpROeMrpLA7HX6zo1sxOp9cibeUpDnzk7d79LvRwAAAP//lzc18GUFAAA=", + "H4sIAAAAAAAC/8xUzW7bOBB+lQF3j4KcYBd70C1Yt4GAIjWQpDnEATKWRhZTasiSoxiC4XcvSMk/adCm", + "uZWnOZDfzPcz3KrKds4ysQRVbFWoWuowlQvywXKs0JjPjSrut+pvT40q1F+z46vZ9GQ23l9468iLpqB2", + "2VZ5+tZrT7Uq7tVH7YNcYUcqU59wKh92D5mqKVReO9Gxn7ppdQAdAMElyAw2WlrokGsU6wdoIhAg12Aw", + "CDB2lMGqF7AJAg2U8yVz363I55DgNrY3NawIPEnvmWpYDYDweEnyCEEGQ3CxKHO4I+jIrwmkpan9kt2B", + "0zgJspWWPFwn5rBpddWCZTOA8/ZZ1xRgzxsaTaYO+ZJVpmRwpAplV09Uidplk8QXUd5rzWtD71Vbva0e", + "eKqsrwHDkXvjbQcI/3tCISCundUsOZQClWVBzWHJaExSYWQAtgGExakhyIB1rSfJPQXb+4rg9rac/5Lv", + "SUSK7avpKRCgP7SVFgWCo0o3w4FTNJWGdC1Oubc9i5lY8sHrPkw+Mzx+qPWp0wfSGWxa8gSEVRtl22ON", + "DNyLUY8BLrZ7ckG85nUkd2mfyXNHLOX8KmUvXmus71BUoTTLf/8eRdEstCYfHx524TXq7qci3mlpy/n7", + "85JtfyA1grw55i57scvl/Hc298/JXpxfc2OTxlripqkbChIgbR8kxULCU5l6Jh9GRuf5WX4WVbeOGJ1W", + "hfonP8vPYzZQ2qTgzBmsqLWmHi1fk7wO9hc0On1fATbIAihgKP5elgkiVAbBgkQBO/xKMfjUQYvODSOh", + "6BlGsLJWhVqctIzOBGc57Beqwd6kEaKgxKlE54yuEsDsafrXx2zE6u3kTHlLQr5kdsp+l873AAAA//8T", + "U6rAVQYAAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/internal/test/all_of/v2/openapi.gen.go b/internal/test/all_of/v2/openapi.gen.go index ab430340d6..13a41161ea 100644 --- a/internal/test/all_of/v2/openapi.gen.go +++ b/internal/test/all_of/v2/openapi.gen.go @@ -22,6 +22,13 @@ type Person struct { LastName string `json:"LastName"` } +// PersonAllOfSingle defines model for PersonAllOfSingle. +type PersonAllOfSingle struct { + FirstName string `json:"FirstName"` + GovernmentIDNumber *int64 `json:"GovernmentIDNumber,omitempty"` + LastName string `json:"LastName"` +} + // PersonProperties These are fields that specify a person. They are all optional, and // would be used by an `Edit` style API endpoint, where each is optional. type PersonProperties struct { @@ -41,17 +48,18 @@ type PersonWithID struct { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/5SUT2/bOBDFv8qAu0dBTrCLPegWrNtAQJEaaNIc4gAZSyOLKTVkyVEMwfB3L0jJ/+AW", - "aX0awOTjvDe/0VZVtnOWiSWoYqtC1VKHqVyQD5ZjhcZ8blTxtFV/e2pUof6aHW/Npiuz8fzCW0deNAW1", - "y7bK0/dee6pV8aQ+ah/kDjtSmfqEU/m8e85UTaHy2omO76n7VgfQARBcksxgo6WFDrlGsX6AJgoBcg0G", - "gwBjRxmsegGbJNBAOV8y992KfA5JbmN7U8OKwJP0nqmG1QAIL7ckLxBkMAQ3izKHR4KO/JpAWpqeX7I7", - "eBo7QbbSkocvyTlsWl21YNkM4Lx90zUF2PuGRpOpQ75klSkZHKlC2dUrVaJ2mbqIrNheZEGBAD1NQiAt", - "CgRHlW6GQ0LRJA3pGBpziCGLGS354L0Pk2+Glw+1PnUOxLWzmiWDTUuegLBq4xD2WqMDd9bqcaDFdm8u", - "iNe8juZu7Rt57oilnN+lWcRjjfUdiiqUZvnv32MomoXW5OPFAxuXqrtfhviopS3nf0prYvTc1Cjybpu7", - "7Iztcv47JIOnyvoaMBw5bLztAOF/Tyh0GEMOpUBlWVBzWHKcaiRygsA2gLA4XQ5kwLrWE/6egu19RfDw", - "UM5/yl7sX3NjU8ZaTPzvnoIEuInxQUosJD2VqTfyYXR0nV/lVzF164jRaVWof/Kr/DqygdKmBGfOYEWt", - "NfU48jXJJdhf0ei0zgE2yAIoYChus2WCKJVBsCAxwA6/UQSfOmjRuWE0FGeGUaysVaEWJ0/GyQRnOewX", - "qsHepBZioMSpROeMrpLA7HX6zo1sxOp9cibeUpDnzk7d79LvRwAAAP//lzc18GUFAAA=", + "H4sIAAAAAAAC/8xUzW7bOBB+lQF3j4KcYBd70C1Yt4GAIjWQpDnEATKWRhZTasiSoxiC4XcvSMk/adCm", + "uZWnOZDfzPcz3KrKds4ysQRVbFWoWuowlQvywXKs0JjPjSrut+pvT40q1F+z46vZ9GQ23l9468iLpqB2", + "2VZ5+tZrT7Uq7tVH7YNcYUcqU59wKh92D5mqKVReO9Gxn7ppdQAdAMElyAw2WlrokGsU6wdoIhAg12Aw", + "CDB2lMGqF7AJAg2U8yVz363I55DgNrY3NawIPEnvmWpYDYDweEnyCEEGQ3CxKHO4I+jIrwmkpan9kt2B", + "0zgJspWWPFwn5rBpddWCZTOA8/ZZ1xRgzxsaTaYO+ZJVpmRwpAplV09Uidplk8QXUd5rzWtD71Vbva0e", + "eKqsrwHDkXvjbQcI/3tCISCundUsOZQClWVBzWHJaExSYWQAtgGExakhyIB1rSfJPQXb+4rg9rac/5Lv", + "SUSK7avpKRCgP7SVFgWCo0o3w4FTNJWGdC1Oubc9i5lY8sHrPkw+Mzx+qPWp0wfSGWxa8gSEVRtl22ON", + "DNyLUY8BLrZ7ckG85nUkd2mfyXNHLOX8KmUvXmus71BUoTTLf/8eRdEstCYfHx524TXq7qci3mlpy/n7", + "85JtfyA1grw55i57scvl/Hc298/JXpxfc2OTxlripqkbChIgbR8kxULCU5l6Jh9GRuf5WX4WVbeOGJ1W", + "hfonP8vPYzZQ2qTgzBmsqLWmHi1fk7wO9hc0On1fATbIAihgKP5elgkiVAbBgkQBO/xKMfjUQYvODSOh", + "6BlGsLJWhVqctIzOBGc57Beqwd6kEaKgxKlE54yuEsDsafrXx2zE6u3kTHlLQr5kdsp+l873AAAA//8T", + "U6rAVQYAAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/pkg/codegen/extension.go b/pkg/codegen/extension.go index 579d8a17c9..f08ab10a6a 100644 --- a/pkg/codegen/extension.go +++ b/pkg/codegen/extension.go @@ -15,7 +15,9 @@ const ( // extGoName is used to override a field name extGoName = "x-go-name" // extGoTypeName is used to override a generated typename for something. - extGoTypeName = "x-go-type-name" + extGoTypeName = "x-go-type-name" + // extGoAllOfEmbedRefs is used to generate allOfs with refs that embedded structs (this generation can fail if it generates objects which have overlapping fields) + extGoAllOfEmbedRefs = "x-go-allof-embed-refs" extPropGoJsonIgnore = "x-go-json-ignore" extPropOmitEmpty = "x-omitempty" extPropOmitZero = "x-omitzero" @@ -37,6 +39,18 @@ func extString(extPropValue interface{}) (string, error) { return str, nil } +func ReadExtGoAllOfEmbedRefs(extensions map[string]interface{}) (bool, error) { + extPropValue, ok := extensions[extGoAllOfEmbedRefs] + if !ok { + return false, nil + } + v, ok := extPropValue.(bool) + if !ok { + return false, fmt.Errorf("for %s failed to read as boolean: %T", extGoAllOfEmbedRefs, extPropValue) + } + return v, nil +} + func extTypeName(extPropValue interface{}) (string, error) { return extString(extPropValue) } diff --git a/pkg/codegen/merge_schemas.go b/pkg/codegen/merge_schemas.go index 04e7b2fa2b..e757d8a271 100644 --- a/pkg/codegen/merge_schemas.go +++ b/pkg/codegen/merge_schemas.go @@ -10,39 +10,42 @@ import ( // MergeSchemas merges all the fields in the schemas supplied into one giant schema. // The idea is that we merge all fields together into one schema. -func MergeSchemas(allOf []*openapi3.SchemaRef, path []string) (Schema, error) { +func MergeSchemas(allOf []*openapi3.SchemaRef, path []string, embedRefs bool) (Schema, error) { // If someone asked for the old way, for backward compatibility, return the // old style result. if globalState.options.Compatibility.OldMergeSchemas { return mergeSchemasV1(allOf, path) } - return mergeSchemas(allOf, path) + // Get the flatten schema for generation in the regular case and for validation when using `embedRefs` + return mergeSchemas(allOf, path, embedRefs) } -func mergeSchemas(allOf []*openapi3.SchemaRef, path []string) (Schema, error) { - n := len(allOf) +func mergeSchemas(allOf []*openapi3.SchemaRef, path []string, embedRefs bool) (Schema, error) { + schema := openapi3.Schema{} - if n == 1 { - return GenerateGoSchema(allOf[0], path) - } - - schema, err := valueWithPropagatedRef(allOf[0]) - if err != nil { - return Schema{}, err - } - - for i := 1; i < n; i++ { + // Generate a flattened version of the schema + for i := range allOf { var err error oneOfSchema, err := valueWithPropagatedRef(allOf[i]) if err != nil { return Schema{}, err } - schema, err = mergeOpenapiSchemas(schema, oneOfSchema, true) + schema, err = mergeOpenapiSchemas(schema, oneOfSchema, true, embedRefs) if err != nil { return Schema{}, fmt.Errorf("error merging schemas for AllOf: %w", err) } } - return GenerateGoSchema(openapi3.NewSchemaRef("", &schema), path) + if !embedRefs { + return GenerateGoSchema(openapi3.NewSchemaRef("", &schema), path) + } + // If using x-go-allof-embed-refs generate a struct with references so use the original schema. + var outSchema Schema + var err error + outSchema.GoType, err = GenStructFromAllOf(allOf, path) + if err != nil { + return Schema{}, fmt.Errorf("unable to generate aggregate type for AllOf: %w", err) + } + return outSchema, nil } // valueWithPropagatedRef returns a copy of ref schema with its Properties refs @@ -70,11 +73,11 @@ func valueWithPropagatedRef(ref *openapi3.SchemaRef) (openapi3.Schema, error) { return schema, nil } -func mergeAllOf(allOf []*openapi3.SchemaRef) (openapi3.Schema, error) { +func mergeAllOf(allOf []*openapi3.SchemaRef, embedRefs bool) (openapi3.Schema, error) { var schema openapi3.Schema for _, schemaRef := range allOf { var err error - schema, err = mergeOpenapiSchemas(schema, *schemaRef.Value, true) + schema, err = mergeOpenapiSchemas(schema, *schemaRef.Value, true, embedRefs) if err != nil { return openapi3.Schema{}, fmt.Errorf("error merging schemas for AllOf: %w", err) } @@ -84,7 +87,10 @@ func mergeAllOf(allOf []*openapi3.SchemaRef) (openapi3.Schema, error) { // mergeOpenapiSchemas merges two openAPI schemas and returns the schema // all of whose fields are composed. -func mergeOpenapiSchemas(s1, s2 openapi3.Schema, allOf bool) (openapi3.Schema, error) { +func mergeOpenapiSchemas(s1, s2 openapi3.Schema, allOf bool, embedRefs bool) (openapi3.Schema, error) { + if s1.IsEmpty() { + return s2, nil + } var result openapi3.Schema result.Extensions = make(map[string]interface{}) @@ -103,7 +109,7 @@ func mergeOpenapiSchemas(s1, s2 openapi3.Schema, allOf bool) (openapi3.Schema, e var err error if s1.AllOf != nil { var merged openapi3.Schema - merged, err = mergeAllOf(s1.AllOf) + merged, err = mergeAllOf(s1.AllOf, embedRefs) if err != nil { return openapi3.Schema{}, fmt.Errorf("error transitive merging AllOf on schema 1") } @@ -111,7 +117,7 @@ func mergeOpenapiSchemas(s1, s2 openapi3.Schema, allOf bool) (openapi3.Schema, e } if s2.AllOf != nil { var merged openapi3.Schema - merged, err = mergeAllOf(s2.AllOf) + merged, err = mergeAllOf(s2.AllOf, embedRefs) if err != nil { return openapi3.Schema{}, fmt.Errorf("error transitive merging AllOf on schema 2") } @@ -187,10 +193,17 @@ func mergeOpenapiSchemas(s1, s2 openapi3.Schema, allOf bool) (openapi3.Schema, e if s1.AllowEmptyValue != s2.AllowEmptyValue { return openapi3.Schema{}, errors.New("merging two schemas with different AllowEmptyValue") - } result.AllowEmptyValue = s1.AllowEmptyValue + if embedRefs { + for _, r := range s2.Required { + if _, exists := s2.Properties[r]; !exists { + return openapi3.Schema{}, fmt.Errorf("impossible to modify required fields outside of their own schema when using %s", extGoAllOfEmbedRefs) + } + } + } + // Required. We merge these. result.Required = append(s1.Required, s2.Required...) @@ -200,7 +213,11 @@ func mergeOpenapiSchemas(s1, s2 openapi3.Schema, allOf bool) (openapi3.Schema, e result.Properties[k] = v } for k, v := range s2.Properties { - // TODO: detect conflicts + if embedRefs { + if _, exists := result.Properties[k]; exists { + return openapi3.Schema{}, fmt.Errorf("overlapping fields (%s) between sibling entities is not allowed when using %s", k, extGoAllOfEmbedRefs) + } + } result.Properties[k] = v } diff --git a/pkg/codegen/schema.go b/pkg/codegen/schema.go index c099752d88..297230ebe4 100644 --- a/pkg/codegen/schema.go +++ b/pkg/codegen/schema.go @@ -318,7 +318,11 @@ func GenerateGoSchema(sref *openapi3.SchemaRef, path []string) (Schema, error) { // so that in a RESTful paradigm, the Create operation can return // (object, id), so that other operations can refer to (id) if schema.AllOf != nil { - mergedSchema, err := MergeSchemas(schema.AllOf, path) + embedRefs, err := ReadExtGoAllOfEmbedRefs(schema.Extensions) + if err != nil { + return Schema{}, fmt.Errorf("error merging schemas: %w", err) + } + mergedSchema, err := MergeSchemas(schema.AllOf, path, embedRefs) if err != nil { return Schema{}, fmt.Errorf("error merging schemas: %w", err) }