Skip to content

Commit

Permalink
Merge 3f486de into 080be91
Browse files Browse the repository at this point in the history
  • Loading branch information
henrod committed Aug 14, 2018
2 parents 080be91 + 3f486de commit e4598c3
Show file tree
Hide file tree
Showing 6 changed files with 492 additions and 0 deletions.
16 changes: 16 additions & 0 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
64 changes: 64 additions & 0 deletions app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -481,3 +481,67 @@ 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{}{
"testtype.sys.bindsession": map[string]interface{}{
"input": map[string]interface{}{
"uid": "string",
"data": "[]byte",
"id": "int64",
},
"output": []interface{}{
map[string]interface{}{
"error": map[string]interface{}{
"msg": "string",
"code": "string",
"metadata": "map[string]string",
},
"data": "[]byte",
},
"error",
},
},
"testtype.sys.kick": map[string]interface{}{
"input": map[string]interface{}{
"userId": "string",
},
"output": []interface{}{
map[string]interface{}{
"kicked": "bool",
},
"error",
},
},
"testtype.sys.pushsession": map[string]interface{}{
"input": map[string]interface{}{
"data": "[]byte",
"id": "int64",
"uid": "string",
},
"output": []interface{}{
map[string]interface{}{
"error": map[string]interface{}{
"code": "string",
"metadata": "map[string]string",
"msg": "string",
},
"data": "[]byte",
},
"error",
},
},
},
}, doc)
}
201 changes: 201 additions & 0 deletions docgenerator/generator.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit e4598c3

Please sign in to comment.