1package codegen 2 3import ( 4 "fmt" 5 "strings" 6 7 "github.com/getkin/kin-openapi/openapi3" 8 "github.com/pkg/errors" 9) 10 11// This describes a Schema, a type definition. 12type Schema struct { 13 GoType string // The Go type needed to represent the schema 14 RefType string // If the type has a type name, this is set 15 16 ArrayType *Schema // The schema of array element 17 18 EnumValues map[string]string // Enum values 19 20 Properties []Property // For an object, the fields with names 21 HasAdditionalProperties bool // Whether we support additional properties 22 AdditionalPropertiesType *Schema // And if we do, their type 23 AdditionalTypes []TypeDefinition // We may need to generate auxiliary helper types, stored here 24 25 SkipOptionalPointer bool // Some types don't need a * in front when they're optional 26 27 Description string // The description of the element 28 29 // The original OpenAPIv3 Schema. 30 OAPISchema *openapi3.Schema 31} 32 33func (s Schema) IsRef() bool { 34 return s.RefType != "" 35} 36 37func (s Schema) TypeDecl() string { 38 if s.IsRef() { 39 return s.RefType 40 } 41 return s.GoType 42} 43 44func (s *Schema) MergeProperty(p Property) error { 45 // Scan all existing properties for a conflict 46 for _, e := range s.Properties { 47 if e.JsonFieldName == p.JsonFieldName && !PropertiesEqual(e, p) { 48 return errors.New(fmt.Sprintf("property '%s' already exists with a different type", e.JsonFieldName)) 49 } 50 } 51 s.Properties = append(s.Properties, p) 52 return nil 53} 54 55func (s Schema) GetAdditionalTypeDefs() []TypeDefinition { 56 var result []TypeDefinition 57 for _, p := range s.Properties { 58 result = append(result, p.Schema.GetAdditionalTypeDefs()...) 59 } 60 result = append(result, s.AdditionalTypes...) 61 return result 62} 63 64type Property struct { 65 Description string 66 JsonFieldName string 67 Schema Schema 68 Required bool 69 Nullable bool 70 ExtensionProps *openapi3.ExtensionProps 71} 72 73func (p Property) GoFieldName() string { 74 return SchemaNameToTypeName(p.JsonFieldName) 75} 76 77func (p Property) GoTypeDef() string { 78 typeDef := p.Schema.TypeDecl() 79 if !p.Schema.SkipOptionalPointer && (!p.Required || p.Nullable) { 80 typeDef = "*" + typeDef 81 } 82 return typeDef 83} 84 85// EnumDefinition holds type information for enum 86type EnumDefinition struct { 87 Schema Schema 88 TypeName string 89 ValueWrapper string 90} 91 92type Constants struct { 93 // SecuritySchemeProviderNames holds all provider names for security schemes. 94 SecuritySchemeProviderNames []string 95 // EnumDefinitions holds type and value information for all enums 96 EnumDefinitions []EnumDefinition 97} 98 99// TypeDefinition describes a Go type definition in generated code. 100// 101// Let's use this example schema: 102// components: 103// schemas: 104// Person: 105// type: object 106// properties: 107// name: 108// type: string 109type TypeDefinition struct { 110 // The name of the type, eg, type <...> Person 111 TypeName string 112 113 // The name of the corresponding JSON description, as it will sometimes 114 // differ due to invalid characters. 115 JsonName string 116 117 // This is the Schema wrapper is used to populate the type description 118 Schema Schema 119} 120 121// ResponseTypeDefinition is an extension of TypeDefinition, specifically for 122// response unmarshaling in ClientWithResponses. 123type ResponseTypeDefinition struct { 124 TypeDefinition 125 // The content type name where this is used, eg, application/json 126 ContentTypeName string 127 128 // The type name of a response model. 129 ResponseName string 130} 131 132func (t *TypeDefinition) CanAlias() bool { 133 return t.Schema.IsRef() || /* actual reference */ 134 (t.Schema.ArrayType != nil && t.Schema.ArrayType.IsRef()) /* array to ref */ 135} 136 137func PropertiesEqual(a, b Property) bool { 138 return a.JsonFieldName == b.JsonFieldName && a.Schema.TypeDecl() == b.Schema.TypeDecl() && a.Required == b.Required 139} 140 141func GenerateGoSchema(sref *openapi3.SchemaRef, path []string) (Schema, error) { 142 // Add a fallback value in case the sref is nil. 143 // i.e. the parent schema defines a type:array, but the array has 144 // no items defined. Therefore we have at least valid Go-Code. 145 if sref == nil { 146 return Schema{GoType: "interface{}"}, nil 147 } 148 149 schema := sref.Value 150 151 // If Ref is set on the SchemaRef, it means that this type is actually a reference to 152 // another type. We're not de-referencing, so simply use the referenced type. 153 if IsGoTypeReference(sref.Ref) { 154 // Convert the reference path to Go type 155 refType, err := RefPathToGoType(sref.Ref) 156 if err != nil { 157 return Schema{}, fmt.Errorf("error turning reference (%s) into a Go type: %s", 158 sref.Ref, err) 159 } 160 return Schema{ 161 GoType: refType, 162 Description: StringToGoComment(schema.Description), 163 }, nil 164 } 165 166 outSchema := Schema{ 167 Description: StringToGoComment(schema.Description), 168 OAPISchema: schema, 169 } 170 171 // We can't support this in any meaningful way 172 if schema.AnyOf != nil { 173 outSchema.GoType = "interface{}" 174 return outSchema, nil 175 } 176 // We can't support this in any meaningful way 177 if schema.OneOf != nil { 178 outSchema.GoType = "interface{}" 179 return outSchema, nil 180 } 181 182 // AllOf is interesting, and useful. It's the union of a number of other 183 // schemas. A common usage is to create a union of an object with an ID, 184 // so that in a RESTful paradigm, the Create operation can return 185 // (object, id), so that other operations can refer to (id) 186 if schema.AllOf != nil { 187 mergedSchema, err := MergeSchemas(schema.AllOf, path) 188 if err != nil { 189 return Schema{}, errors.Wrap(err, "error merging schemas") 190 } 191 mergedSchema.OAPISchema = schema 192 return mergedSchema, nil 193 } 194 195 // Check for custom Go type extension 196 if extension, ok := schema.Extensions[extPropGoType]; ok { 197 typeName, err := extTypeName(extension) 198 if err != nil { 199 return outSchema, errors.Wrapf(err, "invalid value for %q", extPropGoType) 200 } 201 outSchema.GoType = typeName 202 return outSchema, nil 203 } 204 205 // Schema type and format, eg. string / binary 206 t := schema.Type 207 // Handle objects and empty schemas first as a special case 208 if t == "" || t == "object" { 209 var outType string 210 211 if len(schema.Properties) == 0 && !SchemaHasAdditionalProperties(schema) { 212 // If the object has no properties or additional properties, we 213 // have some special cases for its type. 214 if t == "object" { 215 // We have an object with no properties. This is a generic object 216 // expressed as a map. 217 outType = "map[string]interface{}" 218 } else { // t == "" 219 // If we don't even have the object designator, we're a completely 220 // generic type. 221 outType = "interface{}" 222 } 223 outSchema.GoType = outType 224 } else { 225 // We've got an object with some properties. 226 for _, pName := range SortedSchemaKeys(schema.Properties) { 227 p := schema.Properties[pName] 228 propertyPath := append(path, pName) 229 pSchema, err := GenerateGoSchema(p, propertyPath) 230 if err != nil { 231 return Schema{}, errors.Wrap(err, fmt.Sprintf("error generating Go schema for property '%s'", pName)) 232 } 233 234 required := StringInArray(pName, schema.Required) 235 236 if pSchema.HasAdditionalProperties && pSchema.RefType == "" { 237 // If we have fields present which have additional properties, 238 // but are not a pre-defined type, we need to define a type 239 // for them, which will be based on the field names we followed 240 // to get to the type. 241 typeName := PathToTypeName(propertyPath) 242 243 typeDef := TypeDefinition{ 244 TypeName: typeName, 245 JsonName: strings.Join(propertyPath, "."), 246 Schema: pSchema, 247 } 248 pSchema.AdditionalTypes = append(pSchema.AdditionalTypes, typeDef) 249 250 pSchema.RefType = typeName 251 } 252 description := "" 253 if p.Value != nil { 254 description = p.Value.Description 255 } 256 prop := Property{ 257 JsonFieldName: pName, 258 Schema: pSchema, 259 Required: required, 260 Description: description, 261 Nullable: p.Value.Nullable, 262 ExtensionProps: &p.Value.ExtensionProps, 263 } 264 outSchema.Properties = append(outSchema.Properties, prop) 265 } 266 267 outSchema.HasAdditionalProperties = SchemaHasAdditionalProperties(schema) 268 outSchema.AdditionalPropertiesType = &Schema{ 269 GoType: "interface{}", 270 } 271 if schema.AdditionalProperties != nil { 272 additionalSchema, err := GenerateGoSchema(schema.AdditionalProperties, path) 273 if err != nil { 274 return Schema{}, errors.Wrap(err, "error generating type for additional properties") 275 } 276 outSchema.AdditionalPropertiesType = &additionalSchema 277 } 278 279 outSchema.GoType = GenStructFromSchema(outSchema) 280 } 281 return outSchema, nil 282 } else if len(schema.Enum) > 0 { 283 err := resolveType(schema, path, &outSchema) 284 if err != nil { 285 return Schema{}, errors.Wrap(err, "error resolving primitive type") 286 } 287 enumValues := make([]string, len(schema.Enum)) 288 for i, enumValue := range schema.Enum { 289 enumValues[i] = fmt.Sprintf("%v", enumValue) 290 } 291 292 sanitizedValues := SanitizeEnumNames(enumValues) 293 outSchema.EnumValues = make(map[string]string, len(sanitizedValues)) 294 var constNamePath []string 295 for k, v := range sanitizedValues { 296 if v == "" { 297 constNamePath = append(path, "Empty") 298 } else { 299 constNamePath = append(path, k) 300 } 301 outSchema.EnumValues[SchemaNameToTypeName(PathToTypeName(constNamePath))] = v 302 } 303 if len(path) > 1 { // handle additional type only on non-toplevel types 304 typeName := SchemaNameToTypeName(PathToTypeName(path)) 305 typeDef := TypeDefinition{ 306 TypeName: typeName, 307 JsonName: strings.Join(path, "."), 308 Schema: outSchema, 309 } 310 outSchema.AdditionalTypes = append(outSchema.AdditionalTypes, typeDef) 311 outSchema.RefType = typeName 312 } 313 //outSchema.RefType = typeName 314 } else { 315 err := resolveType(schema, path, &outSchema) 316 if err != nil { 317 return Schema{}, errors.Wrap(err, "error resolving primitive type") 318 } 319 } 320 return outSchema, nil 321} 322 323// resolveType resolves primitive type or array for schema 324func resolveType(schema *openapi3.Schema, path []string, outSchema *Schema) error { 325 f := schema.Format 326 t := schema.Type 327 328 switch t { 329 case "array": 330 // For arrays, we'll get the type of the Items and throw a 331 // [] in front of it. 332 arrayType, err := GenerateGoSchema(schema.Items, path) 333 if err != nil { 334 return errors.Wrap(err, "error generating type for array") 335 } 336 outSchema.ArrayType = &arrayType 337 outSchema.GoType = "[]" + arrayType.TypeDecl() 338 additionalTypes := arrayType.GetAdditionalTypeDefs() 339 // Check also types defined in array item 340 if len(additionalTypes) > 0 { 341 outSchema.AdditionalTypes = append(outSchema.AdditionalTypes, additionalTypes...) 342 } 343 outSchema.Properties = arrayType.Properties 344 case "integer": 345 // We default to int if format doesn't ask for something else. 346 if f == "int64" { 347 outSchema.GoType = "int64" 348 } else if f == "int32" { 349 outSchema.GoType = "int32" 350 } else if f == "int16" { 351 outSchema.GoType = "int16" 352 } else if f == "int8" { 353 outSchema.GoType = "int8" 354 } else if f == "int" { 355 outSchema.GoType = "int" 356 } else if f == "uint64" { 357 outSchema.GoType = "uint64" 358 } else if f == "uint32" { 359 outSchema.GoType = "uint32" 360 } else if f == "uint16" { 361 outSchema.GoType = "uint16" 362 } else if f == "uint8" { 363 outSchema.GoType = "uint8" 364 } else if f == "uint" { 365 outSchema.GoType = "uint" 366 } else if f == "" { 367 outSchema.GoType = "int" 368 } else { 369 return fmt.Errorf("invalid integer format: %s", f) 370 } 371 case "number": 372 // We default to float for "number" 373 if f == "double" { 374 outSchema.GoType = "float64" 375 } else if f == "float" || f == "" { 376 outSchema.GoType = "float32" 377 } else { 378 return fmt.Errorf("invalid number format: %s", f) 379 } 380 case "boolean": 381 if f != "" { 382 return fmt.Errorf("invalid format (%s) for boolean", f) 383 } 384 outSchema.GoType = "bool" 385 case "string": 386 // Special case string formats here. 387 switch f { 388 case "byte": 389 outSchema.GoType = "[]byte" 390 case "email": 391 outSchema.GoType = "openapi_types.Email" 392 case "date": 393 outSchema.GoType = "openapi_types.Date" 394 case "date-time": 395 outSchema.GoType = "time.Time" 396 case "json": 397 outSchema.GoType = "json.RawMessage" 398 outSchema.SkipOptionalPointer = true 399 default: 400 // All unrecognized formats are simply a regular string. 401 outSchema.GoType = "string" 402 } 403 default: 404 return fmt.Errorf("unhandled Schema type: %s", t) 405 } 406 return nil 407} 408 409// This describes a Schema, a type definition. 410type SchemaDescriptor struct { 411 Fields []FieldDescriptor 412 HasAdditionalProperties bool 413 AdditionalPropertiesType string 414} 415 416type FieldDescriptor struct { 417 Required bool // Is the schema required? If not, we'll pass by pointer 418 GoType string // The Go type needed to represent the json type. 419 GoName string // The Go compatible type name for the type 420 JsonName string // The json type name for the type 421 IsRef bool // Is this schema a reference to predefined object? 422} 423 424// Given a list of schema descriptors, produce corresponding field names with 425// JSON annotations 426func GenFieldsFromProperties(props []Property) []string { 427 var fields []string 428 for i, p := range props { 429 field := "" 430 // Add a comment to a field in case we have one, otherwise skip. 431 if p.Description != "" { 432 // Separate the comment from a previous-defined, unrelated field. 433 // Make sure the actual field is separated by a newline. 434 if i != 0 { 435 field += "\n" 436 } 437 field += fmt.Sprintf("%s\n", StringToGoComment(p.Description)) 438 } 439 field += fmt.Sprintf(" %s %s", p.GoFieldName(), p.GoTypeDef()) 440 441 // Support x-omitempty 442 omitEmpty := true 443 if _, ok := p.ExtensionProps.Extensions[extPropOmitEmpty]; ok { 444 if extOmitEmpty, err := extParseOmitEmpty(p.ExtensionProps.Extensions[extPropOmitEmpty]); err == nil { 445 omitEmpty = extOmitEmpty 446 } 447 } 448 449 fieldTags := make(map[string]string) 450 451 if p.Required || p.Nullable || !omitEmpty { 452 fieldTags["json"] = p.JsonFieldName 453 } else { 454 fieldTags["json"] = p.JsonFieldName + ",omitempty" 455 } 456 if extension, ok := p.ExtensionProps.Extensions[extPropExtraTags]; ok { 457 if tags, err := extExtraTags(extension); err == nil { 458 keys := SortedStringKeys(tags) 459 for _, k := range keys { 460 fieldTags[k] = tags[k] 461 } 462 } 463 } 464 // Convert the fieldTags map into Go field annotations. 465 keys := SortedStringKeys(fieldTags) 466 tags := make([]string, len(keys)) 467 for i, k := range keys { 468 tags[i] = fmt.Sprintf(`%s:"%s"`, k, fieldTags[k]) 469 } 470 field += "`" + strings.Join(tags, " ") + "`" 471 fields = append(fields, field) 472 } 473 return fields 474} 475 476func GenStructFromSchema(schema Schema) string { 477 // Start out with struct { 478 objectParts := []string{"struct {"} 479 // Append all the field definitions 480 objectParts = append(objectParts, GenFieldsFromProperties(schema.Properties)...) 481 // Close the struct 482 if schema.HasAdditionalProperties { 483 addPropsType := schema.AdditionalPropertiesType.GoType 484 if schema.AdditionalPropertiesType.RefType != "" { 485 addPropsType = schema.AdditionalPropertiesType.RefType 486 } 487 488 objectParts = append(objectParts, 489 fmt.Sprintf("AdditionalProperties map[string]%s `json:\"-\"`", addPropsType)) 490 } 491 objectParts = append(objectParts, "}") 492 return strings.Join(objectParts, "\n") 493} 494 495// Merge all the fields in the schemas supplied into one giant schema. 496func MergeSchemas(allOf []*openapi3.SchemaRef, path []string) (Schema, error) { 497 var outSchema Schema 498 for _, schemaOrRef := range allOf { 499 ref := schemaOrRef.Ref 500 501 var refType string 502 var err error 503 if IsGoTypeReference(ref) { 504 refType, err = RefPathToGoType(ref) 505 if err != nil { 506 return Schema{}, errors.Wrap(err, "error converting reference path to a go type") 507 } 508 } 509 510 schema, err := GenerateGoSchema(schemaOrRef, path) 511 if err != nil { 512 return Schema{}, errors.Wrap(err, "error generating Go schema in allOf") 513 } 514 schema.RefType = refType 515 516 for _, p := range schema.Properties { 517 err = outSchema.MergeProperty(p) 518 if err != nil { 519 return Schema{}, errors.Wrap(err, "error merging properties") 520 } 521 } 522 523 if schema.HasAdditionalProperties { 524 if outSchema.HasAdditionalProperties { 525 // Both this schema, and the aggregate schema have additional 526 // properties, they must match. 527 if schema.AdditionalPropertiesType.TypeDecl() != outSchema.AdditionalPropertiesType.TypeDecl() { 528 return Schema{}, errors.New("additional properties in allOf have incompatible types") 529 } 530 } else { 531 // We're switching from having no additional properties to having 532 // them 533 outSchema.HasAdditionalProperties = true 534 outSchema.AdditionalPropertiesType = schema.AdditionalPropertiesType 535 } 536 } 537 } 538 539 // Now, we generate the struct which merges together all the fields. 540 var err error 541 outSchema.GoType, err = GenStructFromAllOf(allOf, path) 542 if err != nil { 543 return Schema{}, errors.Wrap(err, "unable to generate aggregate type for AllOf") 544 } 545 return outSchema, nil 546} 547 548// This function generates an object that is the union of the objects in the 549// input array. In the case of Ref objects, we use an embedded struct, otherwise, 550// we inline the fields. 551func GenStructFromAllOf(allOf []*openapi3.SchemaRef, path []string) (string, error) { 552 // Start out with struct { 553 objectParts := []string{"struct {"} 554 for _, schemaOrRef := range allOf { 555 ref := schemaOrRef.Ref 556 if IsGoTypeReference(ref) { 557 // We have a referenced type, we will generate an inlined struct 558 // member. 559 // struct { 560 // InlinedMember 561 // ... 562 // } 563 goType, err := RefPathToGoType(ref) 564 if err != nil { 565 return "", err 566 } 567 objectParts = append(objectParts, 568 fmt.Sprintf(" // Embedded struct due to allOf(%s)", ref)) 569 objectParts = append(objectParts, 570 fmt.Sprintf(" %s `yaml:\",inline\"`", goType)) 571 } else { 572 // Inline all the fields from the schema into the output struct, 573 // just like in the simple case of generating an object. 574 goSchema, err := GenerateGoSchema(schemaOrRef, path) 575 if err != nil { 576 return "", err 577 } 578 objectParts = append(objectParts, " // Embedded fields due to inline allOf schema") 579 objectParts = append(objectParts, GenFieldsFromProperties(goSchema.Properties)...) 580 581 if goSchema.HasAdditionalProperties { 582 addPropsType := goSchema.AdditionalPropertiesType.GoType 583 if goSchema.AdditionalPropertiesType.RefType != "" { 584 addPropsType = goSchema.AdditionalPropertiesType.RefType 585 } 586 587 additionalPropertiesPart := fmt.Sprintf("AdditionalProperties map[string]%s `json:\"-\"`", addPropsType) 588 if !StringInArray(additionalPropertiesPart, objectParts) { 589 objectParts = append(objectParts, additionalPropertiesPart) 590 } 591 } 592 } 593 } 594 objectParts = append(objectParts, "}") 595 return strings.Join(objectParts, "\n"), nil 596} 597 598// This constructs a Go type for a parameter, looking at either the schema or 599// the content, whichever is available 600func paramToGoType(param *openapi3.Parameter, path []string) (Schema, error) { 601 if param.Content == nil && param.Schema == nil { 602 return Schema{}, fmt.Errorf("parameter '%s' has no schema or content", param.Name) 603 } 604 605 // We can process the schema through the generic schema processor 606 if param.Schema != nil { 607 return GenerateGoSchema(param.Schema, path) 608 } 609 610 // At this point, we have a content type. We know how to deal with 611 // application/json, but if multiple formats are present, we can't do anything, 612 // so we'll return the parameter as a string, not bothering to decode it. 613 if len(param.Content) > 1 { 614 return Schema{ 615 GoType: "string", 616 Description: StringToGoComment(param.Description), 617 }, nil 618 } 619 620 // Otherwise, look for application/json in there 621 mt, found := param.Content["application/json"] 622 if !found { 623 // If we don't have json, it's a string 624 return Schema{ 625 GoType: "string", 626 Description: StringToGoComment(param.Description), 627 }, nil 628 } 629 630 // For json, we go through the standard schema mechanism 631 return GenerateGoSchema(mt.Schema, path) 632} 633