Skip to content

Commit

Permalink
feat: support union types
Browse files Browse the repository at this point in the history
  • Loading branch information
franklinkim committed Mar 2, 2022
1 parent 40c94da commit b1eeafc
Show file tree
Hide file tree
Showing 72 changed files with 3,253 additions and 185 deletions.
56 changes: 56 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Minimum golangci-lint version required: v1.42.0
run:
timeout: 3m
skip-dirs:
- tmp

linters-settings:
gci:
local-prefixes: github.com/foomo/keel
revive:
rules:
- name: indent-error-flow
disabled: true
gocritic:
enabled-tags:
- diagnostic
- performance
- style
disabled-tags:
- experimental
- opinionated
disabled-checks:
- ifElseChain
settings:
hugeParam:
sizeThreshold: 512

linters:
enable:
- bodyclose
- exhaustive
- dogsled
- dupl
- exportloopref
- goconst
- gocritic
- gocyclo
- gofmt
- goprintffuncname
- gosec
- ifshort
- misspell
- nakedret
- noctx
- nolintlint
- prealloc
- revive
- promlinter
- rowserrcheck
- sqlclosecheck
- stylecheck
- thelper
- tparallel
- unconvert
- unparam
- whitespace
39 changes: 37 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,58 @@ build.debug:
go build -gcflags "all=-N -l" -o bin/gotsrpc cmd/gotsrpc/gotsrpc.go


EXAMPLES=basic nullable
## === Tools ===

EXAMPLES=basic errors nullable multi
define examples
.PHONY: example.$(1)
example.$(1):
cd example/${1} && go run ../../cmd/gotsrpc/gotsrpc.go gotsrpc.yml
cd example/${1}/client && tsc --build

.PHONY: example.$(1).run
example.$(1).run: example.${1}
cd example/${1}/client && tsc --build
cd example/${1} && go run main.go

.PHONY: example.$(1).debug
example.$(1).debug: build.debug
cd example/${1} && dlv --listen=:2345 --headless=true --api-version=2 --accept-multiclient exec ../../bin/gotsrpc gotsrpc.yml

.PHONY: example.$(1).lint
example.$(1).lint:
cd example/${1} && golangci-lint run
endef
$(foreach p,$(EXAMPLES),$(eval $(call examples,$(p))))

## Run go mod tidy recursive
.PHONY: lint
lint:
# @golangci-lint run
@for name in example/*/; do\
echo "-------- $${name} ------------";\
sh -c "cd $$(pwd)/$${name} && golangci-lint run";\
done

## Run go mod tidy recursive
.PHONY: gomod
gomod:
@go mod tidy
@for name in example/*/; do\
echo "-------- $${name} ------------";\
sh -c "cd $$(pwd)/$${name} && go mod tidy";\
done

## === Examples ===

.PHONY: examples
## Build examples
examples:
@for name in example/*/; do\
echo "-------- $${name} ------------";\
$(MAKE) example.`basename $${name}`;\
done
.PHONY: examples

## === Utils ===

## Show help text
Expand Down
13 changes: 11 additions & 2 deletions build.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,14 @@ func Build(conf *config.Config, goPath string) {
os.Exit(2)
}

// collect all union structs
unions := map[string][]string{}
for _, s := range structs {
if len(s.Fields) == 0 && len(s.UnionFields) > 0 {
unions[s.Package] = append(unions[s.Package], s.Name)
}
}

if target.Out != "" {

ts, err := RenderTypeScriptServices(services, conf.Mappings, scalars, structs, target)
Expand Down Expand Up @@ -192,13 +200,14 @@ func Build(conf *config.Config, goPath string) {
}
}
if len(target.TSRPC) > 0 {
goTSRPCProxiesCode, goerr := RenderGoTSRPCProxies(services, packageName, pkgName, target)
goTSRPCProxiesCode, goerr := RenderGoTSRPCProxies(services, packageName, pkgName, target, unions)
if goerr != nil {
fmt.Fprintln(os.Stderr, " could not generate go ts rpc proxies code in target", name, goerr)
os.Exit(4)
}
formatAndWrite(goTSRPCProxiesCode, goTSRPCProxiesFilename)

}
if len(target.TSRPC) > 0 && !target.SkipTSRPCClient {
goTSRPCClientsCode, goerr := RenderGoTSRPCClients(services, packageName, pkgName, target)
if goerr != nil {
fmt.Fprintln(os.Stderr, " could not generate go ts rpc clients code in target", name, goerr)
Expand Down
43 changes: 31 additions & 12 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,15 @@ func (c *bufferedClient) SetTransportHttpClient(client *http.Client) {
c.client = client
}

// CallClient calls a method on the remove service
func (c *bufferedClient) Call(ctx context.Context, url string, endpoint string, method string, args []interface{}, reply []interface{}) (err error) {
// Call calls a method on the remove service
func (c *bufferedClient) Call(ctx context.Context, url string, endpoint string, method string, args []interface{}, reply []interface{}) error {
// Marshall args
b := new(bytes.Buffer)

// If no arguments are set, remove
if len(args) > 0 {
if err := codec.NewEncoder(b, c.handle.handle).Encode(args); err != nil {
return errors.Wrap(err, "could not encode argument")
return NewClientError(errors.Wrap(err, "failed to encode arguments"))
}
}

Expand All @@ -93,28 +93,47 @@ func (c *bufferedClient) Call(ctx context.Context, url string, endpoint string,

request, errRequest := newRequest(ctx, postURL, c.handle.contentType, b, c.headers.Clone())
if errRequest != nil {
return errRequest
return NewClientError(errors.Wrap(errRequest, "failed to create request"))
}

resp, errDo := c.client.Do(request)
if errDo != nil {
return errors.Wrap(errDo, "could not execute request")
return NewClientError(errors.Wrap(errDo, "failed to send request"))
}
defer resp.Body.Close()

// Check status
if resp.StatusCode != http.StatusOK {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
body := "request failed"
if value, err := ioutil.ReadAll(resp.Body); err == nil {
body = string(value)
}
return NewClientError(NewHTTPError(body, resp.StatusCode))
}

wrappedReply := make([]interface{}, len(reply))
for k, v := range reply {
if _, ok := v.(*error); ok {
var e *Error
wrappedReply[k] = e
} else {
wrappedReply[k] = v
}
return fmt.Errorf("[%d] %s", resp.StatusCode, string(body))
}

responseHandle := getHandlerForContentType(resp.Header.Get("Content-Type")).handle
if err := codec.NewDecoder(resp.Body, responseHandle).Decode(reply); err != nil {
return errors.Wrap(err, "could not decode response from client")
if err := codec.NewDecoder(resp.Body, responseHandle).Decode(wrappedReply); err != nil {
return NewClientError(errors.Wrap(err, "failed to decode response"))
}

// replace error
for k, v := range wrappedReply {
if x, ok := v.(*Error); ok && x != nil {
if y, ok := reply[k].(*error); ok {
*y = x
}
}
}

return err
return nil
}
11 changes: 11 additions & 0 deletions clienterror.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package gotsrpc

type ClientError struct {
error
}

func NewClientError(err error) *ClientError {
return &ClientError{
error: err,
}
}
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Target struct {
Out string `yaml:"out"`
GoRPC []string `yaml:"gorpc"`
TSRPC []string `yaml:"tsrpc"`
SkipTSRPCClient bool `yaml:"skipTSRPCClient"`
}

func (t *Target) IsGoRPC(service string) bool {
Expand Down
126 changes: 126 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package gotsrpc

import (
"fmt"
"io"
"reflect"
"strings"

"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
)

type Error struct {
Msg string `json:"m"`
Pkg string `json:"p"`
Type string `json:"t"`
Data interface{} `json:"d,omitempty"`
ErrCause *Error `json:"c,omitempty"`
}

// NewError returns a new instance
func NewError(err error) *Error {
// check if already transformed
if v, ok := err.(*Error); ok {
return v
}

// skip *withStack error type
if _, ok := err.(interface {
StackTrace() errors.StackTrace
}); ok && errors.Unwrap(err) != nil {
err = errors.Unwrap(err)
}

// retrieve error details
errType := reflect.TypeOf(err)

inst := &Error{
Msg: err.Error(),
Type: errType.String(),
Pkg: errType.Elem().PkgPath(),
Data: err,
}

// unwrap error
if unwrappedErr := errors.Unwrap(err); unwrappedErr != nil {
inst.ErrCause = NewError(unwrappedErr)
inst.Msg = strings.TrimSuffix(inst.Msg, ": "+unwrappedErr.Error())
}

return inst
}

// As interface
func (e *Error) As(err interface{}) bool {
if e == nil || err == nil {
return false
}
if reflect.TypeOf(err).Elem().String() == e.Type {
if decodeErr := mapstructure.Decode(e.Data, &err); decodeErr != nil {
fmt.Printf("ERROR: failed to decode error data\n%+v", decodeErr)
return false
} else {
return true
}
}
return false
}

// Cause interface
func (e *Error) Cause() error {
if e.ErrCause != nil {
return e.ErrCause
}
return e
}

// Format interface
func (e *Error) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
if s.Flag('+') {
fmt.Fprintf(s, "%s.(%s)\n", e.Pkg, e.Type)
if e.Data != nil {
fmt.Fprintf(s, "Data: %v\n", e.Data)
}
}
fallthrough
case 's', 'q':
io.WriteString(s, e.Error())
}
}

// Unwrap interface
func (e *Error) Unwrap() error {
if e != nil && e.ErrCause != nil {
return e.ErrCause
}
return nil
}

// Is interface
func (e *Error) Is(err error) bool {
if e == nil || err == nil {
return false
}

errType := reflect.TypeOf(err)

if e.Msg == err.Error() &&
errType.String() == e.Type &&
errType.Elem().PkgPath() == e.Pkg {
return true
}

return false
}

// Error interface
func (e *Error) Error() string {
msg := e.Msg
if e.ErrCause != nil {
msg += ": " + e.ErrCause.Error()
}
return msg
}
2 changes: 1 addition & 1 deletion example/basic/client/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import transport from "./transport.js";
export const init = () => {
const client = new ServiceClient(transport("/service"));

client.string("hello world").then((res) => {
client.boolPtr(true).then((res) => {
console.log(res);
});
};
3 changes: 3 additions & 0 deletions example/basic/client/src/service-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export class ServiceClient {
async bool(v:boolean):Promise<boolean> {
return (await this.transport<{0:boolean}>("Bool", [v]))[0]
}
async boolPtr(v:boolean):Promise<boolean|null> {
return (await this.transport<{0:boolean|null}>("BoolPtr", [v]))[0]
}
async boolSlice(v:Array<boolean>|null):Promise<Array<boolean>|null> {
return (await this.transport<{0:Array<boolean>|null}>("BoolSlice", [v]))[0]
}
Expand Down
Loading

0 comments on commit b1eeafc

Please sign in to comment.