From 5a96bf5ef04f433bd860775fb5c8194d95f948f0 Mon Sep 17 00:00:00 2001 From: Asim Aslam Date: Thu, 23 Jul 2020 23:03:27 +0100 Subject: [PATCH] First --- README.md | 97 ++++++++++++++++++++ client/client.go | 207 ++++++++++++++++++++++++++++++++++++++++++ client/client_test.go | 22 +++++ client/go.mod | 5 + client/go.sum | 2 + 5 files changed, 333 insertions(+) create mode 100644 README.md create mode 100644 client/client.go create mode 100644 client/client_test.go create mode 100644 client/go.mod create mode 100644 client/go.sum diff --git a/README.md b/README.md new file mode 100644 index 0000000..74c72d3 --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +# Go Micro Client + +By default the client connects to api.micro.mu/client + +```go +package main + +import ( + "fmt" + "os" + + "github.com/micro/clients/go/client" +) + +type Request struct { + Name string `json:"name"` +} + +type Response struct { + Msg string `json:"msg"` +} + +var ( + token, _ = os.Getenv("TOKEN") +) + +func main() { + c := client.NewClient(nil) + + // set your api token + c.SetToken(token) + + req := &Request{ + Name: "John", + } + var rsp Response + + if err := c.Call("go.micro.srv.greeter", "Say.Hello", req, &rsp); err != nil { + fmt.Println(err) + return + } + fmt.Println(rsp) +} +``` + +If you want to access your local micro: + +```go + c := client.NewClient(client.Options{Local: true}) +``` + +You can also set the api address explicitly: + +```go + c := client.NewClient(client.Options{Address: "https://api.yourdomain.com/client"}) +``` + +## Streaming + +The client supports streaming + +```go +package main + +import ( + "fmt" + + "github.com/micro/clients/go/client" +) + +type Request struct { + Count string `json:"count"` +} + +type Response struct { + Count string `json:"count"` +} + +func main() { + c := client.NewClient(&client.Options{Local: true}) + + stream, err := c.Stream("go.micro.srv.stream", "Streamer.ServerStream", Request{Count: "10"}) + if err != nil { + fmt.Println(err) + return + } + + for { + var rsp Response + if err := stream.Recv(&rsp); err != nil { + fmt.Println(err) + return + } + fmt.Println("got", rsp.Count) + } +} +``` diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..babf13e --- /dev/null +++ b/client/client.go @@ -0,0 +1,207 @@ +package client + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/gorilla/websocket" +) + +const ( + // local address for api + localAddress = "http://localhost:8080/" + // public address for api + liveAddress = "https://api.micro.mu/" +) + +// Options of the Client +type Options struct { + // Token for authentication + Token string + // Address of the micro platform. + // By default it connects to live. Change it or use the local flag + // to connect to your local installation. + Address string + // Helper flag to help users connect to the default local address + Local bool +} + +// Request is the request of the generic `api-client` call +type Request struct { + // eg. "go.micro.srv.greeter" + Service string `json:"service"` + // eg. "Say.Hello" + Endpoint string `json:"endpoint"` + // json and then base64 encoded body + Body string `json:"body"` +} + +// Response is the response of the generic `api-client` call. +type Response struct { + // json and base64 encoded response body + Body string `json:"body"` + // error fields. Error json example + // {"id":"go.micro.client","code":500,"detail":"malformed method name: \"\"","status":"Internal Server Error"} + Code int `json:"code"` + ID string `json:"id"` + Detail string `json:"detail"` + Status string `json:"status"` +} + +// Client enables generic calls to micro +type Client struct { + options Options +} + +type Stream struct { + conn *websocket.Conn + service, endpoint string +} + +// NewClient returns a generic micro client that connects to live by default +func NewClient(options *Options) *Client { + ret := new(Client) + if options != nil { + ret.options = *options + } else { + ret.options = Options{ + Address: liveAddress, + } + } + if options != nil && options.Local { + ret.options.Address = localAddress + } + return ret +} + +// SetToken sets the api auth token +func (client *Client) SetToken(t string) { + client.options.Token = t +} + +// Call enables you to access any endpoint of any service on Micro +func (client *Client) Call(service, endpoint string, request, response interface{}) error { + // example curl: curl -XPOST -d '{"service": "go.micro.srv.greeter", "endpoint": "Say.Hello"}' + // -H 'Content-Type: application/json' http://localhost:8080/client {"body":"eyJtc2ciOiJIZWxsbyAifQ=="} + uri, err := url.Parse(client.options.Address) + if err != nil { + return err + } + // TODO: make optional + uri.Path = "/client" + + b, err := marshalRequest(service, endpoint, request) + if err != nil { + return err + } + + req, err := http.NewRequest("POST", uri.String(), bytes.NewBuffer(b)) + if err != nil { + return err + } + + // set the token if it exists + if len(client.options.Token) > 0 { + req.Header.Set("Authorization", "Bearer " + client.options.Token) + } + + req.Header.Set("Content-Type", "application/json") + + httpClient := &http.Client{} + resp, err := httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + return unmarshalResponse(body, response) +} + +// Stream enables the ability to stream via websockets +func (client *Client) Stream(service, endpoint string, request interface{}) (*Stream, error) { + b, err := marshalRequest(service, endpoint, request) + if err != nil { + return nil, err + } + + uri, err := url.Parse(client.options.Address) + if err != nil { + return nil, err + } + // TODO: make optional + uri.Path = "/client/stream" + + // replace http with websocket + uri.Scheme = strings.Replace(uri.Scheme, "http", "ws", 1) + + // create the headers + header := make(http.Header) + // set the token if it exists + if len(client.options.Token) > 0 { + header.Set("Authorization", "Bearer " + client.options.Token) + } + header.Set("Content-Type", "application/json") + + // dial the connection + conn, _, err := websocket.DefaultDialer.Dial(uri.String(), header) + if err != nil { + return nil, err + } + + // send the first request + if err := conn.WriteMessage(websocket.TextMessage, b); err != nil { + return nil, err + } + + return &Stream{conn, service, endpoint}, nil +} + +func (s *Stream) Recv(v interface{}) error { + // read response + _, message, err := s.conn.ReadMessage() + if err != nil { + return err + } + return unmarshalResponse(message, v) +} + +func (s *Stream) Send(v interface{}) error { + b, err := marshalRequest(s.service, s.endpoint, v) + if err != nil { + return err + } + return s.conn.WriteMessage(websocket.TextMessage, b) +} + +func marshalRequest(service, endpoint string, v interface{}) ([]byte, error) { + b, err := json.Marshal(v) + if err != nil { + return nil, err + } + return json.Marshal(&Request{ + Service: service, + Endpoint: endpoint, + Body: base64.StdEncoding.EncodeToString(b), + }) +} + +func unmarshalResponse(body []byte, v interface{}) error { + rsp := new(Response) + if err := json.Unmarshal(body, rsp); err != nil { + return err + } + b, err := base64.StdEncoding.DecodeString(rsp.Body) + if err != nil { + return err + } + return json.Unmarshal(b, v) +} diff --git a/client/client_test.go b/client/client_test.go new file mode 100644 index 0000000..47ba59c --- /dev/null +++ b/client/client_test.go @@ -0,0 +1,22 @@ +package client + +import "testing" + +type req struct { + Name string `json:"name"` +} + +type rsp struct { + Msg string `json:"msg"` +} + +func TestBasicCall(t *testing.T) { + response := rsp{} + if err := NewClient(nil).Call("go.micro.srv.greeter", "Say.Hello", req{Name: "John"}, &response); err != nil { + t.Fail() + } + if response.Msg != "Hello John" { + t.Logf("Message is not as expected: %v", response.Msg) + t.Fail() + } +} diff --git a/client/go.mod b/client/go.mod new file mode 100644 index 0000000..1af6118 --- /dev/null +++ b/client/go.mod @@ -0,0 +1,5 @@ +module github.com/micro/clients/go/client + +go 1.13 + +require github.com/gorilla/websocket v1.4.1 diff --git a/client/go.sum b/client/go.sum new file mode 100644 index 0000000..1f9b923 --- /dev/null +++ b/client/go.sum @@ -0,0 +1,2 @@ +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=