Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions doc/datastructures.go
Original file line number Diff line number Diff line change
@@ -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
}
9 changes: 9 additions & 0 deletions doc/parse/response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package parse

import "reflect"

var ResponseType *reflect.Type

func SetResponseType(responseType *reflect.Type) {
ResponseType = responseType
}
169 changes: 163 additions & 6 deletions doc/response.go
Original file line number Diff line number Diff line change
@@ -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}}
`
)
Expand All @@ -23,20 +31,20 @@ type Response struct {
Description string
Header *Header
Body *Body

// TODO:
// Attributes
// Schema
DataStructure *DataStructure
}

func NewResponse(resp *httptest.ResponseRecorder) *Response {
content := resp.Body.Bytes()
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),
}
}

Expand All @@ -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
}
33 changes: 33 additions & 0 deletions doc/responseattribute.go
Original file line number Diff line number Diff line change
@@ -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)
}
17 changes: 17 additions & 0 deletions test/response.go
Original file line number Diff line number Diff line change
@@ -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)
}