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 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 } 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 } 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 } type TypeDefinition struct { TypeName string JsonName string ResponseName string Schema Schema } 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) { // 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. var refType string // 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{}", RefType: refType}, nil } schema := sref.Value if sref.Ref != "" { var err error // 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, }, nil } // We can't support this in any meaningful way if schema.AnyOf != nil { return Schema{GoType: "interface{}", RefType: refType}, nil } // We can't support this in any meaningful way if schema.OneOf != nil { return Schema{GoType: "interface{}", RefType: refType}, 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.RefType = refType return mergedSchema, nil } outSchema := Schema{ RefType: refType, } // 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, } 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 { f := schema.Format 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 Schema{}, errors.Wrap(err, "error generating type for array") } outSchema.GoType = "[]" + arrayType.TypeDecl() 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 == "" { outSchema.GoType = "int" } else { return Schema{}, 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 Schema{}, fmt.Errorf("invalid number format: %s", f) } case "boolean": if f != "" { return Schema{}, fmt.Errorf("invalid format (%s) for boolean", f) } outSchema.GoType = "bool" case "string": enumValues := make([]string, len(schema.Enum)) for i, enumValue := range schema.Enum { enumValues[i] = enumValue.(string) } outSchema.EnumValues = SanitizeEnumNames(enumValues) // 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 Schema{}, fmt.Errorf("unhandled Schema type: %s", t) } } return outSchema, 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 _, 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. field += fmt.Sprintf("\n%s\n", StringToGoComment(p.Description)) } field += fmt.Sprintf(" %s %s", p.GoFieldName(), p.GoTypeDef()) if p.Required || p.Nullable { field += fmt.Sprintf(" `json:\"%s\"`", p.JsonFieldName) } else { field += fmt.Sprintf(" `json:\"%s,omitempty\"`", p.JsonFieldName) } 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 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 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", 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", }, 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", }, nil } // For json, we go through the standard schema mechanism return GenerateGoSchema(mt.Schema, path) }