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