async client flavor added

This commit is contained in:
Jan Halfar 2018-06-12 14:09:46 +02:00
parent dcc105e15f
commit a8855fa401
12 changed files with 395 additions and 107 deletions

View File

@ -38,6 +38,8 @@ Will generate client and server side go and TypeScript code. Have fun!
```yaml
---
modulekind: commonjs
# if you want an async api vs classic callbacks - here you are
tsclientflavor: async
targets:
demo:
services:
@ -59,6 +61,44 @@ mappings:
out: /tmp/test-files-demo-nested.ts
...
```
#### an async example
How to use async clients in this case with axios:
```TypeScript
import axios, { AxiosPromise } from "axios";
import { ServiceClient as ExampleClient } from "./some/generated/client";
// axios transport
let getTransport = endpoint => async <T>(method, args = []) => {
return new Promise<T>(async (resolve, reject) => {
try {
let axiosPromise: any = await axios.post<T>(
endpoint + encodeURIComponent(method),
JSON.stringify(args),
);
return resolve(axiosPromise.data);
} catch (e) {
return reject(e);
}
});
};
let client = new ExampleClient(getTransport(ExampleClient.defaultEndpoint));
export async function test() {
try {
let result = await client.getResult();
console.log("here is the result", result);
} catch (e) {
// e => network?
// e => json
// e => domain error type
console.error("something went wrong ...", e);
}
}
```
### oldschool TypeScript

View File

@ -110,7 +110,7 @@ func Build(conf *config.Config, goPath string) {
os.Exit(2)
}
ts, err := RenderTypeScriptServices(conf.ModuleKind, services, conf.Mappings, scalarTypes, target)
ts, err := RenderTypeScriptServices(conf.ModuleKind, conf.TSClientFlavor, services, conf.Mappings, scalarTypes, target)
if err != nil {
fmt.Fprintln(os.Stderr, " could not generate ts code", err)
os.Exit(3)
@ -128,7 +128,8 @@ func Build(conf *config.Config, goPath string) {
fmt.Fprintln(os.Stderr, " could not write service file", target.Out, updateErr)
os.Exit(3)
}
err = RenderStructsToPackages(structs, conf.Mappings, constants, scalarTypes, mappedTypeScript)
err = renderTypescriptStructsToPackages(conf.ModuleKind, structs, conf.Mappings, constants, scalarTypes, mappedTypeScript)
if err != nil {
fmt.Fprintln(os.Stderr, "struct gen err for target", name, err)
os.Exit(4)

View File

@ -27,11 +27,10 @@ var GoTSRPC;
};
};
})(GoTSRPC || (GoTSRPC = {})); // close
var GoTSRPC;
(function (GoTSRPC) {
var Demo;
(function (Demo) {
var FooClient = (function () {
var FooClient = /** @class */ (function () {
function FooClient(endPoint, transport) {
if (endPoint === void 0) { endPoint = "/service/foo"; }
if (transport === void 0) { transport = GoTSRPC.call; }
@ -45,7 +44,7 @@ var GoTSRPC;
return FooClient;
}());
Demo.FooClient = FooClient;
var DemoClient = (function () {
var DemoClient = /** @class */ (function () {
function DemoClient(endPoint, transport) {
if (endPoint === void 0) { endPoint = "/service/demo"; }
if (transport === void 0) { transport = GoTSRPC.call; }
@ -64,6 +63,9 @@ var GoTSRPC;
DemoClient.prototype.helloInterface = function (anything, anythingMap, anythingSlice, success, err) {
this.transport(this.endPoint, "HelloInterface", [anything, anythingMap, anythingSlice], success, err);
};
DemoClient.prototype.helloScalarError = function (success, err) {
this.transport(this.endPoint, "HelloScalarError", [], success, err);
};
DemoClient.prototype.mapCrap = function (success, err) {
this.transport(this.endPoint, "MapCrap", [], success, err);
};

View File

@ -42,5 +42,9 @@ func (c *code) app(str string) *code {
}
func (c *code) string() string {
if c.line != "" {
c.lines = append(c.lines, c.line)
c.line = ""
}
return strings.Join(c.lines, "\n")
}

View File

@ -65,16 +65,19 @@ type Mapping struct {
type TypeScriptMappings map[string]*Mapping
type ModuleKind string
type TSClientFlavor string
const (
ModuleKindDefault ModuleKind = "default"
ModuleKindCommonJS ModuleKind = "commonjs"
ModuleKindDefault ModuleKind = "default"
ModuleKindCommonJS ModuleKind = "commonjs"
TSClientFlavorAsync TSClientFlavor = "async"
)
type Config struct {
ModuleKind ModuleKind
Targets map[string]*Target
Mappings TypeScriptMappings
ModuleKind ModuleKind
TSClientFlavor TSClientFlavor
Targets map[string]*Target
Mappings TypeScriptMappings
}
func LoadConfigFile(file string) (conf *Config, err error) {
@ -93,6 +96,11 @@ func loadConfig(yamlBytes []byte) (conf *Config, err error) {
err = errors.New("could not parse yaml: " + yamlErr.Error())
return
}
switch conf.TSClientFlavor {
case "", TSClientFlavorAsync:
default:
err = errors.New("unknown ts client flavor: " + conf.TSClientFlavor + " must be empty or " + TSClientFlavorAsync)
}
switch conf.ModuleKind {
case ModuleKindCommonJS, ModuleKindDefault:
case "":

View File

@ -23,6 +23,7 @@ type StructType struct {
}
type Value struct {
IsError bool `json:",omitempty"`
IsInterface bool `json:",omitempty"`
Scalar *Scalar `json:",omitempty"`
ScalarType ScalarType `json:",omitempty"`
@ -88,6 +89,7 @@ type Method struct {
}
type Struct struct {
IsError bool
Package string
Name string
Fields []*Field

View File

@ -259,11 +259,13 @@ func Read(goPaths []string, packageName string, serviceMap map[string]string) (s
func fixFieldStructs(fields []*Field, structs map[string]*Struct, scalars map[string]*Scalar) {
for _, f := range fields {
if f.Value.StructType != nil {
// do we have that struct or is it a hidden scalar
name := f.Value.StructType.FullName()
_, strctExists := structs[name]
s, strctExists := structs[name]
if strctExists {
f.Value.IsError = s.IsError
continue
}
scalar, scalarExists := scalars[name]

View File

@ -15,13 +15,27 @@ func readStructs(pkg *ast.Package, packageName string) (structs map[string]*Stru
structs = map[string]*Struct{}
trace("reading files in package", packageName)
scalarTypes = map[string]*Scalar{}
errorTypes := map[string]bool{}
for _, file := range pkg.Files {
err = extractTypes(file, packageName, structs, scalarTypes)
if err != nil {
return
}
err = extractErrorTypes(file, packageName, errorTypes)
if err != nil {
return
}
}
// jsonDump(scalarTypes)
for name, structType := range structs {
_, isErrorType := errorTypes[name]
if isErrorType {
structType.IsError = true
}
}
//jsonDump(errorTypes)
//jsonDump(scalarTypes)
//jsonDump(structs)
return
}
@ -291,8 +305,36 @@ func readFieldList(fieldList []*ast.Field, fileImports fileImportSpecMap) (field
return
}
func extractErrorTypes(file *ast.File, packageName string, errorTypes map[string]bool) (err error) {
for _, d := range file.Decls {
if reflect.ValueOf(d).Type().String() == "*ast.FuncDecl" {
funcDecl := d.(*ast.FuncDecl)
if funcDecl.Recv != nil && len(funcDecl.Recv.List) == 1 {
firstReceiverField := funcDecl.Recv.List[0]
if "*ast.StarExpr" == reflect.ValueOf(firstReceiverField.Type).Type().String() {
starExpr := firstReceiverField.Type.(*ast.StarExpr)
if "*ast.Ident" == reflect.ValueOf(starExpr.X).Type().String() {
ident := starExpr.X.(*ast.Ident)
if funcDecl.Name.Name == "Error" && funcDecl.Type.Params.NumFields() == 0 && funcDecl.Type.Results.NumFields() == 1 {
returnValueField := funcDecl.Type.Results.List[0]
refl := reflect.ValueOf(returnValueField.Type)
if refl.Type().String() == "*ast.Ident" {
returnValueIdent := returnValueField.Type.(*ast.Ident)
if returnValueIdent.Name == "string" {
errorTypes[packageName+"."+ident.Name] = true
}
//fmt.Println("error for:", ident.Name, returnValueIdent.Name)
}
}
}
}
}
}
}
return
}
func extractTypes(file *ast.File, packageName string, structs map[string]*Struct, scalarTypes map[string]*Scalar) error {
trace("reading file", file.Name.Name)
fileImports := getFileImports(file, packageName)
for name, obj := range file.Scope.Objects {
if obj.Kind == ast.Typ && obj.Decl != nil {

View File

@ -95,7 +95,7 @@ func renderStructFields(fields []*Field, mappings config.TypeScriptMappings, sca
}
func renderStruct(str *Struct, mappings config.TypeScriptMappings, scalarTypes map[string]*Scalar, ts *code) error {
func renderTypescriptStruct(str *Struct, mappings config.TypeScriptMappings, scalarTypes map[string]*Scalar, ts *code) error {
ts.l("// " + str.FullName())
ts.l("export interface " + str.Name + " {").ind(1)
renderStructFields(str.Fields, mappings, scalarTypes, ts)
@ -103,92 +103,14 @@ func renderStruct(str *Struct, mappings config.TypeScriptMappings, scalarTypes m
return nil
}
func renderService(skipGoTSRPC bool, moduleKind config.ModuleKind, service *Service, mappings config.TypeScriptMappings, scalarTypes map[string]*Scalar, ts *code) error {
clientName := service.Name + "Client"
ts.l("export class " + clientName + " {").ind(1)
if moduleKind == config.ModuleKindCommonJS {
if skipGoTSRPC {
ts.l("constructor(public endPoint:string = \"" + service.Endpoint + "\", public transport:(endPoint:string, method:string, args:any[], success:any, err:any) => void) { }")
} else {
ts.l("static defaultInst = new " + clientName + ";")
ts.l("constructor(public endPoint:string = \"" + service.Endpoint + "\", public transport = call) { }")
}
} else {
ts.l("static defaultInst = new " + clientName + ";")
ts.l("constructor(public endPoint:string = \"" + service.Endpoint + "\", public transport = GoTSRPC.call) { }")
}
for _, method := range service.Methods {
ts.app(lcfirst(method.Name) + "(")
// actual args
//args := []string{}
callArgs := []string{}
argOffset := 0
for index, arg := range method.Args {
if index == 0 && arg.Value.isHTTPResponseWriter() {
trace("skipping first arg is a http.ResponseWriter")
argOffset = 1
continue
}
if index == 1 && arg.Value.isHTTPRequest() {
trace("skipping second arg is a *http.Request")
argOffset = 2
continue
}
}
argCount := 0
for index, arg := range method.Args {
if index < argOffset {
continue
}
if index > argOffset {
ts.app(", ")
}
ts.app(arg.tsName() + ":")
arg.Value.tsType(mappings, scalarTypes, ts)
callArgs = append(callArgs, arg.Name)
argCount++
}
if argCount > 0 {
ts.app(", ")
}
ts.app("success:(")
// + strings.Join(retArgs, ", ") +
for index, retField := range method.Return {
retArgName := retField.tsName()
if len(retArgName) == 0 {
retArgName = "ret"
if index > 0 {
retArgName += "_" + fmt.Sprint(index)
}
}
if index > 0 {
ts.app(", ")
}
ts.app(retArgName + ":")
retField.Value.tsType(mappings, scalarTypes, ts)
}
ts.app(") => void")
ts.app(", err:(request:XMLHttpRequest, e?:Error) => void) {").nl()
ts.ind(1)
// generic framework call
ts.l("this.transport(this.endPoint, \"" + method.Name + "\", [" + strings.Join(callArgs, ", ") + "], success, err);")
ts.ind(-1)
ts.app("}")
ts.nl()
}
ts.ind(-1)
ts.l("}")
return nil
}
func RenderStructsToPackages(structs map[string]*Struct, mappings config.TypeScriptMappings, constants map[string]map[string]*ast.BasicLit, scalarTypes map[string]*Scalar, mappedTypeScript map[string]map[string]*code) (err error) {
func renderTypescriptStructsToPackages(
moduleKind config.ModuleKind,
structs map[string]*Struct,
mappings config.TypeScriptMappings,
constants map[string]map[string]*ast.BasicLit,
scalarTypes map[string]*Scalar,
mappedTypeScript map[string]map[string]*code,
) (err error) {
codeMap := map[string]map[string]*code{}
for _, mapping := range mappings {
@ -204,8 +126,12 @@ func RenderStructsToPackages(structs map[string]*Struct, mappings config.TypeScr
err = errors.New("missing code mapping for go package : " + str.Package + " => you have to add a mapping from this go package to a TypeScript module in your build-config.yml in the mappings section")
return
}
packageCodeMap[str.Name] = newCode(" ").ind(1)
err = renderStruct(str, mappings, scalarTypes, packageCodeMap[str.Name])
packageCodeMap[str.Name] = newCode(" ")
// fmt.Println("--------------------------->", moduleKind == config.ModuleKindCommonJS)
if !(moduleKind == config.ModuleKindCommonJS) {
packageCodeMap[str.Name].ind(1)
}
err = renderTypescriptStruct(str, mappings, scalarTypes, packageCodeMap[str.Name])
if err != nil {
return
}
@ -230,7 +156,11 @@ func RenderStructsToPackages(structs map[string]*Struct, mappings config.TypeScr
if done {
continue
}
constCode := newCode(" ").ind(1).l("// constants from " + packageName).l("export const GoConst = {").ind(1)
constCode := newCode(" ")
if moduleKind != config.ModuleKindCommonJS {
constCode.ind(1)
}
constCode.l("// constants from " + packageName).l("export const GoConst = {").ind(1)
//constCode.l()
mappedTypeScript[packageName][goConstPseudoPackage] = constCode
constPrefixParts := split(packageName, []string{"/", ".", "-"})
@ -289,9 +219,9 @@ func ucFirst(str string) string {
return constPrefix
}
func RenderTypeScriptServices(moduleKind config.ModuleKind, services ServiceList, mappings config.TypeScriptMappings, scalarTypes map[string]*Scalar, target *config.Target) (typeScript string, err error) {
func RenderTypeScriptServices(moduleKind config.ModuleKind, tsClientFlavor config.TSClientFlavor, services ServiceList, mappings config.TypeScriptMappings, scalarTypes map[string]*Scalar, target *config.Target) (typeScript string, err error) {
ts := newCode(" ")
if !SkipGoTSRPC {
if !SkipGoTSRPC && tsClientFlavor == "" {
if moduleKind != config.ModuleKindCommonJS {
ts.l(`module GoTSRPC {`)
@ -337,7 +267,12 @@ func RenderTypeScriptServices(moduleKind config.ModuleKind, services ServiceList
if !target.IsTSRPC(service.Name) {
continue
}
err = renderService(SkipGoTSRPC, moduleKind, service, mappings, scalarTypes, ts)
switch tsClientFlavor {
case config.TSClientFlavorAsync:
err = renderTypescriptClientAsync(service, mappings, scalarTypes, ts)
default:
err = renderTypescriptClient(SkipGoTSRPC, moduleKind, service, mappings, scalarTypes, ts)
}
if err != nil {
return
}

View File

@ -10,5 +10,4 @@ func TestSplit(t *testing.T) {
t.Fatal("expected", expected, "got", actual)
}
}
}

94
typescriptclient.go Normal file
View File

@ -0,0 +1,94 @@
package gotsrpc
import (
"fmt"
"strings"
"github.com/foomo/gotsrpc/config"
)
func renderTypescriptClient(skipGoTSRPC bool, moduleKind config.ModuleKind, service *Service, mappings config.TypeScriptMappings, scalarTypes map[string]*Scalar, ts *code) error {
clientName := service.Name + "Client"
ts.l("export class " + clientName + " {").ind(1)
if moduleKind == config.ModuleKindCommonJS {
if skipGoTSRPC {
ts.l("constructor(public endPoint:string = \"" + service.Endpoint + "\", public transport:(endPoint:string, method:string, args:any[], success:any, err:any) => void) { }")
} else {
ts.l("static defaultInst = new " + clientName + ";")
ts.l("constructor(public endPoint:string = \"" + service.Endpoint + "\", public transport = call) { }")
}
} else {
ts.l("static defaultInst = new " + clientName + ";")
ts.l("constructor(public endPoint:string = \"" + service.Endpoint + "\", public transport = GoTSRPC.call) { }")
}
for _, method := range service.Methods {
ts.app(lcfirst(method.Name) + "(")
// actual args
//args := []string{}
callArgs := []string{}
argOffset := 0
for index, arg := range method.Args {
if index == 0 && arg.Value.isHTTPResponseWriter() {
trace("skipping first arg is a http.ResponseWriter")
argOffset = 1
continue
}
if index == 1 && arg.Value.isHTTPRequest() {
trace("skipping second arg is a *http.Request")
argOffset = 2
continue
}
}
argCount := 0
for index, arg := range method.Args {
if index < argOffset {
continue
}
if index > argOffset {
ts.app(", ")
}
ts.app(arg.tsName() + ":")
arg.Value.tsType(mappings, scalarTypes, ts)
callArgs = append(callArgs, arg.Name)
argCount++
}
if argCount > 0 {
ts.app(", ")
}
ts.app("success:(")
// + strings.Join(retArgs, ", ") +
for index, retField := range method.Return {
retArgName := retField.tsName()
if len(retArgName) == 0 {
retArgName = "ret"
if index > 0 {
retArgName += "_" + fmt.Sprint(index)
}
}
if index > 0 {
ts.app(", ")
}
ts.app(retArgName + ":")
retField.Value.tsType(mappings, scalarTypes, ts)
}
ts.app(") => void")
ts.app(", err:(request:XMLHttpRequest, e?:Error) => void) {").nl()
ts.ind(1)
// generic framework call
ts.l("this.transport(this.endPoint, \"" + method.Name + "\", [" + strings.Join(callArgs, ", ") + "], success, err);")
ts.ind(-1)
ts.app("}")
ts.nl()
}
ts.ind(-1)
ts.l("}")
return nil
}

159
typscriptclientasync.go Normal file
View File

@ -0,0 +1,159 @@
package gotsrpc
import (
"fmt"
"strconv"
"strings"
"github.com/foomo/gotsrpc/config"
)
func renderTypescriptClientAsync(service *Service, mappings config.TypeScriptMappings, scalarTypes map[string]*Scalar, ts *code) error {
clientName := service.Name + "Client"
ts.l("export class " + clientName + " {")
ts.ind(1)
//ts.l(`static defaultInst = new ` + clientName + `()`)
//ts.l(`constructor(public endpoint = "` + service.Endpoint + `") {}`)
ts.l(`public static defaultEndpoint = "` + service.Endpoint + `";`)
ts.l("constructor(")
ts.ind(1)
ts.l("public transport:<T>(method: string, data?: any[]) => Promise<T>")
ts.ind(-1)
ts.l(") {}")
for _, method := range service.Methods {
ts.app("async " + lcfirst(method.Name) + "(")
callArgs := []string{}
argOffset := 0
for index, arg := range method.Args {
if index == 0 && arg.Value.isHTTPResponseWriter() {
trace("skipping first arg is a http.ResponseWriter")
argOffset = 1
continue
}
if index == 1 && arg.Value.isHTTPRequest() {
trace("skipping second arg is a *http.Request")
argOffset = 2
continue
}
}
argCount := 0
for index, arg := range method.Args {
if index < argOffset {
continue
}
if index > argOffset {
ts.app(", ")
}
ts.app(arg.tsName() + ":")
arg.Value.tsType(mappings, scalarTypes, ts)
callArgs = append(callArgs, arg.Name)
argCount++
}
ts.app("):")
throwLastError := false
//lastErrorName := ""
returnTypeTS := newCode(" ")
returnTypeTS.app("{")
innerReturnTypeTS := newCode(" ")
innerReturnTypeTS.app("{")
firstReturnType := ""
//firstReturnFieldName := ""
countReturns := 0
countInnerReturns := 0
errIndex := 0
responseObjectPrefix := ""
responseObject := "let responseObject = {"
for index, retField := range method.Return {
countInnerReturns++
retArgName := retField.tsName()
if len(retArgName) == 0 {
retArgName = "ret"
if index > 0 {
retArgName += "_" + fmt.Sprint(index)
}
}
if index > 0 {
returnTypeTS.app("; ")
innerReturnTypeTS.app("; ")
}
innerReturnTypeTS.app(strconv.Itoa(index) + ":")
retField.Value.tsType(mappings, scalarTypes, innerReturnTypeTS)
if index == len(method.Return)-1 && retField.Value.IsError {
throwLastError = true
//lastErrorName = retArgName
errIndex = index
} else {
if index == 0 {
firstReturnTypeTS := newCode(" ")
retField.Value.tsType(mappings, scalarTypes, firstReturnTypeTS)
firstReturnType = firstReturnTypeTS.string()
//firstReturnFieldName = retArgName
}
countReturns++
returnTypeTS.app(retArgName + ":")
responseObject += responseObjectPrefix + retArgName + " : response[" + strconv.Itoa(index) + "]"
retField.Value.tsType(mappings, scalarTypes, returnTypeTS)
}
responseObjectPrefix = ", "
}
responseObject += "};"
returnTypeTS.app("}")
innerReturnTypeTS.app("}")
if countReturns == 0 {
ts.app("Promise<void> {")
} else if countReturns == 1 {
ts.app("Promise<" + firstReturnType + "> {")
} else if countReturns > 1 {
ts.app("Promise<" + returnTypeTS.string() + "> {")
}
ts.nl()
ts.ind(1)
innerCallTypeString := "void"
if countInnerReturns > 0 {
innerCallTypeString = innerReturnTypeTS.string()
}
call := "this.transport<" + innerCallTypeString + ">(\"" + method.Name + "\", [" + strings.Join(callArgs, ", ") + "])"
if throwLastError {
ts.l("let response = await " + call)
ts.l("let err = response[" + strconv.Itoa(errIndex) + "];")
//ts.l("delete response." + lastErrorName + ";")
ts.l("if(err) { throw err }")
if countReturns == 1 {
ts.l("return response[0]")
} else if countReturns == 0 {
//ts.l("return response;")
} else {
ts.l(responseObject)
ts.l("return responseObject;")
}
} else {
if countReturns == 1 {
ts.l("return (await " + call + ")[0]")
} else {
ts.l("let response = await " + call)
ts.l(responseObject)
ts.l("return responseObject;")
}
}
ts.ind(-1)
ts.app("}")
ts.nl()
}
ts.ind(-1)
ts.l("}")
return nil
}