diff --git a/doc/datastructures.go b/doc/datastructures.go new file mode 100644 index 0000000..e20b141 --- /dev/null +++ b/doc/datastructures.go @@ -0,0 +1,70 @@ +package doc + +import ( + "os" + "path/filepath" + "strings" + "text/template" +) + +type ObjectName string + +var ( + dataStructuresTmpl *template.Template + dataStructuresFmt = ` +# Data Structures + +## {{.Name}} (object){{if .ResponseAttributes}} ++ data + + {{.ObjectName.ToLower}} ({{.ObjectName}}) ++ Include Meta + +## {{.ObjectName}} (object) +{{range .ResponseAttributes}}{{.Render}} +{{end}}{{end}} +` +) + +func init() { + dataStructuresTmpl = template.Must(template.New("datastructures").Parse(dataStructuresFmt)) +} + +var StructureOutDir string + +type DataStructure struct { + Name string + ObjectName ObjectName + ResponseAttributes []ResponseAttribute + File *os.File +} + +func (d *DataStructure) Render() string { + return render(dataStructuresTmpl, d) +} + +func (d *DataStructure) Write() error { + return dataStructuresTmpl.Execute(d.File, d) +} + +func (r ObjectName) ToLower() string { + return strings.ToLower(string(r)) +} + +func NewDataStructuresDoc(pkgDir, name string) (dataStructure *DataStructure, err error) { + fiPath := filepath.Join(pkgDir, name+".apib") + + fi, err := os.Create(fiPath) + if err != nil { + return dataStructure, err + } + + dataStructure = &DataStructure{ + File: fi, + } + + return +} + +func SetStructuresOutDir(path string) { + StructureOutDir = path +} diff --git a/doc/parse/response.go b/doc/parse/response.go new file mode 100644 index 0000000..aa0f291 --- /dev/null +++ b/doc/parse/response.go @@ -0,0 +1,9 @@ +package parse + +import "reflect" + +var ResponseType *reflect.Type + +func SetResponseType(responseType *reflect.Type) { + ResponseType = responseType +} diff --git a/doc/response.go b/doc/response.go index 00e54bf..51daf52 100644 --- a/doc/response.go +++ b/doc/response.go @@ -1,15 +1,23 @@ package doc import ( + "fmt" + "log" "net/http/httptest" + "reflect" + "strings" "text/template" + + "github.com/everytv/test2doc/doc/parse" ) var ( responseTmpl *template.Template responseFmt = `+ Response {{.StatusCode}} {{if .HasContentType}}({{.Header.ContentType}}){{end}}{{with .Header}} -{{.Render}}{{end}}{{with .Body}} +{{.Render}}{{end}} + + Attributes {{if .DataStructure}}({{.DataStructure.Name}}){{end}} +{{with .Body}} {{.Render}}{{end}} ` ) @@ -23,10 +31,9 @@ type Response struct { Description string Header *Header Body *Body - // TODO: - // Attributes // Schema + DataStructure *DataStructure } func NewResponse(resp *httptest.ResponseRecorder) *Response { @@ -34,9 +41,10 @@ func NewResponse(resp *httptest.ResponseRecorder) *Response { contentType := resp.Header().Get("Content-Type") return &Response{ - StatusCode: resp.Code, - Header: NewHeader(resp.Header()), - Body: NewBody(content, contentType), + StatusCode: resp.Code, + Header: NewHeader(resp.Header()), + Body: NewBody(content, contentType), + DataStructure: getAttributesOfResponse(content, contentType), } } @@ -47,3 +55,152 @@ func (r *Response) Render() string { func (r *Response) HasContentType() bool { return r.Header != nil && len(r.Header.ContentType) > 0 } + +func getAttributesOfResponse(content []byte, contentType string) *DataStructure { + if len(contentType) == 0 { + return nil + } + + // TODO: mapにしてレスポンスしている場合の処理 + //var mapData map[string]interface{} + //if err := json.Unmarshal(content, &mapData); err != nil { + // return nil + //} + + if parse.ResponseType == nil { + return nil + } + responseType := *parse.ResponseType + if responseType.Kind() == reflect.Slice { + responseType = responseType.Elem() + } + if responseType.Kind() == reflect.Struct { + m := make(map[reflect.Type]struct{}, responseType.NumField()) + return recursiveWriteDataStructure(responseType, m) + } + + return nil +} + +func recursiveWriteDataStructure(reflectType reflect.Type, m map[reflect.Type]struct{}) *DataStructure { + var attrs []ResponseAttribute + if _, ok := m[reflectType]; ok { // ループしないようにするため + return nil + } + m[reflectType] = struct{}{} + + var field *reflect.StructField + for i := 0; i < reflectType.NumField(); i++ { + wk := reflectType.Field(i) + field = &wk + + // `apidoc:"required,default=...,description=..."` + apidocTag := field.Tag.Get("apidoc") + var description, defaultValue string + var isRequired bool + for _, pair := range strings.Split(apidocTag, ",") { + if len(pair) == 0 { + continue + } + kv := strings.Split(pair, "=") + if kv[0] == "description" && len(kv) == 2 { + description = kv[1] + } else if kv[0] == "required" && len(kv) == 1 { + isRequired = true + } else if kv[0] == "default" && len(kv) == 2 { + defaultValue = kv[1] + } else { + // shoddy validation + panic(fmt.Sprintf("unknown format: %v", pair)) + } + } + + if field.Type.Kind() == reflect.Ptr { + field.Type = field.Type.Elem() + } + var isSlice bool + if field.Type.Kind() == reflect.Slice { + field.Type = field.Type.Elem() + isSlice = true + } + + var attributeType string + switch field.Type.Kind() { + case reflect.Bool: + attributeType = "boolean" + case reflect.String: + attributeType = "string" + case reflect.Struct: + attributeType = fmt.Sprintf("%s", field.Type) + case reflect.Map: + case reflect.Interface: + case reflect.Array: + case reflect.Chan: + case reflect.Func: + + default: + attributeType = "number" + } + + // TODO: マップ型を考慮に入れる + var isObject bool + if field.Type.Kind() == reflect.Struct { + isObject = true + split := strings.Split(attributeType, ".") + if strings.HasPrefix(split[1], "Null") { + nullType := split[1][4:] // Nullを除いた型名を取得 + if nullType == "String" || nullType == "Time" { + attributeType = "string" + } else if nullType == "Bool" { + attributeType = "boolean" + } else if nullType == "Int64" || nullType == "Float64" { + attributeType = "number" + } + } else if split[1] == "Time" { + attributeType = "string" + } else { + attributeType = split[1] + recursiveWriteDataStructure(field.Type, m) + } + } + + str := reflectType.Field(i).Tag.Get("json") + if str == "-" || str == "" { + continue + } + if strings.Contains(str, ",") { + str = strings.Split(str, ",")[0] // 不要なタグを除く + } + attr := ResponseAttribute{ + Name: fmt.Sprintf("`%s`", str), + Description: description, + Type: attributeType, + IsRequired: isRequired, + DefaultValue: defaultValue, + IsObject: isObject, + IsSlice: isSlice, + IsEmbedded: field.Anonymous, + } + attrs = append(attrs, attr) + } + + responsePackageStr := fmt.Sprintf("%s", reflectType) + responseStructName := strings.Split(responsePackageStr, ".")[1] + dataStructure, err := NewDataStructuresDoc(StructureOutDir, responseStructName) + if err != nil { + log.Println("Error:", err.Error()) + } + + dataStructure = &DataStructure{ + Name: responseStructName + "Response", + ObjectName: ObjectName(responseStructName), + ResponseAttributes: attrs, + File: dataStructure.File, + } + + if err := dataStructure.Write(); err != nil { + log.Println("Error:", err.Error()) + } + + return dataStructure +} diff --git a/doc/responseattribute.go b/doc/responseattribute.go new file mode 100644 index 0000000..5476b2c --- /dev/null +++ b/doc/responseattribute.go @@ -0,0 +1,33 @@ +package doc + +import ( + "text/template" +) + +var ( + responseAttributeTmpl *template.Template + responseAttributeFmt = ` ++ {{if .IsEmbedded}}Include {{.Type}}{{else}}{{.Name}} ({{if .IsSlice}}array[{{.Type}}], fixed-type{{else}}{{.Type}}{{end}}, {{with .IsRequired}}required{{else}}optional{{end}}){{with .Description}} - {{.}}{{end}}{{with .DefaultValue}} ++ Default: {{.}}{{end}}{{end}} +` +) + +func init() { + responseAttributeTmpl = template.Must(template.New("responseattribute").Parse(responseAttributeFmt)) +} + +type ResponseAttribute struct { + Name string + Description string + Type string + IsRequired bool + + DefaultValue string + IsObject bool + IsSlice bool + IsEmbedded bool +} + +func (p *ResponseAttribute) Render() string { + return render(responseAttributeTmpl, p) +} diff --git a/test/response.go b/test/response.go new file mode 100644 index 0000000..0ffe21b --- /dev/null +++ b/test/response.go @@ -0,0 +1,17 @@ +package test + +import ( + "reflect" + + "github.com/everytv/test2doc/doc" + + "github.com/everytv/test2doc/doc/parse" +) + +func RegisterResponseType(responseType *reflect.Type) { + parse.SetResponseType(responseType) +} + +func RegisterStructuresOutDir(path string) { + doc.SetStructuresOutDir(path) +}