Skip to content

Commit b1eeafc

Browse files
committed
feat: support union types
1 parent 40c94da commit b1eeafc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+3253
-185
lines changed

.golangci.yaml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Minimum golangci-lint version required: v1.42.0
2+
run:
3+
timeout: 3m
4+
skip-dirs:
5+
- tmp
6+
7+
linters-settings:
8+
gci:
9+
local-prefixes: github.com/foomo/keel
10+
revive:
11+
rules:
12+
- name: indent-error-flow
13+
disabled: true
14+
gocritic:
15+
enabled-tags:
16+
- diagnostic
17+
- performance
18+
- style
19+
disabled-tags:
20+
- experimental
21+
- opinionated
22+
disabled-checks:
23+
- ifElseChain
24+
settings:
25+
hugeParam:
26+
sizeThreshold: 512
27+
28+
linters:
29+
enable:
30+
- bodyclose
31+
- exhaustive
32+
- dogsled
33+
- dupl
34+
- exportloopref
35+
- goconst
36+
- gocritic
37+
- gocyclo
38+
- gofmt
39+
- goprintffuncname
40+
- gosec
41+
- ifshort
42+
- misspell
43+
- nakedret
44+
- noctx
45+
- nolintlint
46+
- prealloc
47+
- revive
48+
- promlinter
49+
- rowserrcheck
50+
- sqlclosecheck
51+
- stylecheck
52+
- thelper
53+
- tparallel
54+
- unconvert
55+
- unparam
56+
- whitespace

Makefile

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,58 @@ build.debug:
2929
go build -gcflags "all=-N -l" -o bin/gotsrpc cmd/gotsrpc/gotsrpc.go
3030

3131

32-
EXAMPLES=basic nullable
32+
## === Tools ===
33+
34+
EXAMPLES=basic errors nullable multi
3335
define examples
3436
.PHONY: example.$(1)
3537
example.$(1):
3638
cd example/${1} && go run ../../cmd/gotsrpc/gotsrpc.go gotsrpc.yml
39+
cd example/${1}/client && tsc --build
3740

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

4345
.PHONY: example.$(1).debug
4446
example.$(1).debug: build.debug
4547
cd example/${1} && dlv --listen=:2345 --headless=true --api-version=2 --accept-multiclient exec ../../bin/gotsrpc gotsrpc.yml
48+
49+
.PHONY: example.$(1).lint
50+
example.$(1).lint:
51+
cd example/${1} && golangci-lint run
4652
endef
4753
$(foreach p,$(EXAMPLES),$(eval $(call examples,$(p))))
4854

55+
## Run go mod tidy recursive
56+
.PHONY: lint
57+
lint:
58+
# @golangci-lint run
59+
@for name in example/*/; do\
60+
echo "-------- $${name} ------------";\
61+
sh -c "cd $$(pwd)/$${name} && golangci-lint run";\
62+
done
63+
64+
## Run go mod tidy recursive
65+
.PHONY: gomod
66+
gomod:
67+
@go mod tidy
68+
@for name in example/*/; do\
69+
echo "-------- $${name} ------------";\
70+
sh -c "cd $$(pwd)/$${name} && go mod tidy";\
71+
done
72+
73+
## === Examples ===
74+
75+
.PHONY: examples
76+
## Build examples
77+
examples:
78+
@for name in example/*/; do\
79+
echo "-------- $${name} ------------";\
80+
$(MAKE) example.`basename $${name}`;\
81+
done
82+
.PHONY: examples
83+
4984
## === Utils ===
5085

5186
## Show help text

build.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,14 @@ func Build(conf *config.Config, goPath string) {
132132
os.Exit(2)
133133
}
134134

135+
// collect all union structs
136+
unions := map[string][]string{}
137+
for _, s := range structs {
138+
if len(s.Fields) == 0 && len(s.UnionFields) > 0 {
139+
unions[s.Package] = append(unions[s.Package], s.Name)
140+
}
141+
}
142+
135143
if target.Out != "" {
136144

137145
ts, err := RenderTypeScriptServices(services, conf.Mappings, scalars, structs, target)
@@ -192,13 +200,14 @@ func Build(conf *config.Config, goPath string) {
192200
}
193201
}
194202
if len(target.TSRPC) > 0 {
195-
goTSRPCProxiesCode, goerr := RenderGoTSRPCProxies(services, packageName, pkgName, target)
203+
goTSRPCProxiesCode, goerr := RenderGoTSRPCProxies(services, packageName, pkgName, target, unions)
196204
if goerr != nil {
197205
fmt.Fprintln(os.Stderr, " could not generate go ts rpc proxies code in target", name, goerr)
198206
os.Exit(4)
199207
}
200208
formatAndWrite(goTSRPCProxiesCode, goTSRPCProxiesFilename)
201-
209+
}
210+
if len(target.TSRPC) > 0 && !target.SkipTSRPCClient {
202211
goTSRPCClientsCode, goerr := RenderGoTSRPCClients(services, packageName, pkgName, target)
203212
if goerr != nil {
204213
fmt.Fprintln(os.Stderr, " could not generate go ts rpc clients code in target", name, goerr)

client.go

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,15 @@ func (c *bufferedClient) SetTransportHttpClient(client *http.Client) {
7575
c.client = client
7676
}
7777

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

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

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

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

9999
resp, errDo := c.client.Do(request)
100100
if errDo != nil {
101-
return errors.Wrap(errDo, "could not execute request")
101+
return NewClientError(errors.Wrap(errDo, "failed to send request"))
102102
}
103103
defer resp.Body.Close()
104104

105105
// Check status
106106
if resp.StatusCode != http.StatusOK {
107-
body, err := ioutil.ReadAll(resp.Body)
108-
if err != nil {
109-
return err
107+
body := "request failed"
108+
if value, err := ioutil.ReadAll(resp.Body); err == nil {
109+
body = string(value)
110+
}
111+
return NewClientError(NewHTTPError(body, resp.StatusCode))
112+
}
113+
114+
wrappedReply := make([]interface{}, len(reply))
115+
for k, v := range reply {
116+
if _, ok := v.(*error); ok {
117+
var e *Error
118+
wrappedReply[k] = e
119+
} else {
120+
wrappedReply[k] = v
110121
}
111-
return fmt.Errorf("[%d] %s", resp.StatusCode, string(body))
112122
}
113123

114124
responseHandle := getHandlerForContentType(resp.Header.Get("Content-Type")).handle
115-
if err := codec.NewDecoder(resp.Body, responseHandle).Decode(reply); err != nil {
116-
return errors.Wrap(err, "could not decode response from client")
125+
if err := codec.NewDecoder(resp.Body, responseHandle).Decode(wrappedReply); err != nil {
126+
return NewClientError(errors.Wrap(err, "failed to decode response"))
127+
}
128+
129+
// replace error
130+
for k, v := range wrappedReply {
131+
if x, ok := v.(*Error); ok && x != nil {
132+
if y, ok := reply[k].(*error); ok {
133+
*y = x
134+
}
135+
}
117136
}
118137

119-
return err
138+
return nil
120139
}

clienterror.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package gotsrpc
2+
3+
type ClientError struct {
4+
error
5+
}
6+
7+
func NewClientError(err error) *ClientError {
8+
return &ClientError{
9+
error: err,
10+
}
11+
}

config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type Target struct {
1818
Out string `yaml:"out"`
1919
GoRPC []string `yaml:"gorpc"`
2020
TSRPC []string `yaml:"tsrpc"`
21+
SkipTSRPCClient bool `yaml:"skipTSRPCClient"`
2122
}
2223

2324
func (t *Target) IsGoRPC(service string) bool {

error.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package gotsrpc
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"reflect"
7+
"strings"
8+
9+
"github.com/mitchellh/mapstructure"
10+
"github.com/pkg/errors"
11+
)
12+
13+
type Error struct {
14+
Msg string `json:"m"`
15+
Pkg string `json:"p"`
16+
Type string `json:"t"`
17+
Data interface{} `json:"d,omitempty"`
18+
ErrCause *Error `json:"c,omitempty"`
19+
}
20+
21+
// NewError returns a new instance
22+
func NewError(err error) *Error {
23+
// check if already transformed
24+
if v, ok := err.(*Error); ok {
25+
return v
26+
}
27+
28+
// skip *withStack error type
29+
if _, ok := err.(interface {
30+
StackTrace() errors.StackTrace
31+
}); ok && errors.Unwrap(err) != nil {
32+
err = errors.Unwrap(err)
33+
}
34+
35+
// retrieve error details
36+
errType := reflect.TypeOf(err)
37+
38+
inst := &Error{
39+
Msg: err.Error(),
40+
Type: errType.String(),
41+
Pkg: errType.Elem().PkgPath(),
42+
Data: err,
43+
}
44+
45+
// unwrap error
46+
if unwrappedErr := errors.Unwrap(err); unwrappedErr != nil {
47+
inst.ErrCause = NewError(unwrappedErr)
48+
inst.Msg = strings.TrimSuffix(inst.Msg, ": "+unwrappedErr.Error())
49+
}
50+
51+
return inst
52+
}
53+
54+
// As interface
55+
func (e *Error) As(err interface{}) bool {
56+
if e == nil || err == nil {
57+
return false
58+
}
59+
if reflect.TypeOf(err).Elem().String() == e.Type {
60+
if decodeErr := mapstructure.Decode(e.Data, &err); decodeErr != nil {
61+
fmt.Printf("ERROR: failed to decode error data\n%+v", decodeErr)
62+
return false
63+
} else {
64+
return true
65+
}
66+
}
67+
return false
68+
}
69+
70+
// Cause interface
71+
func (e *Error) Cause() error {
72+
if e.ErrCause != nil {
73+
return e.ErrCause
74+
}
75+
return e
76+
}
77+
78+
// Format interface
79+
func (e *Error) Format(s fmt.State, verb rune) {
80+
switch verb {
81+
case 'v':
82+
if s.Flag('+') {
83+
fmt.Fprintf(s, "%s.(%s)\n", e.Pkg, e.Type)
84+
if e.Data != nil {
85+
fmt.Fprintf(s, "Data: %v\n", e.Data)
86+
}
87+
}
88+
fallthrough
89+
case 's', 'q':
90+
io.WriteString(s, e.Error())
91+
}
92+
}
93+
94+
// Unwrap interface
95+
func (e *Error) Unwrap() error {
96+
if e != nil && e.ErrCause != nil {
97+
return e.ErrCause
98+
}
99+
return nil
100+
}
101+
102+
// Is interface
103+
func (e *Error) Is(err error) bool {
104+
if e == nil || err == nil {
105+
return false
106+
}
107+
108+
errType := reflect.TypeOf(err)
109+
110+
if e.Msg == err.Error() &&
111+
errType.String() == e.Type &&
112+
errType.Elem().PkgPath() == e.Pkg {
113+
return true
114+
}
115+
116+
return false
117+
}
118+
119+
// Error interface
120+
func (e *Error) Error() string {
121+
msg := e.Msg
122+
if e.ErrCause != nil {
123+
msg += ": " + e.ErrCause.Error()
124+
}
125+
return msg
126+
}

example/basic/client/src/app.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import transport from "./transport.js";
55
export const init = () => {
66
const client = new ServiceClient(transport("/service"));
77

8-
client.string("hello world").then((res) => {
8+
client.boolPtr(true).then((res) => {
99
console.log(res);
1010
});
1111
};

example/basic/client/src/service-client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ export class ServiceClient {
1010
async bool(v:boolean):Promise<boolean> {
1111
return (await this.transport<{0:boolean}>("Bool", [v]))[0]
1212
}
13+
async boolPtr(v:boolean):Promise<boolean|null> {
14+
return (await this.transport<{0:boolean|null}>("BoolPtr", [v]))[0]
15+
}
1316
async boolSlice(v:Array<boolean>|null):Promise<Array<boolean>|null> {
1417
return (await this.transport<{0:Array<boolean>|null}>("BoolSlice", [v]))[0]
1518
}

0 commit comments

Comments
 (0)