Files
rpcx/reflection/server_reflection.go
2024-04-23 19:22:55 +08:00

216 lines
4.4 KiB
Go

package reflection
import (
"bytes"
"context"
"encoding/json"
"fmt"
"path/filepath"
"reflect"
"strings"
"text/template"
"unicode"
"unicode/utf8"
"github.com/ChimeraCoder/gojson"
"github.com/smallnest/rpcx/log"
)
var (
typeOfError = reflect.TypeOf((*error)(nil)).Elem()
typeOfContext = reflect.TypeOf((*context.Context)(nil)).Elem()
)
// ServiceInfo service info.
type ServiceInfo struct {
Name string
PkgPath string
Methods []*MethodInfo
}
// MethodInfo method info
type MethodInfo struct {
Name string
ReqName string
Req string
ReplyName string
Reply string
}
var siTemplate = `package {{.PkgPath}}
type {{.Name}} struct{}
{{$name := .Name}}
{{range .Methods}}
{{.Req}}
{{.Reply}}
type (s *{{$name}}) {{.Name}}(ctx context.Context, arg *{{.ReqName}}, reply *{{.ReplyName}}) error {
return nil
}
{{end}}
`
func (si ServiceInfo) String() string {
tpl := template.Must(template.New("service").Parse(siTemplate))
var buf bytes.Buffer
_ = tpl.Execute(&buf, si)
return buf.String()
}
type Reflection struct {
Services map[string]*ServiceInfo
}
func New() *Reflection {
return &Reflection{
Services: make(map[string]*ServiceInfo),
}
}
func (r *Reflection) Register(name string, rcvr interface{}, metadata string) error {
si := &ServiceInfo{}
val := reflect.ValueOf(rcvr)
typ := reflect.TypeOf(rcvr)
vTyp := reflect.Indirect(val).Type()
si.Name = vTyp.Name()
pkg := vTyp.PkgPath()
if strings.Index(pkg, ".") > 0 {
pkg = pkg[strings.LastIndex(pkg, ".")+1:]
}
pkg = filepath.Base(pkg)
pkg = strings.ReplaceAll(pkg, "-", "_")
si.PkgPath = pkg
for m := 0; m < val.NumMethod(); m++ {
method := typ.Method(m)
mtype := method.Type
if method.PkgPath != "" {
continue
}
if mtype.NumIn() != 4 {
continue
}
// First arg must be context.Context
ctxType := mtype.In(1)
if !ctxType.Implements(typeOfContext) {
continue
}
// Second arg need not be a pointer.
argType := mtype.In(2)
if !isExportedOrBuiltinType(argType) {
continue
}
// Third arg must be a pointer.
replyType := mtype.In(3)
if replyType.Kind() != reflect.Ptr {
continue
}
// Reply type must be exported.
if !isExportedOrBuiltinType(replyType) {
continue
}
// Method needs one out.
if mtype.NumOut() != 1 {
continue
}
// The return type of the method must be error.
if returnType := mtype.Out(0); returnType != typeOfError {
continue
}
mi := &MethodInfo{}
mi.Name = method.Name
if argType.Kind() == reflect.Ptr {
argType = argType.Elem()
}
replyType = replyType.Elem()
mi.ReqName = argType.Name()
mi.Req = generateTypeDefination(mi.ReqName, si.PkgPath, generateJSON(argType))
mi.ReplyName = replyType.Name()
mi.Reply = generateTypeDefination(mi.ReplyName, si.PkgPath, generateJSON(replyType))
si.Methods = append(si.Methods, mi)
}
if len(si.Methods) > 0 {
r.Services[name] = si
}
return nil
}
func (r *Reflection) Unregister(name string) error {
delete(r.Services, name)
return nil
}
func (r *Reflection) GetService(ctx context.Context, s string, reply *string) error {
si, ok := r.Services[s]
if !ok {
return fmt.Errorf("not found service %s", s)
}
*reply = si.String()
return nil
}
func (r *Reflection) GetServices(ctx context.Context, s string, reply *string) error {
var buf bytes.Buffer
pkg := `package `
for _, si := range r.Services {
if pkg == `package ` {
pkg = pkg + si.PkgPath + "\n\n"
}
buf.WriteString(strings.ReplaceAll(si.String(), pkg, ""))
}
if pkg != `package ` {
*reply = pkg + buf.String()
} else {
*reply = buf.String()
}
return nil
}
func generateTypeDefination(name, pkg string, jsonValue string) string {
jsonValue = strings.TrimSpace(jsonValue)
if jsonValue == "" || jsonValue == `""` {
return ""
}
r := strings.NewReader(jsonValue)
output, err := gojson.Generate(r, gojson.ParseJson, name, pkg, nil, false, true)
if err != nil {
log.Errorf("failed to generate json: %v", err)
return ""
}
rt := strings.ReplaceAll(string(output), "``", "")
return strings.ReplaceAll(rt, "package "+pkg+"\n\n", "")
}
func generateJSON(typ reflect.Type) string {
v := reflect.New(typ).Interface()
data, _ := json.Marshal(v)
return string(data)
}
func isExported(name string) bool {
rune, _ := utf8.DecodeRuneInString(name)
return unicode.IsUpper(rune)
}
func isExportedOrBuiltinType(t reflect.Type) bool {
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
return isExported(t.Name()) || t.PkgPath() == ""
}