Skip to content

Commit

Permalink
pr/tiehm/66 (#93)
Browse files Browse the repository at this point in the history
* proposal: add extension x-go-type-external for external type references
* convert to x-go-type and allow for custom types in bodies
* remove examples with x-go-type
* introduce the import:[import-name] format to import by specified name without an alias
* fix: schema imports were missing
* add first testing
* fix: old example

Authored-by: tiehm <[email protected]>
  • Loading branch information
Karitham authored Jul 25, 2022
1 parent e7fa6cd commit 9494cfa
Show file tree
Hide file tree
Showing 12 changed files with 320 additions and 60 deletions.
32 changes: 27 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ takes as input `FindPetsParams`, which is defined as follows:
```go
// Parameters object for FindPets
type FindPetsParams struct {
Tags *[]string `json:"tags,omitempty"`
Limit *int32 `json:"limit,omitempty"`
Tags *[]string `json:"tags,omitempty"`
Limit *int32 `json:"limit,omitempty"`
}
```

Expand Down Expand Up @@ -235,9 +235,31 @@ look through those tests for more usage examples.
`goapi-gen` supports the following extended properties:

- `x-go-type`: specifies Go type name. It allows you to specify the type name for a schema, and
will override any default value. This extended property isn't supported in all parts of
OpenAPI, so please refer to the spec as to where it's allowed. Swagger validation tools will
flag incorrect usage of this property.
will override any default value. When using this with external types the fields `type,import` are required while
`alias` is only needed if your import collides with any existing imports. **If no alias is given, the generator
assumes that the import name is the last word from the import path (github.com/example/time => time). This can produce
issues as some projects (like chi with /v5) have different import names than paths. To be sure, always declare an alias.
For those cases, please attach a `:[import-name]` to the path like `github.com/go-chi/chi/v5:chi`, this will then also be
imported as `chi.*` without the use of an alias.**
This extended property isn't supported in all parts of OpenAPI, so please refer to the spec as to where it's allowed.
Swagger validation tools will flag incorrect usage of this property.

```yaml
components:
schemas:
Object:
properties:
name:
type: string
x-go-type: MyCustomString
time:
type: integer
x-go-type:
type: MyCustomTime
import: github.com/example/time
alias: time2
```
- `x-go-extra-tags`: adds extra Go field tags to the generated struct field. This is
useful for interfacing with tag based ORM or validation libraries. The extra tags that
are added are in addition to the regular json tags that are generated. If you specify your
Expand Down
42 changes: 30 additions & 12 deletions codegen/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,20 @@ func Generate(swagger *openapi3.T, packageName string, opts Options) (string, er
}
}

var finalCustomImports []string
ops, err := OperationDefinitions(swagger)
if err != nil {
return "", fmt.Errorf("error creating operation definitions: %w", err)
}

for _, op := range ops {
finalCustomImports = append(finalCustomImports, op.CustomImports...)
}

var typeDefinitions, constantDefinitions string
var customImports []string
if opts.GenerateTypes {
typeDefinitions, err = GenerateTypeDefinitions(t, swagger, ops, opts.ExcludeSchemas)
typeDefinitions, customImports, err = GenerateTypeDefinitions(t, swagger, ops, opts.ExcludeSchemas)
if err != nil {
return "", fmt.Errorf("error generating type definitions: %w", err)
}
Expand All @@ -150,6 +156,9 @@ func Generate(swagger *openapi3.T, packageName string, opts Options) (string, er

}

// TODO: check for exact double imports and merge them together with 1 alias, otherwise we might run into double imports under different names
finalCustomImports = append(finalCustomImports, customImports...)

var serverOut string
if opts.GenerateServer {
serverOut, err = GenerateChiServer(t, ops)
Expand All @@ -170,6 +179,7 @@ func Generate(swagger *openapi3.T, packageName string, opts Options) (string, er
w := bufio.NewWriter(&buf)

externalImports := importMapping.GoImports()
externalImports = append(externalImports, finalCustomImports...)
importsOut, err := GenerateImports(t, externalImports, packageName)
if err != nil {
return "", fmt.Errorf("error generating imports: %w", err)
Expand Down Expand Up @@ -227,57 +237,65 @@ func Generate(swagger *openapi3.T, packageName string, opts Options) (string, er

// GenerateTypeDefinitions produces the type definitions in ops and executes
// the template.
func GenerateTypeDefinitions(t *template.Template, swagger *openapi3.T, ops []OperationDefinition, excludeSchemas []string) (string, error) {
func GenerateTypeDefinitions(t *template.Template, swagger *openapi3.T, ops []OperationDefinition, excludeSchemas []string) (string, []string, error) {
schemaTypes, err := GenerateTypesForSchemas(t, swagger.Components.Schemas, excludeSchemas)
if err != nil {
return "", fmt.Errorf("error generating Go types for component schemas: %w", err)
return "", nil, fmt.Errorf("error generating Go types for component schemas: %w", err)
}

paramTypes, err := GenerateTypesForParameters(t, swagger.Components.Parameters)
if err != nil {
return "", fmt.Errorf("error generating Go types for component parameters: %w", err)
return "", nil, fmt.Errorf("error generating Go types for component parameters: %w", err)
}
allTypes := append(schemaTypes, paramTypes...)

responseTypes, err := GenerateTypesForResponses(t, swagger.Components.Responses)
if err != nil {
return "", fmt.Errorf("error generating Go types for component responses: %w", err)
return "", nil, fmt.Errorf("error generating Go types for component responses: %w", err)
}
allTypes = append(allTypes, responseTypes...)

bodyTypes, err := GenerateTypesForRequestBodies(t, swagger.Components.RequestBodies)
if err != nil {
return "", fmt.Errorf("error generating Go types for component request bodies: %w", err)
return "", nil, fmt.Errorf("error generating Go types for component request bodies: %w", err)
}
allTypes = append(allTypes, bodyTypes...)

paramTypesOut, err := GenerateTypesForOperations(t, ops)
if err != nil {
return "", fmt.Errorf("error generating Go types for component request bodies: %w", err)
return "", nil, fmt.Errorf("error generating Go types for component request bodies: %w", err)
}

enumsOut, err := GenerateEnums(t, allTypes)
if err != nil {
return "", fmt.Errorf("error generating code for type enums: %w", err)
return "", nil, fmt.Errorf("error generating code for type enums: %w", err)
}

enumTypesOut, err := GenerateEnumTypes(t, allTypes)
if err != nil {
return "", fmt.Errorf("error generating code for enum type definitions: %w", err)
return "", nil, fmt.Errorf("error generating code for enum type definitions: %w", err)
}

typesOut, err := GenerateTypes(t, allTypes)
if err != nil {
return "", fmt.Errorf("error generating code for type definitions: %w", err)
return "", nil, fmt.Errorf("error generating code for type definitions: %w", err)
}

allOfBoilerplate, err := GenerateAdditionalPropertyBoilerplate(t, allTypes)
if err != nil {
return "", fmt.Errorf("error generating allOf boilerplate: %w", err)
return "", nil, fmt.Errorf("error generating allOf boilerplate: %w", err)
}

var customImports []string
for _, allType := range allTypes {
customImports = append(customImports, allType.Schema.CustomImports...)
for _, prop := range allType.Schema.Properties {
customImports = append(customImports, prop.Schema.CustomImports...)
}
}

typeDefinitions := enumsOut + typesOut + enumTypesOut + paramTypesOut + allOfBoilerplate
return typeDefinitions, nil
return typeDefinitions, customImports, nil
}

// GenerateConstants creates operation ids, context keys, paths, etc. to be
Expand Down
25 changes: 22 additions & 3 deletions codegen/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,29 @@ const (
extMiddlewares = "x-go-middlewares"
)

func extTypeName(extPropValue interface{}) (string, error) {
type extImportPathDetails struct {
Import string `json:"import"`
Alias string `json:"alias"`
Type string `json:"type"`
}


func extImportPath(extPropValue interface{}) (extImportPathDetails, error) {
var details extImportPathDetails
raw, ok := extPropValue.(json.RawMessage)
if !ok {
return details, fmt.Errorf("failed to convert type: %T", extPropValue)
}

var name string
err := extParseAny(extPropValue, &name)
return name, err
if err := json.Unmarshal(raw, &name); err == nil {
details.Type = name
return details, nil
}

var path extImportPathDetails
err := extParseAny(extPropValue, &path)
return path, err
}

func extExtraTags(extPropValue interface{}) (map[string]string, error) {
Expand Down
12 changes: 6 additions & 6 deletions codegen/extension_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,38 +7,38 @@ import (
"github.com/stretchr/testify/assert"
)

func Test_extTypeName(t *testing.T) {
func Test_extImportPath(t *testing.T) {
type args struct {
extPropValue interface{}
}
tests := []struct {
name string
args args
want string
want extImportPathDetails
wantErr bool
}{
{
name: "success",
args: args{json.RawMessage(`"uint64"`)},
want: "uint64",
want: extImportPathDetails{Type: "uint64"},
wantErr: false,
},
{
name: "type conversion error",
args: args{nil},
want: "",
want: extImportPathDetails{},
wantErr: true,
},
{
name: "json unmarshal error",
args: args{json.RawMessage("invalid json format")},
want: "",
want: extImportPathDetails{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := extTypeName(tt.args.extPropValue)
got, err := extImportPath(tt.args.extPropValue)
if tt.wantErr {
assert.Error(t, err)
return
Expand Down
17 changes: 12 additions & 5 deletions codegen/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ func DescribeSecurityDefinition(srs openapi3.SecurityRequirements) []SecurityDef
type OperationDefinition struct {
OperationID string // The operation_id description from Swagger, used to generate function names

CustomImports []string // Custom needed imports due to parameters being of external types
PathParams []ParameterDefinition // Parameters in the path, eg, /path/:param
HeaderParams []ParameterDefinition // Parameters in HTTP headers
QueryParams []ParameterDefinition // Parameters in the query, /path?param
Expand Down Expand Up @@ -436,12 +437,18 @@ func OperationDefinitions(swagger *openapi3.T) ([]OperationDefinition, error) {
return nil, fmt.Errorf("error generating body definitions: %w", err)
}

var customImports []string
for _, allParam := range allParams {
customImports = append(customImports, allParam.Schema.CustomImports...)
}

opDef := OperationDefinition{
PathParams: pathParams,
HeaderParams: FilterParameterDefinitionByType(allParams, "header"),
QueryParams: FilterParameterDefinitionByType(allParams, "query"),
CookieParams: FilterParameterDefinitionByType(allParams, "cookie"),
OperationID: ToCamelCase(op.OperationID),
PathParams: pathParams,
CustomImports: customImports,
HeaderParams: FilterParameterDefinitionByType(allParams, "header"),
QueryParams: FilterParameterDefinitionByType(allParams, "query"),
CookieParams: FilterParameterDefinitionByType(allParams, "cookie"),
OperationID: ToCamelCase(op.OperationID),
// Replace newlines in summary.
Summary: op.Summary,
Method: opName,
Expand Down
39 changes: 32 additions & 7 deletions codegen/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import (

// Schema represents an OpenAPI type definition.
type Schema struct {
GoType string // The Go type needed to represent the schema
RefType string // If the type has a type name, this is set
CustomImports []string // The custom imports which are needed for x-go-type-external
GoType string // The Go type needed to represent the schema
RefType string // If the type has a type name, this is set

ArrayType *Schema // The schema of array element

Expand Down Expand Up @@ -184,9 +185,10 @@ func GenerateGoSchema(sref *openapi3.SchemaRef, path []string) (Schema, error) {
}

outSchema := Schema{
Description: StringToGoComment(schema.Description),
OAPISchema: schema,
Bindable: true,
CustomImports: []string{},
Description: StringToGoComment(schema.Description),
OAPISchema: schema,
Bindable: true,
}

// FIXME(hhhapz): We can probably support this in a meaningful way.
Expand All @@ -208,11 +210,34 @@ func GenerateGoSchema(sref *openapi3.SchemaRef, path []string) (Schema, error) {

// Check for custom Go type extension
if extension, ok := schema.Extensions[extPropGoType]; ok {
typeName, err := extTypeName(extension)
typeDetails, err := extImportPath(extension)
if err != nil {
return outSchema, fmt.Errorf("invalid value for %q: %w", extPropGoType, err)
}
outSchema.GoType = typeName

outSchema.GoType = typeDetails.Type
// if we have a custom import for our type as it is an external type we need to the imports
if typeDetails.Import != "" {
// if the import should have an alias it needs to be specified
if typeDetails.Alias != "" {
// we need to set the gotype with the correct import name
outSchema.GoType = fmt.Sprintf("%s.%s", typeDetails.Alias, typeDetails.Type)
outSchema.CustomImports = append(outSchema.CustomImports, fmt.Sprintf("%s \"%s\"", typeDetails.Alias, typeDetails.Import))
} else {
// as no alias is provided we need to take the import

// if there is an import name specified instead of an alias, take it
if strings.Contains(typeDetails.Import, ":") {
splitImport := strings.Split(typeDetails.Import, ":")
outSchema.GoType = fmt.Sprintf("%s.%s", splitImport[1], typeDetails.Type)
typeDetails.Import = splitImport[0]
} else {
splitImport := strings.Split(typeDetails.Import, "/")
outSchema.GoType = fmt.Sprintf("%s.%s", splitImport[len(splitImport)-1], typeDetails.Type)
}
outSchema.CustomImports = append(outSchema.CustomImports, fmt.Sprintf("\"%s\"", typeDetails.Import))
}
}
return outSchema, nil
}

Expand Down
2 changes: 1 addition & 1 deletion examples/petstore-expanded/api/petstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func (p *PetStore) DeletePet(w http.ResponseWriter, r *http.Request, id int64) *
if !found {
return DeletePetJSONDefaultResponse(Error{fmt.Sprintf(petNotFoundMsg, id)}).Status(http.StatusNotFound)
}
delete(p.Pets, id)
delete(p.Pets, int64(id))

return &Response{Code: http.StatusNoContent}
}
Loading

0 comments on commit 9494cfa

Please sign in to comment.