From a8855fa4015a0760a1fe010e3e1db489ba0d4f1d Mon Sep 17 00:00:00 2001 From: Jan Halfar Date: Tue, 12 Jun 2018 14:09:46 +0200 Subject: [PATCH] async client flavor added --- README.md | 40 ++++++++++ build.go | 5 +- cmd/demo/demo.js | 8 +- code.go | 4 + config/config.go | 18 +++-- model.go | 2 + servicereader.go | 4 +- typereader.go | 46 +++++++++++- typescript.go | 121 +++++++----------------------- typescript_test.go | 1 - typescriptclient.go | 94 ++++++++++++++++++++++++ typscriptclientasync.go | 159 ++++++++++++++++++++++++++++++++++++++++ 12 files changed, 395 insertions(+), 107 deletions(-) create mode 100644 typescriptclient.go create mode 100644 typscriptclientasync.go diff --git a/README.md b/README.md index 80d0b12..b57a549 100644 --- a/README.md +++ b/README.md @@ -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 (method, args = []) => { + return new Promise(async (resolve, reject) => { + try { + let axiosPromise: any = await axios.post( + 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 diff --git a/build.go b/build.go index a8fe888..79d5bc8 100644 --- a/build.go +++ b/build.go @@ -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) diff --git a/cmd/demo/demo.js b/cmd/demo/demo.js index b17d4df..88ba9c8 100644 --- a/cmd/demo/demo.js +++ b/cmd/demo/demo.js @@ -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); }; diff --git a/code.go b/code.go index 777538d..9fd3ca4 100644 --- a/code.go +++ b/code.go @@ -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") } diff --git a/config/config.go b/config/config.go index ba74fad..fd8d6c9 100644 --- a/config/config.go +++ b/config/config.go @@ -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 "": diff --git a/model.go b/model.go index 24af4a2..efbc1b7 100644 --- a/model.go +++ b/model.go @@ -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 diff --git a/servicereader.go b/servicereader.go index bbb3967..283a67f 100644 --- a/servicereader.go +++ b/servicereader.go @@ -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] diff --git a/typereader.go b/typereader.go index 3721ddf..4c065e1 100644 --- a/typereader.go +++ b/typereader.go @@ -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 { diff --git a/typescript.go b/typescript.go index b55ce2e..a92003b 100644 --- a/typescript.go +++ b/typescript.go @@ -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 } diff --git a/typescript_test.go b/typescript_test.go index 7ef8f2b..e5555b5 100644 --- a/typescript_test.go +++ b/typescript_test.go @@ -10,5 +10,4 @@ func TestSplit(t *testing.T) { t.Fatal("expected", expected, "got", actual) } } - } diff --git a/typescriptclient.go b/typescriptclient.go new file mode 100644 index 0000000..170e5bd --- /dev/null +++ b/typescriptclient.go @@ -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 +} diff --git a/typscriptclientasync.go b/typscriptclientasync.go new file mode 100644 index 0000000..0ee4ec6 --- /dev/null +++ b/typscriptclientasync.go @@ -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:(method: string, data?: any[]) => Promise") + 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 {") + } 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 +}