Compare commits

...

4 Commits

Author SHA1 Message Date
1787f44c54 cleanup
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-03-24 15:49:20 +03:00
1bbdf03f60 add aditional check for nil message passed
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-03-24 10:44:53 +03:00
Renovate Bot
2bb1299e56 Update module github.com/unistack-org/micro/v3 to v3.2.26 2021-03-23 17:35:57 +00:00
fd1acd56cb add tag support
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
2021-03-23 00:29:27 +03:00
8 changed files with 210 additions and 67 deletions

2
go.mod
View File

@@ -2,4 +2,4 @@ module github.com/unistack-org/micro-client-http/v3
go 1.16
require github.com/unistack-org/micro/v3 v3.2.24
require github.com/unistack-org/micro/v3 v3.3.0

4
go.sum
View File

@@ -5,8 +5,8 @@ github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/silas/dag v0.0.0-20210121180416-41cf55125c34/go.mod h1:7RTUFBdIRC9nZ7/3RyRNH1bdqIShrDejd1YbLwgPS+I=
github.com/unistack-org/micro/v3 v3.2.24 h1:3sQ72wy8Vap9KDD7bdLOn+dhlPT0iXSJRXhs2qHoKxA=
github.com/unistack-org/micro/v3 v3.2.24/go.mod h1:iJwCWq2PECMxigfqe6TPC5GLWvj6P94Kk+PTVZGL3w8=
github.com/unistack-org/micro/v3 v3.3.0 h1:pEj/8QVFzMlNMEL//q/Te8qgG+XI6LTYIQrb6hMymgk=
github.com/unistack-org/micro/v3 v3.3.0/go.mod h1:iJwCWq2PECMxigfqe6TPC5GLWvj6P94Kk+PTVZGL3w8=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

108
http.go
View File

@@ -34,11 +34,15 @@ type httpClient struct {
opts client.Options
dialer *net.Dialer
httpcli *http.Client
init bool
}
func newRequest(addr string, req client.Request, cf codec.Codec, msg interface{}, opts client.CallOptions) (*http.Request, error) {
func newRequest(addr string, req client.Request, ct string, cf codec.Codec, msg interface{}, opts client.CallOptions) (*http.Request, error) {
hreq := &http.Request{Method: http.MethodPost}
body := "*" // as like google api http annotation
var tags []string
var scheme string
u, err := url.Parse(addr)
if err != nil {
hreq.URL = &url.URL{
@@ -47,6 +51,7 @@ func newRequest(addr string, req client.Request, cf codec.Codec, msg interface{}
Path: req.Endpoint(),
}
hreq.Host = addr
scheme = "http"
} else {
ep := req.Endpoint()
if opts.Context != nil {
@@ -59,6 +64,10 @@ func newRequest(addr string, req client.Request, cf codec.Codec, msg interface{}
if b, ok := opts.Context.Value(bodyKey{}).(string); ok {
body = b
}
if t, ok := opts.Context.Value(structTagsKey{}).([]string); ok && len(t) > 0 {
tags = t
}
}
hreq.URL, err = u.Parse(ep)
if err != nil {
@@ -66,29 +75,40 @@ func newRequest(addr string, req client.Request, cf codec.Codec, msg interface{}
}
}
path, nmsg, err := newPathRequest(hreq.URL.Path, hreq.Method, body, msg)
if len(tags) == 0 {
switch ct {
default:
tags = append(tags, "json", "protobuf")
case "text/xml":
tags = append(tags, "xml")
}
}
path, nmsg, err := newPathRequest(hreq.URL.Path, hreq.Method, body, msg, tags)
if err != nil {
return nil, errors.BadRequest("go.micro.client", err.Error())
}
hreq.URL, err = url.Parse(addr + path)
if scheme != "" {
hreq.URL, err = url.Parse(scheme + "://" + addr + path)
} else {
hreq.URL, err = url.Parse(addr + path)
}
if err != nil {
return nil, errors.BadRequest("go.micro.client", err.Error())
}
var b []byte
// marshal request is struct not empty
if nmsg != nil {
var b []byte
b, err = cf.Marshal(nmsg)
if err != nil {
return nil, errors.BadRequest("go.micro.client", err.Error())
}
hreq.Body = ioutil.NopCloser(bytes.NewBuffer(b))
hreq.ContentLength = int64(len(b))
}
hreq.Body = ioutil.NopCloser(bytes.NewBuffer(b))
hreq.ContentLength = int64(len(b))
return hreq, nil
}
@@ -100,18 +120,20 @@ func (h *httpClient) call(ctx context.Context, addr string, req client.Request,
}
}
ct := req.ContentType()
// set timeout in nanoseconds
header.Set("Timeout", fmt.Sprintf("%d", opts.RequestTimeout))
// set the content type for the request
header.Set("Content-Type", req.ContentType())
header.Set("Content-Type", ct)
// get codec
cf, err := h.newCodec(req.ContentType())
cf, err := h.newCodec(ct)
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
hreq, err := newRequest(addr, req, cf, req.Body(), opts)
hreq, err := newRequest(addr, req, ct, cf, req.Body(), opts)
if err != nil {
return err
}
@@ -151,10 +173,11 @@ func (h *httpClient) stream(ctx context.Context, addr string, req client.Request
header = make(http.Header, 2)
}
ct := req.ContentType()
// set timeout in nanoseconds
header.Set("Timeout", fmt.Sprintf("%d", opts.RequestTimeout))
// set the content type for the request
header.Set("Content-Type", req.ContentType())
header.Set("Content-Type", ct)
// get codec
cf, err := h.newCodec(req.ContentType())
@@ -178,7 +201,8 @@ func (h *httpClient) stream(ctx context.Context, addr string, req client.Request
closed: make(chan bool),
opts: opts,
conn: cc,
codec: cf,
ct: ct,
cf: cf,
header: header,
reader: bufio.NewReader(cc),
request: req,
@@ -194,9 +218,32 @@ func (h *httpClient) newCodec(ct string) (codec.Codec, error) {
}
func (h *httpClient) Init(opts ...client.Option) error {
if len(opts) == 0 && h.init {
return nil
}
for _, o := range opts {
o(&h.opts)
}
if err := h.opts.Broker.Init(); err != nil {
return err
}
if err := h.opts.Tracer.Init(); err != nil {
return err
}
if err := h.opts.Router.Init(); err != nil {
return err
}
if err := h.opts.Logger.Init(); err != nil {
return err
}
if err := h.opts.Meter.Init(); err != nil {
return err
}
if err := h.opts.Transport.Init(); err != nil {
return err
}
return nil
}
@@ -560,38 +607,3 @@ func NewClient(opts ...client.Option) client.Client {
return c
}
func parseRsp(ctx context.Context, hrsp *http.Response, cf codec.Codec, rsp interface{}, opts client.CallOptions) error {
b, err := ioutil.ReadAll(hrsp.Body)
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
if hrsp.StatusCode < 400 {
// unmarshal
if err := cf.Unmarshal(b, rsp); err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
return nil
}
errmap, ok := opts.Context.Value(errorMapKey{}).(map[string]interface{})
if !ok || errmap == nil {
// user not provide map of errors
// id: req.Service() ??
return errors.New("go.micro.client", string(b), int32(hrsp.StatusCode))
}
if err, ok = errmap[fmt.Sprintf("%d", hrsp.StatusCode)].(error); !ok {
err, ok = errmap["default"].(error)
}
if !ok {
return errors.New("go.micro.client", string(b), int32(hrsp.StatusCode))
}
if cerr := cf.Unmarshal(b, err); cerr != nil {
return errors.InternalServerError("go.micro.client", cerr.Error())
}
return err
}

View File

@@ -15,7 +15,7 @@ type Request struct {
func TestValidPath(t *testing.T) {
req := &Request{Name: "vtolstov", Field1: "field1", Field2: "field2", Field3: 10}
p, m, err := newPathRequest("/api/v1/{name}/list", "GET", "", req)
p, m, err := newPathRequest("/api/v1/{name}/list", "GET", "", req, nil)
if err != nil {
t.Fatal(err)
}
@@ -32,7 +32,7 @@ func TestValidPath(t *testing.T) {
func TestInvalidPath(t *testing.T) {
req := &Request{Name: "vtolstov", Field1: "field1", Field2: "field2", Field3: 10}
p, m, err := newPathRequest("/api/v1/{xname}/list", "GET", "", req)
p, m, err := newPathRequest("/api/v1/{xname}/list", "GET", "", req, nil)
if err == nil {
t.Fatalf("path param must not be filled")
}

View File

@@ -95,3 +95,9 @@ type errorMapKey struct{}
func ErrorMap(m map[string]interface{}) client.CallOption {
return client.SetCallOption(errorMapKey{}, m)
}
type structTagsKey struct{}
func StructTags(tags []string) client.CallOption {
return client.SetCallOption(structTagsKey{}, tags)
}

View File

@@ -18,7 +18,8 @@ type httpStream struct {
sync.RWMutex
address string
opts client.CallOptions
codec codec.Codec
ct string
cf codec.Codec
context context.Context
header http.Header
seq uint64
@@ -63,7 +64,7 @@ func (h *httpStream) Send(msg interface{}) error {
return errShutdown
}
hreq, err := newRequest(h.address, h.request, h.codec, msg, h.opts)
hreq, err := newRequest(h.address, h.request, h.ct, h.cf, msg, h.opts)
if err != nil {
return err
}
@@ -88,7 +89,7 @@ func (h *httpStream) Recv(msg interface{}) error {
}
defer hrsp.Body.Close()
return parseRsp(h.context, hrsp, h.codec, msg, h.opts)
return parseRsp(h.context, hrsp, h.cf, msg, h.opts)
}
func (h *httpStream) Error() error {

89
util.go
View File

@@ -1,12 +1,17 @@
package http
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"reflect"
"strings"
"sync"
"github.com/unistack-org/micro/v3/client"
"github.com/unistack-org/micro/v3/codec"
"github.com/unistack-org/micro/v3/errors"
rutil "github.com/unistack-org/micro/v3/util/reflect"
util "github.com/unistack-org/micro/v3/util/router"
)
@@ -16,13 +21,17 @@ var (
mu sync.RWMutex
)
func newPathRequest(path string, method string, body string, msg interface{}) (string, interface{}, error) {
func newPathRequest(path string, method string, body string, msg interface{}, tags []string) (string, interface{}, error) {
// parse via https://github.com/googleapis/googleapis/blob/master/google/api/http.proto definition
tpl, err := newTemplate(path)
if err != nil {
return "", nil, err
}
if len(tpl.Fields) > 0 && msg == nil {
return "", nil, fmt.Errorf("nil message but path params requested: %v", path)
}
fieldsmap := make(map[string]string, len(tpl.Fields))
for _, v := range tpl.Fields {
fieldsmap[v] = ""
@@ -52,20 +61,45 @@ func newPathRequest(path string, method string, body string, msg interface{}) (s
continue
}
fld := tmsg.Type().Field(i)
lfield := strings.ToLower(fld.Name)
if _, ok := fieldsmap[lfield]; ok {
fieldsmap[lfield] = fmt.Sprintf("%v", val.Interface())
} else if (body == "*" || body == lfield) && method != http.MethodGet {
t := &tag{}
for _, tn := range tags {
ts, ok := fld.Tag.Lookup(tn)
if !ok {
continue
}
tp := strings.Split(ts, ",")
// special
switch tn {
case "protobuf": // special
t = &tag{key: tn, name: tp[3][5:], opts: append(tp[:3], tp[4:]...)}
default:
t = &tag{key: tn, name: tp[0], opts: tp[1:]}
}
if t.name != "" {
break
}
}
if t.name == "" {
// fallback to lowercase
t.name = strings.ToLower(fld.Name)
}
if _, ok := fieldsmap[t.name]; ok {
fieldsmap[t.name] = fmt.Sprintf("%v", val.Interface())
} else if (body == "*" || body == t.name) && method != http.MethodGet {
tnmsg.Field(i).Set(val)
} else {
values[lfield] = fmt.Sprintf("%v", val.Interface())
values[t.name] = fmt.Sprintf("%v", val.Interface())
}
}
// check not filled stuff
for k, v := range fieldsmap {
if v == "" {
return "", nil, fmt.Errorf("path param %s not filled %s", k, v)
return "", nil, fmt.Errorf("path param %s not filled", k)
}
}
@@ -120,3 +154,44 @@ func newTemplate(path string) (util.Template, error) {
return tpl, nil
}
func parseRsp(ctx context.Context, hrsp *http.Response, cf codec.Codec, rsp interface{}, opts client.CallOptions) error {
b, err := ioutil.ReadAll(hrsp.Body)
if err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
if hrsp.StatusCode < 400 {
// unmarshal
if err := cf.Unmarshal(b, rsp); err != nil {
return errors.InternalServerError("go.micro.client", err.Error())
}
return nil
}
errmap, ok := opts.Context.Value(errorMapKey{}).(map[string]interface{})
if !ok || errmap == nil {
// user not provide map of errors
// id: req.Service() ??
return errors.New("go.micro.client", string(b), int32(hrsp.StatusCode))
}
if err, ok = errmap[fmt.Sprintf("%d", hrsp.StatusCode)].(error); !ok {
err, ok = errmap["default"].(error)
}
if !ok {
return errors.New("go.micro.client", string(b), int32(hrsp.StatusCode))
}
if cerr := cf.Unmarshal(b, err); cerr != nil {
return errors.InternalServerError("go.micro.client", cerr.Error())
}
return err
}
type tag struct {
key string
name string
opts []string
}

View File

@@ -5,10 +5,19 @@ import (
"testing"
)
func TestTemplate(t *testing.T) {
tpl, err := newTemplate("/v1/{ClientID}/list")
if err != nil {
t.Fatal(err)
}
_ = tpl
// fmt.Printf("%#+v\n", tpl.Pool)
}
func TestNewPathRequest(t *testing.T) {
type Message struct {
Name string
Val1 string
Name string `json:"name"`
Val1 string `protobuf:"bytes,1,opt,name=val1,proto3" json:"val1"`
Val2 int64
Val3 []string
}
@@ -16,7 +25,8 @@ func TestNewPathRequest(t *testing.T) {
omsg := &Message{Name: "test_name", Val1: "test_val1", Val2: 100, Val3: []string{"slice"}}
for _, m := range []string{"POST", "PUT", "PATCH", "GET", "DELETE"} {
path, nmsg, err := newPathRequest("/v1/test", m, "", omsg)
body := ""
path, nmsg, err := newPathRequest("/v1/test", m, body, omsg, []string{"protobuf", "json"})
if err != nil {
t.Fatal(err)
}
@@ -26,7 +36,46 @@ func TestNewPathRequest(t *testing.T) {
}
vals := u.Query()
if v, ok := vals["name"]; !ok || v[0] != "test_name" {
t.Fatalf("invlid path: %v nmsg: %v", path, nmsg)
t.Fatalf("invalid path: %v nmsg: %v", path, nmsg)
}
}
}
func TestNewPathVarRequest(t *testing.T) {
type Message struct {
Name string `json:"name"`
Val1 string `protobuf:"bytes,1,opt,name=val1,proto3" json:"val1"`
Val2 int64
Val3 []string
}
omsg := &Message{Name: "test_name", Val1: "test_val1", Val2: 100, Val3: []string{"slice"}}
for _, m := range []string{"POST", "PUT", "PATCH", "GET", "DELETE"} {
body := ""
if m != "GET" {
body = "*"
}
path, nmsg, err := newPathRequest("/v1/test/{val1}", m, body, omsg, []string{"protobuf", "json"})
if err != nil {
t.Fatal(err)
}
u, err := url.Parse(path)
if err != nil {
t.Fatal(err)
}
if m != "GET" {
if _, ok := nmsg.(*Message); !ok {
t.Fatalf("invalid nmsg: %#+v\n", nmsg)
}
if nmsg.(*Message).Name != "test_name" {
t.Fatalf("invalid nmsg: %v nmsg: %v", path, nmsg)
}
} else {
vals := u.Query()
if v, ok := vals["val2"]; !ok || v[0] != "100" {
t.Fatalf("invalid path: %v nmsg: %v", path, nmsg)
}
}
}
}