package codegen import ( "fmt" "strings" "github.com/getkin/kin-openapi/openapi3" "github.com/pkg/errors" ) // This describes a Schema, a 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 ArrayType *Schema // The schema of array element EnumValues map[string]string // Enum values Properties []Property // For an object, the fields with names HasAdditionalProperties bool // Whether we support additional properties AdditionalPropertiesType *Schema // And if we do, their type AdditionalTypes []TypeDefinition // We may need to generate auxiliary helper types, stored here SkipOptionalPointer bool // Some types don't need a * in front when they're optional Description string // The description of the element // The original OpenAPIv3 Schema. OAPISchema *openapi3.Schema } func (s Schema) IsRef() bool { return s.RefType != "" } func (s Schema) TypeDecl() string { if s.IsRef() { return s.RefType } return s.GoType } func (s *Schema) MergeProperty(p Property) error { // Scan all existing properties for a conflict for _, e := range s.Properties { if e.JsonFieldName == p.JsonFieldName && !PropertiesEqual(e, p) { return errors.New(fmt.Sprintf("property '%s' already exists with a different type", e.JsonFieldName)) } } s.Properties = append(s.Properties, p) return nil } func (s Schema) GetAdditionalTypeDefs() []TypeDefinition { var result []TypeDefinition for _, p := range s.Properties { result = append(result, p.Schema.GetAdditionalTypeDefs()...) } result = append(result, s.AdditionalTypes...) return result } type Property struct { Description string JsonFieldName string Schema Schema Required bool Nullable bool ExtensionProps *openapi3.ExtensionProps } func (p Property) GoFieldName() string { return SchemaNameToTypeName(p.JsonFieldName) } func (p Property) GoTypeDef() string { typeDef := p.Schema.TypeDecl() if !p.Schema.SkipOptionalPointer && (!p.Required || p.Nullable) { typeDef = "*" + typeDef } return typeDef } // EnumDefinition holds type information for enum type EnumDefinition struct { Schema Schema TypeName string ValueWrapper string } type Constants struct { // SecuritySchemeProviderNames holds all provider names for security schemes. SecuritySchemeProviderNames []string // EnumDefinitions holds type and value information for all enums EnumDefinitions []EnumDefinition } // TypeDefinition describes a Go type definition in generated code. // // Let's use this example schema: // components: // schemas: // Person: // type: object // properties: // name: // type: string type TypeDefinition struct { // The name of the type, eg, type <...> Person TypeName string // The name of the corresponding JSON description, as it will sometimes // differ due to invalid characters. JsonName string // This is the Schema wrapper is used to populate the type description Schema Schema } // ResponseTypeDefinition is an extension of TypeDefinition, specifically for // response unmarshaling in ClientWithResponses. type ResponseTypeDefinition struct { TypeDefinition // The content type name where this is used, eg, application/json ContentTypeName string // The type name of a response model. ResponseName string } func (t *TypeDefinition) CanAlias() bool { return t.Schema.IsRef() || /* actual reference */ (t.Schema.ArrayType != nil && t.Schema.ArrayType.IsRef()) /* array to ref */ } func PropertiesEqual(a, b Property) bool { return a.JsonFieldName == b.JsonFieldName && a.Schema.TypeDecl() == b.Schema.TypeDecl() && a.Required == b.Required } func GenerateGoSchema(sref *openapi3.SchemaRef, path []string) (Schema, error) { // Add a fallback value in case the sref is nil. // i.e. the parent schema defines a type:array, but the array has // no items defined. Therefore we have at least valid Go-Code. if sref == nil { return Schema{GoType: "interface{}"}, nil } schema := sref.Value // If Ref is set on the SchemaRef, it means that this type is actually a reference to // another type. We're not de-referencing, so simply use the referenced type. if IsGoTypeReference(sref.Ref) { // Convert the reference path to Go type refType, err := RefPathToGoType(sref.Ref) if err != nil { return Schema{}, fmt.Errorf("error turning reference (%s) into a Go type: %s", sref.Ref, err) } return Schema{ GoType: refType, Description: StringToGoComment(schema.Description), }, nil } outSchema := Schema{ Description: StringToGoComment(schema.Description), OAPISchema: schema, } // We can't support this in any meaningful way if schema.AnyOf != nil { outSchema.GoType = "interface{}" return outSchema, nil } // We can't support this in any meaningful way if schema.OneOf != nil { outSchema.GoType = "interface{}" return outSchema, nil } // AllOf is interesting, and useful. It's the union of a number of other // schemas. A common usage is to create a union of an object with an ID, // 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) if err != nil { return Schema{}, errors.Wrap(err, "error merging schemas") } mergedSchema.OAPISchema = schema return mergedSchema, nil } // Check for custom Go type extension if extension, ok := schema.Extensions[extPropGoType]; ok { typeName, err := extTypeName(extension) if err != nil { return outSchema, errors.Wrapf(err, "invalid value for %q", extPropGoType) } outSchema.GoType = typeName return outSchema, nil } // Schema type and format, eg. string / binary t := schema.Type // Handle objects and empty schemas first as a special case if t == "" || t == "object" { var outType string if len(schema.Properties) == 0 && !SchemaHasAdditionalProperties(schema) { // If the object has no properties or additional properties, we // have some special cases for its type. if t == "object" { // We have an object with no properties. This is a generic object // expressed as a map. outType = "map[string]interface{}" } else { // t == "" // If we don't even have the object designator, we're a completely // generic type. outType = "interface{}" } outSchema.GoType = outType } else { // We've got an object with some properties. for _, pName := range SortedSchemaKeys(schema.Properties) { p := schema.Properties[pName] propertyPath := append(path, pName) pSchema, err := GenerateGoSchema(p, propertyPath) if err != nil { return Schema{}, errors.Wrap(err, fmt.Sprintf("error generating Go schema for property '%s'", pName)) } required := StringInArray(pName, schema.Required) if pSchema.HasAdditionalProperties && pSchema.RefType == "" { // If we have fields present which have additional properties, // but are not a pre-defined type, we need to define a type // for them, which will be based on the field names we followed // to get to the type. typeName := PathToTypeName(propertyPath) typeDef := TypeDefinition{ TypeName: typeName, JsonName: strings.Join(propertyPath, "."), Schema: pSchema, } pSchema.AdditionalTypes = append(pSchema.AdditionalTypes, typeDef) pSchema.RefType = typeName } description := "" if p.Value != nil { description = p.Value.Description } prop := Property{ JsonFieldName: pName, Schema: pSchema, Required: required, Description: description, Nullable: p.Value.Nullable, ExtensionProps: &p.Value.ExtensionProps, } outSchema.Properties = append(outSchema.Properties, prop) } outSchema.HasAdditionalProperties = SchemaHasAdditionalProperties(schema) outSchema.AdditionalPropertiesType = &Schema{ GoType: "interface{}", } if schema.AdditionalProperties != nil { additionalSchema, err := GenerateGoSchema(schema.AdditionalProperties, path) if err != nil { return Schema{}, errors.Wrap(err, "error generating type for additional properties") } outSchema.AdditionalPropertiesType = &additionalSchema } outSchema.GoType = GenStructFromSchema(outSchema) } return outSchema, nil } else if len(schema.Enum) > 0 { err := resolveType(schema, path, &outSchema) if err != nil { return Schema{}, errors.Wrap(err, "error resolving primitive type") } enumValues := make([]string, len(schema.Enum)) for i, enumValue := range schema.Enum { enumValues[i] = fmt.Sprintf("%v", enumValue) } sanitizedValues := SanitizeEnumNames(enumValues) outSchema.EnumValues = make(map[string]string, len(sanitizedValues)) var constNamePath []string for k, v := range sanitizedValues { if v == "" { constNamePath = append(path, "Empty") } else { constNamePath = append(path, k) } outSchema.EnumValues[SchemaNameToTypeName(PathToTypeName(constNamePath))] = v } if len(path) > 1 { // handle additional type only on non-toplevel types typeName := SchemaNameToTypeName(PathToTypeName(path)) typeDef := TypeDefinition{ TypeName: typeName, JsonName: strings.Join(path, "."), Schema: outSchema, } outSchema.AdditionalTypes = append(outSchema.AdditionalTypes, typeDef) outSchema.RefType = typeName } //outSchema.RefType = typeName } else { err := resolveType(schema, path, &outSchema) if err != nil { return Schema{}, errors.Wrap(err, "error resolving primitive type") } } return outSchema, nil } // resolveType resolves primitive type or array for schema func resolveType(schema *openapi3.Schema, path []string, outSchema *Schema) error { f := schema.Format t := schema.Type switch t { case "array": // For arrays, we'll get the type of the Items and throw a // [] in front of it. arrayType, err := GenerateGoSchema(schema.Items, path) if err != nil { return errors.Wrap(err, "error generating type for array") } outSchema.ArrayType = &arrayType outSchema.GoType = "[]" + arrayType.TypeDecl() additionalTypes := arrayType.GetAdditionalTypeDefs() // Check also types defined in array item if len(additionalTypes) > 0 { outSchema.AdditionalTypes = append(outSchema.AdditionalTypes, additionalTypes...) } outSchema.Properties = arrayType.Properties case "integer": // We default to int if format doesn't ask for something else. if f == "int64" { outSchema.GoType = "int64" } else if f == "int32" { outSchema.GoType = "int32" } else if f == "int16" { outSchema.GoType = "int16" } else if f == "int8" { outSchema.GoType = "int8" } else if f == "int" { outSchema.GoType = "int" } else if f == "uint64" { outSchema.GoType = "uint64" } else if f == "uint32" { outSchema.GoType = "uint32" } else if f == "uint16" { outSchema.GoType = "uint16" } else if f == "uint8" { outSchema.GoType = "uint8" } else if f == "uint" { outSchema.GoType = "uint" } else if f == "" { outSchema.GoType = "int" } else { return fmt.Errorf("invalid integer format: %s", f) } case "number": // We default to float for "number" if f == "double" { outSchema.GoType = "float64" } else if f == "float" || f == "" { outSchema.GoType = "float32" } else { return fmt.Errorf("invalid number format: %s", f) } case "boolean": if f != "" { return fmt.Errorf("invalid format (%s) for boolean", f) } outSchema.GoType = "bool" case "string": // Special case string formats here. switch f { case "byte": outSchema.GoType = "[]byte" case "email": outSchema.GoType = "openapi_types.Email" case "date": outSchema.GoType = "openapi_types.Date" case "date-time": outSchema.GoType = "time.Time" case "json": outSchema.GoType = "json.RawMessage" outSchema.SkipOptionalPointer = true default: // All unrecognized formats are simply a regular string. outSchema.GoType = "string" } default: return fmt.Errorf("unhandled Schema type: %s", t) } return nil } // This describes a Schema, a type definition. type SchemaDescriptor struct { Fields []FieldDescriptor HasAdditionalProperties bool AdditionalPropertiesType string } type FieldDescriptor struct { Required bool // Is the schema required? If not, we'll pass by pointer GoType string // The Go type needed to represent the json type. GoName string // The Go compatible type name for the type JsonName string // The json type name for the type IsRef bool // Is this schema a reference to predefined object? } // Given a list of schema descriptors, produce corresponding field names with // JSON annotations func GenFieldsFromProperties(props []Property) []string { var fields []string for i, p := range props { field := "" // Add a comment to a field in case we have one, otherwise skip. if p.Description != "" { // Separate the comment from a previous-defined, unrelated field. // Make sure the actual field is separated by a newline. if i != 0 { field += "\n" } field += fmt.Sprintf("%s\n", StringToGoComment(p.Description)) } field += fmt.Sprintf(" %s %s", p.GoFieldName(), p.GoTypeDef()) // Support x-omitempty omitEmpty := true if _, ok := p.ExtensionProps.Extensions[extPropOmitEmpty]; ok { if extOmitEmpty, err := extParseOmitEmpty(p.ExtensionProps.Extensions[extPropOmitEmpty]); err == nil { omitEmpty = extOmitEmpty } } fieldTags := make(map[string]string) if p.Required || p.Nullable || !omitEmpty { fieldTags["json"] = p.JsonFieldName } else { fieldTags["json"] = p.JsonFieldName + ",omitempty" } if extension, ok := p.ExtensionProps.Extensions[extPropExtraTags]; ok { if tags, err := extExtraTags(extension); err == nil { keys := SortedStringKeys(tags) for _, k := range keys { fieldTags[k] = tags[k] } } } // Convert the fieldTags map into Go field annotations. keys := SortedStringKeys(fieldTags) tags := make([]string, len(keys)) for i, k := range keys { tags[i] = fmt.Sprintf(`%s:"%s"`, k, fieldTags[k]) } field += "`" + strings.Join(tags, " ") + "`" fields = append(fields, field) } return fields } func GenStructFromSchema(schema Schema) string { // Start out with struct { objectParts := []string{"struct {"} // Append all the field definitions objectParts = append(objectParts, GenFieldsFromProperties(schema.Properties)...) // Close the struct if schema.HasAdditionalProperties { addPropsType := schema.AdditionalPropertiesType.GoType if schema.AdditionalPropertiesType.RefType != "" { addPropsType = schema.AdditionalPropertiesType.RefType } objectParts = append(objectParts, fmt.Sprintf("AdditionalProperties map[string]%s `json:\"-\"`", addPropsType)) } objectParts = append(objectParts, "}") return strings.Join(objectParts, "\n") } // Merge all the fields in the schemas supplied into one giant schema. func MergeSchemas(allOf []*openapi3.SchemaRef, path []string) (Schema, error) { var outSchema Schema for _, schemaOrRef := range allOf { ref := schemaOrRef.Ref var refType string var err error if IsGoTypeReference(ref) { refType, err = RefPathToGoType(ref) if err != nil { return Schema{}, errors.Wrap(err, "error converting reference path to a go type") } } schema, err := GenerateGoSchema(schemaOrRef, path) if err != nil { return Schema{}, errors.Wrap(err, "error generating Go schema in allOf") } schema.RefType = refType for _, p := range schema.Properties { err = outSchema.MergeProperty(p) if err != nil { return Schema{}, errors.Wrap(err, "error merging properties") } } if schema.HasAdditionalProperties { if outSchema.HasAdditionalProperties { // Both this schema, and the aggregate schema have additional // properties, they must match. if schema.AdditionalPropertiesType.TypeDecl() != outSchema.AdditionalPropertiesType.TypeDecl() { return Schema{}, errors.New("additional properties in allOf have incompatible types") } } else { // We're switching from having no additional properties to having // them outSchema.HasAdditionalProperties = true outSchema.AdditionalPropertiesType = schema.AdditionalPropertiesType } } } // Now, we generate the struct which merges together all the fields. var err error outSchema.GoType, err = GenStructFromAllOf(allOf, path) if err != nil { return Schema{}, errors.Wrap(err, "unable to generate aggregate type for AllOf") } return outSchema, nil } // This function generates an object that is the union of the objects in the // input array. In the case of Ref objects, we use an embedded struct, otherwise, // we inline the fields. func GenStructFromAllOf(allOf []*openapi3.SchemaRef, path []string) (string, error) { // Start out with struct { objectParts := []string{"struct {"} for _, schemaOrRef := range allOf { ref := schemaOrRef.Ref if IsGoTypeReference(ref) { // We have a referenced type, we will generate an inlined struct // member. // struct { // InlinedMember // ... // } goType, err := RefPathToGoType(ref) if err != nil { return "", err } objectParts = append(objectParts, fmt.Sprintf(" // Embedded struct due to allOf(%s)", ref)) objectParts = append(objectParts, fmt.Sprintf(" %s `yaml:\",inline\"`", goType)) } else { // Inline all the fields from the schema into the output struct, // just like in the simple case of generating an object. goSchema, err := GenerateGoSchema(schemaOrRef, path) if err != nil { return "", err } objectParts = append(objectParts, " // Embedded fields due to inline allOf schema") objectParts = append(objectParts, GenFieldsFromProperties(goSchema.Properties)...) if goSchema.HasAdditionalProperties { addPropsType := goSchema.AdditionalPropertiesType.GoType if goSchema.AdditionalPropertiesType.RefType != "" { addPropsType = goSchema.AdditionalPropertiesType.RefType } additionalPropertiesPart := fmt.Sprintf("AdditionalProperties map[string]%s `json:\"-\"`", addPropsType) if !StringInArray(additionalPropertiesPart, objectParts) { objectParts = append(objectParts, additionalPropertiesPart) } } } } objectParts = append(objectParts, "}") return strings.Join(objectParts, "\n"), nil } // This constructs a Go type for a parameter, looking at either the schema or // the content, whichever is available func paramToGoType(param *openapi3.Parameter, path []string) (Schema, error) { if param.Content == nil && param.Schema == nil { return Schema{}, fmt.Errorf("parameter '%s' has no schema or content", param.Name) } // We can process the schema through the generic schema processor if param.Schema != nil { return GenerateGoSchema(param.Schema, path) } // At this point, we have a content type. We know how to deal with // application/json, but if multiple formats are present, we can't do anything, // so we'll return the parameter as a string, not bothering to decode it. if len(param.Content) > 1 { return Schema{ GoType: "string", Description: StringToGoComment(param.Description), }, nil } // Otherwise, look for application/json in there mt, found := param.Content["application/json"] if !found { // If we don't have json, it's a string return Schema{ GoType: "string", Description: StringToGoComment(param.Description), }, nil } // For json, we go through the standard schema mechanism return GenerateGoSchema(mt.Schema, path) }