Skip to content
Snippets Groups Projects
api_documentation.go 11.2 KiB
Newer Older
Francé Wilke's avatar
Francé Wilke committed
package api_documentation

import (
	"fmt"
	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/logs"
	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/string_utils"
	"go/doc"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
Johan de Klerk's avatar
Johan de Klerk committed
	"reflect"
Francé Wilke's avatar
Francé Wilke committed
	"strings"

	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors"
	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/handler_utils"
Francé Wilke's avatar
Francé Wilke committed
)

type NoParams struct{}
type DocPath map[string]DocMethodInfo

Francé Wilke's avatar
Francé Wilke committed
type Docs struct {
Johan de Klerk's avatar
Johan de Klerk committed
	Paths      map[string]DocPath `json:"paths"`
	Components DocSchemas         `json:"components"`
Francé Wilke's avatar
Francé Wilke committed
}

type DocSchemas struct {
	Schemas map[string]interface{} `json:"schemas"`
}
Francé Wilke's avatar
Francé Wilke committed

type DocMethodInfo struct {
Johan de Klerk's avatar
Johan de Klerk committed
	Description string                       `json:"description"`
Johan de Klerk's avatar
Johan de Klerk committed
	Summary     string                       `json:"summary"`
Johan de Klerk's avatar
Johan de Klerk committed
	Tags        []string                     `json:"tags"`
	RequestBody *DocRequestBody              `json:"requestBody,omitempty"`
	Parameters  []DocParam                   `json:"parameters,omitempty"`
	Responses   *map[string]DocResponseValue `json:"responses,omitempty"`
Francé Wilke's avatar
Francé Wilke committed
}

type DocRequestBody struct {
Johan de Klerk's avatar
Johan de Klerk committed
	Description string                 `json:"description"`
	Required    bool                   `json:"required"`
	Content     map[string]interface{} `json:"content"`
Francé Wilke's avatar
Francé Wilke committed
type DocParam struct {
Johan de Klerk's avatar
Johan de Klerk committed
	Name        string      `json:"name"`
	In          string      `json:"in"`
	Description string      `json:"description"`
Johan de Klerk's avatar
Johan de Klerk committed
	Required    bool        `json:"required,omitempty"`
Johan de Klerk's avatar
Johan de Klerk committed
	Schema      interface{} `json:"schema"`
Francé Wilke's avatar
Francé Wilke committed
}

type DocSchema struct {
	Ref string `json:"$ref"`
}

type DocResponseValue struct {
Johan de Klerk's avatar
Johan de Klerk committed
	Description string                  `json:"description"`
	Content     *map[string]interface{} `json:"content,omitempty"`
}

type DocSchemaResponse struct {
Johan de Klerk's avatar
Johan de Klerk committed
	Type  *string   `json:"type,omitempty"`
	Items DocSchema `json:"items"`
func GetDocs(endpointHandlers map[string]map[string]interface{}, corePath string) (Docs, error) {
Francé Wilke's avatar
Francé Wilke committed
	docs := Docs{
		Paths: map[string]DocPath{},
		Components: DocSchemas{
			Schemas: map[string]interface{}{},
		},
	}

	// Add default error
	addDefaultSchemas(docs)

	var validationError error
	if endpointHandlers, validationError = handler_utils.ValidateAPIEndpoints(endpointHandlers); validationError != nil {
		return Docs{}, validationError
	}

	functionDocs := GetStructDocs(corePath)

Francé Wilke's avatar
Francé Wilke committed
	for path, methods := range endpointHandlers {
		docPath := DocPath{}
Francé Wilke's avatar
Francé Wilke committed
		for method, methodHandler := range methods {
			docMethod := DocMethodInfo{}
Francé Wilke's avatar
Francé Wilke committed
			if handler, ok := methodHandler.(handler_utils.Handler); !ok {
				docMethod.Description = "Not available"
Francé Wilke's avatar
Francé Wilke committed
			} else {
Johan de Klerk's avatar
Johan de Klerk committed
				// Meta data
				functionName := GetFunctionName(handler.FuncValue.Interface())
				docMethod.Description = functionDocs[functionName]
Johan de Klerk's avatar
Johan de Klerk committed
				docMethod.Summary = functionNameToSummary(functionName, method)
				docMethod.Tags = []string{path}
Francé Wilke's avatar
Francé Wilke committed

Johan de Klerk's avatar
Johan de Klerk committed
				// Parameters
				docParameters, err := FillParameters(&docs, handler)
Johan de Klerk's avatar
Johan de Klerk committed
				if err != nil {
					return Docs{}, err
Francé Wilke's avatar
Francé Wilke committed
				}
Johan de Klerk's avatar
Johan de Klerk committed
				docMethod.Parameters = docParameters
Francé Wilke's avatar
Francé Wilke committed

Johan de Klerk's avatar
Johan de Klerk committed
				// Request body
Johan de Klerk's avatar
Johan de Klerk committed
				if handler.RequestBodyType != nil {
Johan de Klerk's avatar
Johan de Klerk committed
					bodyTypeString, requestBody := GetRequestBody(handler, functionDocs)
					docMethod.RequestBody = &requestBody
					// Fill Component schemas
Johan de Klerk's avatar
Johan de Klerk committed
					if handler.RequestBodyType.Kind() == reflect.Struct && handler.RequestBodyType.NumField() > 0 {
						err := FillStructSchema(&docs, handler.RequestBodyType.Field(0).Type, bodyTypeString)
Johan de Klerk's avatar
Johan de Klerk committed
						if err != nil {
							return Docs{}, err
						}
					}
				}
Francé Wilke's avatar
Francé Wilke committed

Johan de Klerk's avatar
Johan de Klerk committed
				// Response
Johan de Klerk's avatar
Johan de Klerk committed
				if handler.ResponseType != nil {
Johan de Klerk's avatar
Johan de Klerk committed
					response, responseBodyTypeString := GetResponse(handler)
					docMethod.Responses = &response

					// Fill Component schemas
Johan de Klerk's avatar
Johan de Klerk committed
					if handler.ResponseType.Kind() == reflect.Struct && handler.ResponseType.NumField() > 0 {
						err := FillStructSchema(&docs, handler.ResponseType, responseBodyTypeString)
Johan de Klerk's avatar
Johan de Klerk committed
						if err != nil {
							return Docs{}, err
						}
					}
				}
			}

			docPath[strings.ToLower(method)] = docMethod
		}
		docs.Paths[path] = docPath
	}
	return docs, nil
Francé Wilke's avatar
Francé Wilke committed
}
Johan de Klerk's avatar
Johan de Klerk committed
func functionNameToSummary(name string, method string) string {

	name = string_utils.ReplaceCaseInsensitive(name, "GET", "")
	name = string_utils.ReplaceCaseInsensitive(name, "POST", "")
	name = string_utils.ReplaceCaseInsensitive(name, "PATCH", "")
	name = string_utils.ReplaceCaseInsensitive(name, "DELETE", "")

	var cleanMethod string
	if method == "GET" {
		cleanMethod = "Fetch"
	} else if method == "POST" {
		cleanMethod = "Create"
	} else if method == "DELETE" {
		cleanMethod = "Delete"
	} else if method == "PATCH" {
		cleanMethod = "Update"
	}

	summary := string_utils.PascalCaseToSentence(name)
	summary = cleanMethod + " " + strings.ToLower(summary)
	return summary
}

func addDefaultSchemas(docs Docs) {
	docs.Components.Schemas["error"] = map[string]string{
		"type":   "string",
		"format": "string",
	}

	docs.Components.Schemas["string"] = map[string]string{
		"type":   "string",
		"format": "string",
	}
}

func FillParameters(docs *Docs, handler handler_utils.Handler) ([]DocParam, error) {
Johan de Klerk's avatar
Johan de Klerk committed

	docParameters := []DocParam{}
	for i := 0; i < handler.RequestParamsType.NumField(); i++ {
		structField := handler.RequestParamsType.Field(i)

		shouldAddDoc := structField.Tag.Get("doc")
		if shouldAddDoc == "-" {
			continue
		}

		schema, err := StructSchema(docs, structField.Type)
Johan de Klerk's avatar
Johan de Klerk committed
		if err != nil {
			return nil, err
		}

Johan de Klerk's avatar
Johan de Klerk committed
		name := StructFieldName(structField)
		if name == "tableName" {
			continue
		}

Johan de Klerk's avatar
Johan de Klerk committed
		parameter := DocParam{
Johan de Klerk's avatar
Johan de Klerk committed
			Name:        name,
Johan de Klerk's avatar
Johan de Klerk committed
			In:          "query",
			Description: structField.Tag.Get("doc"),
			Schema:      schema,
		}

		if structField.Tag.Get("doc_required") == "true" {
			parameter.Required = true
		}

		docParameters = append(docParameters, parameter)
	}

	return docParameters, nil
}

func GetRequestBody(handler handler_utils.Handler, functionDocs map[string]string) (string, DocRequestBody) {
	body := reflect.New(handler.RequestBodyType).Interface()
	bodyTypeString := getType(body)

	requestBody := DocRequestBody{
		Description: functionDocs[bodyTypeString],
		Required:    true,
		Content: map[string]interface{}{
			"application/json": map[string]interface{}{
				"schema": DocSchema{Ref: "#/components/schemas/" + bodyTypeString},
			},
		},
	}
	return bodyTypeString, requestBody
}

func GetResponse(handler handler_utils.Handler) (map[string]DocResponseValue, string) {
	responses := map[string]DocResponseValue{}
	responseBody := reflect.New(handler.ResponseType).Interface()
	responseBodyTypeString := getType(responseBody)

	response := DocResponseValue{}
	if responseBodyTypeString != "" {
		response.Content = &map[string]interface{}{
			"application/json": map[string]interface{}{
				"schema": DocSchema{Ref: "#/components/schemas/" + responseBodyTypeString},
			},
		}
	}

	responses["200"] = response

	return responses, responseBodyTypeString
}

func StructFieldName(structField reflect.StructField) string {
	name := structField.Tag.Get("json")
	if name == "" {
		name = structField.Name
	}

	name = strings.Replace(name, ",omitempty", "", -1)
	return name
}

Johan de Klerk's avatar
Johan de Klerk committed
func getType(myvar interface{}) string {
	if t := reflect.TypeOf(myvar); t.Kind() == reflect.Ptr {
		return t.Elem().Name()
	} else {
		return t.Name()
	}
}
func StructSchema(docs *Docs, t reflect.Type) (interface{}, error) {
	if t == nil {
		return nil, nil
	}
	schema := map[string]interface{}{}

	description := ""

	if t.Kind() == reflect.Ptr {
Johan de Klerk's avatar
Johan de Klerk committed
		// schema["optional"] = true
		t = t.Elem()
	}

	switch t.Kind() {
	case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int,
Johan de Klerk's avatar
Johan de Klerk committed
		reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uint:
		schema["type"] = "integer"
Johan de Klerk's avatar
Johan de Klerk committed
		schema["format"] = fmt.Sprintf("%v", t)
Johan de Klerk's avatar
Johan de Klerk committed
	case reflect.Float64, reflect.Float32:
		schema["type"] = "number"
Johan de Klerk's avatar
Johan de Klerk committed
		schema["format"] = "float"
Johan de Klerk's avatar
Johan de Klerk committed
	case reflect.Bool:
		schema["type"] = "boolean"
Johan de Klerk's avatar
Johan de Klerk committed
		schema["format"] = fmt.Sprintf("%v", t)
Johan de Klerk's avatar
Johan de Klerk committed
	case reflect.String:
		schema["type"] = "string"
Johan de Klerk's avatar
Johan de Klerk committed
		schema["format"] = fmt.Sprintf("%v", t)

	case reflect.Interface:
Johan de Klerk's avatar
Johan de Klerk committed
		schema["type"] = "object" // any value...?

	case reflect.Struct:
Johan de Klerk's avatar
Johan de Klerk committed

		if t.String() == "time.Time" {
			schema["type"] = "string"
Johan de Klerk's avatar
Johan de Klerk committed
			schema["format"] = "date"
Johan de Klerk's avatar
Johan de Klerk committed
			return schema, nil
		}

		schema["type"] = "object"
		properties := map[string]interface{}{}

		for i := 0; i < t.NumField(); i++ {
			f := t.Field(i)
			if !f.Anonymous {
				fieldName := f.Tag.Get("json")
				if fieldName == "" {
					fieldName = f.Name
				}
Johan de Klerk's avatar
Johan de Klerk committed
				if fieldName == "-" || fieldName == "tableName" {
Johan de Klerk's avatar
Johan de Klerk committed
					continue // json does not marshal these
				}
				fieldName = strings.Replace(fieldName, ",omitempty", "", -1)

				var err error
				fieldDesc := f.Tag.Get("doc")
				if fieldDesc == "-" {
Johan de Klerk's avatar
Johan de Klerk committed
					continue
				}
				if fieldDesc == "" {
					fieldDesc = description + "." + fieldName
				}
				properties[fieldName], err = StructSchema(docs, f.Type)
				if err != nil {
					return nil, errors.Wrapf(err, "failed to document %v.%s", t, fieldName)
				}
			}
		}
		schema["properties"] = properties

	case reflect.Map:
Johan de Klerk's avatar
Johan de Klerk committed
		schema["type"] = "object"
	case reflect.Slice:
		schema["type"] = "array"
		element := t.Elem()

		if element.Kind() == reflect.Struct || element.Kind() == reflect.Ptr {
Johan de Klerk's avatar
Johan de Klerk committed
			elementName := t.Elem().Name()
			if elementName == "" || elementName == "error" {
				return schema, nil
			}
			schema["items"] = map[string]string{"$ref": "#/components/schemas/" + elementName}

			// Check if object is already in the struct schema
			_, containsValue := docs.Components.Schemas[elementName]
			if !containsValue {
				err := FillStructSchema(docs, element, elementName)
				if err != nil {
					return nil, err
				}
			}
		} else {
			items, err := StructSchema(docs, element)
			if err != nil {
				return nil, errors.Wrapf(err, "failed to document")
			}
			schema["items"] = items
	default:
		return nil, errors.Errorf("cannot generate schema for %v kind=%v", t, t.Kind())
	}

	return schema, nil
}
func FillStructSchema(docs *Docs, element reflect.Type, elementName string) error {
	schema, err := StructSchema(docs, element)
	if err != nil {
		return errors.Wrapf(err, "failed to fill struct schema for %v", elementName)
	}
	docs.Components.Schemas[elementName] = schema
	return nil
}

func GetStructDocs(corePath string) map[string]string {
	docs := map[string]string{}

	walkFunction := func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if !info.IsDir() {
			return nil
		}

		fset := token.NewFileSet()
		d, err := parser.ParseDir(fset, path, nil, parser.ParseComments)
		if err != nil {
			fmt.Println(err)
			return nil
		}

Johan de Klerk's avatar
Johan de Klerk committed
		for _, f := range d {
			p := doc.New(f, "./", 2)

			for _, objectTypes := range p.Types {
				doc := strings.ReplaceAll(objectTypes.Doc, objectTypes.Name, "")
Johan de Klerk's avatar
Johan de Klerk committed
				doc = strings.ReplaceAll(doc, "\t", "")
				docs[objectTypes.Name] = doc
			}

			for _, function := range p.Funcs {
				doc := strings.ReplaceAll(function.Doc, function.Name, "")
Johan de Klerk's avatar
Johan de Klerk committed
				doc = strings.ReplaceAll(doc, "\t", "")
				docs[function.Name] = doc
			}

			for _, n := range p.Notes {
				fmt.Println("body", n[0].Body)
			}
		}
		return nil
	}

	err := filepath.Walk(corePath, walkFunction)
	if err != nil {
		logs.ErrorWithMsg("Failed to upload files to s3", err)
	}

	return docs
}

func GetFunctionName(temp interface{}) string {
	strs := strings.Split((runtime.FuncForPC(reflect.ValueOf(temp).Pointer()).Name()), ".")
	return strs[len(strs)-1]
}