From 4500dac43cc83ab78451eeb7bcea8ad3bcc1bfbc Mon Sep 17 00:00:00 2001 From: Henrique Rodrigues Date: Sat, 28 Jul 2018 18:31:31 -0300 Subject: [PATCH] documentation function --- app.go | 16 +++ app_test.go | 16 +++ docgenerator/generator.go | 201 +++++++++++++++++++++++++++++++++ docgenerator/generator_test.go | 193 +++++++++++++++++++++++++++++++ service/handler.go | 9 ++ service/remote.go | 9 ++ 6 files changed, 444 insertions(+) create mode 100644 docgenerator/generator.go create mode 100644 docgenerator/generator_test.go diff --git a/app.go b/app.go index 3161ab1a..37d45e20 100644 --- a/app.go +++ b/app.go @@ -496,3 +496,19 @@ func GetFromPropagateCtx(ctx context.Context, key string) interface{} { func ExtractSpan(ctx context.Context) (opentracing.SpanContext, error) { return tracing.ExtractSpan(ctx) } + +// Documentation returns handler and remotes documentacion +func Documentation() (map[string]interface{}, error) { + handlerDocs, err := handlerService.Docs() + if err != nil { + return nil, err + } + remoteDocs, err := remoteService.Docs() + if err != nil { + return nil, err + } + return map[string]interface{}{ + "handlers": handlerDocs, + "remotes": remoteDocs, + }, nil +} diff --git a/app_test.go b/app_test.go index 7c59cce0..ad44f926 100644 --- a/app_test.go +++ b/app_test.go @@ -481,3 +481,19 @@ func TestExtractSpan(t *testing.T) { assert.NoError(t, err) assert.Equal(t, span.Context(), spanCtx) } + +func TestDocumentation(t *testing.T) { + initApp() + Configure(true, "testtype", Standalone, map[string]string{}, viper.New()) + acc := acceptor.NewTCPAcceptor("0.0.0.0:0") + AddAcceptor(acc) + + go Start() + + doc, err := Documentation() + assert.NoError(t, err) + assert.Equal(t, map[string]interface{}{ + "handlers": map[string]interface{}{}, + "remotes": map[string]interface{}{}, + }, doc) +} diff --git a/docgenerator/generator.go b/docgenerator/generator.go new file mode 100644 index 00000000..53065e02 --- /dev/null +++ b/docgenerator/generator.go @@ -0,0 +1,201 @@ +// Copyright (c) TFG Co. All Rights Reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package docgenerator + +import ( + "encoding/json" + "reflect" + "strings" + "unicode" + + "github.com/topfreegames/pitaya/component" + "github.com/topfreegames/pitaya/route" +) + +type docs struct { + Handlers docMap `json:"handlers"` + Remotes docMap `json:"remotes"` +} + +type docMap map[string]*doc + +type doc struct { + Input interface{} `json:"input"` + Output []interface{} `json:"output"` +} + +// HandlersDocs returns a map from route to input and output +func HandlersDocs(serverType string, services map[string]*component.Service) (map[string]interface{}, error) { + docs := &docs{ + Handlers: map[string]*doc{}, + } + + for serviceName, service := range services { + for name, handler := range service.Handlers { + routeName := route.NewRoute(serverType, serviceName, name) + docs.Handlers[routeName.String()] = docForMethod(handler.Method) + } + } + + return docs.Handlers.toMap() +} + +// RemotesDocs returns a map from route to input and output +func RemotesDocs(serverType string, services map[string]*component.Service) (map[string]interface{}, error) { + docs := &docs{ + Remotes: map[string]*doc{}, + } + + for serviceName, service := range services { + for name, remote := range service.Remotes { + routeName := route.NewRoute(serverType, serviceName, name) + docs.Remotes[routeName.String()] = docForMethod(remote.Method) + } + } + + return docs.Remotes.toMap() +} + +func (d docMap) toMap() (map[string]interface{}, error) { + var m map[string]interface{} + bts, err := json.Marshal(d) + if err != nil { + return nil, err + } + err = json.Unmarshal(bts, &m) + if err != nil { + return nil, err + } + return m, nil +} + +func docForMethod(method reflect.Method) *doc { + doc := &doc{ + Output: []interface{}{}, + } + + if method.Type.NumIn() > 2 { + isOutput := false + doc.Input = docForType(method.Type.In(2), isOutput) + } + + for i := 0; i < method.Type.NumOut(); i++ { + isOutput := true + doc.Output = append(doc.Output, docForType(method.Type.Out(i), isOutput)) + } + + return doc +} + +func parseStruct(typ reflect.Type) reflect.Type { + switch typ.String() { + case "time.Time": + return nil + default: + return typ + } +} + +func docForType(typ reflect.Type, isOutput bool) interface{} { + if typ.Kind() == reflect.Ptr { + fields := map[string]interface{}{} + elm := typ.Elem() + for i := 0; i < elm.NumField(); i++ { + if name, valid := getName(elm.Field(i), isOutput); valid { + fields[name] = parseType(elm.Field(i).Type, isOutput) + } + } + return fields + } + + return parseType(typ, isOutput) +} + +func validName(field reflect.StructField) bool { + isProtoField := func(name string) bool { + return strings.HasPrefix(name, "XXX_") + } + + isPrivateField := func(name string) bool { + for _, r := range name { + return unicode.IsLower(r) + } + + return true + } + + isIgnored := func(field reflect.StructField) bool { + return field.Tag.Get("json") == "-" + } + + return !isProtoField(field.Name) && !isPrivateField(field.Name) && !isIgnored(field) +} + +func firstLetterToLower(name string, isOutput bool) string { + if isOutput { + return name + } + + return string(append([]byte{strings.ToLower(name)[0]}, name[1:]...)) +} + +func getName(field reflect.StructField, isOutput bool) (name string, valid bool) { + if !validName(field) { + return "", false + } + + name, ok := field.Tag.Lookup("json") + if !ok { + return firstLetterToLower(field.Name, isOutput), true + } + + return strings.Split(name, ",")[0], true +} + +func parseType(typ reflect.Type, isOutput bool) interface{} { + var elm reflect.Type + + switch typ.Kind() { + case reflect.Ptr: + elm = typ.Elem() + case reflect.Struct: + elm = parseStruct(typ) + if elm == nil { + return typ.String() + } + case reflect.Slice: + parsed := parseType(typ.Elem(), isOutput) + if parsed == "uint8" { + return "[]byte" + } + return []interface{}{parsed} + default: + return typ.String() + } + + fields := map[string]interface{}{} + for i := 0; i < elm.NumField(); i++ { + if name, valid := getName(elm.Field(i), isOutput); valid { + fields[name] = parseType(elm.Field(i).Type, isOutput) + } + } + return fields +} diff --git a/docgenerator/generator_test.go b/docgenerator/generator_test.go new file mode 100644 index 00000000..138fef7f --- /dev/null +++ b/docgenerator/generator_test.go @@ -0,0 +1,193 @@ +// Copyright (c) TFG Co. All Rights Reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package docgenerator + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/topfreegames/pitaya/component" + "github.com/topfreegames/pitaya/protos/test" +) + +type MyComp struct { + component.Base +} + +type MyStruct struct { + Str string + privateInt int + Int int `json:"int"` + Time time.Time `json:"time"` + NotOnJSONString string `json:"-"` + Bytes []byte `json:"bytes"` + Struct *struct { + Int int `json:"int"` + NotPointer struct { + Str string `json:"str"` + Int int + } `json:"notPointer"` + } `json:"struct"` + Slice []*struct { + Int int `json:"int"` + Str string + } `json:"slice"` + NotPointer struct { + Str string `json:"str"` + Int int + } `json:"notPointer"` +} + +func (m *MyComp) Init() {} + +func (m *MyComp) Shutdown() {} + +func (m *MyComp) HandlerEmpty(ctx context.Context) {} + +func (m *MyComp) HandlerRaw(ctx context.Context, b []byte) ([]byte, error) { + return nil, nil +} + +func (m *MyComp) HandlerOrRemoteStruct(ctx context.Context, s *MyStruct) (*MyStruct, error) { + s.privateInt = 0 + return nil, nil +} + +func (m *MyComp) RemoteStruct(ctx context.Context, ss *test.SomeStruct) (*test.SomeStruct, error) { + return nil, nil +} + +func TestHandlersDoc(t *testing.T) { + t.Parallel() + + handlerServices := map[string]*component.Service{} + s := component.NewService(&MyComp{}, []component.Option{}) + err := s.ExtractHandler() + assert.NoError(t, err) + handlerServices[s.Name] = s + + doc, err := HandlersDocs("metagame", handlerServices) + assert.NoError(t, err) + assert.Equal(t, map[string]interface{}{ + "metagame.MyComp.HandlerEmpty": map[string]interface{}{ + "output": []interface{}{}, + "input": interface{}(nil), + }, + "metagame.MyComp.HandlerRaw": map[string]interface{}{ + "input": "[]byte", + "output": []interface{}{"[]byte", "error"}, + }, + "metagame.MyComp.HandlerOrRemoteStruct": map[string]interface{}{ + "input": map[string]interface{}{ + "bytes": "[]byte", + "int": "int", + "notPointer": map[string]interface{}{ + "int": "int", + "str": "string", + }, + "str": "string", + "struct": map[string]interface{}{ + "int": "int", + "notPointer": map[string]interface{}{ + "int": "int", + "str": "string", + }, + }, + "slice": []interface{}{ + map[string]interface{}{ + "int": "int", + "str": "string", + }, + }, + "time": "time.Time", + }, + "output": []interface{}{ + map[string]interface{}{ + "int": "int", + "notPointer": map[string]interface{}{ + "Int": "int", + "str": "string", + }, + "Str": "string", + "struct": map[string]interface{}{ + "int": "int", + "notPointer": map[string]interface{}{ + "Int": "int", + "str": "string", + }, + }, + "slice": []interface{}{ + map[string]interface{}{ + "int": "int", + "Str": "string", + }, + }, + "time": "time.Time", + "bytes": "[]byte", + }, + "error", + }, + }, + "metagame.MyComp.RemoteStruct": map[string]interface{}{ + "input": map[string]interface{}{ + "A": "int32", + "B": "string", + }, + "output": []interface{}{ + map[string]interface{}{ + "A": "int32", + "B": "string", + }, + "error", + }, + }, + }, doc) +} + +func TestRemotesDoc(t *testing.T) { + t.Parallel() + + remoteServices := map[string]*component.Service{} + s := component.NewService(&MyComp{}, []component.Option{}) + err := s.ExtractRemote() + assert.NoError(t, err) + remoteServices[s.Name] = s + + doc, err := RemotesDocs("metagame", remoteServices) + assert.NoError(t, err) + assert.Equal(t, map[string]interface{}{ + "metagame.MyComp.RemoteStruct": map[string]interface{}{ + "input": map[string]interface{}{ + "A": "int32", + "B": "string", + }, + "output": []interface{}{ + map[string]interface{}{ + "A": "int32", + "B": "string", + }, + "error", + }, + }, + }, doc) +} diff --git a/service/handler.go b/service/handler.go index 07016145..51008417 100644 --- a/service/handler.go +++ b/service/handler.go @@ -34,6 +34,7 @@ import ( "github.com/topfreegames/pitaya/component" "github.com/topfreegames/pitaya/constants" pcontext "github.com/topfreegames/pitaya/context" + "github.com/topfreegames/pitaya/docgenerator" e "github.com/topfreegames/pitaya/errors" "github.com/topfreegames/pitaya/internal/codec" "github.com/topfreegames/pitaya/internal/message" @@ -316,3 +317,11 @@ func (h *HandlerService) DumpServices() { logger.Log.Infof("registered handler %s, isRawArg: %s", name, handlers[name].IsRawArg) } } + +// Docs returns documentation for handlers +func (h *HandlerService) Docs() (map[string]interface{}, error) { + if h == nil { + return map[string]interface{}{}, nil + } + return docgenerator.HandlersDocs(h.server.Type, h.services) +} diff --git a/service/remote.go b/service/remote.go index 939dddce..b48979f1 100644 --- a/service/remote.go +++ b/service/remote.go @@ -32,6 +32,7 @@ import ( "github.com/topfreegames/pitaya/cluster" "github.com/topfreegames/pitaya/component" "github.com/topfreegames/pitaya/constants" + "github.com/topfreegames/pitaya/docgenerator" e "github.com/topfreegames/pitaya/errors" "github.com/topfreegames/pitaya/internal/codec" "github.com/topfreegames/pitaya/internal/message" @@ -428,3 +429,11 @@ func (r *RemoteService) DumpServices() { logger.Log.Infof("registered remote %s", name) } } + +// Docs returns documentation for remotes +func (r *RemoteService) Docs() (map[string]interface{}, error) { + if r == nil { + return map[string]interface{}{}, nil + } + return docgenerator.RemotesDocs(r.server.Type, r.services) +}