initial support for path param
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
This commit is contained in:
parent
7a29127d17
commit
d611a6aed5
2
go.mod
2
go.mod
@ -2,4 +2,4 @@ module github.com/unistack-org/micro-client-http/v3
|
||||
|
||||
go 1.14
|
||||
|
||||
require github.com/unistack-org/micro/v3 v3.1.1
|
||||
require github.com/unistack-org/micro/v3 v3.1.2
|
||||
|
4
go.sum
4
go.sum
@ -257,8 +257,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
|
||||
github.com/timewasted/linode v0.0.0-20160829202747-37e84520dcf7/go.mod h1:imsgLplxEC/etjIhdr3dNzV3JeT27LbVu5pYWm0JCBY=
|
||||
github.com/transip/gotransip v0.0.0-20190812104329-6d8d9179b66f/go.mod h1:i0f4R4o2HM0m3DZYQWsj6/MEowD57VzoH0v3d7igeFY=
|
||||
github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
|
||||
github.com/unistack-org/micro/v3 v3.1.1 h1:kWL0BVzUBdotjfDbl1qL9lNYmZqvebQWPNCyqrjUSAk=
|
||||
github.com/unistack-org/micro/v3 v3.1.1/go.mod h1:0DgOy4OdJxQCDER8YSKitZugd2+1bddrRSNfeooTHDc=
|
||||
github.com/unistack-org/micro/v3 v3.1.2 h1:NZnO6uhdRmoW/IhbWT1HWRCNWwgKbLlX4XikNx1cMzI=
|
||||
github.com/unistack-org/micro/v3 v3.1.2/go.mod h1:0DgOy4OdJxQCDER8YSKitZugd2+1bddrRSNfeooTHDc=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/vultr/govultr v0.1.4/go.mod h1:9H008Uxr/C4vFNGLqKx232C206GL0PBHzOP0809bGNA=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
|
40
http.go
40
http.go
@ -32,8 +32,9 @@ type httpClient struct {
|
||||
httpcli *http.Client
|
||||
}
|
||||
|
||||
func newRequest(addr string, req client.Request, opts client.CallOptions) (*http.Request, error) {
|
||||
func newRequest(addr string, req client.Request, cf codec.Codec, msg interface{}, opts client.CallOptions) (*http.Request, error) {
|
||||
hreq := &http.Request{Method: http.MethodPost}
|
||||
body := "*" // as like google api http annotation
|
||||
u, err := url.Parse(addr)
|
||||
if err != nil {
|
||||
hreq.URL = &url.URL{
|
||||
@ -51,18 +52,36 @@ func newRequest(addr string, req client.Request, opts client.CallOptions) (*http
|
||||
if p, ok := opts.Context.Value(pathKey{}).(string); ok {
|
||||
ep = p
|
||||
}
|
||||
if b, ok := opts.Context.Value(bodyKey{}).(string); ok {
|
||||
body = b
|
||||
}
|
||||
}
|
||||
|
||||
hreq.URL, err = u.Parse(ep)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errors.BadRequest("go.micro.client", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
path, nmsg, err := newPathRequest(hreq.URL.Path, hreq.Method, body, msg)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequest("go.micro.client", err.Error())
|
||||
}
|
||||
|
||||
hreq.URL.Path = path
|
||||
// marshal request
|
||||
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))
|
||||
|
||||
return hreq, nil
|
||||
}
|
||||
|
||||
func (h *httpClient) call(ctx context.Context, addr string, req client.Request, rsp interface{}, opts client.CallOptions) error {
|
||||
header := make(http.Header)
|
||||
header := make(http.Header, 2)
|
||||
if md, ok := metadata.FromContext(ctx); ok {
|
||||
for k, v := range md {
|
||||
header.Set(k, v)
|
||||
@ -80,20 +99,13 @@ func (h *httpClient) call(ctx context.Context, addr string, req client.Request,
|
||||
return errors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
|
||||
// marshal request
|
||||
b, err := cf.Marshal(req.Body())
|
||||
hreq, err := newRequest(addr, req, cf, req.Body(), opts)
|
||||
if err != nil {
|
||||
return errors.InternalServerError("go.micro.client", err.Error())
|
||||
}
|
||||
|
||||
hreq, err := newRequest(addr, req, opts)
|
||||
if err != nil {
|
||||
return errors.InternalServerError("go.micro.client", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
hreq.Header = header
|
||||
hreq.Body = ioutil.NopCloser(bytes.NewBuffer(b))
|
||||
hreq.ContentLength = int64(len(b))
|
||||
|
||||
// make the request
|
||||
hrsp, err := h.httpcli.Do(hreq.WithContext(ctx))
|
||||
if err != nil {
|
||||
|
30
http_test.go
Normal file
30
http_test.go
Normal file
@ -0,0 +1,30 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
Name string `json:"name"`
|
||||
Field1 string
|
||||
Field2 string
|
||||
Field3 int64
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _ = p, m
|
||||
}
|
||||
|
||||
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)
|
||||
if err == nil {
|
||||
t.Fatalf("path param must not be filled")
|
||||
}
|
||||
_, _ = p, m
|
||||
}
|
79
options.go
79
options.go
@ -1,7 +1,6 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
@ -36,109 +35,63 @@ type maxSendMsgSizeKey struct{}
|
||||
|
||||
// maximum streams on a connectioin
|
||||
func PoolMaxStreams(n int) client.Option {
|
||||
return func(o *client.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, poolMaxStreams{}, n)
|
||||
}
|
||||
return client.SetOption(poolMaxStreams{}, n)
|
||||
}
|
||||
|
||||
// maximum idle conns of a pool
|
||||
func PoolMaxIdle(d int) client.Option {
|
||||
return func(o *client.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, poolMaxIdle{}, d)
|
||||
}
|
||||
return client.SetOption(poolMaxIdle{}, d)
|
||||
}
|
||||
|
||||
// AuthTLS should be used to setup a secure authentication using TLS
|
||||
func AuthTLS(t *tls.Config) client.Option {
|
||||
return func(o *client.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, tlsAuth{}, t)
|
||||
}
|
||||
return client.SetOption(tlsAuth{}, t)
|
||||
}
|
||||
|
||||
//
|
||||
// MaxRecvMsgSize set the maximum size of message that client can receive.
|
||||
//
|
||||
func MaxRecvMsgSize(s int) client.Option {
|
||||
return func(o *client.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, maxRecvMsgSizeKey{}, s)
|
||||
}
|
||||
return client.SetOption(maxRecvMsgSizeKey{}, s)
|
||||
}
|
||||
|
||||
//
|
||||
// MaxSendMsgSize set the maximum size of message that client can send.
|
||||
//
|
||||
func MaxSendMsgSize(s int) client.Option {
|
||||
return func(o *client.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, maxSendMsgSizeKey{}, s)
|
||||
}
|
||||
return client.SetOption(maxSendMsgSizeKey{}, s)
|
||||
}
|
||||
|
||||
type httpClientKey struct{}
|
||||
|
||||
func HTTPClient(c *http.Client) client.Option {
|
||||
return func(o *client.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, httpClientKey{}, c)
|
||||
}
|
||||
return client.SetOption(httpClientKey{}, c)
|
||||
}
|
||||
|
||||
type httpDialerKey struct{}
|
||||
|
||||
func HTTPDialer(d *net.Dialer) client.Option {
|
||||
return func(o *client.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, httpDialerKey{}, d)
|
||||
}
|
||||
return client.SetOption(httpDialerKey{}, d)
|
||||
}
|
||||
|
||||
type methodKey struct{}
|
||||
|
||||
func Method(m string) client.CallOption {
|
||||
return func(o *client.CallOptions) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, methodKey{}, m)
|
||||
}
|
||||
return client.SetCallOption(methodKey{}, m)
|
||||
}
|
||||
|
||||
type pathKey struct{}
|
||||
|
||||
func Path(p string) client.CallOption {
|
||||
return func(o *client.CallOptions) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, pathKey{}, p)
|
||||
}
|
||||
return client.SetCallOption(pathKey{}, p)
|
||||
}
|
||||
|
||||
type bodyKey struct{}
|
||||
|
||||
func Body(b string) client.CallOption {
|
||||
return client.SetCallOption(bodyKey{}, b)
|
||||
}
|
||||
|
||||
type errorMapKey struct{}
|
||||
|
||||
func ErrorMap(m map[string]interface{}) client.CallOption {
|
||||
return func(o *client.CallOptions) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, errorMapKey{}, m)
|
||||
}
|
||||
return client.SetCallOption(errorMapKey{}, m)
|
||||
}
|
||||
|
16
stream.go
16
stream.go
@ -2,10 +2,8 @@ package http
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
@ -65,22 +63,14 @@ func (h *httpStream) Send(msg interface{}) error {
|
||||
return errShutdown
|
||||
}
|
||||
|
||||
b, err := h.codec.Marshal(msg)
|
||||
hreq, err := newRequest(h.address, h.request, h.codec, msg, h.opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(b)
|
||||
req, err := newRequest(h.address, h.request, h.opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hreq.Header = h.header
|
||||
|
||||
req.Header = h.header
|
||||
req.Body = ioutil.NopCloser(buf)
|
||||
req.ContentLength = int64(len(b))
|
||||
|
||||
return req.Write(h.conn)
|
||||
return hreq.Write(h.conn)
|
||||
}
|
||||
|
||||
func (h *httpStream) Recv(msg interface{}) error {
|
||||
|
124
util.go
Normal file
124
util.go
Normal file
@ -0,0 +1,124 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
rutil "github.com/unistack-org/micro/v3/util/reflect"
|
||||
util "github.com/unistack-org/micro/v3/util/router"
|
||||
)
|
||||
|
||||
var (
|
||||
templateCache = make(map[string]util.Template)
|
||||
mu sync.RWMutex
|
||||
)
|
||||
|
||||
func newPathRequest(path string, method string, body string, msg interface{}) (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 {
|
||||
return path, msg, nil
|
||||
}
|
||||
|
||||
fieldsmap := make(map[string]string, len(tpl.Fields))
|
||||
for _, v := range tpl.Fields {
|
||||
fieldsmap[v] = ""
|
||||
}
|
||||
|
||||
nmsg, err := rutil.Zero(msg)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// we cant switch on message and use proto helpers, to avoid dependency to protobuf
|
||||
tmsg := reflect.ValueOf(msg)
|
||||
if tmsg.Kind() == reflect.Ptr {
|
||||
tmsg = tmsg.Elem()
|
||||
}
|
||||
|
||||
tnmsg := reflect.ValueOf(nmsg)
|
||||
if tnmsg.Kind() == reflect.Ptr {
|
||||
tnmsg = tnmsg.Elem()
|
||||
}
|
||||
|
||||
values := make(map[string]string)
|
||||
// copy cycle
|
||||
for i := 0; i < tmsg.NumField(); i++ {
|
||||
val := tmsg.Field(i)
|
||||
if val.IsZero() {
|
||||
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 {
|
||||
tnmsg.Field(i).Set(val)
|
||||
} else if method == http.MethodGet {
|
||||
values[lfield] = 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)
|
||||
}
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
for _, fld := range tpl.Pool {
|
||||
_, _ = b.WriteRune('/')
|
||||
if v, ok := fieldsmap[fld]; ok {
|
||||
_, _ = b.WriteString(v)
|
||||
} else {
|
||||
_, _ = b.WriteString(fld)
|
||||
}
|
||||
}
|
||||
|
||||
if method == http.MethodGet {
|
||||
idx := 0
|
||||
for k, v := range values {
|
||||
if idx == 0 {
|
||||
_, _ = b.WriteRune('?')
|
||||
} else {
|
||||
_, _ = b.WriteRune('&')
|
||||
}
|
||||
_, _ = b.WriteString(k)
|
||||
_, _ = b.WriteRune('=')
|
||||
_, _ = b.WriteString(v)
|
||||
idx++
|
||||
}
|
||||
}
|
||||
|
||||
return b.String(), nmsg, nil
|
||||
}
|
||||
|
||||
func newTemplate(path string) (util.Template, error) {
|
||||
mu.RLock()
|
||||
tpl, ok := templateCache[path]
|
||||
if ok {
|
||||
mu.RUnlock()
|
||||
return tpl, nil
|
||||
}
|
||||
mu.RUnlock()
|
||||
|
||||
rule, err := util.Parse(path)
|
||||
if err != nil {
|
||||
return tpl, err
|
||||
}
|
||||
|
||||
tpl = rule.Compile()
|
||||
mu.Lock()
|
||||
templateCache[path] = tpl
|
||||
mu.Unlock()
|
||||
|
||||
return tpl, nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user