mirror of https://github.com/dapr/go-sdk.git
HTTP serving implementation (#45)
* http service implementation * http coverage * common serving interface * multi pubsub support * multi-pubsub serving * ensures unique componennt/topic * cleaned samples in docs * cleaned up examples in callback docs * removes Parallel from callback tests * multi-pupsub support * updated tests for empty pubsub data * cleaned up example client * header parsing in HTTP per issue 1894 in dapr * refactored client binding * refactored service invoke * updated client and serving docs * removed v0.9 from makefile action descr * renamed based on PR review * updated names globally
This commit is contained in:
parent
74a1ab5682
commit
d6de57c71a
23
Makefile
23
Makefile
|
|
@ -13,20 +13,21 @@ test: mod ## Tests the entire project
|
||||||
|
|
||||||
cover: mod ## Displays test coverage in the client and service packages
|
cover: mod ## Displays test coverage in the client and service packages
|
||||||
go test -coverprofile=cover-client.out ./client && go tool cover -html=cover-client.out
|
go test -coverprofile=cover-client.out ./client && go tool cover -html=cover-client.out
|
||||||
go test -coverprofile=cover-service.out ./service/grpc && go tool cover -html=cover-service.out
|
go test -coverprofile=cover-grpc.out ./service/grpc && go tool cover -html=cover-grpc.out
|
||||||
|
go test -coverprofile=cover-http.out ./service/http && go tool cover -html=cover-http.out
|
||||||
|
|
||||||
service: mod ## Runs the uncompiled example service code
|
service-http: mod ## Runs the uncompiled HTTP example service code
|
||||||
dapr run --app-id serving \
|
dapr run --app-id serving \
|
||||||
--app-protocol grpc \
|
--app-protocol http \
|
||||||
--app-port 50001 \
|
--app-port 8080 \
|
||||||
--port 3500 \
|
--port 3500 \
|
||||||
--log-level debug \
|
--log-level debug \
|
||||||
--components-path example/serving/grpc/config \
|
--components-path example/serving/http/config \
|
||||||
go run example/serving/grpc/main.go
|
go run example/serving/http/main.go
|
||||||
|
|
||||||
service09: mod ## Runs the uncompiled example service code using the Dapr v0.9 flags
|
service-grpc: mod ## Runs the uncompiled gRPC example service code
|
||||||
dapr run --app-id serving \
|
dapr run --app-id serving \
|
||||||
--protocol grpc \
|
--app-protocol grpc \
|
||||||
--app-port 50001 \
|
--app-port 50001 \
|
||||||
--port 3500 \
|
--port 3500 \
|
||||||
--log-level debug \
|
--log-level debug \
|
||||||
|
|
@ -42,13 +43,13 @@ client: mod ## Runs the uncompiled example client code
|
||||||
pubsub: ## Submits pub/sub events in different cotnent types
|
pubsub: ## Submits pub/sub events in different cotnent types
|
||||||
curl -d '{ "from": "John", "to": "Lary", "message": "hi" }' \
|
curl -d '{ "from": "John", "to": "Lary", "message": "hi" }' \
|
||||||
-H "Content-type: application/json" \
|
-H "Content-type: application/json" \
|
||||||
"http://localhost:3500/v1.0/publish/messages"
|
"http://localhost:3500/v1.0/publish/messages/topic1"
|
||||||
curl -d '<message><from>John</from><to>Lary</to></message>' \
|
curl -d '<message><from>John</from><to>Lary</to></message>' \
|
||||||
-H "Content-type: application/xml" \
|
-H "Content-type: application/xml" \
|
||||||
"http://localhost:3500/v1.0/publish/messages"
|
"http://localhost:3500/v1.0/publish/messages/topic1"
|
||||||
curl -d '0x18, 0x2d, 0x44, 0x54, 0xfb, 0x21, 0x09, 0x40' \
|
curl -d '0x18, 0x2d, 0x44, 0x54, 0xfb, 0x21, 0x09, 0x40' \
|
||||||
-H "Content-type: application/octet-stream" \
|
-H "Content-type: application/octet-stream" \
|
||||||
"http://localhost:3500/v1.0/publish/messages"
|
"http://localhost:3500/v1.0/publish/messages/topic1"
|
||||||
|
|
||||||
invoke: ## Invokes service method with different operations
|
invoke: ## Invokes service method with different operations
|
||||||
curl -d '{ "from": "John", "to": "Lary", "message": "hi" }' \
|
curl -d '{ "from": "John", "to": "Lary", "message": "hi" }' \
|
||||||
|
|
|
||||||
36
Readme.md
36
Readme.md
|
|
@ -169,29 +169,37 @@ resp, err = client.InvokeService(ctx, "service-name", "method-name")
|
||||||
And to invoke a service with data:
|
And to invoke a service with data:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
data := []byte(`{ "id": "a123", "value": "abcdefg", "valid": true }`)
|
content := &DataContent{
|
||||||
resp, err := client.InvokeServiceWithContent(ctx, "service-name", "method-name", "application/json", data)
|
ContentType: "application/json",
|
||||||
|
Data: []byte(`{ "id": "a123", "value": "demo", "valid": true }`)
|
||||||
|
}
|
||||||
|
resp, err := client.InvokeServiceWithContent(ctx, "service-name", "method-name", content)
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Bindings
|
##### Bindings
|
||||||
|
|
||||||
Similarly to Service, Dapr client provides two methods to invoke an operation on a [Dapr-defined binding](https://github.com/dapr/docs/tree/master/concepts/bindings). Dapr supports input, output, and bidirectional bindings so the first methods supports all of them along with metadata options:
|
Similarly to Service, Dapr client provides two methods to invoke an operation on a [Dapr-defined binding](https://github.com/dapr/docs/tree/master/concepts/bindings). Dapr supports input, output, and bidirectional bindings.
|
||||||
|
|
||||||
|
For simple, output only biding:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
data := []byte("hello")
|
in := &BindingInvocation{ Name: "binding-name", Operation: "operation-name" }
|
||||||
opt := map[string]string{
|
err = client.InvokeOutputBinding(ctx, in)
|
||||||
"opt1": "value1",
|
```
|
||||||
"opt2": "value2",
|
|
||||||
|
To invoke method with content and metadata:
|
||||||
|
|
||||||
|
```go
|
||||||
|
in := &BindingInvocation{
|
||||||
|
Name: "binding-name",
|
||||||
|
Operation: "operation-name",
|
||||||
|
Data: []byte("hello"),
|
||||||
|
Metadata: map[string]string{"k1": "v1", "k2": "v2"}
|
||||||
}
|
}
|
||||||
resp, meta, err := client.InvokeBinding(ctx, "binding-name", "operation-name", data, opt)
|
out, err := client.InvokeBinding(ctx, in)
|
||||||
```
|
```
|
||||||
|
|
||||||
And for simple, output only biding:
|
|
||||||
|
|
||||||
```go
|
|
||||||
data := []byte("hello")
|
|
||||||
err = client.InvokeOutputBinding(ctx, "binding-name", "operation-name", data)
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Secrets
|
##### Secrets
|
||||||
|
|
||||||
|
|
@ -206,7 +214,7 @@ secret, err = client.GetSecret(ctx, "store-name", "secret-name", opt)
|
||||||
|
|
||||||
## Service (callback)
|
## Service (callback)
|
||||||
|
|
||||||
In addition to a an easy to use client, Dapr go package also provides implementation for `service`. Instructions on how to use it are located [here](./service/grpc/Readme.md)
|
In addition to a an easy to use client, Dapr go package also provides implementation for `service`. Instructions on how to use it are located [here](./service/Readme.md)
|
||||||
|
|
||||||
|
|
||||||
## Contributing to Dapr go client
|
## Contributing to Dapr go client
|
||||||
|
|
|
||||||
|
|
@ -7,37 +7,65 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// BindingInvocation represents binding invocation request
|
||||||
|
type BindingInvocation struct {
|
||||||
|
// Name is name of binding to invoke.
|
||||||
|
Name string
|
||||||
|
// Operation is the name of the operation type for the binding to invoke
|
||||||
|
Operation string
|
||||||
|
// Data is the input bindings sent
|
||||||
|
Data []byte
|
||||||
|
// Metadata is the input binding metadata
|
||||||
|
Metadata map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// BindingEvent represents the binding event handler input
|
||||||
|
type BindingEvent struct {
|
||||||
|
// Data is the input bindings sent
|
||||||
|
Data []byte
|
||||||
|
// Metadata is the input binding metadata
|
||||||
|
Metadata map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
// InvokeBinding invokes specific operation on the configured Dapr binding.
|
// InvokeBinding invokes specific operation on the configured Dapr binding.
|
||||||
// This method covers input, output, and bi-directional bindings.
|
// This method covers input, output, and bi-directional bindings.
|
||||||
func (c *GRPCClient) InvokeBinding(ctx context.Context, name, op string, in []byte, min map[string]string) (out []byte, mout map[string]string, err error) {
|
func (c *GRPCClient) InvokeBinding(ctx context.Context, in *BindingInvocation) (out *BindingEvent, err error) {
|
||||||
if name == "" {
|
if in == nil {
|
||||||
return nil, nil, errors.New("nil topic")
|
return nil, errors.New("binding invocation required")
|
||||||
|
}
|
||||||
|
if in.Name == "" {
|
||||||
|
return nil, errors.New("binding invocation name required")
|
||||||
|
}
|
||||||
|
if in.Operation == "" {
|
||||||
|
return nil, errors.New("binding invocation operation required")
|
||||||
}
|
}
|
||||||
|
|
||||||
req := &pb.InvokeBindingRequest{
|
req := &pb.InvokeBindingRequest{
|
||||||
Name: name,
|
Name: in.Name,
|
||||||
Operation: op,
|
Operation: in.Operation,
|
||||||
Data: in,
|
Data: in.Data,
|
||||||
Metadata: min,
|
Metadata: in.Metadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := c.protoClient.InvokeBinding(authContext(ctx), req)
|
resp, err := c.protoClient.InvokeBinding(authContext(ctx), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, errors.Wrapf(err, "error invoking binding %s", name)
|
return nil, errors.Wrapf(err, "error invoking binding %s/%s", in.Name, in.Operation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
out = &BindingEvent{}
|
||||||
|
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
return resp.Data, resp.Metadata, nil
|
out.Data = resp.Data
|
||||||
|
out.Metadata = resp.Metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil, nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// InvokeOutputBinding invokes configured Dapr binding with data (allows nil).InvokeOutputBinding
|
// InvokeOutputBinding invokes configured Dapr binding with data (allows nil).InvokeOutputBinding
|
||||||
// This method differs from InvokeBinding in that it doesn't expect any content being returned from the invoked method.
|
// This method differs from InvokeBinding in that it doesn't expect any content being returned from the invoked method.
|
||||||
func (c *GRPCClient) InvokeOutputBinding(ctx context.Context, name, operation string, data []byte) error {
|
func (c *GRPCClient) InvokeOutputBinding(ctx context.Context, in *BindingInvocation) error {
|
||||||
_, _, err := c.InvokeBinding(ctx, name, operation, data, nil)
|
if _, err := c.InvokeBinding(ctx, in); err != nil {
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "error invoking output binding")
|
return errors.Wrap(err, "error invoking output binding")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -11,32 +11,36 @@ import (
|
||||||
|
|
||||||
func TestInvokeBinding(t *testing.T) {
|
func TestInvokeBinding(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
data := "ping"
|
in := &BindingInvocation{
|
||||||
|
Name: "test",
|
||||||
|
Operation: "fn",
|
||||||
|
}
|
||||||
|
|
||||||
t.Run("output binding", func(t *testing.T) {
|
t.Run("output binding without data", func(t *testing.T) {
|
||||||
err := testClient.InvokeOutputBinding(ctx, "test", "fn", []byte(data))
|
err := testClient.InvokeOutputBinding(ctx, in)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("output binding without data", func(t *testing.T) {
|
t.Run("output binding", func(t *testing.T) {
|
||||||
err := testClient.InvokeOutputBinding(ctx, "test", "fn", []byte(data))
|
in.Data = []byte("test")
|
||||||
|
err := testClient.InvokeOutputBinding(ctx, in)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("binding without data", func(t *testing.T) {
|
t.Run("binding without data", func(t *testing.T) {
|
||||||
out, mOut, err := testClient.InvokeBinding(ctx, "test", "fn", nil, nil)
|
in.Data = nil
|
||||||
|
out, err := testClient.InvokeBinding(ctx, in)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.NotNil(t, mOut)
|
|
||||||
assert.NotNil(t, out)
|
assert.NotNil(t, out)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("binding with data and meta", func(t *testing.T) {
|
t.Run("binding with data and meta", func(t *testing.T) {
|
||||||
mIn := map[string]string{"k1": "v1", "k2": "v2"}
|
in.Data = []byte("test")
|
||||||
out, mOut, err := testClient.InvokeBinding(ctx, "test", "fn", []byte(data), mIn)
|
in.Metadata = map[string]string{"k1": "v1", "k2": "v2"}
|
||||||
|
out, err := testClient.InvokeBinding(ctx, in)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.NotNil(t, mOut)
|
|
||||||
assert.NotNil(t, out)
|
assert.NotNil(t, out)
|
||||||
assert.Equal(t, data, string(out))
|
assert.Equal(t, "test", string(out.Data))
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,17 +30,17 @@ var (
|
||||||
type Client interface {
|
type Client interface {
|
||||||
// InvokeBinding invokes specific operation on the configured Dapr binding.
|
// InvokeBinding invokes specific operation on the configured Dapr binding.
|
||||||
// This method covers input, output, and bi-directional bindings.
|
// This method covers input, output, and bi-directional bindings.
|
||||||
InvokeBinding(ctx context.Context, name, op string, in []byte, min map[string]string) (out []byte, mout map[string]string, err error)
|
InvokeBinding(ctx context.Context, in *BindingInvocation) (out *BindingEvent, err error)
|
||||||
|
|
||||||
// InvokeOutputBinding invokes configured Dapr binding with data (allows nil).InvokeOutputBinding
|
// InvokeOutputBinding invokes configured Dapr binding with data.InvokeOutputBinding
|
||||||
// This method differs from InvokeBinding in that it doesn't expect any content being returned from the invoked method.
|
// This method differs from InvokeBinding in that it doesn't expect any content being returned from the invoked method.
|
||||||
InvokeOutputBinding(ctx context.Context, name, operation string, data []byte) error
|
InvokeOutputBinding(ctx context.Context, in *BindingInvocation) error
|
||||||
|
|
||||||
// InvokeService invokes service without raw data ([]byte).
|
// InvokeService invokes service without raw data
|
||||||
InvokeService(ctx context.Context, serviceID, method string) (out []byte, err error)
|
InvokeService(ctx context.Context, serviceID, method string) (out []byte, err error)
|
||||||
|
|
||||||
// InvokeServiceWithContent invokes service without content (data + content type).
|
// InvokeServiceWithContent invokes service with content
|
||||||
InvokeServiceWithContent(ctx context.Context, serviceID, method, contentType string, data []byte) (out []byte, err error)
|
InvokeServiceWithContent(ctx context.Context, serviceID, method string, content *DataContent) (out []byte, err error)
|
||||||
|
|
||||||
// PublishEvent pubishes data onto topic in specific pubsub component.
|
// PublishEvent pubishes data onto topic in specific pubsub component.
|
||||||
PublishEvent(ctx context.Context, component, topic string, in []byte) error
|
PublishEvent(ctx context.Context, component, topic string, in []byte) error
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,14 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DataContent the service invocation content
|
||||||
|
type DataContent struct {
|
||||||
|
// Data is the input data
|
||||||
|
Data []byte
|
||||||
|
// ContentType is the type of the data content
|
||||||
|
ContentType string
|
||||||
|
}
|
||||||
|
|
||||||
func (c *GRPCClient) invokeServiceWithRequest(ctx context.Context, req *pb.InvokeServiceRequest) (out []byte, err error) {
|
func (c *GRPCClient) invokeServiceWithRequest(ctx context.Context, req *pb.InvokeServiceRequest) (out []byte, err error) {
|
||||||
if req == nil {
|
if req == nil {
|
||||||
return nil, errors.New("nil request")
|
return nil, errors.New("nil request")
|
||||||
|
|
@ -50,23 +58,23 @@ func (c *GRPCClient) InvokeService(ctx context.Context, serviceID, method string
|
||||||
}
|
}
|
||||||
|
|
||||||
// InvokeServiceWithContent invokes service without content (data + content type).
|
// InvokeServiceWithContent invokes service without content (data + content type).
|
||||||
func (c *GRPCClient) InvokeServiceWithContent(ctx context.Context, serviceID, method, contentType string, data []byte) (out []byte, err error) {
|
func (c *GRPCClient) InvokeServiceWithContent(ctx context.Context, serviceID, method string, content *DataContent) (out []byte, err error) {
|
||||||
if serviceID == "" {
|
if serviceID == "" {
|
||||||
return nil, errors.New("nil serviceID")
|
return nil, errors.New("serviceID is required")
|
||||||
}
|
}
|
||||||
if method == "" {
|
if method == "" {
|
||||||
return nil, errors.New("nil method")
|
return nil, errors.New("method name is required")
|
||||||
}
|
}
|
||||||
if contentType == "" {
|
if content == nil {
|
||||||
return nil, errors.New("nil contentType")
|
return nil, errors.New("content required")
|
||||||
}
|
}
|
||||||
|
|
||||||
req := &pb.InvokeServiceRequest{
|
req := &pb.InvokeServiceRequest{
|
||||||
Id: serviceID,
|
Id: serviceID,
|
||||||
Message: &v1.InvokeRequest{
|
Message: &v1.InvokeRequest{
|
||||||
Method: method,
|
Method: method,
|
||||||
Data: &anypb.Any{Value: data},
|
Data: &anypb.Any{Value: content.Data},
|
||||||
ContentType: contentType,
|
ContentType: content.ContentType,
|
||||||
HttpExtension: &v1.HTTPExtension{
|
HttpExtension: &v1.HTTPExtension{
|
||||||
Verb: v1.HTTPExtension_POST,
|
Verb: v1.HTTPExtension_POST,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,11 @@ func TestInvokeServiceWithContent(t *testing.T) {
|
||||||
data := "ping"
|
data := "ping"
|
||||||
|
|
||||||
t.Run("with content", func(t *testing.T) {
|
t.Run("with content", func(t *testing.T) {
|
||||||
resp, err := testClient.InvokeServiceWithContent(ctx, "test", "fn", "text/plain", []byte(data))
|
content := &DataContent{
|
||||||
|
ContentType: "text/plain",
|
||||||
|
Data: []byte(data),
|
||||||
|
}
|
||||||
|
resp, err := testClient.InvokeServiceWithContent(ctx, "test", "fn", content)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.NotNil(t, resp)
|
assert.NotNil(t, resp)
|
||||||
assert.Equal(t, string(resp), data)
|
assert.Equal(t, string(resp), data)
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,13 @@ func TestPublishEvent(t *testing.T) {
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("without data", func(t *testing.T) {
|
||||||
|
err := testClient.PublishEvent(ctx, "messagebus", "test", nil)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("with empty topic name", func(t *testing.T) {
|
t.Run("with empty topic name", func(t *testing.T) {
|
||||||
err := testClient.PublishEvent(ctx, "messagebus", "", []byte("ping"))
|
err := testClient.PublishEvent(ctx, "messagebus", "", []byte("ping"))
|
||||||
assert.NotNil(t, err)
|
assert.NotNil(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("without data", func(t *testing.T) {
|
|
||||||
err := testClient.PublishEvent(ctx, "messagebus", "test", nil)
|
|
||||||
assert.NotNil(t, err)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,22 @@
|
||||||
|
|
||||||
The `example` folder contains a Dapr enabled `serving` app and a `client` app that uses this SDK to invoke Dapr API for state and events, The `serving` app is available as HTTP or gRPC. The `client` app can target either one of these for service to service and binding invocations.
|
The `example` folder contains a Dapr enabled `serving` app and a `client` app that uses this SDK to invoke Dapr API for state and events, The `serving` app is available as HTTP or gRPC. The `client` app can target either one of these for service to service and binding invocations.
|
||||||
|
|
||||||
To run this example, start by first launching the service:
|
To run this example, start by first launching the service in ether HTTP or gRPC:
|
||||||
|
|
||||||
|
### HTTP
|
||||||
|
|
||||||
|
```
|
||||||
|
cd example/serving/http
|
||||||
|
dapr run --app-id serving \
|
||||||
|
--app-protocol http \
|
||||||
|
--app-port 8080 \
|
||||||
|
--port 3500 \
|
||||||
|
--log-level debug \
|
||||||
|
--components-path ./config \
|
||||||
|
go run main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### gRPC
|
||||||
|
|
||||||
```
|
```
|
||||||
cd example/serving/grpc
|
cd example/serving/grpc
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ func main() {
|
||||||
if err := client.SaveStateItems(ctx, store, item2); err != nil {
|
if err := client.SaveStateItems(ctx, store, item2); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
fmt.Println("data item saved")
|
||||||
|
|
||||||
// delete state for key key1
|
// delete state for key key1
|
||||||
if err := client.DeleteState(ctx, store, "key1"); err != nil {
|
if err := client.DeleteState(ctx, store, "key1"); err != nil {
|
||||||
|
|
@ -64,13 +65,21 @@ func main() {
|
||||||
fmt.Println("data deleted")
|
fmt.Println("data deleted")
|
||||||
|
|
||||||
// invoke a method called EchoMethod on another dapr enabled service
|
// invoke a method called EchoMethod on another dapr enabled service
|
||||||
resp, err := client.InvokeServiceWithContent(ctx, "serving", "echo", "text/plain", data)
|
content := &dapr.DataContent{
|
||||||
|
ContentType: "text/plain",
|
||||||
|
Data: []byte(data),
|
||||||
|
}
|
||||||
|
resp, err := client.InvokeServiceWithContent(ctx, "serving", "echo", content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
fmt.Printf("service method invoked, response: %s", string(resp))
|
fmt.Printf("service method invoked, response: %s", string(resp))
|
||||||
|
|
||||||
if err := client.InvokeOutputBinding(ctx, "example-http-binding", "create", nil); err != nil {
|
in := &dapr.BindingInvocation{
|
||||||
|
Name: "example-http-binding",
|
||||||
|
Operation: "create",
|
||||||
|
}
|
||||||
|
if err := client.InvokeOutputBinding(ctx, in); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
fmt.Println("output binding invoked")
|
fmt.Println("output binding invoked")
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
|
"github.com/dapr/go-sdk/service/common"
|
||||||
daprd "github.com/dapr/go-sdk/service/grpc"
|
daprd "github.com/dapr/go-sdk/service/grpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -16,20 +17,21 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// add some topic subscriptions
|
// add some topic subscriptions
|
||||||
err = s.AddTopicEventHandler("messages", "demo", eventHandler)
|
sub := &common.Subscription{
|
||||||
if err != nil {
|
PubsubName: "messages",
|
||||||
|
Topic: "topic1",
|
||||||
|
}
|
||||||
|
if err := s.AddTopicEventHandler(sub, eventHandler); err != nil {
|
||||||
log.Fatalf("error adding topic subscription: %v", err)
|
log.Fatalf("error adding topic subscription: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// add a service to service invocation handler
|
// add a service to service invocation handler
|
||||||
err = s.AddServiceInvocationHandler("echo", echoHandler)
|
if err := s.AddServiceInvocationHandler("echo", echoHandler); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("error adding invocation handler: %v", err)
|
log.Fatalf("error adding invocation handler: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// add a binding invocation handler
|
// add a binding invocation handler
|
||||||
err = s.AddBindingInvocationHandler("run", runHandler)
|
if err := s.AddBindingInvocationHandler("run", runHandler); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("error adding binding handler: %v", err)
|
log.Fatalf("error adding binding handler: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,12 +41,12 @@ func main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func eventHandler(ctx context.Context, e *daprd.TopicEvent) error {
|
func eventHandler(ctx context.Context, e *common.TopicEvent) error {
|
||||||
log.Printf("event - PubsubName:%s, Topic:%s, ID:%s, Data: %v", e.PubsubName, e.Topic, e.ID, e.Data)
|
log.Printf("event - PubsubName:%s, Topic:%s, ID:%s, Data: %v", e.PubsubName, e.Topic, e.ID, e.Data)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func echoHandler(ctx context.Context, in *daprd.InvocationEvent) (out *daprd.Content, err error) {
|
func echoHandler(ctx context.Context, in *common.InvocationEvent) (out *common.Content, err error) {
|
||||||
if in == nil {
|
if in == nil {
|
||||||
err = errors.New("nil invocation parameter")
|
err = errors.New("nil invocation parameter")
|
||||||
return
|
return
|
||||||
|
|
@ -53,7 +55,7 @@ func echoHandler(ctx context.Context, in *daprd.InvocationEvent) (out *daprd.Con
|
||||||
"echo - ContentType:%s, Verb:%s, QueryString:%s, %+v",
|
"echo - ContentType:%s, Verb:%s, QueryString:%s, %+v",
|
||||||
in.ContentType, in.Verb, in.QueryString, string(in.Data),
|
in.ContentType, in.Verb, in.QueryString, string(in.Data),
|
||||||
)
|
)
|
||||||
out = &daprd.Content{
|
out = &common.Content{
|
||||||
Data: in.Data,
|
Data: in.Data,
|
||||||
ContentType: in.ContentType,
|
ContentType: in.ContentType,
|
||||||
DataTypeURL: in.DataTypeURL,
|
DataTypeURL: in.DataTypeURL,
|
||||||
|
|
@ -61,7 +63,7 @@ func echoHandler(ctx context.Context, in *daprd.InvocationEvent) (out *daprd.Con
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func runHandler(ctx context.Context, in *daprd.BindingEvent) (out []byte, err error) {
|
func runHandler(ctx context.Context, in *common.BindingEvent) (out []byte, err error) {
|
||||||
log.Printf("binding - Data:%v, Meta:%v", in.Data, in.Metadata)
|
log.Printf("binding - Data:%v, Meta:%v", in.Data, in.Metadata)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
apiVersion: dapr.io/v1alpha1
|
||||||
|
kind: Component
|
||||||
|
metadata:
|
||||||
|
name: run
|
||||||
|
spec:
|
||||||
|
type: bindings.cron
|
||||||
|
metadata:
|
||||||
|
- name: schedule
|
||||||
|
value: "@every 10s"
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
apiVersion: dapr.io/v1alpha1
|
||||||
|
kind: Component
|
||||||
|
metadata:
|
||||||
|
name: messages
|
||||||
|
spec:
|
||||||
|
type: pubsub.redis
|
||||||
|
metadata:
|
||||||
|
- name: redisHost
|
||||||
|
value: localhost:6379
|
||||||
|
- name: redisPassword
|
||||||
|
value: ""
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
apiVersion: dapr.io/v1alpha1
|
||||||
|
kind: Component
|
||||||
|
metadata:
|
||||||
|
name: statestore
|
||||||
|
spec:
|
||||||
|
type: state.redis
|
||||||
|
metadata:
|
||||||
|
- name: redisHost
|
||||||
|
value: localhost:6379
|
||||||
|
- name: redisPassword
|
||||||
|
value: ""
|
||||||
|
- name: actorStateStore
|
||||||
|
value: "true"
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/dapr/go-sdk/service/common"
|
||||||
|
daprd "github.com/dapr/go-sdk/service/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// create a Dapr service (e.g. ":8080", "0.0.0.0:8080", "10.1.1.1:8080" )
|
||||||
|
s := daprd.NewService(":8080")
|
||||||
|
|
||||||
|
// add some topic subscriptions
|
||||||
|
sub := &common.Subscription{
|
||||||
|
PubsubName: "messages",
|
||||||
|
Topic: "topic1",
|
||||||
|
Route: "/events",
|
||||||
|
}
|
||||||
|
if err := s.AddTopicEventHandler(sub, eventHandler); err != nil {
|
||||||
|
log.Fatalf("error adding topic subscription: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add a service to service invocation handler
|
||||||
|
if err := s.AddServiceInvocationHandler("/echo", echoHandler); err != nil {
|
||||||
|
log.Fatalf("error adding invocation handler: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add an input binding invocation handler
|
||||||
|
if err := s.AddBindingInvocationHandler("/run", runHandler); err != nil {
|
||||||
|
log.Fatalf("error adding binding handler: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Start(); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Fatalf("error listenning: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func eventHandler(ctx context.Context, e *common.TopicEvent) error {
|
||||||
|
log.Printf("event - PubsubName:%s, Topic:%s, ID:%s, Data: %v", e.PubsubName, e.Topic, e.ID, e.Data)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func echoHandler(ctx context.Context, in *common.InvocationEvent) (out *common.Content, err error) {
|
||||||
|
if in == nil {
|
||||||
|
err = errors.New("invocation parameter required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf(
|
||||||
|
"echo - ContentType:%s, Verb:%s, QueryString:%s, %+v",
|
||||||
|
in.ContentType, in.Verb, in.QueryString, string(in.Data),
|
||||||
|
)
|
||||||
|
out = &common.Content{
|
||||||
|
Data: in.Data,
|
||||||
|
ContentType: in.ContentType,
|
||||||
|
DataTypeURL: in.DataTypeURL,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func runHandler(ctx context.Context, in *common.BindingEvent) (out []byte, err error) {
|
||||||
|
log.Printf("binding - Data:%v, Meta:%v", in.Data, in.Metadata)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Dapr Service (Callback) SDK for Go
|
||||||
|
|
||||||
|
In addition to a an easy to use client, Dapr go package also provides implementation for `service` or `callback` in both HTTP and gRPC protocols:
|
||||||
|
|
||||||
|
* [HTTP Service](./http/Readme.md)
|
||||||
|
* [gRPC Service](./grpc/Readme.md)
|
||||||
|
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
See the [Contribution Guide](../CONTRIBUTING.md) to get started with building and developing.
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// Service represents Dapr callback service
|
||||||
|
type Service interface {
|
||||||
|
// AddServiceInvocationHandler appends provided service invocation handler with its name to the service.
|
||||||
|
AddServiceInvocationHandler(name string, fn func(ctx context.Context, in *InvocationEvent) (out *Content, err error)) error
|
||||||
|
// AddTopicEventHandler appends provided event handler with it's topic and optional metadata to the service.
|
||||||
|
AddTopicEventHandler(sub *Subscription, fn func(ctx context.Context, e *TopicEvent) error) error
|
||||||
|
// AddBindingInvocationHandler appends provided binding invocation handler with its name to the service.
|
||||||
|
AddBindingInvocationHandler(name string, fn func(ctx context.Context, in *BindingEvent) (out []byte, err error)) error
|
||||||
|
// Start starts service.
|
||||||
|
Start() error
|
||||||
|
// Stop stops the previously started service.
|
||||||
|
Stop() error
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
// TopicEvent is the content of the inbound topic message
|
||||||
|
type TopicEvent struct {
|
||||||
|
// ID identifies the event.
|
||||||
|
ID string `json:"id"`
|
||||||
|
// The version of the CloudEvents specification.
|
||||||
|
SpecVersion string `json:"specversion"`
|
||||||
|
// The type of event related to the originating occurrence.
|
||||||
|
Type string `json:"type"`
|
||||||
|
// Source identifies the context in which an event happened.
|
||||||
|
Source string `json:"source"`
|
||||||
|
// The content type of data value.
|
||||||
|
DataContentType string `json:"datacontenttype"`
|
||||||
|
// The content of the event.
|
||||||
|
// Note, this is why the gRPC and HTTP implementations need separate structs for cloud events.
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
// Cloud event subject
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
// The pubsub topic which publisher sent to.
|
||||||
|
Topic string `json:"topic"`
|
||||||
|
// PubsubName is name of the pub/sub this message came from
|
||||||
|
PubsubName string `json:"pubsubname"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvocationEvent represents the input and output of binding invocation
|
||||||
|
type InvocationEvent struct {
|
||||||
|
// Data is the payload that the input bindings sent.
|
||||||
|
Data []byte `json:"data"`
|
||||||
|
// ContentType of the Data
|
||||||
|
ContentType string `json:"contentType"`
|
||||||
|
// DataTypeURL is the resource URL that uniquely identifies the type of the serialized
|
||||||
|
DataTypeURL string `json:"typeUrl,omitempty"`
|
||||||
|
// Verb is the HTTP verb that was used to invoke this service.
|
||||||
|
Verb string `json:"-"`
|
||||||
|
// QueryString is the HTTP query string that was used to invoke this service.
|
||||||
|
QueryString map[string]string `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content is a generic data content
|
||||||
|
type Content struct {
|
||||||
|
// Data is the payload that the input bindings sent.
|
||||||
|
Data []byte `json:"data"`
|
||||||
|
// ContentType of the Data
|
||||||
|
ContentType string `json:"contentType"`
|
||||||
|
// DataTypeURL is the resource URL that uniquely identifies the type of the serialized
|
||||||
|
DataTypeURL string `json:"typeUrl,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BindingEvent represents the binding event handler input
|
||||||
|
type BindingEvent struct {
|
||||||
|
// Data is the input bindings sent
|
||||||
|
Data []byte `json:"data"`
|
||||||
|
// Metadata is the input binding metadata
|
||||||
|
Metadata map[string]string `json:"metadata,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscription represents single topic subscription
|
||||||
|
type Subscription struct {
|
||||||
|
// PubsubName is name of the pub/sub this message came from
|
||||||
|
PubsubName string `json:"pubsubname"`
|
||||||
|
// Topic is the name of the topic
|
||||||
|
Topic string `json:"topic"`
|
||||||
|
// Route is the route of the handler where HTTP topic events should be published (not used in gRPC)
|
||||||
|
Route string `json:"route"`
|
||||||
|
// Metadata is the subscription metadata
|
||||||
|
Metadata map[string]string `json:"metadata,omitempty"`
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Dapr Service SDK for Go
|
# Dapr gRPC Service SDK for Go
|
||||||
|
|
||||||
Start by importing Dapr go `service` package:
|
Start by importing Dapr go `service/grpc` package:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
daprd "github.com/dapr/go-sdk/service/grpc"
|
daprd "github.com/dapr/go-sdk/service/grpc"
|
||||||
|
|
@ -25,7 +25,7 @@ if err := s.Start(); err != nil {
|
||||||
}
|
}
|
||||||
|
|
||||||
func eventHandler(ctx context.Context, e *daprd.TopicEvent) error {
|
func eventHandler(ctx context.Context, e *daprd.TopicEvent) error {
|
||||||
log.Printf("event - Topic:%s, ID:%s, Data: %v", e.Topic, e.ID, e.Data)
|
log.Printf("event - PubsubName:%s, Topic:%s, ID:%s, Data: %v", e.PubsubName, e.Topic, e.ID, e.Data)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -48,16 +48,16 @@ if err := s.Start(); err != nil {
|
||||||
log.Fatalf("server error: %v", err)
|
log.Fatalf("server error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func echoHandler(ctx context.Context, in *daprd.InvocationEvent) (out *daprd.Content, err error) {
|
func echoHandler(ctx context.Context, in *common.InvocationEvent) (out *common.Content, err error) {
|
||||||
if in == nil {
|
if in == nil {
|
||||||
err = errors.New("nil invocation parameter")
|
err = errors.New("invocation parameter required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf(
|
log.Printf(
|
||||||
"echo - ContentType:%s, Verb:%s, QueryString:%s, %+v",
|
"echo - ContentType:%s, Verb:%s, QueryString:%s, %+v",
|
||||||
in.ContentType, in.Verb, in.QueryString, string(in.Data),
|
in.ContentType, in.Verb, in.QueryString, string(in.Data),
|
||||||
)
|
)
|
||||||
out = &daprd.Content{
|
out = &common.Content{
|
||||||
Data: in.Data,
|
Data: in.Data,
|
||||||
ContentType: in.ContentType,
|
ContentType: in.ContentType,
|
||||||
DataTypeURL: in.DataTypeURL,
|
DataTypeURL: in.DataTypeURL,
|
||||||
|
|
@ -84,7 +84,7 @@ if err := s.Start(); err != nil {
|
||||||
log.Fatalf("server error: %v", err)
|
log.Fatalf("server error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runHandler(ctx context.Context, in *daprd.BindingEvent) (out []byte, err error) {
|
func runHandler(ctx context.Context, in *common.BindingEvent) (out []byte, err error) {
|
||||||
log.Printf("binding - Data:%v, Meta:%v", in.Data, in.Metadata)
|
log.Printf("binding - Data:%v, Meta:%v", in.Data, in.Metadata)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
@ -93,4 +93,4 @@ func runHandler(ctx context.Context, in *daprd.BindingEvent) (out []byte, err er
|
||||||
|
|
||||||
## Contributing to Dapr go client
|
## Contributing to Dapr go client
|
||||||
|
|
||||||
See the [Contribution Guide](../CONTRIBUTING.md) to get started with building and developing.
|
See the [Contribution Guide](../../CONTRIBUTING.md) to get started with building and developing.
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,14 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
pb "github.com/dapr/go-sdk/dapr/proto/runtime/v1"
|
||||||
|
"github.com/dapr/go-sdk/service/common"
|
||||||
"github.com/golang/protobuf/ptypes/empty"
|
"github.com/golang/protobuf/ptypes/empty"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
pb "github.com/dapr/go-sdk/dapr/proto/runtime/v1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// AddBindingInvocationHandler appends provided binding invocation handler with its name to the service
|
// AddBindingInvocationHandler appends provided binding invocation handler with its name to the service
|
||||||
func (s *Server) AddBindingInvocationHandler(name string, fn func(ctx context.Context, in *BindingEvent) (out []byte, err error)) error {
|
func (s *Server) AddBindingInvocationHandler(name string, fn func(ctx context.Context, in *common.BindingEvent) (out []byte, err error)) error {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return fmt.Errorf("binding name required")
|
return fmt.Errorf("binding name required")
|
||||||
}
|
}
|
||||||
|
|
@ -38,7 +38,7 @@ func (s *Server) OnBindingEvent(ctx context.Context, in *pb.BindingEventRequest)
|
||||||
return nil, errors.New("nil binding event request")
|
return nil, errors.New("nil binding event request")
|
||||||
}
|
}
|
||||||
if fn, ok := s.bindingHandlers[in.Name]; ok {
|
if fn, ok := s.bindingHandlers[in.Name]; ok {
|
||||||
e := &BindingEvent{
|
e := &common.BindingEvent{
|
||||||
Data: in.Data,
|
Data: in.Data,
|
||||||
Metadata: in.Metadata,
|
Metadata: in.Metadata,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,11 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/dapr/go-sdk/dapr/proto/runtime/v1"
|
"github.com/dapr/go-sdk/dapr/proto/runtime/v1"
|
||||||
|
"github.com/dapr/go-sdk/service/common"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testBindingHandler(ctx context.Context, in *BindingEvent) (out []byte, err error) {
|
func testBindingHandler(ctx context.Context, in *common.BindingEvent) (out []byte, err error) {
|
||||||
if in == nil {
|
if in == nil {
|
||||||
return nil, errors.New("nil event")
|
return nil, errors.New("nil event")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,14 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
cpb "github.com/dapr/go-sdk/dapr/proto/common/v1"
|
||||||
|
cc "github.com/dapr/go-sdk/service/common"
|
||||||
"github.com/golang/protobuf/ptypes/any"
|
"github.com/golang/protobuf/ptypes/any"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
cpb "github.com/dapr/go-sdk/dapr/proto/common/v1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// AddServiceInvocationHandler appends provided service invocation handler with its method to the service
|
// AddServiceInvocationHandler appends provided service invocation handler with its method to the service
|
||||||
func (s *Server) AddServiceInvocationHandler(method string, fn func(ctx context.Context, in *InvocationEvent) (our *Content, err error)) error {
|
func (s *Server) AddServiceInvocationHandler(method string, fn func(ctx context.Context, in *cc.InvocationEvent) (our *cc.Content, err error)) error {
|
||||||
if method == "" {
|
if method == "" {
|
||||||
return fmt.Errorf("servie name required")
|
return fmt.Errorf("servie name required")
|
||||||
}
|
}
|
||||||
|
|
@ -25,7 +25,7 @@ func (s *Server) OnInvoke(ctx context.Context, in *cpb.InvokeRequest) (*cpb.Invo
|
||||||
return nil, errors.New("nil invoke request")
|
return nil, errors.New("nil invoke request")
|
||||||
}
|
}
|
||||||
if fn, ok := s.invokeHandlers[in.Method]; ok {
|
if fn, ok := s.invokeHandlers[in.Method]; ok {
|
||||||
e := &InvocationEvent{}
|
e := &cc.InvocationEvent{}
|
||||||
if in != nil {
|
if in != nil {
|
||||||
e.ContentType = in.ContentType
|
e.ContentType = in.ContentType
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,16 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/dapr/go-sdk/dapr/proto/common/v1"
|
"github.com/dapr/go-sdk/dapr/proto/common/v1"
|
||||||
|
cc "github.com/dapr/go-sdk/service/common"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"google.golang.org/protobuf/types/known/anypb"
|
"google.golang.org/protobuf/types/known/anypb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testInvokeHandler(ctx context.Context, in *InvocationEvent) (out *Content, err error) {
|
func testInvokeHandler(ctx context.Context, in *cc.InvocationEvent) (out *cc.Content, err error) {
|
||||||
if in == nil {
|
if in == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
out = &Content{
|
out = &cc.Content{
|
||||||
ContentType: in.ContentType,
|
ContentType: in.ContentType,
|
||||||
Data: in.Data,
|
Data: in.Data,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,89 +7,12 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
pb "github.com/dapr/go-sdk/dapr/proto/runtime/v1"
|
pb "github.com/dapr/go-sdk/dapr/proto/runtime/v1"
|
||||||
|
"github.com/dapr/go-sdk/service/common"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Service represents Dapr callback service
|
|
||||||
type Service interface {
|
|
||||||
// AddServiceInvocationHandler appends provided service invocation handler with its name to the service.
|
|
||||||
AddServiceInvocationHandler(name string, fn func(ctx context.Context, in *InvocationEvent) (out *Content, err error)) error
|
|
||||||
// AddTopicEventHandler appends provided event handler with it's topic and optional metadata to the service.
|
|
||||||
AddTopicEventHandler(component, topic string, fn func(ctx context.Context, e *TopicEvent) error) error
|
|
||||||
// AddTopicEventHandlerWithMetadata appends provided event handler with topic name and metadata to the service.
|
|
||||||
AddTopicEventHandlerWithMetadata(component, topic string, m map[string]string, fn func(ctx context.Context, e *TopicEvent) error) error
|
|
||||||
// AddBindingInvocationHandler appends provided binding invocation handler with its name to the service.
|
|
||||||
AddBindingInvocationHandler(name string, fn func(ctx context.Context, in *BindingEvent) (out []byte, err error)) error
|
|
||||||
// Start starts service.
|
|
||||||
Start() error
|
|
||||||
// Stop stops the previously started service.
|
|
||||||
Stop() error
|
|
||||||
}
|
|
||||||
|
|
||||||
// TopicEvent is the content of the inbound topic message.
|
|
||||||
type TopicEvent struct {
|
|
||||||
// ID identifies the event.
|
|
||||||
ID string
|
|
||||||
// The version of the CloudEvents specification.
|
|
||||||
SpecVersion string
|
|
||||||
// The type of event related to the originating occurrence.
|
|
||||||
Type string
|
|
||||||
// Source identifies the context in which an event happened.
|
|
||||||
Source string
|
|
||||||
// The content type of data value.
|
|
||||||
DataContentType string
|
|
||||||
// The content of the event.
|
|
||||||
Data interface{}
|
|
||||||
// Cloud event subject
|
|
||||||
Subject string
|
|
||||||
// The pubsub topic which publisher sent to.
|
|
||||||
Topic string
|
|
||||||
// PubsubName is the pubsub topic which publisher sent to.
|
|
||||||
PubsubName string
|
|
||||||
}
|
|
||||||
|
|
||||||
// InvocationEvent represents the input and output of binding invocation.
|
|
||||||
type InvocationEvent struct {
|
|
||||||
// Data is the payload that the input bindings sent.
|
|
||||||
Data []byte
|
|
||||||
// ContentType of the Data
|
|
||||||
ContentType string
|
|
||||||
// DataTypeURL is the resource URL that uniquely identifies the type of the serialized.
|
|
||||||
DataTypeURL string
|
|
||||||
// Verb is the HTTP verb that was used to invoke this service.
|
|
||||||
Verb string
|
|
||||||
// QueryString is the HTTP query string that was used to invoke this service.
|
|
||||||
QueryString map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content is a generic data content.
|
|
||||||
type Content struct {
|
|
||||||
// Data is the payload that the input bindings sent.
|
|
||||||
Data []byte
|
|
||||||
// ContentType of the Data
|
|
||||||
ContentType string
|
|
||||||
// DataTypeURL is the resource URL that uniquely identifies the type of the serialized.
|
|
||||||
DataTypeURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
// BindingEvent represents the binding event handler input.
|
|
||||||
type BindingEvent struct {
|
|
||||||
// Data is the input bindings sent.
|
|
||||||
Data []byte
|
|
||||||
// Metadata is the input binging components.
|
|
||||||
Metadata map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscription represents single topic subscription.
|
|
||||||
type Subscription struct {
|
|
||||||
// Topic is the name of the topic.
|
|
||||||
Topic string
|
|
||||||
// Route is the route of the handler where topic events should be published.
|
|
||||||
Route string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewService creates new Service.
|
// NewService creates new Service.
|
||||||
func NewService(address string) (s Service, err error) {
|
func NewService(address string) (s common.Service, err error) {
|
||||||
if address == "" {
|
if address == "" {
|
||||||
return nil, errors.New("nil address")
|
return nil, errors.New("nil address")
|
||||||
}
|
}
|
||||||
|
|
@ -103,31 +26,31 @@ func NewService(address string) (s Service, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServiceWithListener creates new Service with specific listener.
|
// NewServiceWithListener creates new Service with specific listener.
|
||||||
func NewServiceWithListener(lis net.Listener) Service {
|
func NewServiceWithListener(lis net.Listener) common.Service {
|
||||||
return newService(lis)
|
return newService(lis)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newService(lis net.Listener) *Server {
|
func newService(lis net.Listener) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
listener: lis,
|
listener: lis,
|
||||||
invokeHandlers: make(map[string]func(ctx context.Context, in *InvocationEvent) (out *Content, err error)),
|
invokeHandlers: make(map[string]func(ctx context.Context, in *common.InvocationEvent) (out *common.Content, err error)),
|
||||||
topicSubscriptions: make(map[string]*topicEventHandler),
|
topicSubscriptions: make(map[string]*topicEventHandler),
|
||||||
bindingHandlers: make(map[string]func(ctx context.Context, in *BindingEvent) (out []byte, err error)),
|
bindingHandlers: make(map[string]func(ctx context.Context, in *common.BindingEvent) (out []byte, err error)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server is the gRPC service implementation for Dapr.
|
// Server is the gRPC service implementation for Dapr.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
invokeHandlers map[string]func(ctx context.Context, in *InvocationEvent) (out *Content, err error)
|
invokeHandlers map[string]func(ctx context.Context, in *common.InvocationEvent) (out *common.Content, err error)
|
||||||
topicSubscriptions map[string]*topicEventHandler
|
topicSubscriptions map[string]*topicEventHandler
|
||||||
bindingHandlers map[string]func(ctx context.Context, in *BindingEvent) (out []byte, err error)
|
bindingHandlers map[string]func(ctx context.Context, in *common.BindingEvent) (out []byte, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type topicEventHandler struct {
|
type topicEventHandler struct {
|
||||||
component string
|
component string
|
||||||
topic string
|
topic string
|
||||||
fn func(ctx context.Context, e *TopicEvent) error
|
fn func(ctx context.Context, e *common.TopicEvent) error
|
||||||
meta map[string]string
|
meta map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,49 +4,34 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
pb "github.com/dapr/go-sdk/dapr/proto/runtime/v1"
|
||||||
|
"github.com/dapr/go-sdk/service/common"
|
||||||
"github.com/golang/protobuf/ptypes/empty"
|
"github.com/golang/protobuf/ptypes/empty"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
pb "github.com/dapr/go-sdk/dapr/proto/runtime/v1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// AddTopicEventHandler appends provided event handler with topic name to the service
|
// AddTopicEventHandler appends provided event handler with topic name to the service
|
||||||
func (s *Server) AddTopicEventHandler(component, topic string, fn func(ctx context.Context, e *TopicEvent) error) error {
|
func (s *Server) AddTopicEventHandler(sub *common.Subscription, fn func(ctx context.Context, e *common.TopicEvent) error) error {
|
||||||
if topic == "" {
|
if sub == nil {
|
||||||
return fmt.Errorf("topic name required")
|
return errors.New("subscription required")
|
||||||
}
|
}
|
||||||
if component == "" {
|
if sub.Topic == "" {
|
||||||
return fmt.Errorf("component name required")
|
return errors.New("topic name required")
|
||||||
}
|
}
|
||||||
key := fmt.Sprintf("%s-%s", component, topic)
|
if sub.PubsubName == "" {
|
||||||
|
return errors.New("pub/sub name required")
|
||||||
|
}
|
||||||
|
key := fmt.Sprintf("%s-%s", sub.PubsubName, sub.Topic)
|
||||||
s.topicSubscriptions[key] = &topicEventHandler{
|
s.topicSubscriptions[key] = &topicEventHandler{
|
||||||
component: component,
|
component: sub.PubsubName,
|
||||||
topic: topic,
|
topic: sub.Topic,
|
||||||
fn: fn,
|
fn: fn,
|
||||||
meta: map[string]string{},
|
meta: sub.Metadata,
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddTopicEventHandlerWithMetadata appends provided event handler with topic name and metadata to the service
|
// ListTopicSubscriptions is called by Dapr to get the list of topics in a pubsub component the app wants to subscribe to.
|
||||||
func (s *Server) AddTopicEventHandlerWithMetadata(component, topic string, m map[string]string, fn func(ctx context.Context, e *TopicEvent) error) error {
|
|
||||||
if topic == "" {
|
|
||||||
return fmt.Errorf("topic name required")
|
|
||||||
}
|
|
||||||
if component == "" {
|
|
||||||
return fmt.Errorf("component name required")
|
|
||||||
}
|
|
||||||
key := fmt.Sprintf("%s-%s", component, topic)
|
|
||||||
s.topicSubscriptions[key] = &topicEventHandler{
|
|
||||||
component: component,
|
|
||||||
topic: topic,
|
|
||||||
fn: fn,
|
|
||||||
meta: m,
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListTopicSubscriptions is called by Dapr to get the list of topics in a pubsub component the app wants to subscribe to.
|
|
||||||
func (s *Server) ListTopicSubscriptions(ctx context.Context, in *empty.Empty) (*pb.ListTopicSubscriptionsResponse, error) {
|
func (s *Server) ListTopicSubscriptions(ctx context.Context, in *empty.Empty) (*pb.ListTopicSubscriptionsResponse, error) {
|
||||||
subs := make([]*pb.TopicSubscription, 0)
|
subs := make([]*pb.TopicSubscription, 0)
|
||||||
for _, v := range s.topicSubscriptions {
|
for _, v := range s.topicSubscriptions {
|
||||||
|
|
@ -63,7 +48,8 @@ func (s *Server) ListTopicSubscriptions(ctx context.Context, in *empty.Empty) (*
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnTopicEvent fired whenever a message has been published to a topic that has been subscribed. Dapr sends published messages in a CloudEvents 0.3 envelope.
|
// OnTopicEvent fired whenever a message has been published to a topic that has been subscribed.
|
||||||
|
// Dapr sends published messages in a CloudEvents 0.3 envelope.
|
||||||
func (s *Server) OnTopicEvent(ctx context.Context, in *pb.TopicEventRequest) (*pb.TopicEventResponse, error) {
|
func (s *Server) OnTopicEvent(ctx context.Context, in *pb.TopicEventRequest) (*pb.TopicEventResponse, error) {
|
||||||
if in == nil {
|
if in == nil {
|
||||||
return nil, errors.New("nil event request")
|
return nil, errors.New("nil event request")
|
||||||
|
|
@ -76,7 +62,7 @@ func (s *Server) OnTopicEvent(ctx context.Context, in *pb.TopicEventRequest) (*p
|
||||||
}
|
}
|
||||||
key := fmt.Sprintf("%s-%s", in.PubsubName, in.Topic)
|
key := fmt.Sprintf("%s-%s", in.PubsubName, in.Topic)
|
||||||
if h, ok := s.topicSubscriptions[key]; ok {
|
if h, ok := s.topicSubscriptions[key]; ok {
|
||||||
e := &TopicEvent{
|
e := &common.TopicEvent{
|
||||||
ID: in.Id,
|
ID: in.Id,
|
||||||
Source: in.Source,
|
Source: in.Source,
|
||||||
Type: in.Type,
|
Type: in.Type,
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,11 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/dapr/go-sdk/dapr/proto/runtime/v1"
|
"github.com/dapr/go-sdk/dapr/proto/runtime/v1"
|
||||||
|
"github.com/dapr/go-sdk/service/common"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func eventHandler(ctx context.Context, event *TopicEvent) error {
|
func eventHandler(ctx context.Context, event *common.TopicEvent) error {
|
||||||
if event == nil {
|
if event == nil {
|
||||||
return errors.New("nil event")
|
return errors.New("nil event")
|
||||||
}
|
}
|
||||||
|
|
@ -18,12 +19,14 @@ func eventHandler(ctx context.Context, event *TopicEvent) error {
|
||||||
|
|
||||||
// go test -timeout 30s ./service/grpc -count 1 -run ^TestTopic$
|
// go test -timeout 30s ./service/grpc -count 1 -run ^TestTopic$
|
||||||
func TestTopic(t *testing.T) {
|
func TestTopic(t *testing.T) {
|
||||||
topicName := "test"
|
|
||||||
componentName := "messages"
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
sub := &common.Subscription{
|
||||||
|
PubsubName: "messages",
|
||||||
|
Topic: "test",
|
||||||
|
}
|
||||||
server := getTestServer()
|
server := getTestServer()
|
||||||
err := server.AddTopicEventHandler(componentName, topicName, eventHandler)
|
|
||||||
|
err := server.AddTopicEventHandler(sub, eventHandler)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
startTestServer(server)
|
startTestServer(server)
|
||||||
|
|
||||||
|
|
@ -48,8 +51,8 @@ func TestTopic(t *testing.T) {
|
||||||
SpecVersion: "v0.3",
|
SpecVersion: "v0.3",
|
||||||
DataContentType: "text/plain",
|
DataContentType: "text/plain",
|
||||||
Data: []byte("test"),
|
Data: []byte("test"),
|
||||||
Topic: topicName,
|
Topic: sub.Topic,
|
||||||
PubsubName: componentName,
|
PubsubName: sub.PubsubName,
|
||||||
}
|
}
|
||||||
_, err := server.OnTopicEvent(ctx, in)
|
_, err := server.OnTopicEvent(ctx, in)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
# Dapr HTTP Service SDK for Go
|
||||||
|
|
||||||
|
Start by importing Dapr go `service/http` package:
|
||||||
|
|
||||||
|
```go
|
||||||
|
daprd "github.com/dapr/go-sdk/service/http"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event Handling
|
||||||
|
|
||||||
|
To handle events from specific topic, first create a Dapr service, add topic event handler, and start the service:
|
||||||
|
|
||||||
|
```go
|
||||||
|
s := daprd.NewService(":8080")
|
||||||
|
|
||||||
|
sub := &common.Subscription{
|
||||||
|
PubsubName: "messages",
|
||||||
|
Topic: "topic1",
|
||||||
|
Route: "/events",
|
||||||
|
}
|
||||||
|
err := s.AddTopicEventHandler(sub, eventHandler)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error adding topic subscription: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = s.Start(); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Fatalf("error listening: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func eventHandler(ctx context.Context, e *common.TopicEvent) error {
|
||||||
|
log.Printf("event - PubsubName:%s, Topic:%s, ID:%s, Data: %v", e.PubsubName, e.Topic, e.ID, e.Data)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Service Invocation Handler
|
||||||
|
|
||||||
|
To handle service invocations, create and start the Dapr service as in the above example. In this case though add the handler for service invocation:
|
||||||
|
|
||||||
|
```go
|
||||||
|
s := daprd.NewService(":8080")
|
||||||
|
|
||||||
|
if err := s.AddServiceInvocationHandler("/echo", echoHandler); err != nil {
|
||||||
|
log.Fatalf("error adding invocation handler: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Start(); err != nil {
|
||||||
|
log.Fatalf("server error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func echoHandler(ctx context.Context, in *common.InvocationEvent) (out *common.Content, err error) {
|
||||||
|
if in == nil {
|
||||||
|
err = errors.New("invocation parameter required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf(
|
||||||
|
"echo - ContentType:%s, Verb:%s, QueryString:%s, %+v",
|
||||||
|
in.ContentType, in.Verb, in.QueryString, string(in.Data),
|
||||||
|
)
|
||||||
|
out = &common.Content{
|
||||||
|
Data: in.Data,
|
||||||
|
ContentType: in.ContentType,
|
||||||
|
DataTypeURL: in.DataTypeURL,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Binding Invocation Handler
|
||||||
|
|
||||||
|
To handle binding invocations, create and start the Dapr service as in the above examples. In this case though add the handler for binding invocation:
|
||||||
|
|
||||||
|
```go
|
||||||
|
s := daprd.NewService(":8080")
|
||||||
|
|
||||||
|
if err := s.AddBindingInvocationHandler("/run", runHandler); err != nil {
|
||||||
|
log.Fatalf("error adding binding handler: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runHandler(ctx context.Context, in *common.BindingEvent) (out []byte, err error) {
|
||||||
|
log.Printf("binding - Data:%v, Meta:%v", in.Data, in.Metadata)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Contributing to Dapr go client
|
||||||
|
|
||||||
|
See the [Contribution Guide](../../CONTRIBUTING.md) to get started with building and developing.
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/dapr/go-sdk/service/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddBindingInvocationHandler appends provided binding invocation handler with its route to the service
|
||||||
|
func (s *Server) AddBindingInvocationHandler(route string, fn func(ctx context.Context, in *common.BindingEvent) (out []byte, err error)) error {
|
||||||
|
if route == "" {
|
||||||
|
return fmt.Errorf("binding route required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(route, "/") {
|
||||||
|
route = fmt.Sprintf("/%s", route)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mux.Handle(route, optionsHandler(http.HandlerFunc(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var content []byte
|
||||||
|
if r.ContentLength > 0 {
|
||||||
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content = body
|
||||||
|
}
|
||||||
|
|
||||||
|
// assuming Dapr doesn't pass multiple values for key
|
||||||
|
meta := map[string]string{}
|
||||||
|
for k, values := range r.Header {
|
||||||
|
// TODO: Need to figure out how to parse out only the headers set in the binding + Traceparent
|
||||||
|
// if k == "raceparent" || strings.HasPrefix(k, "dapr") {
|
||||||
|
for _, v := range values {
|
||||||
|
meta[k] = v
|
||||||
|
}
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute handler
|
||||||
|
in := &common.BindingEvent{
|
||||||
|
Data: content,
|
||||||
|
Metadata: meta,
|
||||||
|
}
|
||||||
|
out, err := fn(r.Context(), in)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if out == nil {
|
||||||
|
out = []byte("{}")
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "application/json")
|
||||||
|
if _, err := w.Write(out); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/dapr/go-sdk/service/common"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBindingHandlerWithoutData(t *testing.T) {
|
||||||
|
s := newService("")
|
||||||
|
err := s.AddBindingInvocationHandler("/", func(ctx context.Context, in *common.BindingEvent) (out []byte, err error) {
|
||||||
|
if in == nil {
|
||||||
|
return nil, errors.New("nil input")
|
||||||
|
}
|
||||||
|
if in.Data != nil {
|
||||||
|
return nil, errors.New("invalid input data")
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
})
|
||||||
|
assert.NoErrorf(t, err, "error adding binding event handler")
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, "/", nil)
|
||||||
|
assert.NoErrorf(t, err, "error creating request")
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
s.mux.ServeHTTP(resp, req)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.Code)
|
||||||
|
assert.Equal(t, "{}", resp.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBindingHandlerWithData(t *testing.T) {
|
||||||
|
data := `{"name": "test"}`
|
||||||
|
s := newService("")
|
||||||
|
err := s.AddBindingInvocationHandler("/", func(ctx context.Context, in *common.BindingEvent) (out []byte, err error) {
|
||||||
|
if in == nil {
|
||||||
|
return nil, errors.New("nil input")
|
||||||
|
}
|
||||||
|
return []byte("test"), nil
|
||||||
|
})
|
||||||
|
assert.NoErrorf(t, err, "error adding binding event handler")
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, "/", strings.NewReader(data))
|
||||||
|
assert.NoErrorf(t, err, "error creating request")
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
s.mux.ServeHTTP(resp, req)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.Code)
|
||||||
|
assert.Equal(t, "test", resp.Body.String())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/dapr/go-sdk/service/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddServiceInvocationHandler appends provided service invocation handler with its route to the service
|
||||||
|
func (s *Server) AddServiceInvocationHandler(route string, fn func(ctx context.Context, in *common.InvocationEvent) (out *common.Content, err error)) error {
|
||||||
|
if route == "" {
|
||||||
|
return fmt.Errorf("service route required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(route, "/") {
|
||||||
|
route = fmt.Sprintf("/%s", route)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mux.Handle(route, optionsHandler(http.HandlerFunc(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// capture http args
|
||||||
|
e := &common.InvocationEvent{
|
||||||
|
Verb: r.Method,
|
||||||
|
QueryString: valuesToMap(r.URL.Query()),
|
||||||
|
ContentType: r.Header.Get("Content-type"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for post with no data
|
||||||
|
if r.ContentLength > 0 {
|
||||||
|
content, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.Data = content
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute handler
|
||||||
|
o, err := fn(r.Context(), e)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// write to response if handler returned data
|
||||||
|
if o != nil && o.Data != nil {
|
||||||
|
if _, err := w.Write(o.Data); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if o.ContentType != "" {
|
||||||
|
w.Header().Set("Content-type", o.ContentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func valuesToMap(in url.Values) map[string]string {
|
||||||
|
out := map[string]string{}
|
||||||
|
for k := range in {
|
||||||
|
out[k] = in.Get(k)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/dapr/go-sdk/service/common"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInvocationHandlerWithData(t *testing.T) {
|
||||||
|
data := `{"name": "test", "data": hellow}`
|
||||||
|
s := newService("")
|
||||||
|
err := s.AddServiceInvocationHandler("/", func(ctx context.Context, in *common.InvocationEvent) (out *common.Content, err error) {
|
||||||
|
if in == nil || in.Data == nil || in.ContentType == "" {
|
||||||
|
err = errors.New("nil input")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out = &common.Content{
|
||||||
|
Data: in.Data,
|
||||||
|
ContentType: in.ContentType,
|
||||||
|
DataTypeURL: in.DataTypeURL,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
})
|
||||||
|
assert.NoErrorf(t, err, "error adding event handler")
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, "/", strings.NewReader(data))
|
||||||
|
assert.NoErrorf(t, err, "error creating request")
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
s.mux.ServeHTTP(resp, req)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.Code)
|
||||||
|
|
||||||
|
b, err := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.NoErrorf(t, err, "error reading response body")
|
||||||
|
assert.Equal(t, data, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvocationHandlerWithoutInputData(t *testing.T) {
|
||||||
|
s := newService("")
|
||||||
|
err := s.AddServiceInvocationHandler("/", func(ctx context.Context, in *common.InvocationEvent) (out *common.Content, err error) {
|
||||||
|
if in == nil || in.Data != nil {
|
||||||
|
err = errors.New("nil input")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return &common.Content{}, nil
|
||||||
|
})
|
||||||
|
assert.NoErrorf(t, err, "error adding event handler")
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, "/", nil)
|
||||||
|
assert.NoErrorf(t, err, "error creating request")
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
s.mux.ServeHTTP(resp, req)
|
||||||
|
assert.Equal(t, http.StatusOK, resp.Code)
|
||||||
|
|
||||||
|
b, err := ioutil.ReadAll(resp.Body)
|
||||||
|
assert.NoErrorf(t, err, "error reading response body")
|
||||||
|
assert.NotNil(t, b)
|
||||||
|
assert.Equal(t, "", string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvocationHandlerWithInvalidRoute(t *testing.T) {
|
||||||
|
s := newService("")
|
||||||
|
err := s.AddServiceInvocationHandler("/a", func(ctx context.Context, in *common.InvocationEvent) (out *common.Content, err error) {
|
||||||
|
return nil, nil
|
||||||
|
})
|
||||||
|
assert.NoErrorf(t, err, "error adding event handler")
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, "/b", nil)
|
||||||
|
assert.NoErrorf(t, err, "error creating request")
|
||||||
|
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
s.mux.ServeHTTP(resp, req)
|
||||||
|
assert.Equal(t, http.StatusNotFound, resp.Code)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/dapr/go-sdk/service/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewService creates new Service
|
||||||
|
func NewService(address string) common.Service {
|
||||||
|
return newService(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newService(address string) *Server {
|
||||||
|
return &Server{
|
||||||
|
address: address,
|
||||||
|
mux: http.NewServeMux(),
|
||||||
|
topicSubscriptions: make([]*common.Subscription, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server is the HTTP server wrapping mux many Dapr helpers
|
||||||
|
type Server struct {
|
||||||
|
address string
|
||||||
|
mux *http.ServeMux
|
||||||
|
topicSubscriptions []*common.Subscription
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the HTTP handler. Blocks while serving
|
||||||
|
func (s *Server) Start() error {
|
||||||
|
s.registerSubscribeHandler()
|
||||||
|
server := http.Server{
|
||||||
|
Addr: s.address,
|
||||||
|
Handler: s.mux,
|
||||||
|
}
|
||||||
|
return server.ListenAndServe()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops previously started HTTP service
|
||||||
|
func (s *Server) Stop() error {
|
||||||
|
// TODO: implement service stop
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func optionsHandler(h http.Handler) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == http.MethodOptions {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "POST,OPTIONS")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "authorization, origin, content-type, accept")
|
||||||
|
w.Header().Set("Allow", "POST,OPTIONS")
|
||||||
|
} else {
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStoppingUnstartedService(t *testing.T) {
|
||||||
|
s := newService("")
|
||||||
|
assert.NotNil(t, s)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/dapr/go-sdk/service/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) registerSubscribeHandler() {
|
||||||
|
f := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if err := json.NewEncoder(w).Encode(s.topicSubscriptions); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.mux.HandleFunc("/dapr/subscribe", f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTopicEventHandler appends provided event handler with it's name to the service
|
||||||
|
func (s *Server) AddTopicEventHandler(sub *common.Subscription, fn func(ctx context.Context, e *common.TopicEvent) error) error {
|
||||||
|
if sub == nil {
|
||||||
|
return errors.New("subscription required")
|
||||||
|
}
|
||||||
|
if sub.Topic == "" {
|
||||||
|
return errors.New("topic name required")
|
||||||
|
}
|
||||||
|
if sub.PubsubName == "" {
|
||||||
|
return errors.New("pub/sub name required")
|
||||||
|
}
|
||||||
|
if sub.Route == "" {
|
||||||
|
return errors.New("handler route name")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(sub.Route, "/") {
|
||||||
|
sub.Route = fmt.Sprintf("/%s", sub.Route)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.topicSubscriptions = append(s.topicSubscriptions, sub)
|
||||||
|
|
||||||
|
s.mux.Handle(sub.Route, optionsHandler(http.HandlerFunc(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// check for post with no data
|
||||||
|
if r.ContentLength == 0 {
|
||||||
|
http.Error(w, "nil content", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// deserialize the event
|
||||||
|
var in common.TopicEvent
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if in.Topic == "" {
|
||||||
|
in.Topic = sub.Topic
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fn(r.Context(), &in); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/dapr/go-sdk/service/common"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEventHandler(t *testing.T) {
|
||||||
|
data := `{
|
||||||
|
"specversion" : "1.0",
|
||||||
|
"type" : "com.github.pull.create",
|
||||||
|
"source" : "https://github.com/cloudevents/spec/pull",
|
||||||
|
"subject" : "123",
|
||||||
|
"id" : "A234-1234-1234",
|
||||||
|
"time" : "2018-04-05T17:31:00Z",
|
||||||
|
"comexampleextension1" : "value",
|
||||||
|
"comexampleothervalue" : 5,
|
||||||
|
"datacontenttype" : "application/json",
|
||||||
|
"data" : "eyJtZXNzYWdlIjoiaGVsbG8ifQ=="
|
||||||
|
}`
|
||||||
|
|
||||||
|
s := newService("")
|
||||||
|
|
||||||
|
sub := &common.Subscription{
|
||||||
|
PubsubName: "messages",
|
||||||
|
Topic: "test",
|
||||||
|
Route: "/",
|
||||||
|
Metadata: map[string]string{},
|
||||||
|
}
|
||||||
|
err := s.AddTopicEventHandler(sub, func(ctx context.Context, e *common.TopicEvent) error {
|
||||||
|
if e == nil {
|
||||||
|
return errors.New("nil content")
|
||||||
|
}
|
||||||
|
if e.DataContentType != "application/json" {
|
||||||
|
return fmt.Errorf("invalid content type: %s", e.DataContentType)
|
||||||
|
}
|
||||||
|
if e.Data == nil {
|
||||||
|
return errors.New("nil data")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
assert.NoErrorf(t, err, "error adding event handler")
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, "/", strings.NewReader(data))
|
||||||
|
assert.NoErrorf(t, err, "error creating request")
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
s.registerSubscribeHandler()
|
||||||
|
s.mux.ServeHTTP(rr, req)
|
||||||
|
assert.Equal(t, http.StatusOK, rr.Code)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue