package api_documentation import ( "fmt" "gitlab.com/uafrica/go-utils/logs" "go/doc" "go/parser" "go/token" "os" "path/filepath" "reflect" "runtime" "strings" "gitlab.com/uafrica/go-utils/errors" "gitlab.com/uafrica/go-utils/handler_utils" ) type NoParams struct{} type DocPath map[string]DocMethodInfo type Docs struct { Paths map[string]DocPath `json:"paths"` Components DocSchemas `json:"components"` } type DocSchemas struct { Schemas map[string]interface{} `json:"schemas"` } type DocMethodInfo struct { Description string `json:"description"` Tags []string `json:"tags"` RequestBody *DocRequestBody `json:"requestBody,omitempty"` Parameters []DocParam `json:"parameters,omitempty"` Responses *map[string]DocResponseValue `json:"responses,omitempty"` } type DocRequestBody struct { Description string `json:"description"` Required bool `json:"required"` Content map[string]interface{} `json:"content"` } type DocParam struct { Name string `json:"name"` In string `json:"in"` Description string `json:"description"` Required bool `json:"required,omitempty"` Schema interface{} `json:"schema"` } type DocSchema struct { Ref string `json:"$ref"` } type DocResponseValue struct { Description string `json:"description"` Content *map[string]interface{} `json:"content,omitempty"` } type DocSchemaResponse struct { Type *string `json:"type,omitempty"` Items DocSchema `json:"items"` } func GetDocs(endpointHandlers map[string]map[string]interface{}, corePath string) (Docs, error) { docs := Docs{ Paths: map[string]DocPath{}, Components: DocSchemas{Schemas: map[string]interface{}{}}, } var validationError error if endpointHandlers, validationError = handler_utils.ValidateAPIEndpoints(endpointHandlers); validationError != nil { return Docs{}, validationError } functionDocs := GetStructDocs(corePath) for path, methods := range endpointHandlers { docPath := DocPath{} for method, methodHandler := range methods { docMethod := DocMethodInfo{} if handler, ok := methodHandler.(handler_utils.Handler); !ok { docMethod.Description = "Not available" } else { // purpose functionName := GetFunctionName(handler.FuncValue.Interface()) docMethod.Description = functionDocs[functionName] docMethod.Tags = []string{path} // describe parameters docMethod.Parameters = []DocParam{} for i := 0; i < handler.RequestParamsType.NumField(); i++ { f := handler.RequestParamsType.Field(i) shouldAddDoc := f.Tag.Get("doc") if shouldAddDoc == "-" { continue } name := f.Tag.Get("json") if name == "" { name = f.Name } name = strings.Replace(name, ",omitempty", "", -1) schema, err := StructSchema(f.Type) if err != nil { return Docs{}, err } parameter := DocParam{ Name: name, In: "query", Description: f.Tag.Get("doc"), Schema: schema, } if f.Tag.Get("doc_required") == "true" { parameter.Required = true } docMethod.Parameters = append(docMethod.Parameters, parameter) } // Request if handler.RequestBodyType != nil { body := reflect.New(handler.RequestBodyType).Interface() bodyTypeString := getType(body) docMethod.RequestBody = &DocRequestBody{ Description: functionDocs[bodyTypeString], Required: true, Content: map[string]interface{}{ "application/json": map[string]interface{}{ "schema": DocSchema{Ref: "#/components/schemas/" + bodyTypeString}, }, }, } if handler.RequestBodyType.Kind() == reflect.Struct && handler.RequestBodyType.NumField() > 0 { schema, err := StructSchema(handler.RequestBodyType.Field(0).Type) if err != nil { return Docs{}, err } docs.Components.Schemas[bodyTypeString] = schema } } // Response if handler.ResponseType != nil { responses := map[string]DocResponseValue{} responseBody := reflect.New(handler.ResponseType).Interface() responseBodyTypeString := getType(responseBody) response := DocResponseValue{ Description: "", } responses["200"] = response if responseBodyTypeString != "" { response.Content = &map[string]interface{}{ "application/json": map[string]interface{}{ "schema": DocSchema{Ref: "#/components/schemas/" + responseBodyTypeString}, }, } } docMethod.Responses = &responses if handler.ResponseType.Kind() == reflect.Struct && handler.ResponseType.NumField() > 0 { schema, err := StructSchema(handler.ResponseType.Field(0).Type) if err != nil { return Docs{}, err } docs.Components.Schemas[responseBodyTypeString] = schema } } } docPath[strings.ToLower(method)] = docMethod } docs.Paths[path] = docPath } return docs, nil } func getType(myvar interface{}) string { if t := reflect.TypeOf(myvar); t.Kind() == reflect.Ptr { return t.Elem().Name() } else { return t.Name() } } func StructSchema(t reflect.Type) (interface{}, error) { if t == nil { return nil, nil } schema := map[string]interface{}{} description := "" if t.Kind() == reflect.Ptr { // schema["optional"] = true t = t.Elem() } switch t.Kind() { case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uint: schema["type"] = "integer" schema["format"] = fmt.Sprintf("%v", t) case reflect.Float64, reflect.Float32: schema["type"] = "number" schema["format"] = "float" case reflect.Bool: schema["type"] = "boolean" schema["format"] = fmt.Sprintf("%v", t) case reflect.String: schema["type"] = "string" schema["format"] = fmt.Sprintf("%v", t) case reflect.Interface: schema["type"] = "object" // any value...? case reflect.Struct: if t.String() == "time.Time" { schema["type"] = "string" schema["format"] = "date" 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 } if fieldName == "-" { continue // json does not marshal these } fieldName = strings.Replace(fieldName, ",omitempty", "", -1) var err error fieldDesc := f.Tag.Get("doc") if fieldDesc == "" { fieldDesc = description + "." + fieldName } properties[fieldName], err = StructSchema(f.Type) if err != nil { return nil, errors.Wrapf(err, "failed to document %v.%s", t, fieldName) } } } schema["properties"] = properties case reflect.Map: schema["type"] = "object" // keySchema, err := DocSchema("key", t.Key()) // if err != nil { // return nil, errors.Wrapf(err, "cannot make schema for %v map key", t) // } // schema["key"] = keySchema // // elemSchema, err := DocSchema("items", t.Elem()) // if err != nil { // return nil, errors.Wrapf(err, "cannot make schema for %v map elem", t) // } // schema["items"] = elemSchema case reflect.Slice: schema["type"] = "array" schema["items"] = t.Elem() // elemSchema, err := DocSchema("items", t.Elem()) // if err != nil { // return nil, errors.Wrapf(err, "cannot make schema for %v slice elem", t) // } // schema["items"] = elemSchema default: return nil, errors.Errorf("cannot generate schema for %v kind=%v", t, t.Kind()) } return schema, 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 } for k, f := range d { fmt.Println("package", k) p := doc.New(f, "./", 2) for _, objectTypes := range p.Types { doc := strings.ReplaceAll(objectTypes.Doc, objectTypes.Name, "") docs[objectTypes.Name] = doc } // // for _, v := range p.Vars { // fmt.Println("type", v.Names) // fmt.Println("docs:", v.Doc) // } for _, function := range p.Funcs { doc := strings.ReplaceAll(function.Doc, function.Name, "") 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] }