package api_documentation import ( "fmt" "reflect" "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"` 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{}) (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 } 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 docMethod.Description = "Not available - see request and response structs" docMethod.Tags = []string{path} // describe parameters docMethod.Parameters = []DocParam{} for i := 0; i < handler.RequestParamsType.NumField(); i++ { f := handler.RequestParamsType.Field(i) 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, } docMethod.Parameters = append(docMethod.Parameters, parameter) } // Request if handler.RequestBodyType != nil { body := reflect.New(handler.RequestBodyType).Interface() bodyTypeString := getType(body) docMethod.RequestBody = &DocRequestBody{ Description: "Some description", 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: "Some 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"] = fmt.Sprintf("%v", t) 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 }