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
|
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/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/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/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.2 h1:NZnO6uhdRmoW/IhbWT1HWRCNWwgKbLlX4XikNx1cMzI=
|
||||||
github.com/unistack-org/micro/v3 v3.1.1/go.mod h1:0DgOy4OdJxQCDER8YSKitZugd2+1bddrRSNfeooTHDc=
|
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/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||||
github.com/vultr/govultr v0.1.4/go.mod h1:9H008Uxr/C4vFNGLqKx232C206GL0PBHzOP0809bGNA=
|
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=
|
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
|
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}
|
hreq := &http.Request{Method: http.MethodPost}
|
||||||
|
body := "*" // as like google api http annotation
|
||||||
u, err := url.Parse(addr)
|
u, err := url.Parse(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
hreq.URL = &url.URL{
|
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 {
|
if p, ok := opts.Context.Value(pathKey{}).(string); ok {
|
||||||
ep = p
|
ep = p
|
||||||
}
|
}
|
||||||
|
if b, ok := opts.Context.Value(bodyKey{}).(string); ok {
|
||||||
|
body = b
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hreq.URL, err = u.Parse(ep)
|
hreq.URL, err = u.Parse(ep)
|
||||||
if err != nil {
|
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
|
return hreq, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *httpClient) call(ctx context.Context, addr string, req client.Request, rsp interface{}, opts client.CallOptions) error {
|
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 {
|
if md, ok := metadata.FromContext(ctx); ok {
|
||||||
for k, v := range md {
|
for k, v := range md {
|
||||||
header.Set(k, v)
|
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())
|
return errors.InternalServerError("go.micro.client", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// marshal request
|
hreq, err := newRequest(addr, req, cf, req.Body(), opts)
|
||||||
b, err := cf.Marshal(req.Body())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.InternalServerError("go.micro.client", err.Error())
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
hreq, err := newRequest(addr, req, opts)
|
|
||||||
if err != nil {
|
|
||||||
return errors.InternalServerError("go.micro.client", err.Error())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hreq.Header = header
|
hreq.Header = header
|
||||||
hreq.Body = ioutil.NopCloser(bytes.NewBuffer(b))
|
|
||||||
hreq.ContentLength = int64(len(b))
|
|
||||||
// make the request
|
// make the request
|
||||||
hrsp, err := h.httpcli.Do(hreq.WithContext(ctx))
|
hrsp, err := h.httpcli.Do(hreq.WithContext(ctx))
|
||||||
if err != nil {
|
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
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -36,109 +35,63 @@ type maxSendMsgSizeKey struct{}
|
|||||||
|
|
||||||
// maximum streams on a connectioin
|
// maximum streams on a connectioin
|
||||||
func PoolMaxStreams(n int) client.Option {
|
func PoolMaxStreams(n int) client.Option {
|
||||||
return func(o *client.Options) {
|
return client.SetOption(poolMaxStreams{}, n)
|
||||||
if o.Context == nil {
|
|
||||||
o.Context = context.Background()
|
|
||||||
}
|
|
||||||
o.Context = context.WithValue(o.Context, poolMaxStreams{}, n)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// maximum idle conns of a pool
|
// maximum idle conns of a pool
|
||||||
func PoolMaxIdle(d int) client.Option {
|
func PoolMaxIdle(d int) client.Option {
|
||||||
return func(o *client.Options) {
|
return client.SetOption(poolMaxIdle{}, d)
|
||||||
if o.Context == nil {
|
|
||||||
o.Context = context.Background()
|
|
||||||
}
|
|
||||||
o.Context = context.WithValue(o.Context, poolMaxIdle{}, d)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthTLS should be used to setup a secure authentication using TLS
|
// AuthTLS should be used to setup a secure authentication using TLS
|
||||||
func AuthTLS(t *tls.Config) client.Option {
|
func AuthTLS(t *tls.Config) client.Option {
|
||||||
return func(o *client.Options) {
|
return client.SetOption(tlsAuth{}, t)
|
||||||
if o.Context == nil {
|
|
||||||
o.Context = context.Background()
|
|
||||||
}
|
|
||||||
o.Context = context.WithValue(o.Context, tlsAuth{}, t)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// MaxRecvMsgSize set the maximum size of message that client can receive.
|
// MaxRecvMsgSize set the maximum size of message that client can receive.
|
||||||
//
|
|
||||||
func MaxRecvMsgSize(s int) client.Option {
|
func MaxRecvMsgSize(s int) client.Option {
|
||||||
return func(o *client.Options) {
|
return client.SetOption(maxRecvMsgSizeKey{}, s)
|
||||||
if o.Context == nil {
|
|
||||||
o.Context = context.Background()
|
|
||||||
}
|
|
||||||
o.Context = context.WithValue(o.Context, maxRecvMsgSizeKey{}, s)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// MaxSendMsgSize set the maximum size of message that client can send.
|
// MaxSendMsgSize set the maximum size of message that client can send.
|
||||||
//
|
|
||||||
func MaxSendMsgSize(s int) client.Option {
|
func MaxSendMsgSize(s int) client.Option {
|
||||||
return func(o *client.Options) {
|
return client.SetOption(maxSendMsgSizeKey{}, s)
|
||||||
if o.Context == nil {
|
|
||||||
o.Context = context.Background()
|
|
||||||
}
|
|
||||||
o.Context = context.WithValue(o.Context, maxSendMsgSizeKey{}, s)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type httpClientKey struct{}
|
type httpClientKey struct{}
|
||||||
|
|
||||||
func HTTPClient(c *http.Client) client.Option {
|
func HTTPClient(c *http.Client) client.Option {
|
||||||
return func(o *client.Options) {
|
return client.SetOption(httpClientKey{}, c)
|
||||||
if o.Context == nil {
|
|
||||||
o.Context = context.Background()
|
|
||||||
}
|
|
||||||
o.Context = context.WithValue(o.Context, httpClientKey{}, c)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type httpDialerKey struct{}
|
type httpDialerKey struct{}
|
||||||
|
|
||||||
func HTTPDialer(d *net.Dialer) client.Option {
|
func HTTPDialer(d *net.Dialer) client.Option {
|
||||||
return func(o *client.Options) {
|
return client.SetOption(httpDialerKey{}, d)
|
||||||
if o.Context == nil {
|
|
||||||
o.Context = context.Background()
|
|
||||||
}
|
|
||||||
o.Context = context.WithValue(o.Context, httpDialerKey{}, d)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type methodKey struct{}
|
type methodKey struct{}
|
||||||
|
|
||||||
func Method(m string) client.CallOption {
|
func Method(m string) client.CallOption {
|
||||||
return func(o *client.CallOptions) {
|
return client.SetCallOption(methodKey{}, m)
|
||||||
if o.Context == nil {
|
|
||||||
o.Context = context.Background()
|
|
||||||
}
|
|
||||||
o.Context = context.WithValue(o.Context, methodKey{}, m)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type pathKey struct{}
|
type pathKey struct{}
|
||||||
|
|
||||||
func Path(p string) client.CallOption {
|
func Path(p string) client.CallOption {
|
||||||
return func(o *client.CallOptions) {
|
return client.SetCallOption(pathKey{}, p)
|
||||||
if o.Context == nil {
|
}
|
||||||
o.Context = context.Background()
|
|
||||||
}
|
type bodyKey struct{}
|
||||||
o.Context = context.WithValue(o.Context, pathKey{}, p)
|
|
||||||
}
|
func Body(b string) client.CallOption {
|
||||||
|
return client.SetCallOption(bodyKey{}, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
type errorMapKey struct{}
|
type errorMapKey struct{}
|
||||||
|
|
||||||
func ErrorMap(m map[string]interface{}) client.CallOption {
|
func ErrorMap(m map[string]interface{}) client.CallOption {
|
||||||
return func(o *client.CallOptions) {
|
return client.SetCallOption(errorMapKey{}, m)
|
||||||
if o.Context == nil {
|
|
||||||
o.Context = context.Background()
|
|
||||||
}
|
|
||||||
o.Context = context.WithValue(o.Context, errorMapKey{}, m)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
16
stream.go
16
stream.go
@ -2,10 +2,8 @@ package http
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
@ -65,22 +63,14 @@ func (h *httpStream) Send(msg interface{}) error {
|
|||||||
return errShutdown
|
return errShutdown
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err := h.codec.Marshal(msg)
|
hreq, err := newRequest(h.address, h.request, h.codec, msg, h.opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := bytes.NewBuffer(b)
|
hreq.Header = h.header
|
||||||
req, err := newRequest(h.address, h.request, h.opts)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header = h.header
|
return hreq.Write(h.conn)
|
||||||
req.Body = ioutil.NopCloser(buf)
|
|
||||||
req.ContentLength = int64(len(b))
|
|
||||||
|
|
||||||
return req.Write(h.conn)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *httpStream) Recv(msg interface{}) error {
|
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…
x
Reference in New Issue
Block a user