updating to the latest cloudevents sdk - v2.0.0-RC1 (#2386)

* updating to the latest cloudevents sdk.

* ran lint

* markdown had an error in it

* update serving sample readme
This commit is contained in:
Scott Nichols 2020-04-13 09:31:11 -07:00 committed by GitHub
parent 2ce03cbb89
commit fc69d36ec6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
193 changed files with 9788 additions and 6341 deletions

74
Gopkg.lock generated
View File

@ -26,25 +26,29 @@
revision = "3a771d992973f24aa725d07868b467d1ddfceafb" revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
[[projects]] [[projects]]
digest = "1:902544577dcb868a5ae31529d73a1ce5031e224923290caedf6176241ee304e0" digest = "1:f2d3857204e90f618f155eef24533ac205b1ce7b3570e21502ad0f0c2f6c288e"
name = "github.com/cloudevents/sdk-go" name = "github.com/cloudevents/sdk-go"
packages = [ packages = [
".", "v2",
"pkg/cloudevents", "v2/binding",
"pkg/cloudevents/client", "v2/binding/format",
"pkg/cloudevents/context", "v2/binding/spec",
"pkg/cloudevents/datacodec", "v2/client",
"pkg/cloudevents/datacodec/json", "v2/context",
"pkg/cloudevents/datacodec/text", "v2/event",
"pkg/cloudevents/datacodec/xml", "v2/event/datacodec",
"pkg/cloudevents/observability", "v2/event/datacodec/json",
"pkg/cloudevents/transport", "v2/event/datacodec/text",
"pkg/cloudevents/transport/http", "v2/event/datacodec/xml",
"pkg/cloudevents/types", "v2/extensions",
"v2/observability",
"v2/protocol",
"v2/protocol/http",
"v2/types",
] ]
pruneopts = "NUT" pruneopts = "NUT"
revision = "2fa4bb1fbb4aac4d906b0173a2a408f701439b82" revision = "6dc020a8df7f3ee38d729e53cde2193ea7edf12a"
version = "v0.10.0" version = "v2.0.0-preview8"
[[projects]] [[projects]]
digest = "1:7a6852b35eb5bbc184561443762d225116ae630c26a7c4d90546619f1e7d2ad2" digest = "1:7a6852b35eb5bbc184561443762d225116ae630c26a7c4d90546619f1e7d2ad2"
@ -143,6 +147,14 @@
revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf" revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf"
version = "v1.6.2" version = "v1.6.2"
[[projects]]
digest = "1:7fae9ec96d10b2afce0da23c378c8b3389319b7f92fa092f2621bba3078cfb4b"
name = "github.com/hashicorp/golang-lru"
packages = ["simplelru"]
pruneopts = "NUT"
revision = "14eae340515388ca95aa8e7b86f0de668e981f54"
version = "v0.5.4"
[[projects]] [[projects]]
digest = "1:08c58ac78a8c1f61e9a96350066d30fe194b8779799bd932a79932a5166a173f" digest = "1:08c58ac78a8c1f61e9a96350066d30fe194b8779799bd932a79932a5166a173f"
name = "github.com/kelseyhightower/envconfig" name = "github.com/kelseyhightower/envconfig"
@ -163,6 +175,17 @@
pruneopts = "NUT" pruneopts = "NUT"
revision = "e8a4306a5d37d2ea18705dbd592ecf1fa9264191" revision = "e8a4306a5d37d2ea18705dbd592ecf1fa9264191"
[[projects]]
branch = "master"
digest = "1:a12508addb76a16593d8a3cee41d782fdf738f727d421f0bcc2dd2ee76821c01"
name = "github.com/lightstep/tracecontext.go"
packages = [
"traceparent",
"tracestate",
]
pruneopts = "NUT"
revision = "1757c391b1acf4147823503f13e003115ea4e5df"
[[projects]] [[projects]]
digest = "1:5985ef4caf91ece5d54817c11ea25f182697534f8ae6521eadcd628c142ac4b6" digest = "1:5985ef4caf91ece5d54817c11ea25f182697534f8ae6521eadcd628c142ac4b6"
name = "github.com/matttproud/golang_protobuf_extensions" name = "github.com/matttproud/golang_protobuf_extensions"
@ -186,6 +209,14 @@
revision = "f197ec29e729f226d23370ea60f0e49b8f44ccf4" revision = "f197ec29e729f226d23370ea60f0e49b8f44ccf4"
version = "v0.1.0" version = "v0.1.0"
[[projects]]
digest = "1:4047c378584616813d610c9f993bf90dd0d07aed8d94bd3bc299cd35ececdcba"
name = "github.com/pkg/errors"
packages = ["."]
pruneopts = "NUT"
revision = "614d223910a179a466c1767a985424175c39b465"
version = "v0.9.1"
[[projects]] [[projects]]
digest = "1:03bca087b180bf24c4f9060775f137775550a0834e18f0bca0520a868679dbd7" digest = "1:03bca087b180bf24c4f9060775f137775550a0834e18f0bca0520a868679dbd7"
name = "github.com/prometheus/client_golang" name = "github.com/prometheus/client_golang"
@ -247,7 +278,7 @@
version = "v1.0.0" version = "v1.0.0"
[[projects]] [[projects]]
digest = "1:bb38c0571e5ffeb394f2a7e4056fa5a7a6ea1acabb7fe71976340719fd104d02" digest = "1:2fe273976b8123b7fcd5d49a7ecbf340b92f370a727bed427ded821f68584d63"
name = "go.opencensus.io" name = "go.opencensus.io"
packages = [ packages = [
".", ".",
@ -255,8 +286,12 @@
"exporter/zipkin", "exporter/zipkin",
"internal", "internal",
"internal/tagencoding", "internal/tagencoding",
"metric/metricdata",
"metric/metricproducer",
"plugin/ochttp", "plugin/ochttp",
"plugin/ochttp/propagation/b3", "plugin/ochttp/propagation/b3",
"plugin/ochttp/propagation/tracecontext",
"resource",
"stats", "stats",
"stats/internal", "stats/internal",
"stats/view", "stats/view",
@ -264,10 +299,11 @@
"trace", "trace",
"trace/internal", "trace/internal",
"trace/propagation", "trace/propagation",
"trace/tracestate",
] ]
pruneopts = "NUT" pruneopts = "NUT"
revision = "e262766cd0d230a1bb7c37281e345e465f19b41b" revision = "75c0cca22312e51bfd4fafdbe9197ae399e18b38"
version = "v0.14.0" version = "v0.20.2"
[[projects]] [[projects]]
digest = "1:cc9d86ec4e6e3bdf87e3a421273bfeed003cf8e21351c0302fe8b0eb7b10efe6" digest = "1:cc9d86ec4e6e3bdf87e3a421273bfeed003cf8e21351c0302fe8b0eb7b10efe6"
@ -491,7 +527,7 @@
analyzer-version = 1 analyzer-version = 1
input-imports = [ input-imports = [
"cloud.google.com/go/storage", "cloud.google.com/go/storage",
"github.com/cloudevents/sdk-go", "github.com/cloudevents/sdk-go/v2",
"github.com/dgrijalva/jwt-go", "github.com/dgrijalva/jwt-go",
"github.com/eclipse/paho.mqtt.golang", "github.com/eclipse/paho.mqtt.golang",
"github.com/golang/protobuf/proto", "github.com/golang/protobuf/proto",

View File

@ -14,3 +14,7 @@ required = [
[[prune.project]] [[prune.project]]
name = "knative.dev/test-infra" name = "knative.dev/test-infra"
non-go = false non-go = false
[[constraint]]
name = "github.com/cloudevents/sdk-go"
version = "v2.0.0-RC1"

View File

@ -1,7 +1,15 @@
A simple web app written in Go that you can use to test knative eventing. It shows how to consume a [CloudEvent](https://cloudevents.io/) in Knative eventing, and optionally how to respond back with another CloudEvent in the http response, using the [Go SDK for CloudEvents](https://github.com/cloudevents/sdk-go) A simple web app written in Go that you can use to test knative eventing. It
shows how to consume a [CloudEvent](https://cloudevents.io/) in Knative
eventing, and optionally how to respond back with another CloudEvent in the http
response, using the
[Go SDK for CloudEvents](https://github.com/cloudevents/sdk-go)
We will deploy the app as a [Kubernetes Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) along with a [Kubernetes Service](https://kubernetes.io/docs/concepts/services-networking/service/). We will deploy the app as a
However, you can also deploy the app as a [Knative Serving Service](../../../../serving/README.md). [Kubernetes Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/)
along with a
[Kubernetes Service](https://kubernetes.io/docs/concepts/services-networking/service/).
However, you can also deploy the app as a
[Knative Serving Service](../../../../serving/README.md).
Follow the steps below to create the sample code and then deploy the app to your Follow the steps below to create the sample code and then deploy the app to your
cluster. You can also download a working copy of the sample, by running the cluster. You can also download a working copy of the sample, by running the
@ -14,7 +22,9 @@ cd knative-docs/docs/eventing/samples/helloworld/helloworld-go
## Before you begin ## Before you begin
- A Kubernetes cluster with [Knative Eventing](../../../getting-started.md#installing-knative-eventing) installed. - A Kubernetes cluster with
[Knative Eventing](../../../getting-started.md#installing-knative-eventing)
installed.
- [Docker](https://www.docker.com) installed and running on your local machine, - [Docker](https://www.docker.com) installed and running on your local machine,
and a Docker Hub account configured (we'll use it for a container registry). and a Docker Hub account configured (we'll use it for a container registry).
@ -24,33 +34,24 @@ cd knative-docs/docs/eventing/samples/helloworld/helloworld-go
code creates a basic web server which listens on port 8080: code creates a basic web server which listens on port 8080:
```go ```go
package main
import ( import (
"context" "context"
"fmt"
"log" "log"
"net/http"
"os"
cloudevents "github.com/cloudevents/sdk-go" cloudevents "github.com/cloudevents/sdk-go/v2"
"github.com/google/uuid" "github.com/google/uuid"
) )
type eventData struct { func receive(ctx context.Context, event cloudevents.Event) (*cloudevents.Event, cloudevents.Result) {
Message string `json:"message,omitempty,string"`
}
func receive(ctx context.Context, event cloudevents.Event, response *cloudevents.EventResponse) error {
// Here is where your code to process the event will go. // Here is where your code to process the event will go.
// In this example we will log the event msg // In this example we will log the event msg
log.Printf("Event Context: %+v\n", event.Context) log.Printf("Event received. \n%s\n", event)
data := &HelloWorld{} data := &HelloWorld{}
if err := event.DataAs(data); err != nil { if err := event.DataAs(data); err != nil {
log.Printf("Error while extracting cloudevent Data: %s\n", err.Error()) log.Printf("Error while extracting cloudevent Data: %s\n", err.Error())
return err return nil, cloudevents.NewHTTPResult(400, "failed to convert data: %s", err)
} }
log.Printf("Hello World Message %q", data.Msg) log.Printf("Hello World Message from received event %q", data.Msg)
// Respond with another event (optional) // Respond with another event (optional)
// This is optional and is intended to show how to respond back with another event after processing. // This is optional and is intended to show how to respond back with another event after processing.
@ -59,21 +60,11 @@ cd knative-docs/docs/eventing/samples/helloworld/helloworld-go
newEvent.SetID(uuid.New().String()) newEvent.SetID(uuid.New().String())
newEvent.SetSource("knative/eventing/samples/hello-world") newEvent.SetSource("knative/eventing/samples/hello-world")
newEvent.SetType("dev.knative.samples.hifromknative") newEvent.SetType("dev.knative.samples.hifromknative")
newEvent.SetData(HiFromKnative{Msg: "Hi from Knative!"}) if err := newEvent.SetData(cloudevents.ApplicationJSON, HiFromKnative{Msg: "Hi from helloworld-go app!"}); err != nil {
response.RespondWith(200, &newEvent) return nil, cloudevents.NewHTTPResult(500, "failed to set response data: %s", err)
log.Printf("Responded with event %v", newEvent)
return nil
} }
log.Printf("Responding with event\n%s\n", newEvent)
func handler(w http.ResponseWriter, r *http.Request) { return &newEvent, nil
log.Print("Hello world received a request.")
target := os.Getenv("TARGET")
if target == "" {
target = "World"
}
fmt.Fprintf(w, "Hello %s!\n", target)
} }
func main() { func main() {
@ -85,7 +76,9 @@ cd knative-docs/docs/eventing/samples/helloworld/helloworld-go
log.Fatal(c.StartReceiver(context.Background(), receive)) log.Fatal(c.StartReceiver(context.Background(), receive))
} }
``` ```
1. Create a new file named `eventschemas.go` and paste the following code. This defines the data schema of the CloudEvents.
1. Create a new file named `eventschemas.go` and paste the following code. This
defines the data schema of the CloudEvents.
```go ```go
package main package main
@ -134,9 +127,9 @@ cd knative-docs/docs/eventing/samples/helloworld/helloworld-go
CMD ["/helloworld"] CMD ["/helloworld"]
``` ```
1. Create a new file, `sample-app.yaml` and copy the following service definition 1. Create a new file, `sample-app.yaml` and copy the following service
into the file. Make sure to replace `{username}` with your Docker Hub definition into the file. Make sure to replace `{username}` with your Docker
username. Hub username.
```yaml ```yaml
# Namespace for sample application with eventing enabled # Namespace for sample application with eventing enabled
@ -218,61 +211,80 @@ folder) you're ready to build and deploy the sample app.
``` ```
1. After the build has completed and the container is pushed to docker hub, you 1. After the build has completed and the container is pushed to docker hub, you
can deploy the sample application into your cluster. Ensure that the container image value can deploy the sample application into your cluster. Ensure that the
in `sample-app.yaml` matches the container you built in the previous step. Apply container image value in `sample-app.yaml` matches the container you built in
the configuration using `kubectl`: the previous step. Apply the configuration using `kubectl`:
```shell ```shell
kubectl apply --filename sample-app.yaml kubectl apply --filename sample-app.yaml
``` ```
1. Above command created a namespace `knative-samples` and labelled it with `knative-eventing-injection=enabled`, to enable eventing in the namespace. Verify using the following command:
1. Above command created a namespace `knative-samples` and labelled it with
`knative-eventing-injection=enabled`, to enable eventing in the namespace.
Verify using the following command:
```shell ```shell
kubectl get ns knative-samples --show-labels kubectl get ns knative-samples --show-labels
``` ```
1. It deployed the helloworld-go app as a K8s Deployment and created a K8s service names helloworld-go. Verify using the following command.
1. It deployed the helloworld-go app as a K8s Deployment and created a K8s
service names helloworld-go. Verify using the following command.
```shell ```shell
kubectl --namespace knative-samples get deployments helloworld-go kubectl --namespace knative-samples get deployments helloworld-go
kubectl --namespace knative-samples get svc helloworld-go kubectl --namespace knative-samples get svc helloworld-go
``` ```
1. It created a Knative Eventing Trigger to route certain events to the helloworld-go application. Make sure that Ready=true
1. It created a Knative Eventing Trigger to route certain events to the
helloworld-go application. Make sure that Ready=true
```shell ```shell
kubectl --namespace knative-samples get trigger helloworld-go kubectl --namespace knative-samples get trigger helloworld-go
``` ```
## Send and verify CloudEvents ## Send and verify CloudEvents
Once you have deployed the application and verified that the namespace, sample application and trigger are ready, let's send a CloudEvent.
Once you have deployed the application and verified that the namespace, sample
application and trigger are ready, let's send a CloudEvent.
### Send CloudEvent to the Broker ### Send CloudEvent to the Broker
We can send an http request directly to the [Broker](../../../broker-trigger.md) with correct CloudEvent headers set.
1. Deploy a curl pod and SSH into it We can send an http request directly to the [Broker](../../../broker-trigger.md)
with correct CloudEvent headers set.
1. Deploy a curl pod and SSH into it
```shell ```shell
kubectl --namespace knative-samples run curl --image=radial/busyboxplus:curl -it kubectl --namespace knative-samples run curl --image=radial/busyboxplus:curl -it
``` ```
1. Run the following in the SSH terminal 1. Run the following in the SSH terminal
```shell ```shell
curl -v "default-broker.knative-samples.svc.cluster.local" \ curl -v "default-broker.knative-samples.svc.cluster.local" \
-X POST \ -X POST \
-H "Ce-Id: 536808d3-88be-4077-9d7a-a3f162705f79" \ -H "Ce-Id: 536808d3-88be-4077-9d7a-a3f162705f79" \
-H "Ce-specversion: 0.3" \ -H "Ce-Specversion: 1.0" \
-H "Ce-Type: dev.knative.samples.helloworld" \ -H "Ce-Type: dev.knative.samples.helloworld" \
-H "Ce-Source: dev.knative.samples/helloworldsource" \ -H "Ce-Source: dev.knative.samples/helloworldsource" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"msg":"Hello World from the curl pod."}' -d '{"msg":"Hello World from the curl pod."}'
exit exit
``` ```
### Verify that event is received by helloworld-go app ### Verify that event is received by helloworld-go app
Helloworld-go app logs the context and the msg of the above event, and replies back with another event.
1. Display helloworld-go app logs Helloworld-go app logs the context and the msg of the above event, and replies
```shell back with another event.
kubectl --namespace knative-samples logs -l app=helloworld-go --tail=50
``` 1. Display helloworld-go app logs
`shell kubectl --namespace knative-samples logs -l app=helloworld-go --tail=50`
You should see something similar to: You should see something similar to:
```shell ```shell
Event received. Context: Context Attributes, Event received.
specversion: 0.3 Validation: valid
Context Attributes,
specversion: 1.0
type: dev.knative.samples.helloworld type: dev.knative.samples.helloworld
source: dev.knative.samples/helloworldsource source: dev.knative.samples/helloworldsource
id: 536808d3-88be-4077-9d7a-a3f162705f79 id: 536808d3-88be-4077-9d7a-a3f162705f79
@ -282,24 +294,36 @@ Helloworld-go app logs the context and the msg of the above event, and replies b
knativearrivaltime: 2019-10-04T22:35:26Z knativearrivaltime: 2019-10-04T22:35:26Z
knativehistory: default-kn2-trigger-kn-channel.knative-samples.svc.cluster.local knativehistory: default-kn2-trigger-kn-channel.knative-samples.svc.cluster.local
traceparent: 00-971d4644229653483d38c46e92a959c7-92c66312e4bb39be-00 traceparent: 00-971d4644229653483d38c46e92a959c7-92c66312e4bb39be-00
Data,
{"msg":"Hello World from the curl pod."}
Hello World Message "Hello World from the curl pod." Hello World Message "Hello World from the curl pod."
Responded with event Validation: valid Responded with event
Validation: valid
Context Attributes, Context Attributes,
specversion: 0.2 specversion: 1.0
type: dev.knative.samples.hifromknative type: dev.knative.samples.hifromknative
source: knative/eventing/samples/hello-world source: knative/eventing/samples/hello-world
id: 37458d77-01f5-411e-a243-a459bbf79682 id: 37458d77-01f5-411e-a243-a459bbf79682
datacontenttype: application/json
Data, Data,
{"msg":"Hi from Knative!"} {"msg":"Hi from Knative!"}
``` ```
Play around with the CloudEvent attributes in the curl command and the trigger specification to understand how [Triggers work](../../../broker-trigger.md#trigger).
Play around with the CloudEvent attributes in the curl command and the
trigger specification to understand how
[Triggers work](../../../broker-trigger.md#trigger).
## Verify reply from helloworld-go app ## Verify reply from helloworld-go app
`helloworld-go` app replies back with an event of `type= dev.knative.samples.hifromknative`, and `source=knative/eventing/samples/hello-world`. This event enters the eventing mesh via the Broker and can be delivered to other services using a Trigger
1. Deploy a pod that receives any CloudEvent and logs the event to its output. `helloworld-go` app replies back with an event of
`type= dev.knative.samples.hifromknative`, and
`source=knative/eventing/samples/hello-world`. This event enters the eventing
mesh via the Broker and can be delivered to other services using a Trigger
1. Deploy a pod that receives any CloudEvent and logs the event to its output.
```shell ```shell
kubectl --namespace knative-samples apply --filename - << END kubectl --namespace knative-samples apply --filename - << END
# event-display app deploment # event-display app deploment
@ -338,7 +362,8 @@ Helloworld-go app logs the context and the msg of the above event, and replies b
END END
``` ```
1. Create a trigger to deliver the event to the above service 1. Create a trigger to deliver the event to the above service
```shell ```shell
kubectl --namespace knative-samples apply --filename - << END kubectl --namespace knative-samples apply --filename - << END
apiVersion: eventing.knative.dev/v1alpha1 apiVersion: eventing.knative.dev/v1alpha1
@ -360,9 +385,9 @@ Helloworld-go app logs the context and the msg of the above event, and replies b
END END
``` ```
1. [Send a CloudEvent to the Broker](###Send-CloudEvent-to-the-Broker) 1. [Send a CloudEvent to the Broker](###Send-CloudEvent-to-the-Broker)
1. Check the logs of event-display service 1. Check the logs of event-display service
```shell ```shell
kubectl --namespace knative-samples logs -l app=event-display --tail=50 kubectl --namespace knative-samples logs -l app=event-display --tail=50
``` ```
@ -387,9 +412,7 @@ Helloworld-go app logs the context and the msg of the above event, and replies b
} }
``` ```
**Note: You could use the above approach to test your applications too.** **Note: You could use the above approach to test your applications too.**
## Removing the sample app deployment ## Removing the sample app deployment

View File

@ -2,27 +2,20 @@ package main
import ( import (
"context" "context"
"fmt"
"log" "log"
"net/http"
"os"
cloudevents "github.com/cloudevents/sdk-go" cloudevents "github.com/cloudevents/sdk-go/v2"
"github.com/google/uuid" "github.com/google/uuid"
) )
type eventData struct { func receive(ctx context.Context, event cloudevents.Event) (*cloudevents.Event, cloudevents.Result) {
Message string `json:"message,omitempty,string"`
}
func receive(ctx context.Context, event cloudevents.Event, response *cloudevents.EventResponse) error {
// Here is where your code to process the event will go. // Here is where your code to process the event will go.
// In this example we will log the event msg // In this example we will log the event msg
log.Printf("Event received. Context: %v\n", event.Context) log.Printf("Event received. \n%s\n", event)
data := &HelloWorld{} data := &HelloWorld{}
if err := event.DataAs(data); err != nil { if err := event.DataAs(data); err != nil {
log.Printf("Error while extracting cloudevent Data: %s\n", err.Error()) log.Printf("Error while extracting cloudevent Data: %s\n", err.Error())
return err return nil, cloudevents.NewHTTPResult(400, "failed to convert data: %s", err)
} }
log.Printf("Hello World Message from received event %q", data.Msg) log.Printf("Hello World Message from received event %q", data.Msg)
@ -33,21 +26,11 @@ func receive(ctx context.Context, event cloudevents.Event, response *cloudevents
newEvent.SetID(uuid.New().String()) newEvent.SetID(uuid.New().String())
newEvent.SetSource("knative/eventing/samples/hello-world") newEvent.SetSource("knative/eventing/samples/hello-world")
newEvent.SetType("dev.knative.samples.hifromknative") newEvent.SetType("dev.knative.samples.hifromknative")
newEvent.SetData(HiFromKnative{Msg: "Hi from helloworld-go app!"}) if err := newEvent.SetData(cloudevents.ApplicationJSON, HiFromKnative{Msg: "Hi from helloworld-go app!"}); err != nil {
response.RespondWith(200, &newEvent) return nil, cloudevents.NewHTTPResult(500, "failed to set response data: %s", err)
log.Printf("Responded with event %v", newEvent)
return nil
}
func handler(w http.ResponseWriter, r *http.Request) {
log.Print("Hello world received a request.")
target := os.Getenv("TARGET")
if target == "" {
target = "World"
} }
fmt.Fprintf(w, "Hello %s!\n", target) log.Printf("Responding with event\n%s\n", newEvent)
return &newEvent, nil
} }
func main() { func main() {

View File

@ -6,7 +6,7 @@ metadata:
labels: labels:
knative-eventing-injection: enabled knative-eventing-injection: enabled
--- ---
# Helloworld-go app deploment # Helloworld-go app deployment
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@ -41,7 +41,7 @@ spec:
targetPort: 8080 targetPort: 8080
--- ---
# Knative Eventing Trigger to trigger the helloworld-go service # Knative Eventing Trigger to trigger the helloworld-go service
apiVersion: eventing.knative.dev/v1alpha1 apiVersion: eventing.knative.dev/v1beta1
kind: Trigger kind: Trigger
metadata: metadata:
name: helloworld-go name: helloworld-go

View File

@ -7,16 +7,18 @@ type: "docs"
A simple web app written in Go that can receive and send Cloud Events that you A simple web app written in Go that can receive and send Cloud Events that you
can use for testing. It supports running in two modes: can use for testing. It supports running in two modes:
1. The default mode has the app reply to your input events with the output
event, which is simplest for demonstrating things working in isolation,
but is also the model for working for the Knative Eventing `Broker` concept.
2. `K_SINK` mode has the app send events to the destination encoded in `$K_SINK`, 1. The default mode has the app reply to your input events with the output
which is useful to demonstrate how folks can synthesize events to send to event, which is simplest for demonstrating things working in isolation, but
a Service or Broker when not initiated by a Broker invocation (e.g. is also the model for working for the Knative Eventing `Broker` concept.
2. `K_SINK` mode has the app send events to the destination encoded in
`$K_SINK`, which is useful to demonstrate how folks can synthesize events to
send to a Service or Broker when not initiated by a Broker invocation (e.g.
implementing an event source) implementing an event source)
The application will use `$K_SINK`-mode whenever the environment variable is specified. The application will use `$K_SINK`-mode whenever the environment variable is
specified.
Follow the steps below to create the sample code and then deploy the app to your Follow the steps below to create the sample code and then deploy the app to your
cluster. You can also download a working copy of the sample, by running the cluster. You can also download a working copy of the sample, by running the
@ -41,20 +43,20 @@ cd knative-docs/docs/serving/samples/cloudevents/cloudevents-go
different modes of operation: different modes of operation:
```go ```go
func (recv *Receiver) ReceiveAndSend(ctx context.Context, event cloudevents.Event) error { func (recv *Receiver) ReceiveAndSend(ctx context.Context, event cloudevents.Event) cloudevents.Result {
// This is called whenever an event is received if $K_SINK is set, and sends a new event // This is called whenever an event is received if $K_SINK is set, and sends a new event
// to the url in $K_SINK. // to the url in $K_SINK.
} }
func (recv *Receiver) ReceiveAndReply(ctx context.Context, event cloudevents.Event, eventResp *cloudevents.EventResponse) error { func (recv *Receiver) ReceiveAndReply(ctx context.Context, event cloudevents.Event) (*cloudevents.Event, cloudevents.Result) {
// This is called whenever an event is received if $K_SINK is NOT set, and it replies with // This is called whenever an event is received if $K_SINK is NOT set, and it replies with
// the new event instead. // the new event instead.
} }
``` ```
1. If you look in `Dockerfile`, you will see a method for pulling in the dependencies and 1. If you look in `Dockerfile`, you will see a method for pulling in the
building a small Go container based on Alpine. You can build and push this to your dependencies and building a small Go container based on Alpine. You can build
registry of choice via: and push this to your registry of choice via:
```shell ```shell
docker build -t <image> . docker build -t <image> .
@ -110,7 +112,6 @@ You will get back:
{"message":"Hello, Dave"} {"message":"Hello, Dave"}
``` ```
## Removing the sample app deployment ## Removing the sample app deployment
To remove the sample app from your cluster, delete the service record: To remove the sample app from your cluster, delete the service record:

View File

@ -4,9 +4,8 @@ import (
"context" "context"
"fmt" "fmt"
"log" "log"
"net/http"
cloudevents "github.com/cloudevents/sdk-go" cloudevents "github.com/cloudevents/sdk-go/v2"
"github.com/kelseyhightower/envconfig" "github.com/kelseyhightower/envconfig"
) )
@ -55,16 +54,15 @@ type Response struct {
} }
// handle shared the logic for producing the Response event from the Request. // handle shared the logic for producing the Response event from the Request.
func handle(req Request) (resp Response) { func handle(req Request) Response {
resp.Message = fmt.Sprintf("Hello, %s", req.Name) return Response{Message: fmt.Sprintf("Hello, %s", req.Name)}
return
} }
// ReceiveAndSend is invoked whenever we receive an event. // ReceiveAndSend is invoked whenever we receive an event.
func (recv *Receiver) ReceiveAndSend(ctx context.Context, event cloudevents.Event) error { func (recv *Receiver) ReceiveAndSend(ctx context.Context, event cloudevents.Event) cloudevents.Result {
req := Request{} req := Request{}
if err := event.DataAs(&req); err != nil { if err := event.DataAs(&req); err != nil {
return err return cloudevents.NewHTTPResult(400, "failed to convert data: %s", err)
} }
log.Printf("Got an event from: %q", req.Name) log.Printf("Got an event from: %q", req.Name)
@ -74,19 +72,19 @@ func (recv *Receiver) ReceiveAndSend(ctx context.Context, event cloudevents.Even
r := cloudevents.NewEvent(cloudevents.VersionV1) r := cloudevents.NewEvent(cloudevents.VersionV1)
r.SetType("dev.knative.docs.sample") r.SetType("dev.knative.docs.sample")
r.SetSource("https://github.com/knative/docs/docs/serving/samples/cloudevents/cloudevents-go") r.SetSource("https://github.com/knative/docs/docs/serving/samples/cloudevents/cloudevents-go")
r.SetDataContentType("application/json") if err := r.SetData("application/json", resp); err != nil {
r.SetData(resp) return cloudevents.NewHTTPResult(500, "failed to set response data: %s", err)
}
ctx = cloudevents.ContextWithTarget(ctx, recv.Target) ctx = cloudevents.ContextWithTarget(ctx, recv.Target)
_, _, err := recv.client.Send(ctx, r) return recv.client.Send(ctx, r)
return err
} }
// ReceiveAndReply is invoked whenever we receive an event. // ReceiveAndReply is invoked whenever we receive an event.
func (recv *Receiver) ReceiveAndReply(ctx context.Context, event cloudevents.Event, eventResp *cloudevents.EventResponse) error { func (recv *Receiver) ReceiveAndReply(ctx context.Context, event cloudevents.Event) (*cloudevents.Event, cloudevents.Result) {
req := Request{} req := Request{}
if err := event.DataAs(&req); err != nil { if err := event.DataAs(&req); err != nil {
return err return nil, cloudevents.NewHTTPResult(400, "failed to convert data: %s", err)
} }
log.Printf("Got an event from: %q", req.Name) log.Printf("Got an event from: %q", req.Name)
@ -96,10 +94,9 @@ func (recv *Receiver) ReceiveAndReply(ctx context.Context, event cloudevents.Eve
r := cloudevents.NewEvent(cloudevents.VersionV1) r := cloudevents.NewEvent(cloudevents.VersionV1)
r.SetType("dev.knative.docs.sample") r.SetType("dev.knative.docs.sample")
r.SetSource("https://github.com/knative/docs/docs/serving/samples/cloudevents/cloudevents-go") r.SetSource("https://github.com/knative/docs/docs/serving/samples/cloudevents/cloudevents-go")
r.SetDataContentType("application/json") if err := r.SetData("application/json", resp); err != nil {
r.SetData(resp) return nil, cloudevents.NewHTTPResult(500, "failed to set response data: %s", err)
}
eventResp.RespondWith(http.StatusOK, &r) return &r, nil
return nil
} }

View File

@ -1,149 +0,0 @@
package cloudevents
// Package cloudevents alias' common functions and types to improve discoverability and reduce
// the number of imports for simple HTTP clients.
import (
"github.com/cloudevents/sdk-go/pkg/cloudevents"
"github.com/cloudevents/sdk-go/pkg/cloudevents/client"
"github.com/cloudevents/sdk-go/pkg/cloudevents/context"
"github.com/cloudevents/sdk-go/pkg/cloudevents/observability"
"github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http"
"github.com/cloudevents/sdk-go/pkg/cloudevents/types"
)
// Client
type ClientOption client.Option
type Client = client.Client
type ConvertFn = client.ConvertFn
// Event
type Event = cloudevents.Event
type EventResponse = cloudevents.EventResponse
// Context
type EventContext = cloudevents.EventContext
type EventContextV1 = cloudevents.EventContextV1
type EventContextV01 = cloudevents.EventContextV01
type EventContextV02 = cloudevents.EventContextV02
type EventContextV03 = cloudevents.EventContextV03
// Custom Types
type Timestamp = types.Timestamp
type URLRef = types.URLRef
// HTTP Transport
type HTTPOption http.Option
type HTTPTransport = http.Transport
type HTTPTransportContext = http.TransportContext
type HTTPTransportResponseContext = http.TransportResponseContext
type HTTPEncoding = http.Encoding
const (
// Encoding
ApplicationXML = cloudevents.ApplicationXML
ApplicationJSON = cloudevents.ApplicationJSON
ApplicationCloudEventsJSON = cloudevents.ApplicationCloudEventsJSON
ApplicationCloudEventsBatchJSON = cloudevents.ApplicationCloudEventsBatchJSON
Base64 = cloudevents.Base64
// Event Versions
VersionV1 = cloudevents.CloudEventsVersionV1
VersionV01 = cloudevents.CloudEventsVersionV01
VersionV02 = cloudevents.CloudEventsVersionV02
VersionV03 = cloudevents.CloudEventsVersionV03
// HTTP Transport Encodings
HTTPBinaryV1 = http.BinaryV1
HTTPStructuredV1 = http.StructuredV1
HTTPBatchedV1 = http.BatchedV1
HTTPBinaryV01 = http.BinaryV01
HTTPStructuredV01 = http.StructuredV01
HTTPBinaryV02 = http.BinaryV02
HTTPStructuredV02 = http.StructuredV02
HTTPBinaryV03 = http.BinaryV03
HTTPStructuredV03 = http.StructuredV03
HTTPBatchedV03 = http.BatchedV03
// Context HTTP Transport Encodings
Binary = http.Binary
Structured = http.Structured
)
var (
// ContentType Helpers
StringOfApplicationJSON = cloudevents.StringOfApplicationJSON
StringOfApplicationXML = cloudevents.StringOfApplicationXML
StringOfApplicationCloudEventsJSON = cloudevents.StringOfApplicationCloudEventsJSON
StringOfApplicationCloudEventsBatchJSON = cloudevents.StringOfApplicationCloudEventsBatchJSON
StringOfBase64 = cloudevents.StringOfBase64
// Client Creation
NewClient = client.New
NewDefaultClient = client.NewDefault
// Client Options
WithEventDefaulter = client.WithEventDefaulter
WithUUIDs = client.WithUUIDs
WithTimeNow = client.WithTimeNow
WithConverterFn = client.WithConverterFn
// Event Creation
NewEvent = cloudevents.New
// Tracing
EnableTracing = observability.EnableTracing
// Context
ContextWithTarget = context.WithTarget
TargetFromContext = context.TargetFrom
ContextWithEncoding = context.WithEncoding
EncodingFromContext = context.EncodingFrom
// Custom Types
ParseTimestamp = types.ParseTimestamp
ParseURLRef = types.ParseURLRef
ParseURIRef = types.ParseURIRef
ParseURI = types.ParseURI
// HTTP Transport
NewHTTPTransport = http.New
// HTTP Transport Options
WithTarget = http.WithTarget
WithMethod = http.WithMethod
WitHHeader = http.WithHeader
WithShutdownTimeout = http.WithShutdownTimeout
WithEncoding = http.WithEncoding
WithContextBasedEncoding = http.WithContextBasedEncoding
WithBinaryEncoding = http.WithBinaryEncoding
WithStructuredEncoding = http.WithStructuredEncoding
WithPort = http.WithPort
WithPath = http.WithPath
WithMiddleware = http.WithMiddleware
WithLongPollTarget = http.WithLongPollTarget
WithListener = http.WithListener
// HTTP Context
HTTPTransportContextFrom = http.TransportContextFrom
ContextWithHeader = http.ContextWithHeader
)

View File

@ -1,195 +0,0 @@
package client
import (
"context"
"fmt"
"sync"
"github.com/cloudevents/sdk-go/pkg/cloudevents"
"github.com/cloudevents/sdk-go/pkg/cloudevents/observability"
"github.com/cloudevents/sdk-go/pkg/cloudevents/transport"
"github.com/cloudevents/sdk-go/pkg/cloudevents/transport/http"
)
// Client interface defines the runtime contract the CloudEvents client supports.
type Client interface {
// Send will transmit the given event over the client's configured transport.
Send(ctx context.Context, event cloudevents.Event) (context.Context, *cloudevents.Event, error)
// StartReceiver will register the provided function for callback on receipt
// of a cloudevent. It will also start the underlying transport as it has
// been configured.
// This call is blocking.
// Valid fn signatures are:
// * func()
// * func() error
// * func(context.Context)
// * func(context.Context) error
// * func(cloudevents.Event)
// * func(cloudevents.Event) error
// * func(context.Context, cloudevents.Event)
// * func(context.Context, cloudevents.Event) error
// * func(cloudevents.Event, *cloudevents.EventResponse)
// * func(cloudevents.Event, *cloudevents.EventResponse) error
// * func(context.Context, cloudevents.Event, *cloudevents.EventResponse)
// * func(context.Context, cloudevents.Event, *cloudevents.EventResponse) error
// Note: if fn returns an error, it is treated as a critical and
// EventResponse will not be processed.
StartReceiver(ctx context.Context, fn interface{}) error
}
// New produces a new client with the provided transport object and applied
// client options.
func New(t transport.Transport, opts ...Option) (Client, error) {
c := &ceClient{
transport: t,
}
if err := c.applyOptions(opts...); err != nil {
return nil, err
}
t.SetReceiver(c)
return c, nil
}
// NewDefault provides the good defaults for the common case using an HTTP
// Transport client. The http transport has had WithBinaryEncoding http
// transport option applied to it. The client will always send Binary
// encoding but will inspect the outbound event context and match the version.
// The WithtimeNow and WithUUIDs client options are also applied to the client,
// all outbound events will have a time and id set if not already present.
func NewDefault() (Client, error) {
t, err := http.New(http.WithBinaryEncoding())
if err != nil {
return nil, err
}
c, err := New(t, WithTimeNow(), WithUUIDs())
if err != nil {
return nil, err
}
return c, nil
}
type ceClient struct {
transport transport.Transport
fn *receiverFn
convertFn ConvertFn
receiverMu sync.Mutex
eventDefaulterFns []EventDefaulter
}
// Send transmits the provided event on a preconfigured Transport.
// Send returns a response event if there is a response or an error if there
// was an an issue validating the outbound event or the transport returns an
// error.
func (c *ceClient) Send(ctx context.Context, event cloudevents.Event) (context.Context, *cloudevents.Event, error) {
ctx, r := observability.NewReporter(ctx, reportSend)
rctx, resp, err := c.obsSend(ctx, event)
if err != nil {
r.Error()
} else {
r.OK()
}
return rctx, resp, err
}
func (c *ceClient) obsSend(ctx context.Context, event cloudevents.Event) (context.Context, *cloudevents.Event, error) {
// Confirm we have a transport set.
if c.transport == nil {
return ctx, nil, fmt.Errorf("client not ready, transport not initialized")
}
// Apply the defaulter chain to the incoming event.
if len(c.eventDefaulterFns) > 0 {
for _, fn := range c.eventDefaulterFns {
event = fn(ctx, event)
}
}
// Validate the event conforms to the CloudEvents Spec.
if err := event.Validate(); err != nil {
return ctx, nil, err
}
// Send the event over the transport.
return c.transport.Send(ctx, event)
}
// Receive is called from from the transport on event delivery.
func (c *ceClient) Receive(ctx context.Context, event cloudevents.Event, resp *cloudevents.EventResponse) error {
ctx, r := observability.NewReporter(ctx, reportReceive)
err := c.obsReceive(ctx, event, resp)
if err != nil {
r.Error()
} else {
r.OK()
}
return err
}
func (c *ceClient) obsReceive(ctx context.Context, event cloudevents.Event, resp *cloudevents.EventResponse) error {
if c.fn != nil {
ctx, rFn := observability.NewReporter(ctx, reportReceiveFn)
err := c.fn.invoke(ctx, event, resp)
if err != nil {
rFn.Error()
} else {
rFn.OK()
}
// Apply the defaulter chain to the outgoing event.
if err == nil && resp != nil && resp.Event != nil && len(c.eventDefaulterFns) > 0 {
for _, fn := range c.eventDefaulterFns {
*resp.Event = fn(ctx, *resp.Event)
}
// Validate the event conforms to the CloudEvents Spec.
if err := resp.Event.Validate(); err != nil {
return fmt.Errorf("cloudevent validation failed on response event: %v", err)
}
}
return err
}
return nil
}
// StartReceiver sets up the given fn to handle Receive.
// See Client.StartReceiver for details. This is a blocking call.
func (c *ceClient) StartReceiver(ctx context.Context, fn interface{}) error {
c.receiverMu.Lock()
defer c.receiverMu.Unlock()
if c.transport == nil {
return fmt.Errorf("client not ready, transport not initialized")
}
if c.fn != nil {
return fmt.Errorf("client already has a receiver")
}
if fn, err := receiver(fn); err != nil {
return err
} else {
c.fn = fn
}
defer func() {
c.fn = nil
}()
return c.transport.StartReceiver(ctx)
}
func (c *ceClient) applyOptions(opts ...Option) error {
for _, fn := range opts {
if err := fn(c); err != nil {
return err
}
}
return nil
}
// Convert implements transport Converter.Convert.
func (c *ceClient) Convert(ctx context.Context, m transport.Message, err error) (*cloudevents.Event, error) {
if c.convertFn != nil {
return c.convertFn(ctx, m, err)
}
return nil, err
}

View File

@ -1,68 +0,0 @@
package client
import (
"github.com/cloudevents/sdk-go/pkg/cloudevents/observability"
"go.opencensus.io/stats"
"go.opencensus.io/stats/view"
)
var (
// LatencyMs measures the latency in milliseconds for the CloudEvents
// client methods.
LatencyMs = stats.Float64("cloudevents.io/sdk-go/client/latency", "The latency in milliseconds for the CloudEvents client methods.", "ms")
)
var (
// LatencyView is an OpenCensus view that shows client method latency.
LatencyView = &view.View{
Name: "client/latency",
Measure: LatencyMs,
Description: "The distribution of latency inside of client for CloudEvents.",
Aggregation: view.Distribution(0, .01, .1, 1, 10, 100, 1000, 10000),
TagKeys: observability.LatencyTags(),
}
)
type observed int32
// Adheres to Observable
var _ observability.Observable = observed(0)
const (
reportSend observed = iota
reportReceive
reportReceiveFn
)
// TraceName implements Observable.TraceName
func (o observed) TraceName() string {
switch o {
case reportSend:
return "client/send"
case reportReceive:
return "client/receive"
case reportReceiveFn:
return "client/receive/fn"
default:
return "client/unknown"
}
}
// MethodName implements Observable.MethodName
func (o observed) MethodName() string {
switch o {
case reportSend:
return "send"
case reportReceive:
return "receive"
case reportReceiveFn:
return "receive/fn"
default:
return "unknown"
}
}
// LatencyMs implements Observable.LatencyMs
func (o observed) LatencyMs() *stats.Float64Measure {
return LatencyMs
}

View File

@ -1,53 +0,0 @@
package client
import (
"fmt"
)
// Option is the function signature required to be considered an client.Option.
type Option func(*ceClient) error
// WithEventDefaulter adds an event defaulter to the end of the defaulter chain.
func WithEventDefaulter(fn EventDefaulter) Option {
return func(c *ceClient) error {
if fn == nil {
return fmt.Errorf("client option was given an nil event defaulter")
}
c.eventDefaulterFns = append(c.eventDefaulterFns, fn)
return nil
}
}
// WithUUIDs adds DefaultIDToUUIDIfNotSet event defaulter to the end of the
// defaulter chain.
func WithUUIDs() Option {
return func(c *ceClient) error {
c.eventDefaulterFns = append(c.eventDefaulterFns, DefaultIDToUUIDIfNotSet)
return nil
}
}
// WithTimeNow adds DefaultTimeToNowIfNotSet event defaulter to the end of the
// defaulter chain.
func WithTimeNow() Option {
return func(c *ceClient) error {
c.eventDefaulterFns = append(c.eventDefaulterFns, DefaultTimeToNowIfNotSet)
return nil
}
}
// WithConverterFn defines the function the transport will use to delegate
// conversion of non-decodable messages.
func WithConverterFn(fn ConvertFn) Option {
return func(c *ceClient) error {
if fn == nil {
return fmt.Errorf("client option was given an nil message converter")
}
if c.transport.HasConverter() {
return fmt.Errorf("transport converter already set")
}
c.convertFn = fn
c.transport.SetConverter(c)
return nil
}
}

View File

@ -1,193 +0,0 @@
package client
import (
"context"
"errors"
"fmt"
"reflect"
"github.com/cloudevents/sdk-go/pkg/cloudevents"
"github.com/cloudevents/sdk-go/pkg/cloudevents/transport"
)
// Receive is the signature of a fn to be invoked for incoming cloudevents.
// If fn returns an error, EventResponse will not be considered by the client or
// or transport.
// This is just an FYI:
type ReceiveFull func(context.Context, cloudevents.Event, *cloudevents.EventResponse) error
type receiverFn struct {
numIn int
fnValue reflect.Value
hasContextIn bool
hasEventIn bool
hasEventResponseIn bool
hasErrorOut bool
}
// ConvertFn defines the signature the client expects to enable conversion
// delegation.
type ConvertFn func(context.Context, transport.Message, error) (*cloudevents.Event, error)
const (
inParamUsage = "expected a function taking either no parameters, one or more of (context.Context, cloudevents.Event, *cloudevents.EventResponse) ordered"
outParamUsage = "expected a function returning either nothing or an error"
)
var (
contextType = reflect.TypeOf((*context.Context)(nil)).Elem()
eventType = reflect.TypeOf((*cloudevents.Event)(nil)).Elem()
eventResponseType = reflect.TypeOf((*cloudevents.EventResponse)(nil)) // want the ptr type
errorType = reflect.TypeOf((*error)(nil)).Elem()
)
// receiver creates a receiverFn wrapper class that is used by the client to
// validate and invoke the provided function.
// Valid fn signatures are:
// * func()
// * func() error
// * func(context.Context)
// * func(context.Context) error
// * func(cloudevents.Event)
// * func(cloudevents.Event) error
// * func(context.Context, cloudevents.Event)
// * func(context.Context, cloudevents.Event) error
// * func(cloudevents.Event, *cloudevents.EventResponse)
// * func(cloudevents.Event, *cloudevents.EventResponse) error
// * func(context.Context, cloudevents.Event, *cloudevents.EventResponse)
// * func(context.Context, cloudevents.Event, *cloudevents.EventResponse) error
//
func receiver(fn interface{}) (*receiverFn, error) {
fnType := reflect.TypeOf(fn)
if fnType.Kind() != reflect.Func {
return nil, errors.New("must pass a function to handle events")
}
r := &receiverFn{
fnValue: reflect.ValueOf(fn),
numIn: fnType.NumIn(),
}
if err := r.validate(fnType); err != nil {
return nil, err
}
return r, nil
}
func (r *receiverFn) invoke(ctx context.Context, event cloudevents.Event, resp *cloudevents.EventResponse) error {
args := make([]reflect.Value, 0, r.numIn)
if r.numIn > 0 {
if r.hasContextIn {
args = append(args, reflect.ValueOf(ctx))
}
if r.hasEventIn {
args = append(args, reflect.ValueOf(event))
}
if r.hasEventResponseIn {
args = append(args, reflect.ValueOf(resp))
}
}
v := r.fnValue.Call(args)
if r.hasErrorOut && len(v) >= 1 {
if err, ok := v[0].Interface().(error); ok {
return err
}
}
return nil
}
// Verifies that the inputs to a function have a valid signature
// Valid input is to be [0, all] of
// context.Context, cloudevents.Event, *cloudevents.EventResponse in this order.
func (r *receiverFn) validateInParamSignature(fnType reflect.Type) error {
r.hasContextIn = false
r.hasEventIn = false
r.hasEventResponseIn = false
switch fnType.NumIn() {
case 3:
// has to be cloudevents.Event, *cloudevents.EventResponse
if !fnType.In(2).ConvertibleTo(eventResponseType) {
return fmt.Errorf("%s; cannot convert parameter 2 from %s to *cloudevents.EventResponse", inParamUsage, fnType.In(2))
} else {
r.hasEventResponseIn = true
}
fallthrough
case 2:
// can be cloudevents.Event or *cloudevents.EventResponse
if !fnType.In(1).ConvertibleTo(eventResponseType) {
if !fnType.In(1).ConvertibleTo(eventType) {
return fmt.Errorf("%s; cannot convert parameter 1 from %s to cloudevents.Event or *cloudevents.EventResponse", inParamUsage, fnType.In(1))
} else {
r.hasEventIn = true
}
} else if r.hasEventResponseIn {
return fmt.Errorf("%s; duplicate parameter of type *cloudevents.EventResponse", inParamUsage)
} else {
r.hasEventResponseIn = true
}
fallthrough
case 1:
if !fnType.In(0).ConvertibleTo(contextType) {
if !fnType.In(0).ConvertibleTo(eventResponseType) {
if !fnType.In(0).ConvertibleTo(eventType) {
return fmt.Errorf("%s; cannot convert parameter 0 from %s to context.Context, cloudevents.Event or *cloudevents.EventResponse", inParamUsage, fnType.In(0))
} else if r.hasEventIn {
return fmt.Errorf("%s; duplicate parameter of type cloudevents.Event", inParamUsage)
} else {
r.hasEventIn = true
}
} else if r.hasEventResponseIn {
return fmt.Errorf("%s; duplicate parameter of type *cloudevents.EventResponse", inParamUsage)
} else if r.hasEventIn {
return fmt.Errorf("%s; out of order parameter 0 for %s", inParamUsage, fnType.In(1))
} else {
r.hasEventResponseIn = true
}
} else {
r.hasContextIn = true
}
fallthrough
case 0:
return nil
default:
return fmt.Errorf("%s; function has too many parameters (%d)", inParamUsage, fnType.NumIn())
}
}
// Verifies that the outputs of a function have a valid signature
// Valid output signatures:
// (), (error)
func (r *receiverFn) validateOutParamSignature(fnType reflect.Type) error {
r.hasErrorOut = false
switch fnType.NumOut() {
case 1:
paramNo := fnType.NumOut() - 1
paramType := fnType.Out(paramNo)
if !paramType.ConvertibleTo(errorType) {
return fmt.Errorf("%s; cannot convert return type %d from %s to error", outParamUsage, paramNo, paramType)
} else {
r.hasErrorOut = true
}
fallthrough
case 0:
return nil
default:
return fmt.Errorf("%s; function has too many return types (%d)", outParamUsage, fnType.NumOut())
}
}
// validateReceiverFn validates that a function has the right number of in and
// out params and that they are of allowed types.
func (r *receiverFn) validate(fnType reflect.Type) error {
if err := r.validateInParamSignature(fnType); err != nil {
return err
}
if err := r.validateOutParamSignature(fnType); err != nil {
return err
}
return nil
}

View File

@ -1,97 +0,0 @@
package json
import (
"context"
"encoding/json"
"fmt"
"reflect"
"strconv"
"github.com/cloudevents/sdk-go/pkg/cloudevents/observability"
)
// Decode takes `in` as []byte, or base64 string, normalizes in to unquoted and
// base64 decoded []byte if required, and then attempts to use json.Unmarshal
// to convert those bytes to `out`. Returns and error if this process fails.
func Decode(ctx context.Context, in, out interface{}) error {
_, r := observability.NewReporter(ctx, reportDecode)
err := obsDecode(ctx, in, out)
if err != nil {
r.Error()
} else {
r.OK()
}
return err
}
func obsDecode(ctx context.Context, in, out interface{}) error {
if in == nil {
return nil
}
if out == nil {
return fmt.Errorf("out is nil")
}
b, ok := in.([]byte) // TODO: I think there is fancy marshaling happening here. Fix with reflection?
if !ok {
var err error
b, err = json.Marshal(in)
if err != nil {
return fmt.Errorf("[json] failed to marshal in: %s", err.Error())
}
}
// TODO: the spec says json could be just data... At the moment we expect wrapped.
if len(b) > 1 && (b[0] == byte('"') || (b[0] == byte('\\') && b[1] == byte('"'))) {
s, err := strconv.Unquote(string(b))
if err != nil {
return fmt.Errorf("[json] failed to unquote in: %s", err.Error())
}
if len(s) > 0 && (s[0] == '{' || s[0] == '[') {
// looks like json, use it
b = []byte(s)
}
}
if err := json.Unmarshal(b, out); err != nil {
return fmt.Errorf("[json] found bytes \"%s\", but failed to unmarshal: %s", string(b), err.Error())
}
return nil
}
// Encode attempts to json.Marshal `in` into bytes. Encode will inspect `in`
// and returns `in` unmodified if it is detected that `in` is already a []byte;
// Or json.Marshal errors.
func Encode(ctx context.Context, in interface{}) ([]byte, error) {
_, r := observability.NewReporter(ctx, reportEncode)
b, err := obsEncode(ctx, in)
if err != nil {
r.Error()
} else {
r.OK()
}
return b, err
}
func obsEncode(ctx context.Context, in interface{}) ([]byte, error) {
if in == nil {
return nil, nil
}
it := reflect.TypeOf(in)
switch it.Kind() {
case reflect.Slice:
if it.Elem().Kind() == reflect.Uint8 {
if b, ok := in.([]byte); ok && len(b) > 0 {
// check to see if it is a pre-encoded byte string.
if b[0] == byte('"') || b[0] == byte('{') || b[0] == byte('[') {
return b, nil
}
}
}
}
return json.Marshal(in)
}

View File

@ -1,90 +0,0 @@
package xml
import (
"context"
"encoding/base64"
"encoding/xml"
"fmt"
"strconv"
"github.com/cloudevents/sdk-go/pkg/cloudevents/observability"
)
// Decode takes `in` as []byte, or base64 string, normalizes in to unquoted and
// base64 decoded []byte if required, and then attempts to use xml.Unmarshal
// to convert those bytes to `out`. Returns and error if this process fails.
func Decode(ctx context.Context, in, out interface{}) error {
_, r := observability.NewReporter(ctx, reportDecode)
err := obsDecode(ctx, in, out)
if err != nil {
r.Error()
} else {
r.OK()
}
return err
}
func obsDecode(ctx context.Context, in, out interface{}) error {
if in == nil {
return nil
}
b, ok := in.([]byte)
if !ok {
var err error
b, err = xml.Marshal(in)
if err != nil {
return fmt.Errorf("[xml] failed to marshal in: %s", err.Error())
}
}
// If the message is encoded as a base64 block as a string, we need to
// decode that first before trying to unmarshal the bytes
if len(b) > 1 && (b[0] == byte('"') || (b[0] == byte('\\') && b[1] == byte('"'))) {
s, err := strconv.Unquote(string(b))
if err != nil {
return fmt.Errorf("[xml] failed to unquote quoted data: %s", err.Error())
}
if len(s) > 0 && s[0] == '<' {
// looks like xml, use it
b = []byte(s)
} else if len(s) > 0 {
// looks like base64, decode
bs, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return fmt.Errorf("[xml] failed to decode base64 encoded string: %s", err.Error())
}
b = bs
}
}
if err := xml.Unmarshal(b, out); err != nil {
return fmt.Errorf("[xml] found bytes, but failed to unmarshal: %s %s", err.Error(), string(b))
}
return nil
}
// Encode attempts to xml.Marshal `in` into bytes. Encode will inspect `in`
// and returns `in` unmodified if it is detected that `in` is already a []byte;
// Or xml.Marshal errors.
func Encode(ctx context.Context, in interface{}) ([]byte, error) {
_, r := observability.NewReporter(ctx, reportEncode)
b, err := obsEncode(ctx, in)
if err != nil {
r.Error()
} else {
r.OK()
}
return b, err
}
func obsEncode(ctx context.Context, in interface{}) ([]byte, error) {
if b, ok := in.([]byte); ok {
// check to see if it is a pre-encoded byte string.
if len(b) > 0 && b[0] == byte('"') {
return b, nil
}
}
return xml.Marshal(in)
}

View File

@ -1,108 +0,0 @@
package cloudevents
import (
"bytes"
"encoding/json"
"fmt"
"strings"
)
// Event represents the canonical representation of a CloudEvent.
type Event struct {
Context EventContext
Data interface{}
DataEncoded bool
DataBinary bool
}
const (
defaultEventVersion = CloudEventsVersionV02
)
// New returns a new Event, an optional version can be passed to change the
// default spec version from 0.2 to the provided version.
func New(version ...string) Event {
specVersion := defaultEventVersion // TODO: should there be a default? or set a default?
if len(version) >= 1 {
specVersion = version[0]
}
e := &Event{}
e.SetSpecVersion(specVersion)
return *e
}
// DEPRECATED: Access extensions directly via the e.Extensions() map.
// Use functions in the types package to convert extension values.
// For example replace this:
//
// var i int
// err := e.ExtensionAs("foo", &i)
//
// With this:
//
// i, err := types.ToInteger(e.Extensions["foo"])
//
func (e Event) ExtensionAs(name string, obj interface{}) error {
return e.Context.ExtensionAs(name, obj)
}
// Validate performs a spec based validation on this event.
// Validation is dependent on the spec version specified in the event context.
func (e Event) Validate() error {
if e.Context == nil {
return fmt.Errorf("every event conforming to the CloudEvents specification MUST include a context")
}
if err := e.Context.Validate(); err != nil {
return err
}
// TODO: validate data.
return nil
}
// String returns a pretty-printed representation of the Event.
func (e Event) String() string {
b := strings.Builder{}
b.WriteString("Validation: ")
valid := e.Validate()
if valid == nil {
b.WriteString("valid\n")
} else {
b.WriteString("invalid\n")
}
if valid != nil {
b.WriteString(fmt.Sprintf("Validation Error: \n%s\n", valid.Error()))
}
b.WriteString(e.Context.String())
if e.Data != nil {
b.WriteString("Data,\n ")
if strings.HasPrefix(e.DataContentType(), ApplicationJSON) {
var prettyJSON bytes.Buffer
data, ok := e.Data.([]byte)
if !ok {
var err error
data, err = json.Marshal(e.Data)
if err != nil {
data = []byte(err.Error())
}
}
err := json.Indent(&prettyJSON, data, " ", " ")
if err != nil {
b.Write(e.Data.([]byte))
} else {
b.Write(prettyJSON.Bytes())
}
} else {
b.Write(e.Data.([]byte))
}
b.WriteString("\n")
}
return b.String()
}

View File

@ -1,135 +0,0 @@
package cloudevents
import (
"context"
"encoding/base64"
"errors"
"fmt"
"strconv"
"github.com/cloudevents/sdk-go/pkg/cloudevents/datacodec"
)
// Data is special. Break it out into it's own file.
// SetData implements EventWriter.SetData
func (e *Event) SetData(obj interface{}) error {
if e.SpecVersion() != CloudEventsVersionV1 {
return e.legacySetData(obj)
}
// Version 1.0 and above.
// TODO: we will have to be smarter about how data relates to media type.
// but the issue is we can not just encode data anymore without understanding
// what the encoding will be on the outbound event. Structured will use
// data_base64, binary will not (if the transport supports binary mode).
// TODO: look at content encoding too.
switch obj.(type) {
case []byte:
e.Data = obj
e.DataEncoded = true
e.DataBinary = true
default:
data, err := datacodec.Encode(context.Background(), e.DataMediaType(), obj)
if err != nil {
return err
}
e.Data = data
e.DataEncoded = true
e.DataBinary = false
}
return nil
}
func (e *Event) legacySetData(obj interface{}) error {
data, err := datacodec.Encode(context.Background(), e.DataMediaType(), obj)
if err != nil {
return err
}
if e.DeprecatedDataContentEncoding() == Base64 {
buf := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
base64.StdEncoding.Encode(buf, data)
e.Data = string(buf)
} else {
e.Data = data
}
e.DataEncoded = true
return nil
}
func (e *Event) DataBytes() ([]byte, error) {
if !e.DataEncoded {
if err := e.SetData(e.Data); err != nil {
return nil, err
}
}
b, ok := e.Data.([]byte)
if !ok {
if s, ok := e.Data.(string); ok {
b = []byte(s)
} else {
// No data.
return []byte(nil), nil
}
}
return b, nil
}
const (
quotes = `"'`
)
// DataAs attempts to populate the provided data object with the event payload.
// data should be a pointer type.
func (e Event) DataAs(data interface{}) error { // TODO: Clean this function up
if e.Data == nil {
return nil
}
obj, ok := e.Data.([]byte)
if !ok {
if s, ok := e.Data.(string); ok {
obj = []byte(s)
} else {
return errors.New("data was not a byte slice or string")
}
}
if len(obj) == 0 {
// No data.
return nil
}
if e.Context.DeprecatedGetDataContentEncoding() == Base64 {
var bs []byte
// test to see if we need to unquote the data.
if obj[0] == quotes[0] || obj[0] == quotes[1] {
str, err := strconv.Unquote(string(obj))
if err != nil {
return err
}
bs = []byte(str)
} else {
bs = obj
}
buf := make([]byte, base64.StdEncoding.DecodedLen(len(bs)))
n, err := base64.StdEncoding.Decode(buf, bs)
if err != nil {
return fmt.Errorf("failed to decode data from base64: %s", err.Error())
}
obj = buf[:n]
}
mediaType := ""
if e.Context.GetDataContentType() != "" {
var err error
mediaType, err = e.Context.GetDataMediaType()
if err != nil {
return err
}
}
return datacodec.Decode(context.Background(), mediaType, obj, data)
}

View File

@ -1,37 +0,0 @@
package cloudevents
// EventResponse represents the canonical representation of a Response to a
// CloudEvent from a receiver. Response implementation is Transport dependent.
type EventResponse struct {
Status int
Event *Event
Reason string
// Context is transport specific struct to allow for controlling transport
// response details.
// For example, see http.TransportResponseContext.
Context interface{}
}
// RespondWith sets up the instance of EventResponse to be set with status and
// an event. Response implementation is Transport dependent.
func (e *EventResponse) RespondWith(status int, event *Event) {
if e == nil {
// if nil, response not supported
return
}
e.Status = status
if event != nil {
e.Event = event
}
}
// Error sets the instance of EventResponse to be set with an error code and
// reason string. Response implementation is Transport dependent.
func (e *EventResponse) Error(status int, reason string) {
if e == nil {
// if nil, response not supported
return
}
e.Status = status
e.Reason = reason
}

View File

@ -1,272 +0,0 @@
package cloudevents
import (
"fmt"
"sort"
"strings"
"github.com/cloudevents/sdk-go/pkg/cloudevents/types"
)
const (
// CloudEventsVersionV01 represents the version 0.1 of the CloudEvents spec.
CloudEventsVersionV01 = "0.1"
)
// EventContextV01 holds standard metadata about an event. See
// https://github.com/cloudevents/spec/blob/v0.1/spec.md#context-attributes for
// details on these fields.
type EventContextV01 struct {
// The version of the CloudEvents specification used by the event.
CloudEventsVersion string `json:"cloudEventsVersion,omitempty"`
// ID of the event; must be non-empty and unique within the scope of the producer.
EventID string `json:"eventID"`
// Timestamp when the event happened.
EventTime *types.Timestamp `json:"eventTime,omitempty"`
// Type of occurrence which has happened.
EventType string `json:"eventType"`
// The version of the `eventType`; this is producer-specific.
EventTypeVersion *string `json:"eventTypeVersion,omitempty"`
// A link to the schema that the `data` attribute adheres to.
SchemaURL *types.URLRef `json:"schemaURL,omitempty"`
// A MIME (RFC 2046) string describing the media type of `data`.
// TODO: Should an empty string assume `application/json`, or auto-detect the content?
ContentType *string `json:"contentType,omitempty"`
// A URI describing the event producer.
Source types.URLRef `json:"source"`
// Additional metadata without a well-defined structure.
Extensions map[string]interface{} `json:"extensions,omitempty"`
}
// Adhere to EventContext
var _ EventContext = (*EventContextV01)(nil)
// ExtensionAs implements EventContextReader.ExtensionAs
func (ec EventContextV01) ExtensionAs(name string, obj interface{}) error {
value, ok := ec.Extensions[name]
if !ok {
return fmt.Errorf("extension %q does not exist", name)
}
// Only support *string for now.
switch v := obj.(type) {
case *string:
if valueAsString, ok := value.(string); ok {
*v = valueAsString
return nil
} else {
return fmt.Errorf("invalid type for extension %q", name)
}
default:
return fmt.Errorf("unknown extension type %T", obj)
}
}
// SetExtension adds the extension 'name' with value 'value' to the CloudEvents context.
func (ec *EventContextV01) SetExtension(name string, value interface{}) error {
if ec.Extensions == nil {
ec.Extensions = make(map[string]interface{})
}
if value == nil {
delete(ec.Extensions, name)
} else {
ec.Extensions[name] = value
}
return nil
}
// Clone implements EventContextConverter.Clone
func (ec EventContextV01) Clone() EventContext {
return ec.AsV01()
}
// AsV01 implements EventContextConverter.AsV01
func (ec EventContextV01) AsV01() *EventContextV01 {
ec.CloudEventsVersion = CloudEventsVersionV01
return &ec
}
// AsV02 implements EventContextConverter.AsV02
func (ec EventContextV01) AsV02() *EventContextV02 {
ret := EventContextV02{
SpecVersion: CloudEventsVersionV02,
Type: ec.EventType,
Source: ec.Source,
ID: ec.EventID,
Time: ec.EventTime,
SchemaURL: ec.SchemaURL,
ContentType: ec.ContentType,
Extensions: make(map[string]interface{}),
}
// eventTypeVersion was retired in v0.2, so put it in an extension.
if ec.EventTypeVersion != nil {
_ = ret.SetExtension(EventTypeVersionKey, *ec.EventTypeVersion)
}
if ec.Extensions != nil {
for k, v := range ec.Extensions {
ret.Extensions[k] = v
}
}
if len(ret.Extensions) == 0 {
ret.Extensions = nil
}
return &ret
}
// AsV03 implements EventContextConverter.AsV03
func (ec EventContextV01) AsV03() *EventContextV03 {
return ec.AsV02().AsV03()
}
// AsV1 implements EventContextConverter.AsV1
func (ec EventContextV01) AsV1() *EventContextV1 {
return ec.AsV02().AsV03().AsV1()
}
// Validate returns errors based on requirements from the CloudEvents spec.
// For more details, see https://github.com/cloudevents/spec/blob/v0.1/spec.md
func (ec EventContextV01) Validate() error {
errors := []string(nil)
// eventType
// Type: String
// Constraints:
// REQUIRED
// MUST be a non-empty string
// SHOULD be prefixed with a reverse-DNS name. The prefixed domain dictates the organization which defines the semantics of this event type.
eventType := strings.TrimSpace(ec.EventType)
if eventType == "" {
errors = append(errors, "eventType: MUST be a non-empty string")
}
// eventTypeVersion
// Type: String
// Constraints:
// OPTIONAL
// If present, MUST be a non-empty string
if ec.EventTypeVersion != nil {
eventTypeVersion := strings.TrimSpace(*ec.EventTypeVersion)
if eventTypeVersion == "" {
errors = append(errors, "eventTypeVersion: if present, MUST be a non-empty string")
}
}
// cloudEventsVersion
// Type: String
// Constraints:
// REQUIRED
// MUST be a non-empty string
cloudEventsVersion := strings.TrimSpace(ec.CloudEventsVersion)
if cloudEventsVersion == "" {
errors = append(errors, "cloudEventsVersion: MUST be a non-empty string")
}
// source
// Type: URI
// Constraints:
// REQUIRED
source := strings.TrimSpace(ec.Source.String())
if source == "" {
errors = append(errors, "source: REQUIRED")
}
// eventID
// Type: String
// Constraints:
// REQUIRED
// MUST be a non-empty string
// MUST be unique within the scope of the producer
eventID := strings.TrimSpace(ec.EventID)
if eventID == "" {
errors = append(errors, "eventID: MUST be a non-empty string")
// no way to test "MUST be unique within the scope of the producer"
}
// eventTime
// Type: Timestamp
// Constraints:
// OPTIONAL
// If present, MUST adhere to the format specified in RFC 3339
// --> no need to test this, no way to set the eventTime without it being valid.
// schemaURL
// Type: URI
// Constraints:
// OPTIONAL
// If present, MUST adhere to the format specified in RFC 3986
if ec.SchemaURL != nil {
schemaURL := strings.TrimSpace(ec.SchemaURL.String())
// empty string is not RFC 3986 compatible.
if schemaURL == "" {
errors = append(errors, "schemaURL: if present, MUST adhere to the format specified in RFC 3986")
}
}
// contentType
// Type: String per RFC 2046
// Constraints:
// OPTIONAL
// If present, MUST adhere to the format specified in RFC 2046
if ec.ContentType != nil {
contentType := strings.TrimSpace(*ec.ContentType)
if contentType == "" {
// TODO: need to test for RFC 2046
errors = append(errors, "contentType: if present, MUST adhere to the format specified in RFC 2046")
}
}
// extensions
// Type: Map
// Constraints:
// OPTIONAL
// If present, MUST contain at least one entry
if ec.Extensions != nil {
if len(ec.Extensions) == 0 {
errors = append(errors, "extensions: if present, MUST contain at least one entry")
}
}
if len(errors) > 0 {
return fmt.Errorf(strings.Join(errors, "\n"))
}
return nil
}
// String returns a pretty-printed representation of the EventContext.
func (ec EventContextV01) String() string {
b := strings.Builder{}
b.WriteString("Context Attributes,\n")
b.WriteString(" cloudEventsVersion: " + ec.CloudEventsVersion + "\n")
b.WriteString(" eventType: " + ec.EventType + "\n")
if ec.EventTypeVersion != nil {
b.WriteString(" eventTypeVersion: " + *ec.EventTypeVersion + "\n")
}
b.WriteString(" source: " + ec.Source.String() + "\n")
b.WriteString(" eventID: " + ec.EventID + "\n")
if ec.EventTime != nil {
b.WriteString(" eventTime: " + ec.EventTime.String() + "\n")
}
if ec.SchemaURL != nil {
b.WriteString(" schemaURL: " + ec.SchemaURL.String() + "\n")
}
if ec.ContentType != nil {
b.WriteString(" contentType: " + *ec.ContentType + "\n")
}
if ec.Extensions != nil && len(ec.Extensions) > 0 {
b.WriteString("Extensions,\n")
keys := make([]string, 0, len(ec.Extensions))
for k := range ec.Extensions {
keys = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
b.WriteString(fmt.Sprintf(" %s: %v\n", key, ec.Extensions[key]))
}
}
return b.String()
}

View File

@ -1,101 +0,0 @@
package cloudevents
import (
"fmt"
"mime"
"time"
)
// Adhere to EventContextReader
var _ EventContextReader = (*EventContextV01)(nil)
// GetSpecVersion implements EventContextReader.GetSpecVersion
func (ec EventContextV01) GetSpecVersion() string {
if ec.CloudEventsVersion != "" {
return ec.CloudEventsVersion
}
return CloudEventsVersionV01
}
// GetDataContentType implements EventContextReader.GetDataContentType
func (ec EventContextV01) GetDataContentType() string {
if ec.ContentType != nil {
return *ec.ContentType
}
return ""
}
// GetDataMediaType implements EventContextReader.GetDataMediaType
func (ec EventContextV01) GetDataMediaType() (string, error) {
if ec.ContentType != nil {
mediaType, _, err := mime.ParseMediaType(*ec.ContentType)
if err != nil {
return "", err
}
return mediaType, nil
}
return "", nil
}
// GetType implements EventContextReader.GetType
func (ec EventContextV01) GetType() string {
return ec.EventType
}
// GetSource implements EventContextReader.GetSource
func (ec EventContextV01) GetSource() string {
return ec.Source.String()
}
// GetSubject implements EventContextReader.GetSubject
func (ec EventContextV01) GetSubject() string {
var sub string
if err := ec.ExtensionAs(SubjectKey, &sub); err != nil {
return ""
}
return sub
}
// GetID implements EventContextReader.GetID
func (ec EventContextV01) GetID() string {
return ec.EventID
}
// GetTime implements EventContextReader.GetTime
func (ec EventContextV01) GetTime() time.Time {
if ec.EventTime != nil {
return ec.EventTime.Time
}
return time.Time{}
}
// GetDataSchema implements EventContextReader.GetDataSchema
func (ec EventContextV01) GetDataSchema() string {
if ec.SchemaURL != nil {
return ec.SchemaURL.String()
}
return ""
}
// DeprecatedGetDataContentEncoding implements EventContextReader.DeprecatedGetDataContentEncoding
func (ec EventContextV01) DeprecatedGetDataContentEncoding() string {
var enc string
if err := ec.ExtensionAs(DataContentEncodingKey, &enc); err != nil {
return ""
}
return enc
}
// GetExtensions implements EventContextReader.GetExtensions
func (ec EventContextV01) GetExtensions() map[string]interface{} {
return ec.Extensions
}
// GetExtension implements EventContextReader.GetExtension
func (ec EventContextV01) GetExtension(key string) (interface{}, error) {
v, ok := caseInsensitiveSearch(key, ec.Extensions)
if !ok {
return "", fmt.Errorf("%q not found", key)
}
return v, nil
}

View File

@ -1,104 +0,0 @@
package cloudevents
import (
"errors"
"fmt"
"net/url"
"strings"
"time"
"github.com/cloudevents/sdk-go/pkg/cloudevents/types"
)
// Adhere to EventContextWriter
var _ EventContextWriter = (*EventContextV01)(nil)
// SetSpecVersion implements EventContextWriter.SetSpecVersion
func (ec *EventContextV01) SetSpecVersion(v string) error {
if v != CloudEventsVersionV01 {
return fmt.Errorf("invalid version %q, expecting %q", v, CloudEventsVersionV01)
}
ec.CloudEventsVersion = CloudEventsVersionV01
return nil
}
// SetDataContentType implements EventContextWriter.SetDataContentType
func (ec *EventContextV01) SetDataContentType(ct string) error {
ct = strings.TrimSpace(ct)
if ct == "" {
ec.ContentType = nil
} else {
ec.ContentType = &ct
}
return nil
}
// SetType implements EventContextWriter.SetType
func (ec *EventContextV01) SetType(t string) error {
t = strings.TrimSpace(t)
ec.EventType = t
return nil
}
// SetSource implements EventContextWriter.SetSource
func (ec *EventContextV01) SetSource(u string) error {
pu, err := url.Parse(u)
if err != nil {
return err
}
ec.Source = types.URLRef{URL: *pu}
return nil
}
// SetSubject implements EventContextWriter.SetSubject
func (ec *EventContextV01) SetSubject(s string) error {
s = strings.TrimSpace(s)
if s == "" {
return ec.SetExtension(SubjectKey, nil)
}
return ec.SetExtension(SubjectKey, s)
}
// SetID implements EventContextWriter.SetID
func (ec *EventContextV01) SetID(id string) error {
id = strings.TrimSpace(id)
if id == "" {
return errors.New("event id is required to be a non-empty string")
}
ec.EventID = id
return nil
}
// SetTime implements EventContextWriter.SetTime
func (ec *EventContextV01) SetTime(t time.Time) error {
if t.IsZero() {
ec.EventTime = nil
} else {
ec.EventTime = &types.Timestamp{Time: t}
}
return nil
}
// SetDataSchema implements EventContextWriter.SetDataSchema
func (ec *EventContextV01) SetDataSchema(u string) error {
u = strings.TrimSpace(u)
if u == "" {
ec.SchemaURL = nil
return nil
}
pu, err := url.Parse(u)
if err != nil {
return err
}
ec.SchemaURL = &types.URLRef{URL: *pu}
return nil
}
// DeprecatedSetDataContentEncoding implements EventContextWriter.DeprecatedSetDataContentEncoding
func (ec *EventContextV01) DeprecatedSetDataContentEncoding(e string) error {
e = strings.ToLower(strings.TrimSpace(e))
if e == "" {
return ec.SetExtension(DataContentEncodingKey, nil)
}
return ec.SetExtension(DataContentEncodingKey, e)
}

View File

@ -1,291 +0,0 @@
package cloudevents
import (
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/cloudevents/sdk-go/pkg/cloudevents/types"
)
const (
// CloudEventsVersionV02 represents the version 0.2 of the CloudEvents spec.
CloudEventsVersionV02 = "0.2"
)
// EventContextV02 represents the non-data attributes of a CloudEvents v0.2
// event.
type EventContextV02 struct {
// The version of the CloudEvents specification used by the event.
SpecVersion string `json:"specversion"`
// The type of the occurrence which has happened.
Type string `json:"type"`
// A URI describing the event producer.
Source types.URLRef `json:"source"`
// ID of the event; must be non-empty and unique within the scope of the producer.
ID string `json:"id"`
// Timestamp when the event happened.
Time *types.Timestamp `json:"time,omitempty"`
// A link to the schema that the `data` attribute adheres to.
SchemaURL *types.URLRef `json:"schemaurl,omitempty"`
// A MIME (RFC2046) string describing the media type of `data`.
// TODO: Should an empty string assume `application/json`, `application/octet-stream`, or auto-detect the content?
ContentType *string `json:"contenttype,omitempty"`
// Additional extension metadata beyond the base spec.
Extensions map[string]interface{} `json:"-"`
}
// Adhere to EventContext
var _ EventContext = (*EventContextV02)(nil)
// ExtensionAs implements EventContext.ExtensionAs
func (ec EventContextV02) ExtensionAs(name string, obj interface{}) error {
value, ok := ec.Extensions[name]
if !ok {
return fmt.Errorf("extension %q does not exist", name)
}
// Try to unmarshal extension if we find it as a RawMessage.
switch v := value.(type) {
case json.RawMessage:
if err := json.Unmarshal(v, obj); err == nil {
// if that worked, return with obj set.
return nil
}
}
// else try as a string ptr.
// Only support *string for now.
switch v := obj.(type) {
case *string:
if valueAsString, ok := value.(string); ok {
*v = valueAsString
return nil
} else {
return fmt.Errorf("invalid type for extension %q", name)
}
default:
return fmt.Errorf("unknown extension type %T", obj)
}
}
// SetExtension adds the extension 'name' with value 'value' to the CloudEvents context.
func (ec *EventContextV02) SetExtension(name string, value interface{}) error {
if ec.Extensions == nil {
ec.Extensions = make(map[string]interface{})
}
if value == nil {
delete(ec.Extensions, name)
} else {
ec.Extensions[name] = value
}
return nil
}
// Clone implements EventContextConverter.Clone
func (ec EventContextV02) Clone() EventContext {
return ec.AsV02()
}
// AsV01 implements EventContextConverter.AsV01
func (ec EventContextV02) AsV01() *EventContextV01 {
ret := EventContextV01{
CloudEventsVersion: CloudEventsVersionV01,
EventID: ec.ID,
EventTime: ec.Time,
EventType: ec.Type,
SchemaURL: ec.SchemaURL,
Source: ec.Source,
ContentType: ec.ContentType,
Extensions: make(map[string]interface{}),
}
for k, v := range ec.Extensions {
// eventTypeVersion was retired in v0.2
if strings.EqualFold(k, EventTypeVersionKey) {
etv, ok := v.(string)
if ok && etv != "" {
ret.EventTypeVersion = &etv
}
continue
}
ret.Extensions[k] = v
}
if len(ret.Extensions) == 0 {
ret.Extensions = nil
}
return &ret
}
// AsV02 implements EventContextConverter.AsV02
func (ec EventContextV02) AsV02() *EventContextV02 {
ec.SpecVersion = CloudEventsVersionV02
return &ec
}
// AsV03 implements EventContextConverter.AsV03
func (ec EventContextV02) AsV03() *EventContextV03 {
ret := EventContextV03{
SpecVersion: CloudEventsVersionV03,
ID: ec.ID,
Time: ec.Time,
Type: ec.Type,
SchemaURL: ec.SchemaURL,
DataContentType: ec.ContentType,
Source: ec.Source,
Extensions: make(map[string]interface{}),
}
for k, v := range ec.Extensions {
// Subject was introduced in 0.3
if strings.EqualFold(k, SubjectKey) {
sub, ok := v.(string)
if ok && sub != "" {
ret.Subject = &sub
}
continue
}
// DeprecatedDataContentEncoding was introduced in 0.3
if strings.EqualFold(k, DataContentEncodingKey) {
etv, ok := v.(string)
if ok && etv != "" {
ret.DataContentEncoding = &etv
}
continue
}
ret.Extensions[k] = v
}
if len(ret.Extensions) == 0 {
ret.Extensions = nil
}
return &ret
}
// AsV1 implements EventContextConverter.AsV1
func (ec EventContextV02) AsV1() *EventContextV1 {
return ec.AsV03().AsV1()
}
// Validate returns errors based on requirements from the CloudEvents spec.
// For more details, see https://github.com/cloudevents/spec/blob/v0.2/spec.md
func (ec EventContextV02) Validate() error {
errors := []string(nil)
// type
// Type: String
// Constraints:
// REQUIRED
// MUST be a non-empty string
// SHOULD be prefixed with a reverse-DNS name. The prefixed domain dictates the organization which defines the semantics of this event type.
eventType := strings.TrimSpace(ec.Type)
if eventType == "" {
errors = append(errors, "type: MUST be a non-empty string")
}
// specversion
// Type: String
// Constraints:
// REQUIRED
// MUST be a non-empty string
specVersion := strings.TrimSpace(ec.SpecVersion)
if specVersion == "" {
errors = append(errors, "specversion: MUST be a non-empty string")
}
// source
// Type: URI-reference
// Constraints:
// REQUIRED
source := strings.TrimSpace(ec.Source.String())
if source == "" {
errors = append(errors, "source: REQUIRED")
}
// id
// Type: String
// Constraints:
// REQUIRED
// MUST be a non-empty string
// MUST be unique within the scope of the producer
id := strings.TrimSpace(ec.ID)
if id == "" {
errors = append(errors, "id: MUST be a non-empty string")
// no way to test "MUST be unique within the scope of the producer"
}
// time
// Type: Timestamp
// Constraints:
// OPTIONAL
// If present, MUST adhere to the format specified in RFC 3339
// --> no need to test this, no way to set the time without it being valid.
// schemaurl
// Type: URI
// Constraints:
// OPTIONAL
// If present, MUST adhere to the format specified in RFC 3986
if ec.SchemaURL != nil {
schemaURL := strings.TrimSpace(ec.SchemaURL.String())
// empty string is not RFC 3986 compatible.
if schemaURL == "" {
errors = append(errors, "schemaurl: if present, MUST adhere to the format specified in RFC 3986")
}
}
// contenttype
// Type: String per RFC 2046
// Constraints:
// OPTIONAL
// If present, MUST adhere to the format specified in RFC 2046
if ec.ContentType != nil {
contentType := strings.TrimSpace(*ec.ContentType)
if contentType == "" {
// TODO: need to test for RFC 2046
errors = append(errors, "contenttype: if present, MUST adhere to the format specified in RFC 2046")
}
}
if len(errors) > 0 {
return fmt.Errorf(strings.Join(errors, "\n"))
}
return nil
}
// String returns a pretty-printed representation of the EventContext.
func (ec EventContextV02) String() string {
b := strings.Builder{}
b.WriteString("Context Attributes,\n")
b.WriteString(" specversion: " + ec.SpecVersion + "\n")
b.WriteString(" type: " + ec.Type + "\n")
b.WriteString(" source: " + ec.Source.String() + "\n")
b.WriteString(" id: " + ec.ID + "\n")
if ec.Time != nil {
b.WriteString(" time: " + ec.Time.String() + "\n")
}
if ec.SchemaURL != nil {
b.WriteString(" schemaurl: " + ec.SchemaURL.String() + "\n")
}
if ec.ContentType != nil {
b.WriteString(" contenttype: " + *ec.ContentType + "\n")
}
if ec.Extensions != nil && len(ec.Extensions) > 0 {
b.WriteString("Extensions,\n")
keys := make([]string, 0, len(ec.Extensions))
for k := range ec.Extensions {
keys = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
b.WriteString(fmt.Sprintf(" %s: %v\n", key, ec.Extensions[key]))
}
}
return b.String()
}

View File

@ -1,101 +0,0 @@
package cloudevents
import (
"fmt"
"mime"
"time"
)
// Adhere to EventContextReader
var _ EventContextReader = (*EventContextV02)(nil)
// GetSpecVersion implements EventContextReader.GetSpecVersion
func (ec EventContextV02) GetSpecVersion() string {
if ec.SpecVersion != "" {
return ec.SpecVersion
}
return CloudEventsVersionV02
}
// GetType implements EventContextReader.GetType
func (ec EventContextV02) GetType() string {
return ec.Type
}
// GetSource implements EventContextReader.GetSource
func (ec EventContextV02) GetSource() string {
return ec.Source.String()
}
// GetSubject implements EventContextReader.GetSubject
func (ec EventContextV02) GetSubject() string {
var sub string
if err := ec.ExtensionAs(SubjectKey, &sub); err != nil {
return ""
}
return sub
}
// GetID implements EventContextReader.GetID
func (ec EventContextV02) GetID() string {
return ec.ID
}
// GetTime implements EventContextReader.GetTime
func (ec EventContextV02) GetTime() time.Time {
if ec.Time != nil {
return ec.Time.Time
}
return time.Time{}
}
// GetDataSchema implements EventContextReader.GetDataSchema
func (ec EventContextV02) GetDataSchema() string {
if ec.SchemaURL != nil {
return ec.SchemaURL.String()
}
return ""
}
// GetDataContentType implements EventContextReader.GetDataContentType
func (ec EventContextV02) GetDataContentType() string {
if ec.ContentType != nil {
return *ec.ContentType
}
return ""
}
// GetDataMediaType implements EventContextReader.GetDataMediaType
func (ec EventContextV02) GetDataMediaType() (string, error) {
if ec.ContentType != nil {
mediaType, _, err := mime.ParseMediaType(*ec.ContentType)
if err != nil {
return "", err
}
return mediaType, nil
}
return "", nil
}
// DeprecatedGetDataContentEncoding implements EventContextReader.DeprecatedGetDataContentEncoding
func (ec EventContextV02) DeprecatedGetDataContentEncoding() string {
var enc string
if err := ec.ExtensionAs(DataContentEncodingKey, &enc); err != nil {
return ""
}
return enc
}
// GetExtensions implements EventContextReader.GetExtensions
func (ec EventContextV02) GetExtensions() map[string]interface{} {
return ec.Extensions
}
// GetExtension implements EventContextReader.GetExtension
func (ec EventContextV02) GetExtension(key string) (interface{}, error) {
v, ok := caseInsensitiveSearch(key, ec.Extensions)
if !ok {
return "", fmt.Errorf("%q not found", key)
}
return v, nil
}

View File

@ -1,104 +0,0 @@
package cloudevents
import (
"errors"
"fmt"
"net/url"
"strings"
"time"
"github.com/cloudevents/sdk-go/pkg/cloudevents/types"
)
// Adhere to EventContextWriter
var _ EventContextWriter = (*EventContextV02)(nil)
// SetSpecVersion implements EventContextWriter.SetSpecVersion
func (ec *EventContextV02) SetSpecVersion(v string) error {
if v != CloudEventsVersionV02 {
return fmt.Errorf("invalid version %q, expecting %q", v, CloudEventsVersionV02)
}
ec.SpecVersion = CloudEventsVersionV02
return nil
}
// SetDataContentType implements EventContextWriter.SetDataContentType
func (ec *EventContextV02) SetDataContentType(ct string) error {
ct = strings.TrimSpace(ct)
if ct == "" {
ec.ContentType = nil
} else {
ec.ContentType = &ct
}
return nil
}
// SetType implements EventContextWriter.SetType
func (ec *EventContextV02) SetType(t string) error {
t = strings.TrimSpace(t)
ec.Type = t
return nil
}
// SetSource implements EventContextWriter.SetSource
func (ec *EventContextV02) SetSource(u string) error {
pu, err := url.Parse(u)
if err != nil {
return err
}
ec.Source = types.URLRef{URL: *pu}
return nil
}
// SetSubject implements EventContextWriter.SetSubject
func (ec *EventContextV02) SetSubject(s string) error {
s = strings.TrimSpace(s)
if s == "" {
return ec.SetExtension(SubjectKey, nil)
}
return ec.SetExtension(SubjectKey, s)
}
// SetID implements EventContextWriter.SetID
func (ec *EventContextV02) SetID(id string) error {
id = strings.TrimSpace(id)
if id == "" {
return errors.New("id is required to be a non-empty string")
}
ec.ID = id
return nil
}
// SetTime implements EventContextWriter.SetTime
func (ec *EventContextV02) SetTime(t time.Time) error {
if t.IsZero() {
ec.Time = nil
} else {
ec.Time = &types.Timestamp{Time: t}
}
return nil
}
// SetDataSchema implements EventContextWriter.SetDataSchema
func (ec *EventContextV02) SetDataSchema(u string) error {
u = strings.TrimSpace(u)
if u == "" {
ec.SchemaURL = nil
return nil
}
pu, err := url.Parse(u)
if err != nil {
return err
}
ec.SchemaURL = &types.URLRef{URL: *pu}
return nil
}
// DeprecatedSetDataContentEncoding implements EventContextWriter.DeprecatedSetDataContentEncoding
func (ec *EventContextV02) DeprecatedSetDataContentEncoding(e string) error {
e = strings.ToLower(strings.TrimSpace(e))
if e == "" {
return ec.SetExtension(DataContentEncodingKey, nil)
}
return ec.SetExtension(DataContentEncodingKey, e)
}

View File

@ -1,35 +0,0 @@
package transport
import (
"context"
"fmt"
"github.com/cloudevents/sdk-go/pkg/cloudevents"
)
// Codec is the interface for transport codecs to convert between transport
// specific payloads and the Message interface.
type Codec interface {
Encode(context.Context, cloudevents.Event) (Message, error)
Decode(context.Context, Message) (*cloudevents.Event, error)
}
// ErrMessageEncodingUnknown is an error produced when the encoding for an incoming
// message can not be understood.
type ErrMessageEncodingUnknown struct {
codec string
transport string
}
// NewErrMessageEncodingUnknown makes a new ErrMessageEncodingUnknown.
func NewErrMessageEncodingUnknown(codec, transport string) *ErrMessageEncodingUnknown {
return &ErrMessageEncodingUnknown{
codec: codec,
transport: transport,
}
}
// Error implements error.Error
func (e *ErrMessageEncodingUnknown) Error() string {
return fmt.Sprintf("message encoding unknown for %s codec on %s transport", e.codec, e.transport)
}

View File

@ -1,154 +0,0 @@
package http
import (
"context"
"errors"
"fmt"
"sync"
"github.com/cloudevents/sdk-go/pkg/cloudevents"
cecontext "github.com/cloudevents/sdk-go/pkg/cloudevents/context"
"github.com/cloudevents/sdk-go/pkg/cloudevents/transport"
)
// Codec is the wrapper for all versions of codecs supported by the http
// transport.
type Codec struct {
// Encoding is the setting to inform the DefaultEncodingSelectionFn for
// selecting a codec.
Encoding Encoding
// DefaultEncodingSelectionFn allows for encoding selection strategies to be injected.
DefaultEncodingSelectionFn EncodingSelector
v01 *CodecV01
v02 *CodecV02
v03 *CodecV03
v1 *CodecV1
_v01 sync.Once
_v02 sync.Once
_v03 sync.Once
_v1 sync.Once
}
// Adheres to Codec
var _ transport.Codec = (*Codec)(nil)
func (c *Codec) loadCodec(encoding Encoding) (transport.Codec, error) {
switch encoding {
case Default:
fallthrough
case BinaryV01, StructuredV01:
c._v01.Do(func() {
c.v01 = &CodecV01{DefaultEncoding: c.Encoding}
})
return c.v01, nil
case BinaryV02, StructuredV02:
c._v02.Do(func() {
c.v02 = &CodecV02{DefaultEncoding: c.Encoding}
})
return c.v02, nil
case BinaryV03, StructuredV03, BatchedV03:
c._v03.Do(func() {
c.v03 = &CodecV03{DefaultEncoding: c.Encoding}
})
return c.v03, nil
case BinaryV1, StructuredV1, BatchedV1:
c._v1.Do(func() {
c.v1 = &CodecV1{DefaultEncoding: c.Encoding}
})
return c.v1, nil
}
return nil, fmt.Errorf("unknown encoding: %s", encoding)
}
// Encode encodes the provided event into a transport message.
func (c *Codec) Encode(ctx context.Context, e cloudevents.Event) (transport.Message, error) {
encoding := c.Encoding
if encoding == Default && c.DefaultEncodingSelectionFn != nil {
encoding = c.DefaultEncodingSelectionFn(ctx, e)
}
codec, err := c.loadCodec(encoding)
if err != nil {
return nil, err
}
ctx = cecontext.WithEncoding(ctx, encoding.Name())
return codec.Encode(ctx, e)
}
// Decode converts a provided transport message into an Event, or error.
func (c *Codec) Decode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) {
codec, err := c.loadCodec(c.inspectEncoding(ctx, msg))
if err != nil {
return nil, err
}
event, err := codec.Decode(ctx, msg)
if err != nil {
return nil, err
}
return c.convertEvent(event)
}
// Give the context back as the user expects
func (c *Codec) convertEvent(event *cloudevents.Event) (*cloudevents.Event, error) {
if event == nil {
return nil, errors.New("event is nil, can not convert")
}
switch c.Encoding {
case Default:
return event, nil
case BinaryV01, StructuredV01:
ca := event.Context.AsV01()
event.Context = ca
return event, nil
case BinaryV02, StructuredV02:
ca := event.Context.AsV02()
event.Context = ca
return event, nil
case BinaryV03, StructuredV03, BatchedV03:
ca := event.Context.AsV03()
event.Context = ca
return event, nil
case BinaryV1, StructuredV1, BatchedV1:
ca := event.Context.AsV03()
event.Context = ca
return event, nil
default:
return nil, fmt.Errorf("unknown encoding: %s", c.Encoding)
}
}
func (c *Codec) inspectEncoding(ctx context.Context, msg transport.Message) Encoding {
// Try v1.0.
_, _ = c.loadCodec(BinaryV1)
encoding := c.v1.inspectEncoding(ctx, msg)
if encoding != Unknown {
return encoding
}
// Try v0.3.
_, _ = c.loadCodec(BinaryV03)
encoding = c.v03.inspectEncoding(ctx, msg)
if encoding != Unknown {
return encoding
}
// Try v0.2.
_, _ = c.loadCodec(BinaryV02)
encoding = c.v02.inspectEncoding(ctx, msg)
if encoding != Unknown {
return encoding
}
// Try v0.1 first.
_, _ = c.loadCodec(BinaryV01)
encoding = c.v01.inspectEncoding(ctx, msg)
if encoding != Unknown {
return encoding
}
// We do not understand the message encoding.
return Unknown
}

View File

@ -1,44 +0,0 @@
package http
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/cloudevents/sdk-go/pkg/cloudevents"
"github.com/cloudevents/sdk-go/pkg/cloudevents/transport"
)
// CodecStructured represents an structured http transport codec for all versions.
// Intended to be used as a base class.
type CodecStructured struct {
DefaultEncoding Encoding
}
func (v CodecStructured) encodeStructured(ctx context.Context, e cloudevents.Event) (transport.Message, error) {
header := http.Header{}
header.Set("Content-Type", cloudevents.ApplicationCloudEventsJSON)
body, err := json.Marshal(e)
if err != nil {
return nil, err
}
msg := &Message{
Header: header,
Body: body,
}
return msg, nil
}
func (v CodecStructured) decodeStructured(ctx context.Context, version string, msg transport.Message) (*cloudevents.Event, error) {
m, ok := msg.(*Message)
if !ok {
return nil, fmt.Errorf("failed to convert transport.Message to http.Message")
}
event := cloudevents.New(version)
err := json.Unmarshal(m.Body, &event)
return &event, err
}

View File

@ -1,232 +0,0 @@
package http
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/textproto"
"strings"
"github.com/cloudevents/sdk-go/pkg/cloudevents"
cecontext "github.com/cloudevents/sdk-go/pkg/cloudevents/context"
"github.com/cloudevents/sdk-go/pkg/cloudevents/observability"
"github.com/cloudevents/sdk-go/pkg/cloudevents/transport"
"github.com/cloudevents/sdk-go/pkg/cloudevents/types"
)
// CodecV01 represents a http transport codec that uses CloudEvents spec v0.1
type CodecV01 struct {
CodecStructured
DefaultEncoding Encoding
}
// Adheres to Codec
var _ transport.Codec = (*CodecV01)(nil)
// Encode implements Codec.Encode
func (v CodecV01) Encode(ctx context.Context, e cloudevents.Event) (transport.Message, error) {
encoding := v.DefaultEncoding
strEnc := cecontext.EncodingFrom(ctx)
if strEnc != "" {
switch strEnc {
case Binary:
encoding = BinaryV01
case Structured:
encoding = StructuredV01
}
}
_, r := observability.NewReporter(context.Background(), CodecObserved{o: reportEncode, c: encoding.Codec()})
m, err := v.obsEncode(ctx, e, encoding)
if err != nil {
r.Error()
} else {
r.OK()
}
return m, err
}
func (v CodecV01) obsEncode(ctx context.Context, e cloudevents.Event, encoding Encoding) (transport.Message, error) {
switch encoding {
case Default:
fallthrough
case BinaryV01:
return v.encodeBinary(ctx, e)
case StructuredV01:
return v.encodeStructured(ctx, e)
default:
return nil, fmt.Errorf("unknown encoding: %d", encoding)
}
}
// Decode implements Codec.Decode
func (v CodecV01) Decode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) {
_, r := observability.NewReporter(ctx, CodecObserved{o: reportDecode, c: v.inspectEncoding(ctx, msg).Codec()}) // TODO: inspectEncoding is not free.
e, err := v.obsDecode(ctx, msg)
if err != nil {
r.Error()
} else {
r.OK()
}
return e, err
}
func (v CodecV01) obsDecode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) {
switch v.inspectEncoding(ctx, msg) {
case BinaryV01:
return v.decodeBinary(ctx, msg)
case StructuredV01:
return v.decodeStructured(ctx, cloudevents.CloudEventsVersionV01, msg)
default:
return nil, transport.NewErrMessageEncodingUnknown("v01", TransportName)
}
}
func (v CodecV01) encodeBinary(ctx context.Context, e cloudevents.Event) (transport.Message, error) {
header, err := v.toHeaders(e.Context.AsV01())
if err != nil {
return nil, err
}
body, err := e.DataBytes()
if err != nil {
panic("encode")
}
msg := &Message{
Header: header,
Body: body,
}
return msg, nil
}
func (v CodecV01) toHeaders(ec *cloudevents.EventContextV01) (http.Header, error) {
// Preserve case in v0.1, even though HTTP headers are case-insensitive.
h := http.Header{}
h["CE-CloudEventsVersion"] = []string{ec.CloudEventsVersion}
h["CE-EventID"] = []string{ec.EventID}
h["CE-EventType"] = []string{ec.EventType}
h["CE-Source"] = []string{ec.Source.String()}
if ec.EventTime != nil && !ec.EventTime.IsZero() {
h["CE-EventTime"] = []string{ec.EventTime.String()}
}
if ec.EventTypeVersion != nil {
h["CE-EventTypeVersion"] = []string{*ec.EventTypeVersion}
}
if ec.SchemaURL != nil {
h["CE-DataSchema"] = []string{ec.SchemaURL.String()}
}
if ec.ContentType != nil && *ec.ContentType != "" {
h.Set("Content-Type", *ec.ContentType)
}
// Regarding Extensions, v0.1 Spec says the following:
// * Each map entry name MUST be prefixed with "CE-X-"
// * Each map entry name's first character MUST be capitalized
for k, v := range ec.Extensions {
encoded, err := json.Marshal(v)
if err != nil {
return nil, err
}
h["CE-X-"+strings.Title(k)] = []string{string(encoded)}
}
return h, nil
}
func (v CodecV01) decodeBinary(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) {
m, ok := msg.(*Message)
if !ok {
return nil, fmt.Errorf("failed to convert transport.Message to http.Message")
}
ca, err := v.fromHeaders(m.Header)
if err != nil {
return nil, err
}
var body interface{}
if len(m.Body) > 0 {
body = m.Body
}
return &cloudevents.Event{
Context: &ca,
Data: body,
DataEncoded: body != nil,
}, nil
}
func (v CodecV01) fromHeaders(h http.Header) (cloudevents.EventContextV01, error) {
// Normalize headers.
for k, v := range h {
ck := textproto.CanonicalMIMEHeaderKey(k)
if k != ck {
h[ck] = v
}
}
ec := cloudevents.EventContextV01{}
ec.CloudEventsVersion = h.Get("CE-CloudEventsVersion")
h.Del("CE-CloudEventsVersion")
ec.EventID = h.Get("CE-EventID")
h.Del("CE-EventID")
ec.EventType = h.Get("CE-EventType")
h.Del("CE-EventType")
source := types.ParseURLRef(h.Get("CE-Source"))
h.Del("CE-Source")
if source != nil {
ec.Source = *source
}
var err error
ec.EventTime, err = types.ParseTimestamp(h.Get("CE-EventTime"))
if err != nil {
return ec, err
}
h.Del("CE-EventTime")
etv := h.Get("CE-EventTypeVersion")
h.Del("CE-EventTypeVersion")
if etv != "" {
ec.EventTypeVersion = &etv
}
ec.SchemaURL = types.ParseURLRef(h.Get("CE-DataSchema"))
h.Del("CE-DataSchema")
et := h.Get("Content-Type")
if et != "" {
ec.ContentType = &et
}
extensions := make(map[string]interface{})
for k, v := range h {
if len(k) > len("CE-X-") && strings.EqualFold(k[:len("CE-X-")], "CE-X-") {
key := k[len("CE-X-"):]
var tmp interface{}
if err := json.Unmarshal([]byte(v[0]), &tmp); err == nil {
extensions[key] = tmp
} else {
// If we can't unmarshal the data, treat it as a string.
extensions[key] = v[0]
}
h.Del(k)
}
}
if len(extensions) > 0 {
ec.Extensions = extensions
}
return ec, nil
}
func (v CodecV01) inspectEncoding(ctx context.Context, msg transport.Message) Encoding {
version := msg.CloudEventsVersion()
if version != cloudevents.CloudEventsVersionV01 {
return Unknown
}
m, ok := msg.(*Message)
if !ok {
return Unknown
}
contentType := m.Header.Get("Content-Type")
if contentType == cloudevents.ApplicationCloudEventsJSON {
return StructuredV01
}
return BinaryV01
}

View File

@ -1,261 +0,0 @@
package http
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/textproto"
"strings"
"github.com/cloudevents/sdk-go/pkg/cloudevents"
cecontext "github.com/cloudevents/sdk-go/pkg/cloudevents/context"
"github.com/cloudevents/sdk-go/pkg/cloudevents/observability"
"github.com/cloudevents/sdk-go/pkg/cloudevents/transport"
"github.com/cloudevents/sdk-go/pkg/cloudevents/types"
)
// CodecV02 represents a http transport codec that uses CloudEvents spec v0.2
type CodecV02 struct {
CodecStructured
DefaultEncoding Encoding
}
// Adheres to Codec
var _ transport.Codec = (*CodecV02)(nil)
// Encode implements Codec.Encode
func (v CodecV02) Encode(ctx context.Context, e cloudevents.Event) (transport.Message, error) {
encoding := v.DefaultEncoding
strEnc := cecontext.EncodingFrom(ctx)
if strEnc != "" {
switch strEnc {
case Binary:
encoding = BinaryV02
case Structured:
encoding = StructuredV02
}
}
_, r := observability.NewReporter(ctx, CodecObserved{o: reportEncode, c: encoding.Codec()})
m, err := v.obsEncode(ctx, e, encoding)
if err != nil {
r.Error()
} else {
r.OK()
}
return m, err
}
func (v CodecV02) obsEncode(ctx context.Context, e cloudevents.Event, encoding Encoding) (transport.Message, error) {
switch encoding {
case Default:
fallthrough
case BinaryV02:
return v.encodeBinary(ctx, e)
case StructuredV02:
return v.encodeStructured(ctx, e)
default:
return nil, fmt.Errorf("unknown encoding: %d", encoding)
}
}
// Decode implements Codec.Decode
func (v CodecV02) Decode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) {
_, r := observability.NewReporter(ctx, CodecObserved{o: reportDecode, c: v.inspectEncoding(ctx, msg).Codec()}) // TODO: inspectEncoding is not free.
e, err := v.obsDecode(ctx, msg)
if err != nil {
r.Error()
} else {
r.OK()
}
return e, err
}
func (v CodecV02) obsDecode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) {
switch v.inspectEncoding(ctx, msg) {
case BinaryV02:
return v.decodeBinary(ctx, msg)
case StructuredV02:
return v.decodeStructured(ctx, cloudevents.CloudEventsVersionV02, msg)
default:
return nil, transport.NewErrMessageEncodingUnknown("v02", TransportName)
}
}
func (v CodecV02) encodeBinary(ctx context.Context, e cloudevents.Event) (transport.Message, error) {
header, err := v.toHeaders(e.Context.AsV02())
if err != nil {
return nil, err
}
body, err := e.DataBytes()
if err != nil {
return nil, err
}
msg := &Message{
Header: header,
Body: body,
}
return msg, nil
}
func (v CodecV02) toHeaders(ec *cloudevents.EventContextV02) (http.Header, error) {
h := http.Header{}
h.Set("ce-specversion", ec.SpecVersion)
h.Set("ce-type", ec.Type)
h.Set("ce-source", ec.Source.String())
h.Set("ce-id", ec.ID)
if ec.Time != nil && !ec.Time.IsZero() {
h.Set("ce-time", ec.Time.String())
}
if ec.SchemaURL != nil {
h.Set("ce-schemaurl", ec.SchemaURL.String())
}
if ec.ContentType != nil && *ec.ContentType != "" {
h.Set("Content-Type", *ec.ContentType)
}
for k, v := range ec.Extensions {
// Per spec, map-valued extensions are converted to a list of headers as:
// CE-attrib-key
if mapVal, ok := v.(map[string]interface{}); ok {
for subkey, subval := range mapVal {
encoded, err := json.Marshal(subval)
if err != nil {
return nil, err
}
h.Set("ce-"+k+"-"+subkey, string(encoded))
}
continue
}
encoded, err := json.Marshal(v)
if err != nil {
return nil, err
}
h.Set("ce-"+k, string(encoded))
}
return h, nil
}
func (v CodecV02) decodeBinary(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) {
m, ok := msg.(*Message)
if !ok {
return nil, fmt.Errorf("failed to convert transport.Message to http.Message")
}
ca, err := v.fromHeaders(m.Header)
if err != nil {
return nil, err
}
var body interface{}
if len(m.Body) > 0 {
body = m.Body
}
return &cloudevents.Event{
Context: &ca,
Data: body,
DataEncoded: body != nil,
}, nil
}
func (v CodecV02) fromHeaders(h http.Header) (cloudevents.EventContextV02, error) {
// Normalize headers.
for k, v := range h {
ck := textproto.CanonicalMIMEHeaderKey(k)
if k != ck {
delete(h, k)
h[ck] = v
}
}
ec := cloudevents.EventContextV02{}
ec.SpecVersion = h.Get("ce-specversion")
h.Del("ce-specversion")
ec.ID = h.Get("ce-id")
h.Del("ce-id")
ec.Type = h.Get("ce-type")
h.Del("ce-type")
source := types.ParseURLRef(h.Get("ce-source"))
if source != nil {
ec.Source = *source
}
h.Del("ce-source")
var err error
ec.Time, err = types.ParseTimestamp(h.Get("ce-time"))
if err != nil {
return ec, err
}
h.Del("ce-time")
ec.SchemaURL = types.ParseURLRef(h.Get("ce-schemaurl"))
h.Del("ce-schemaurl")
contentType := h.Get("Content-Type")
if contentType != "" {
ec.ContentType = &contentType
}
h.Del("Content-Type")
// At this point, we have deleted all the known headers.
// Everything left is assumed to be an extension.
extensions := make(map[string]interface{})
for k, v := range h {
if len(k) > len("ce-") && strings.EqualFold(k[:len("ce-")], "ce-") {
ak := strings.ToLower(k[len("ce-"):])
if i := strings.Index(ak, "-"); i > 0 {
// attrib-key
attrib := ak[:i]
key := ak[(i + 1):]
if xv, ok := extensions[attrib]; ok {
if m, ok := xv.(map[string]interface{}); ok {
m[key] = v
continue
}
// TODO: revisit how we want to bubble errors up.
return ec, fmt.Errorf("failed to process map type extension")
} else {
m := make(map[string]interface{})
m[key] = v
extensions[attrib] = m
}
} else {
// key
var tmp interface{}
if err := json.Unmarshal([]byte(v[0]), &tmp); err == nil {
extensions[ak] = tmp
} else {
// If we can't unmarshal the data, treat it as a string.
extensions[ak] = v[0]
}
}
}
}
if len(extensions) > 0 {
ec.Extensions = extensions
}
return ec, nil
}
func (v CodecV02) inspectEncoding(ctx context.Context, msg transport.Message) Encoding {
version := msg.CloudEventsVersion()
if version != cloudevents.CloudEventsVersionV02 {
return Unknown
}
m, ok := msg.(*Message)
if !ok {
return Unknown
}
contentType := m.Header.Get("Content-Type")
if contentType == cloudevents.ApplicationCloudEventsJSON {
return StructuredV02
}
return BinaryV02
}

View File

@ -1,302 +0,0 @@
package http
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/textproto"
"strings"
"github.com/cloudevents/sdk-go/pkg/cloudevents"
cecontext "github.com/cloudevents/sdk-go/pkg/cloudevents/context"
"github.com/cloudevents/sdk-go/pkg/cloudevents/observability"
"github.com/cloudevents/sdk-go/pkg/cloudevents/transport"
"github.com/cloudevents/sdk-go/pkg/cloudevents/types"
)
// CodecV03 represents a http transport codec that uses CloudEvents spec v0.3
type CodecV03 struct {
CodecStructured
DefaultEncoding Encoding
}
// Adheres to Codec
var _ transport.Codec = (*CodecV03)(nil)
// Encode implements Codec.Encode
func (v CodecV03) Encode(ctx context.Context, e cloudevents.Event) (transport.Message, error) {
encoding := v.DefaultEncoding
strEnc := cecontext.EncodingFrom(ctx)
if strEnc != "" {
switch strEnc {
case Binary:
encoding = BinaryV03
case Structured:
encoding = StructuredV03
}
}
_, r := observability.NewReporter(ctx, CodecObserved{o: reportEncode, c: encoding.Codec()})
m, err := v.obsEncode(ctx, e, encoding)
if err != nil {
r.Error()
} else {
r.OK()
}
return m, err
}
func (v CodecV03) obsEncode(ctx context.Context, e cloudevents.Event, encoding Encoding) (transport.Message, error) {
switch encoding {
case Default:
fallthrough
case BinaryV03:
return v.encodeBinary(ctx, e)
case StructuredV03:
return v.encodeStructured(ctx, e)
case BatchedV03:
return nil, fmt.Errorf("not implemented")
default:
return nil, fmt.Errorf("unknown encoding: %d", encoding)
}
}
// Decode implements Codec.Decode
func (v CodecV03) Decode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) {
_, r := observability.NewReporter(ctx, CodecObserved{o: reportDecode, c: v.inspectEncoding(ctx, msg).Codec()}) // TODO: inspectEncoding is not free.
e, err := v.obsDecode(ctx, msg)
if err != nil {
r.Error()
} else {
r.OK()
}
return e, err
}
func (v CodecV03) obsDecode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) {
switch v.inspectEncoding(ctx, msg) {
case BinaryV03:
return v.decodeBinary(ctx, msg)
case StructuredV03:
return v.decodeStructured(ctx, cloudevents.CloudEventsVersionV03, msg)
case BatchedV03:
return nil, fmt.Errorf("not implemented")
default:
return nil, transport.NewErrMessageEncodingUnknown("v03", TransportName)
}
}
func (v CodecV03) encodeBinary(ctx context.Context, e cloudevents.Event) (transport.Message, error) {
header, err := v.toHeaders(e.Context.AsV03())
if err != nil {
return nil, err
}
body, err := e.DataBytes()
if err != nil {
return nil, err
}
msg := &Message{
Header: header,
Body: body,
}
return msg, nil
}
func (v CodecV03) toHeaders(ec *cloudevents.EventContextV03) (http.Header, error) {
h := http.Header{}
h.Set("ce-specversion", ec.SpecVersion)
h.Set("ce-type", ec.Type)
h.Set("ce-source", ec.Source.String())
if ec.Subject != nil {
h.Set("ce-subject", *ec.Subject)
}
h.Set("ce-id", ec.ID)
if ec.Time != nil && !ec.Time.IsZero() {
h.Set("ce-time", ec.Time.String())
}
if ec.SchemaURL != nil {
h.Set("ce-schemaurl", ec.SchemaURL.String())
}
if ec.DataContentType != nil && *ec.DataContentType != "" {
h.Set("Content-Type", *ec.DataContentType)
}
if ec.DataContentEncoding != nil {
h.Set("ce-datacontentencoding", *ec.DataContentEncoding)
}
for k, v := range ec.Extensions {
k = strings.ToLower(k)
// Per spec, map-valued extensions are converted to a list of headers as:
// CE-attrib-key
switch v.(type) {
case string:
h.Set("ce-"+k, v.(string))
case map[string]interface{}:
mapVal := v.(map[string]interface{})
for subkey, subval := range mapVal {
if subvalstr, ok := v.(string); ok {
h.Set("ce-"+k+"-"+subkey, subvalstr)
continue
}
encoded, err := json.Marshal(subval)
if err != nil {
return nil, err
}
h.Set("ce-"+k+"-"+subkey, string(encoded))
}
default:
encoded, err := json.Marshal(v)
if err != nil {
return nil, err
}
h.Set("ce-"+k, string(encoded))
}
}
return h, nil
}
func (v CodecV03) decodeBinary(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) {
m, ok := msg.(*Message)
if !ok {
return nil, fmt.Errorf("failed to convert transport.Message to http.Message")
}
ca, err := v.fromHeaders(m.Header)
if err != nil {
return nil, err
}
var body interface{}
if len(m.Body) > 0 {
body = m.Body
}
return &cloudevents.Event{
Context: &ca,
Data: body,
DataEncoded: body != nil,
}, nil
}
func (v CodecV03) fromHeaders(h http.Header) (cloudevents.EventContextV03, error) {
// Normalize headers.
for k, v := range h {
ck := textproto.CanonicalMIMEHeaderKey(k)
if k != ck {
delete(h, k)
h[ck] = v
}
}
ec := cloudevents.EventContextV03{}
ec.SpecVersion = h.Get("ce-specversion")
h.Del("ce-specversion")
ec.ID = h.Get("ce-id")
h.Del("ce-id")
ec.Type = h.Get("ce-type")
h.Del("ce-type")
source := types.ParseURLRef(h.Get("ce-source"))
if source != nil {
ec.Source = *source
}
h.Del("ce-source")
subject := h.Get("ce-subject")
if subject != "" {
ec.Subject = &subject
}
h.Del("ce-subject")
var err error
ec.Time, err = types.ParseTimestamp(h.Get("ce-time"))
if err != nil {
return ec, err
}
h.Del("ce-time")
ec.SchemaURL = types.ParseURLRef(h.Get("ce-schemaurl"))
h.Del("ce-schemaurl")
contentType := h.Get("Content-Type")
if contentType != "" {
ec.DataContentType = &contentType
}
h.Del("Content-Type")
dataContentEncoding := h.Get("ce-datacontentencoding")
if dataContentEncoding != "" {
ec.DataContentEncoding = &dataContentEncoding
}
h.Del("ce-datacontentencoding")
// At this point, we have deleted all the known headers.
// Everything left is assumed to be an extension.
extensions := make(map[string]interface{})
for k, v := range h {
k = strings.ToLower(k)
if len(k) > len("ce-") && strings.EqualFold(k[:len("ce-")], "ce-") {
ak := strings.ToLower(k[len("ce-"):])
if i := strings.Index(ak, "-"); i > 0 {
// attrib-key
attrib := ak[:i]
key := ak[(i + 1):]
if xv, ok := extensions[attrib]; ok {
if m, ok := xv.(map[string]interface{}); ok {
m[key] = v
continue
}
// TODO: revisit how we want to bubble errors up.
return ec, fmt.Errorf("failed to process map type extension")
} else {
m := make(map[string]interface{})
m[key] = v
extensions[attrib] = m
}
} else {
// key
var tmp interface{}
if err := json.Unmarshal([]byte(v[0]), &tmp); err == nil {
extensions[ak] = tmp
} else {
// If we can't unmarshal the data, treat it as a string.
extensions[ak] = v[0]
}
}
}
}
if len(extensions) > 0 {
ec.Extensions = extensions
}
return ec, nil
}
func (v CodecV03) inspectEncoding(ctx context.Context, msg transport.Message) Encoding {
version := msg.CloudEventsVersion()
if version != cloudevents.CloudEventsVersionV03 {
return Unknown
}
m, ok := msg.(*Message)
if !ok {
return Unknown
}
contentType := m.Header.Get("Content-Type")
if contentType == cloudevents.ApplicationCloudEventsJSON {
return StructuredV03
}
if contentType == cloudevents.ApplicationCloudEventsBatchJSON {
return BatchedV03
}
return BinaryV03
}

View File

@ -1,245 +0,0 @@
package http
import (
"context"
"fmt"
"net/http"
"net/textproto"
"strings"
"github.com/cloudevents/sdk-go/pkg/cloudevents"
cecontext "github.com/cloudevents/sdk-go/pkg/cloudevents/context"
"github.com/cloudevents/sdk-go/pkg/cloudevents/observability"
"github.com/cloudevents/sdk-go/pkg/cloudevents/transport"
"github.com/cloudevents/sdk-go/pkg/cloudevents/types"
)
// CodecV1 represents a http transport codec that uses CloudEvents spec v1.0
type CodecV1 struct {
CodecStructured
DefaultEncoding Encoding
}
// Adheres to Codec
var _ transport.Codec = (*CodecV1)(nil)
// Encode implements Codec.Encode
func (v CodecV1) Encode(ctx context.Context, e cloudevents.Event) (transport.Message, error) {
encoding := v.DefaultEncoding
strEnc := cecontext.EncodingFrom(ctx)
if strEnc != "" {
switch strEnc {
case Binary:
encoding = BinaryV1
case Structured:
encoding = StructuredV1
}
}
_, r := observability.NewReporter(ctx, CodecObserved{o: reportEncode, c: encoding.Codec()})
m, err := v.obsEncode(ctx, e, encoding)
if err != nil {
r.Error()
} else {
r.OK()
}
return m, err
}
func (v CodecV1) obsEncode(ctx context.Context, e cloudevents.Event, encoding Encoding) (transport.Message, error) {
switch encoding {
case Default:
fallthrough
case BinaryV1:
return v.encodeBinary(ctx, e)
case StructuredV1:
return v.encodeStructured(ctx, e)
case BatchedV1:
return nil, fmt.Errorf("not implemented")
default:
return nil, fmt.Errorf("unknown encoding: %d", encoding)
}
}
// Decode implements Codec.Decode
func (v CodecV1) Decode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) {
_, r := observability.NewReporter(ctx, CodecObserved{o: reportDecode, c: v.inspectEncoding(ctx, msg).Codec()}) // TODO: inspectEncoding is not free.
e, err := v.obsDecode(ctx, msg)
if err != nil {
r.Error()
} else {
r.OK()
}
return e, err
}
func (v CodecV1) obsDecode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) {
switch v.inspectEncoding(ctx, msg) {
case BinaryV1:
return v.decodeBinary(ctx, msg)
case StructuredV1:
return v.decodeStructured(ctx, cloudevents.CloudEventsVersionV1, msg)
case BatchedV1:
return nil, fmt.Errorf("not implemented")
default:
return nil, transport.NewErrMessageEncodingUnknown("V1", TransportName)
}
}
func (v CodecV1) encodeBinary(ctx context.Context, e cloudevents.Event) (transport.Message, error) {
header, err := v.toHeaders(e.Context.AsV1())
if err != nil {
return nil, err
}
body, err := e.DataBytes()
if err != nil {
return nil, err
}
msg := &Message{
Header: header,
Body: body,
}
return msg, nil
}
func (v CodecV1) toHeaders(ec *cloudevents.EventContextV1) (http.Header, error) {
h := http.Header{}
h.Set("ce-specversion", ec.SpecVersion)
h.Set("ce-type", ec.Type)
h.Set("ce-source", ec.Source.String())
if ec.Subject != nil {
h.Set("ce-subject", *ec.Subject)
}
h.Set("ce-id", ec.ID)
if ec.Time != nil && !ec.Time.IsZero() {
h.Set("ce-time", ec.Time.String())
}
if ec.DataSchema != nil {
h.Set("ce-dataschema", ec.DataSchema.String())
}
if ec.DataContentType != nil && *ec.DataContentType != "" {
h.Set("Content-Type", *ec.DataContentType)
}
for k, v := range ec.Extensions {
k = strings.ToLower(k)
// Per spec, extensions are strings and converted to a list of headers as:
// ce-key: value
cstr, err := types.Format(v)
if err != nil {
return h, err
}
h.Set("ce-"+k, cstr)
}
return h, nil
}
func (v CodecV1) decodeBinary(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) {
m, ok := msg.(*Message)
if !ok {
return nil, fmt.Errorf("failed to convert transport.Message to http.Message")
}
ca, err := v.fromHeaders(m.Header)
if err != nil {
return nil, err
}
var body interface{}
if len(m.Body) > 0 {
body = m.Body
}
return &cloudevents.Event{
Context: &ca,
Data: body,
DataEncoded: body != nil,
}, nil
}
func (v CodecV1) fromHeaders(h http.Header) (cloudevents.EventContextV1, error) {
// Normalize headers.
for k, v := range h {
ck := textproto.CanonicalMIMEHeaderKey(k)
if k != ck {
delete(h, k)
h[ck] = v
}
}
ec := cloudevents.EventContextV1{}
ec.SpecVersion = h.Get("ce-specversion")
h.Del("ce-specversion")
ec.ID = h.Get("ce-id")
h.Del("ce-id")
ec.Type = h.Get("ce-type")
h.Del("ce-type")
source := types.ParseURIRef(h.Get("ce-source"))
if source != nil {
ec.Source = *source
}
h.Del("ce-source")
subject := h.Get("ce-subject")
if subject != "" {
ec.Subject = &subject
}
h.Del("ce-subject")
var err error
ec.Time, err = types.ParseTimestamp(h.Get("ce-time"))
if err != nil {
return ec, err
}
h.Del("ce-time")
ec.DataSchema = types.ParseURI(h.Get("ce-dataschema"))
h.Del("ce-dataschema")
contentType := h.Get("Content-Type")
if contentType != "" {
ec.DataContentType = &contentType
}
h.Del("Content-Type")
// At this point, we have deleted all the known headers.
// Everything left is assumed to be an extension.
extensions := make(map[string]interface{})
for k := range h {
k = strings.ToLower(k)
if len(k) > len("ce-") && strings.EqualFold(k[:len("ce-")], "ce-") {
ak := strings.ToLower(k[len("ce-"):])
extensions[ak] = h.Get(k)
}
}
if len(extensions) > 0 {
ec.Extensions = extensions
}
return ec, nil
}
func (v CodecV1) inspectEncoding(ctx context.Context, msg transport.Message) Encoding {
version := msg.CloudEventsVersion()
if version != cloudevents.CloudEventsVersionV1 {
return Unknown
}
m, ok := msg.(*Message)
if !ok {
return Unknown
}
contentType := m.Header.Get("Content-Type")
if contentType == cloudevents.ApplicationCloudEventsJSON {
return StructuredV1
}
if contentType == cloudevents.ApplicationCloudEventsBatchJSON {
return BatchedV1
}
return BinaryV1
}

View File

@ -1,207 +0,0 @@
package http
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
)
// TransportContext allows a Receiver to understand the context of a request.
type TransportContext struct {
URI string
Host string
Method string
Header http.Header
StatusCode int
// IgnoreHeaderPrefixes controls what comes back from AttendToHeaders.
// AttendToHeaders controls what is output for .String()
IgnoreHeaderPrefixes []string
}
// NewTransportContext creates a new TransportContext from a http.Request.
func NewTransportContext(req *http.Request) TransportContext {
var tx *TransportContext
if req != nil {
tx = &TransportContext{
URI: req.RequestURI,
Host: req.Host,
Method: req.Method,
Header: req.Header,
}
} else {
tx = &TransportContext{}
}
tx.AddIgnoreHeaderPrefix("accept-encoding", "user-agent", "connection", "content-type")
return *tx
}
// NewTransportContextFromResponse creates a new TransportContext from a http.Response.
// If `res` is nil, it returns a context with a http.StatusInternalServerError status code.
func NewTransportContextFromResponse(res *http.Response) TransportContext {
var tx *TransportContext
if res != nil {
tx = &TransportContext{
Header: res.Header,
StatusCode: res.StatusCode,
}
} else {
tx = &TransportContext{StatusCode: http.StatusInternalServerError}
}
tx.AddIgnoreHeaderPrefix("accept-encoding", "user-agent", "connection", "content-type")
return *tx
}
// TransportResponseContext allows a Receiver response with http transport specific fields.
type TransportResponseContext struct {
// Header will be merged with the response headers.
Header http.Header
}
// AttendToHeaders returns the list of headers that exist in the TransportContext that are not currently in
// tx.IgnoreHeaderPrefix.
func (tx TransportContext) AttendToHeaders() []string {
a := []string(nil)
if tx.Header != nil && len(tx.Header) > 0 {
for k := range tx.Header {
if tx.shouldIgnoreHeader(k) {
continue
}
a = append(a, k)
}
}
return a
}
func (tx TransportContext) shouldIgnoreHeader(h string) bool {
for _, v := range tx.IgnoreHeaderPrefixes {
if strings.HasPrefix(strings.ToLower(h), strings.ToLower(v)) {
return true
}
}
return false
}
// String generates a pretty-printed version of the resource as a string.
func (tx TransportContext) String() string {
b := strings.Builder{}
b.WriteString("Transport Context,\n")
empty := b.Len()
if tx.URI != "" {
b.WriteString(" URI: " + tx.URI + "\n")
}
if tx.Host != "" {
b.WriteString(" Host: " + tx.Host + "\n")
}
if tx.Method != "" {
b.WriteString(" Method: " + tx.Method + "\n")
}
if tx.StatusCode != 0 {
b.WriteString(" StatusCode: " + strconv.Itoa(tx.StatusCode) + "\n")
}
if tx.Header != nil && len(tx.Header) > 0 {
b.WriteString(" Header:\n")
for _, k := range tx.AttendToHeaders() {
b.WriteString(fmt.Sprintf(" %s: %s\n", k, tx.Header.Get(k)))
}
}
if b.Len() == empty {
b.WriteString(" nil\n")
}
return b.String()
}
// AddIgnoreHeaderPrefix controls what header key is to be attended to and/or printed.
func (tx *TransportContext) AddIgnoreHeaderPrefix(prefix ...string) {
if tx.IgnoreHeaderPrefixes == nil {
tx.IgnoreHeaderPrefixes = []string(nil)
}
tx.IgnoreHeaderPrefixes = append(tx.IgnoreHeaderPrefixes, prefix...)
}
// Opaque key type used to store TransportContext
type transportContextKeyType struct{}
var transportContextKey = transportContextKeyType{}
// WithTransportContext return a context with the given TransportContext into the provided context object.
func WithTransportContext(ctx context.Context, tcxt TransportContext) context.Context {
return context.WithValue(ctx, transportContextKey, tcxt)
}
// TransportContextFrom pulls a TransportContext out of a context. Always
// returns a non-nil object.
func TransportContextFrom(ctx context.Context) TransportContext {
tctx := ctx.Value(transportContextKey)
if tctx != nil {
if tx, ok := tctx.(TransportContext); ok {
return tx
}
if tx, ok := tctx.(*TransportContext); ok {
return *tx
}
}
return TransportContext{}
}
// Opaque key type used to store Headers
type headerKeyType struct{}
var headerKey = headerKeyType{}
// ContextWithHeader returns a context with a header added to the given context.
// Can be called multiple times to set multiple header key/value pairs.
func ContextWithHeader(ctx context.Context, key, value string) context.Context {
header := HeaderFrom(ctx)
header.Add(key, value)
return context.WithValue(ctx, headerKey, header)
}
// HeaderFrom extracts the header object in the given context. Always returns a non-nil Header.
func HeaderFrom(ctx context.Context) http.Header {
ch := http.Header{}
header := ctx.Value(headerKey)
if header != nil {
if h, ok := header.(http.Header); ok {
copyHeaders(h, ch)
}
}
return ch
}
// Opaque key type used to store long poll target.
type longPollTargetKeyType struct{}
var longPollTargetKey = longPollTargetKeyType{}
// WithLongPollTarget returns a new context with the given long poll target.
// `target` should be a full URL and will be injected into the long polling
// http request within StartReceiver.
func ContextWithLongPollTarget(ctx context.Context, target string) context.Context {
return context.WithValue(ctx, longPollTargetKey, target)
}
// LongPollTargetFrom looks in the given context and returns `target` as a
// parsed url if found and valid, otherwise nil.
func LongPollTargetFrom(ctx context.Context) *url.URL {
c := ctx.Value(longPollTargetKey)
if c != nil {
if s, ok := c.(string); ok && s != "" {
if target, err := url.Parse(s); err == nil {
return target
}
}
}
return nil
}

View File

@ -1,4 +0,0 @@
/*
Package http implements the CloudEvent transport implementation using HTTP.
*/
package http

View File

@ -1,205 +0,0 @@
package http
import (
"context"
"github.com/cloudevents/sdk-go/pkg/cloudevents"
cecontext "github.com/cloudevents/sdk-go/pkg/cloudevents/context"
)
// Encoding to use for HTTP transport.
type Encoding int32
type EncodingSelector func(context.Context, cloudevents.Event) Encoding
const (
// Default
Default Encoding = iota
// BinaryV01 is Binary CloudEvents spec v0.1.
BinaryV01
// StructuredV01 is Structured CloudEvents spec v0.1.
StructuredV01
// BinaryV02 is Binary CloudEvents spec v0.2.
BinaryV02
// StructuredV02 is Structured CloudEvents spec v0.2.
StructuredV02
// BinaryV03 is Binary CloudEvents spec v0.3.
BinaryV03
// StructuredV03 is Structured CloudEvents spec v0.3.
StructuredV03
// BatchedV03 is Batched CloudEvents spec v0.3.
BatchedV03
// BinaryV1 is Binary CloudEvents spec v1.0.
BinaryV1
// StructuredV03 is Structured CloudEvents spec v1.0.
StructuredV1
// BatchedV1 is Batched CloudEvents spec v1.0.
BatchedV1
// Unknown is unknown.
Unknown
// Binary is used for Context Based Encoding Selections to use the
// DefaultBinaryEncodingSelectionStrategy
Binary = "binary"
// Structured is used for Context Based Encoding Selections to use the
// DefaultStructuredEncodingSelectionStrategy
Structured = "structured"
// Batched is used for Context Based Encoding Selections to use the
// DefaultStructuredEncodingSelectionStrategy
Batched = "batched"
)
func ContextBasedEncodingSelectionStrategy(ctx context.Context, e cloudevents.Event) Encoding {
encoding := cecontext.EncodingFrom(ctx)
switch encoding {
case "", Binary:
return DefaultBinaryEncodingSelectionStrategy(ctx, e)
case Structured:
return DefaultStructuredEncodingSelectionStrategy(ctx, e)
}
return Default
}
// DefaultBinaryEncodingSelectionStrategy implements a selection process for
// which binary encoding to use based on spec version of the event.
func DefaultBinaryEncodingSelectionStrategy(ctx context.Context, e cloudevents.Event) Encoding {
switch e.SpecVersion() {
case cloudevents.CloudEventsVersionV01:
return BinaryV01
case cloudevents.CloudEventsVersionV02:
return BinaryV02
case cloudevents.CloudEventsVersionV03:
return BinaryV03
case cloudevents.CloudEventsVersionV1:
return BinaryV1
}
// Unknown version, return Default.
return Default
}
// DefaultStructuredEncodingSelectionStrategy implements a selection process
// for which structured encoding to use based on spec version of the event.
func DefaultStructuredEncodingSelectionStrategy(ctx context.Context, e cloudevents.Event) Encoding {
switch e.SpecVersion() {
case cloudevents.CloudEventsVersionV01:
return StructuredV01
case cloudevents.CloudEventsVersionV02:
return StructuredV02
case cloudevents.CloudEventsVersionV03:
return StructuredV03
case cloudevents.CloudEventsVersionV1:
return StructuredV1
}
// Unknown version, return Default.
return Default
}
// String pretty-prints the encoding as a string.
func (e Encoding) String() string {
switch e {
case Default:
return "Default Encoding " + e.Version()
// Binary
case BinaryV01, BinaryV02, BinaryV03, BinaryV1:
return "Binary Encoding " + e.Version()
// Structured
case StructuredV01, StructuredV02, StructuredV03, StructuredV1:
return "Structured Encoding " + e.Version()
// Batched
case BatchedV03, BatchedV1:
return "Batched Encoding " + e.Version()
default:
return "Unknown Encoding"
}
}
// Version pretty-prints the encoding version as a string.
func (e Encoding) Version() string {
switch e {
case Default:
return "Default"
// Version 0.1
case BinaryV01, StructuredV01:
return "v0.1"
// Version 0.2
case BinaryV02, StructuredV02:
return "v0.2"
// Version 0.3
case BinaryV03, StructuredV03, BatchedV03:
return "v0.3"
// Version 1.0
case BinaryV1, StructuredV1, BatchedV1:
return "v1.0"
// Unknown
default:
return "Unknown"
}
}
// Codec creates a structured string to represent the the codec version.
func (e Encoding) Codec() string {
switch e {
case Default:
return "default"
// Version 0.1
case BinaryV01:
return "binary/v0.1"
case StructuredV01:
return "structured/v0.1"
// Version 0.2
case BinaryV02:
return "binary/v0.2"
case StructuredV02:
return "structured/v0.2"
// Version 0.3
case BinaryV03:
return "binary/v0.3"
case StructuredV03:
return "structured/v0.3"
case BatchedV03:
return "batched/v0.3"
// Version 1.0
case BinaryV1:
return "binary/v1.0"
case StructuredV1:
return "structured/v1.0"
case BatchedV1:
return "batched/v1.0"
// Unknown
default:
return "unknown"
}
}
// Name creates a string to represent the the codec name.
func (e Encoding) Name() string {
switch e {
case Default:
return Binary
case BinaryV01, BinaryV02, BinaryV03, BinaryV1:
return Binary
case StructuredV01, StructuredV02, StructuredV03, StructuredV1:
return Structured
case BatchedV03, BatchedV1:
return Batched
default:
return Binary
}
}

View File

@ -1,148 +0,0 @@
package http
import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"github.com/cloudevents/sdk-go/pkg/cloudevents/transport"
)
// type check that this transport message impl matches the contract
var _ transport.Message = (*Message)(nil)
// Message is an http transport message.
type Message struct {
Header http.Header
Body []byte
}
// Response is an http transport response.
type Response struct {
StatusCode int
Message
}
// CloudEventsVersion inspects a message and tries to discover and return the
// CloudEvents spec version.
func (m Message) CloudEventsVersion() string {
// TODO: the impl of this method needs to move into the codec.
if m.Header != nil {
// Try headers first.
// v0.1, cased from the spec
// Note: don't pass literal string direct to m.Header[] so that
// go vet won't complain about non-canonical case.
name := "CE-CloudEventsVersion"
if v := m.Header[name]; len(v) == 1 {
return v[0]
}
// v0.2, canonical casing
if ver := m.Header.Get("CE-CloudEventsVersion"); ver != "" {
return ver
}
// v0.2, cased from the spec
name = "ce-specversion"
if v := m.Header[name]; len(v) == 1 {
return v[0]
}
// v0.2, canonical casing
name = "ce-specversion"
if ver := m.Header.Get(name); ver != "" {
return ver
}
}
// Then try the data body.
// TODO: we need to use the correct decoding based on content type.
raw := make(map[string]json.RawMessage)
if err := json.Unmarshal(m.Body, &raw); err != nil {
return ""
}
// v0.1
if v, ok := raw["cloudEventsVersion"]; ok {
var version string
if err := json.Unmarshal(v, &version); err != nil {
return ""
}
return version
}
// v0.2
if v, ok := raw["specversion"]; ok {
var version string
if err := json.Unmarshal(v, &version); err != nil {
return ""
}
return version
}
return ""
}
func readAllClose(r io.ReadCloser) ([]byte, error) {
if r != nil {
defer r.Close()
return ioutil.ReadAll(r)
}
return nil, nil
}
// NewMessage creates a new message from the Header and Body of
// an http.Request or http.Response
func NewMessage(header http.Header, body io.ReadCloser) (*Message, error) {
var m Message
err := m.Init(header, body)
return &m, err
}
// NewResponse creates a new response from the Header and Body of
// an http.Request or http.Response
func NewResponse(header http.Header, body io.ReadCloser, statusCode int) (*Response, error) {
resp := Response{StatusCode: statusCode}
err := resp.Init(header, body)
return &resp, err
}
// Copy copies a new Body and Header into a message, replacing any previous data.
func (m *Message) Init(header http.Header, body io.ReadCloser) error {
m.Header = make(http.Header, len(header))
copyHeadersEnsure(header, &m.Header)
var err error
m.Body, err = readAllClose(body)
return err
}
func (m *Message) copyOut(header *http.Header, body *io.ReadCloser) {
copyHeadersEnsure(m.Header, header)
*body = nil
if m.Body != nil {
copy := append([]byte(nil), m.Body...)
*body = ioutil.NopCloser(bytes.NewBuffer(copy))
}
}
// ToRequest updates a http.Request from a Message.
// Replaces Body, ContentLength and Method, updates Headers.
// Panic if req is nil
func (m *Message) ToRequest(req *http.Request) {
m.copyOut(&req.Header, &req.Body)
req.ContentLength = int64(len(m.Body))
req.Method = http.MethodPost
}
// ToResponse updates a http.Response from a Response.
// Replaces Body, updates Headers.
// Panic if resp is nil
func (m *Response) ToResponse(resp *http.Response) {
m.copyOut(&resp.Header, &resp.Body)
resp.ContentLength = int64(len(m.Body))
resp.StatusCode = m.StatusCode
}

View File

@ -1,109 +0,0 @@
package http
import (
"fmt"
"github.com/cloudevents/sdk-go/pkg/cloudevents/observability"
"go.opencensus.io/stats"
"go.opencensus.io/stats/view"
)
var (
// LatencyMs measures the latency in milliseconds for the http transport
// methods for CloudEvents.
LatencyMs = stats.Float64(
"cloudevents.io/sdk-go/transport/http/latency",
"The latency in milliseconds for the http transport methods for CloudEvents.",
"ms")
)
var (
// LatencyView is an OpenCensus view that shows http transport method latency.
LatencyView = &view.View{
Name: "transport/http/latency",
Measure: LatencyMs,
Description: "The distribution of latency inside of http transport for CloudEvents.",
Aggregation: view.Distribution(0, .01, .1, 1, 10, 100, 1000, 10000),
TagKeys: observability.LatencyTags(),
}
)
type observed int32
// Adheres to Observable
var _ observability.Observable = observed(0)
const (
reportSend observed = iota
reportReceive
reportServeHTTP
reportEncode
reportDecode
)
// TraceName implements Observable.TraceName
func (o observed) TraceName() string {
switch o {
case reportSend:
return "transport/http/send"
case reportReceive:
return "transport/http/receive"
case reportServeHTTP:
return "transport/http/servehttp"
case reportEncode:
return "transport/http/encode"
case reportDecode:
return "transport/http/decode"
default:
return "transport/http/unknown"
}
}
// MethodName implements Observable.MethodName
func (o observed) MethodName() string {
switch o {
case reportSend:
return "send"
case reportReceive:
return "receive"
case reportServeHTTP:
return "servehttp"
case reportEncode:
return "encode"
case reportDecode:
return "decode"
default:
return "unknown"
}
}
// LatencyMs implements Observable.LatencyMs
func (o observed) LatencyMs() *stats.Float64Measure {
return LatencyMs
}
// CodecObserved is a wrapper to append version to observed.
type CodecObserved struct {
// Method
o observed
// Codec
c string
}
// Adheres to Observable
var _ observability.Observable = (*CodecObserved)(nil)
// TraceName implements Observable.TraceName
func (c CodecObserved) TraceName() string {
return fmt.Sprintf("%s/%s", c.o.TraceName(), c.c)
}
// MethodName implements Observable.MethodName
func (c CodecObserved) MethodName() string {
return fmt.Sprintf("%s/%s", c.o.MethodName(), c.c)
}
// LatencyMs implements Observable.LatencyMs
func (c CodecObserved) LatencyMs() *stats.Float64Measure {
return c.o.LatencyMs()
}

View File

@ -1,266 +0,0 @@
package http
import (
"fmt"
"net"
nethttp "net/http"
"net/url"
"strings"
"time"
)
// Option is the function signature required to be considered an http.Option.
type Option func(*Transport) error
// WithTarget sets the outbound recipient of cloudevents when using an HTTP
// request.
func WithTarget(targetUrl string) Option {
return func(t *Transport) error {
if t == nil {
return fmt.Errorf("http target option can not set nil transport")
}
targetUrl = strings.TrimSpace(targetUrl)
if targetUrl != "" {
var err error
var target *url.URL
target, err = url.Parse(targetUrl)
if err != nil {
return fmt.Errorf("http target option failed to parse target url: %s", err.Error())
}
if t.Req == nil {
t.Req = &nethttp.Request{
Method: nethttp.MethodPost,
}
}
t.Req.URL = target
return nil
}
return fmt.Errorf("http target option was empty string")
}
}
// WithMethod sets the outbound recipient of cloudevents when using an HTTP
// request.
func WithMethod(method string) Option {
return func(t *Transport) error {
if t == nil {
return fmt.Errorf("http method option can not set nil transport")
}
method = strings.TrimSpace(method)
if method != "" {
if t.Req == nil {
t.Req = &nethttp.Request{}
}
t.Req.Method = method
return nil
}
return fmt.Errorf("http method option was empty string")
}
}
// WithHeader sets an additional default outbound header for all cloudevents
// when using an HTTP request.
func WithHeader(key, value string) Option {
return func(t *Transport) error {
if t == nil {
return fmt.Errorf("http header option can not set nil transport")
}
key = strings.TrimSpace(key)
if key != "" {
if t.Req == nil {
t.Req = &nethttp.Request{}
}
if t.Req.Header == nil {
t.Req.Header = nethttp.Header{}
}
t.Req.Header.Add(key, value)
return nil
}
return fmt.Errorf("http header option was empty string")
}
}
// WithShutdownTimeout sets the shutdown timeout when the http server is being shutdown.
func WithShutdownTimeout(timeout time.Duration) Option {
return func(t *Transport) error {
if t == nil {
return fmt.Errorf("http shutdown timeout option can not set nil transport")
}
t.ShutdownTimeout = &timeout
return nil
}
}
// WithEncoding sets the encoding for clients with HTTP transports.
func WithEncoding(encoding Encoding) Option {
return func(t *Transport) error {
if t == nil {
return fmt.Errorf("http encoding option can not set nil transport")
}
t.Encoding = encoding
return nil
}
}
// WithDefaultEncodingSelector sets the encoding selection strategy for
// default encoding selections based on Event.
func WithDefaultEncodingSelector(fn EncodingSelector) Option {
return func(t *Transport) error {
if t == nil {
return fmt.Errorf("http default encoding selector option can not set nil transport")
}
if fn != nil {
t.DefaultEncodingSelectionFn = fn
return nil
}
return fmt.Errorf("http fn for DefaultEncodingSelector was nil")
}
}
// WithContextBasedEncoding sets the encoding selection strategy for
// default encoding selections based context and then on Event, the encoded
// event will be the given version in the encoding specified by the given
// context, or Binary if not set.
func WithContextBasedEncoding() Option {
return func(t *Transport) error {
if t == nil {
return fmt.Errorf("http context based encoding option can not set nil transport")
}
t.DefaultEncodingSelectionFn = ContextBasedEncodingSelectionStrategy
return nil
}
}
// WithBinaryEncoding sets the encoding selection strategy for
// default encoding selections based on Event, the encoded event will be the
// given version in Binary form.
func WithBinaryEncoding() Option {
return func(t *Transport) error {
if t == nil {
return fmt.Errorf("http binary encoding option can not set nil transport")
}
t.DefaultEncodingSelectionFn = DefaultBinaryEncodingSelectionStrategy
return nil
}
}
// WithStructuredEncoding sets the encoding selection strategy for
// default encoding selections based on Event, the encoded event will be the
// given version in Structured form.
func WithStructuredEncoding() Option {
return func(t *Transport) error {
if t == nil {
return fmt.Errorf("http structured encoding option can not set nil transport")
}
t.DefaultEncodingSelectionFn = DefaultStructuredEncodingSelectionStrategy
return nil
}
}
func checkListen(t *Transport, prefix string) error {
switch {
case t.Port != nil:
return fmt.Errorf("%v port already set", prefix)
case t.listener != nil:
return fmt.Errorf("%v listener already set", prefix)
}
return nil
}
// WithPort sets the listening port for StartReceiver.
// Only one of WithListener or WithPort is allowed.
func WithPort(port int) Option {
return func(t *Transport) error {
if t == nil {
return fmt.Errorf("http port option can not set nil transport")
}
if port < 0 || port > 65535 {
return fmt.Errorf("http port option was given an invalid port: %d", port)
}
if err := checkListen(t, "http port option"); err != nil {
return err
}
t.setPort(port)
return nil
}
}
// WithListener sets the listener for StartReceiver.
// Only one of WithListener or WithPort is allowed.
func WithListener(l net.Listener) Option {
return func(t *Transport) error {
if t == nil {
return fmt.Errorf("http listener option can not set nil transport")
}
if err := checkListen(t, "http port option"); err != nil {
return err
}
t.listener = l
_, err := t.listen()
return err
}
}
// WithPath sets the path to receive cloudevents on for HTTP transports.
func WithPath(path string) Option {
return func(t *Transport) error {
if t == nil {
return fmt.Errorf("http path option can not set nil transport")
}
path = strings.TrimSpace(path)
if len(path) == 0 {
return fmt.Errorf("http path option was given an invalid path: %q", path)
}
t.Path = path
return nil
}
}
// Middleware is a function that takes an existing http.Handler and wraps it in middleware,
// returning the wrapped http.Handler.
type Middleware func(next nethttp.Handler) nethttp.Handler
// WithMiddleware adds an HTTP middleware to the transport. It may be specified multiple times.
// Middleware is applied to everything before it. For example
// `NewClient(WithMiddleware(foo), WithMiddleware(bar))` would result in `bar(foo(original))`.
func WithMiddleware(middleware Middleware) Option {
return func(t *Transport) error {
if t == nil {
return fmt.Errorf("http middleware option can not set nil transport")
}
t.middleware = append(t.middleware, middleware)
return nil
}
}
// WithLongPollTarget sets the receivers URL to perform long polling after
// StartReceiver is called.
func WithLongPollTarget(targetUrl string) Option {
return func(t *Transport) error {
if t == nil {
return fmt.Errorf("http long poll target option can not set nil transport")
}
targetUrl = strings.TrimSpace(targetUrl)
if targetUrl != "" {
var err error
var target *url.URL
target, err = url.Parse(targetUrl)
if err != nil {
return fmt.Errorf("http long poll target option failed to parse target url: %s", err.Error())
}
if t.LongPollReq == nil {
t.LongPollReq = &nethttp.Request{
Method: nethttp.MethodGet,
}
}
t.LongPollReq.URL = target
return nil
}
return fmt.Errorf("http long poll target option was empty string")
}
}

View File

@ -1,681 +0,0 @@
package http
import (
"context"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
"go.uber.org/zap"
"github.com/cloudevents/sdk-go/pkg/cloudevents"
cecontext "github.com/cloudevents/sdk-go/pkg/cloudevents/context"
"github.com/cloudevents/sdk-go/pkg/cloudevents/observability"
"github.com/cloudevents/sdk-go/pkg/cloudevents/transport"
)
// Transport adheres to transport.Transport.
var _ transport.Transport = (*Transport)(nil)
const (
// DefaultShutdownTimeout defines the default timeout given to the http.Server when calling Shutdown.
DefaultShutdownTimeout = time.Minute * 1
// TransportName is the name of this transport.
TransportName = "HTTP"
)
// Transport acts as both a http client and a http handler.
type Transport struct {
// The encoding used to select the codec for outbound events.
Encoding Encoding
// DefaultEncodingSelectionFn allows for other encoding selection strategies to be injected.
DefaultEncodingSelectionFn EncodingSelector
// ShutdownTimeout defines the timeout given to the http.Server when calling Shutdown.
// If nil, DefaultShutdownTimeout is used.
ShutdownTimeout *time.Duration
// Sending
// Client is the http client that will be used to send requests.
// If nil, the Transport will create a one.
Client *http.Client
// Req is the base http request that is used for http.Do.
// Only .Method, .URL, .Close, and .Header is considered.
// If not set, Req.Method defaults to POST.
// Req.URL or context.WithTarget(url) are required for sending.
Req *http.Request
// Receiving
// Receiver is invoked target for incoming events.
Receiver transport.Receiver
// Converter is invoked if the incoming transport receives an undecodable
// message.
Converter transport.Converter
// Port is the port to bind the receiver to. Defaults to 8080.
Port *int
// Path is the path to bind the receiver to. Defaults to "/".
Path string
// Handler is the handler the http Server will use. Use this to reuse the
// http server. If nil, the Transport will create a one.
Handler *http.ServeMux
// LongPollClient is the http client that will be used to long poll.
// If nil and LongPollReq is set, the Transport will create a one.
LongPollClient *http.Client
// LongPollReq is the base http request that is used for long poll.
// Only .Method, .URL, .Close, and .Header is considered.
// If not set, LongPollReq.Method defaults to GET.
// LongPollReq.URL or context.WithLongPollTarget(url) are required to long
// poll on StartReceiver.
LongPollReq *http.Request
listener net.Listener
server *http.Server
handlerRegistered bool
codec transport.Codec
// Create Mutex
crMu sync.Mutex
// Receive Mutex
reMu sync.Mutex
middleware []Middleware
}
func New(opts ...Option) (*Transport, error) {
t := &Transport{
Req: &http.Request{
Method: http.MethodPost,
},
}
if err := t.applyOptions(opts...); err != nil {
return nil, err
}
return t, nil
}
func (t *Transport) applyOptions(opts ...Option) error {
for _, fn := range opts {
if err := fn(t); err != nil {
return err
}
}
return nil
}
func (t *Transport) loadCodec(ctx context.Context) bool {
if t.codec == nil {
t.crMu.Lock()
if t.DefaultEncodingSelectionFn != nil && t.Encoding != Default {
logger := cecontext.LoggerFrom(ctx)
logger.Warn("transport has a DefaultEncodingSelectionFn set but Encoding is not Default. DefaultEncodingSelectionFn will be ignored.")
t.codec = &Codec{
Encoding: t.Encoding,
}
} else {
t.codec = &Codec{
Encoding: t.Encoding,
DefaultEncodingSelectionFn: t.DefaultEncodingSelectionFn,
}
}
t.crMu.Unlock()
}
return true
}
func copyHeaders(from, to http.Header) {
if from == nil || to == nil {
return
}
for header, values := range from {
for _, value := range values {
to.Add(header, value)
}
}
}
// Ensure to is a non-nil map before copying
func copyHeadersEnsure(from http.Header, to *http.Header) {
if len(from) > 0 {
if *to == nil {
*to = http.Header{}
}
copyHeaders(from, *to)
}
}
// Send implements Transport.Send
func (t *Transport) Send(ctx context.Context, event cloudevents.Event) (context.Context, *cloudevents.Event, error) {
ctx, r := observability.NewReporter(ctx, reportSend)
rctx, resp, err := t.obsSend(ctx, event)
if err != nil {
r.Error()
} else {
r.OK()
}
return rctx, resp, err
}
func (t *Transport) obsSend(ctx context.Context, event cloudevents.Event) (context.Context, *cloudevents.Event, error) {
if t.Client == nil {
t.crMu.Lock()
t.Client = &http.Client{}
t.crMu.Unlock()
}
req := http.Request{
Header: HeaderFrom(ctx),
}
if t.Req != nil {
req.Method = t.Req.Method
req.URL = t.Req.URL
req.Close = t.Req.Close
req.Host = t.Req.Host
copyHeadersEnsure(t.Req.Header, &req.Header)
}
// Override the default request with target from context.
if target := cecontext.TargetFrom(ctx); target != nil {
req.URL = target
}
if ok := t.loadCodec(ctx); !ok {
return WithTransportContext(ctx, NewTransportContextFromResponse(nil)), nil, fmt.Errorf("unknown encoding set on transport: %d", t.Encoding)
}
msg, err := t.codec.Encode(ctx, event)
if err != nil {
return WithTransportContext(ctx, NewTransportContextFromResponse(nil)), nil, err
}
if m, ok := msg.(*Message); ok {
m.ToRequest(&req)
return httpDo(ctx, t.Client, &req, func(resp *http.Response, err error) (context.Context, *cloudevents.Event, error) {
rctx := WithTransportContext(ctx, NewTransportContextFromResponse(resp))
if err != nil {
return rctx, nil, err
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
respEvent, err := t.MessageToEvent(ctx, &Message{
Header: resp.Header,
Body: body,
})
if err != nil {
isErr := true
handled := false
if txerr, ok := err.(*transport.ErrTransportMessageConversion); ok {
if !txerr.IsFatal() {
isErr = false
}
if txerr.Handled() {
handled = true
}
}
if isErr {
return rctx, nil, err
}
if handled {
return rctx, nil, nil
}
}
if accepted(resp) {
return rctx, respEvent, nil
}
return rctx, respEvent, fmt.Errorf("error sending cloudevent: %s", resp.Status)
})
}
return WithTransportContext(ctx, NewTransportContextFromResponse(nil)), nil, fmt.Errorf("failed to encode Event into a Message")
}
func (t *Transport) MessageToEvent(ctx context.Context, msg *Message) (*cloudevents.Event, error) {
logger := cecontext.LoggerFrom(ctx)
var event *cloudevents.Event
var err error
if msg.CloudEventsVersion() != "" {
// This is likely a cloudevents encoded message, try to decode it.
if ok := t.loadCodec(ctx); !ok {
err = transport.NewErrTransportMessageConversion("http", fmt.Sprintf("unknown encoding set on transport: %d", t.Encoding), false, true)
logger.Error("failed to load codec", zap.Error(err))
} else {
event, err = t.codec.Decode(ctx, msg)
}
} else {
err = transport.NewErrTransportMessageConversion("http", "cloudevents version unknown", false, false)
}
// If codec returns and error, or could not load the correct codec, try
// with the converter if it is set.
if err != nil && t.HasConverter() {
event, err = t.Converter.Convert(ctx, msg, err)
}
// If err is still set, it means that there was no converter, or the
// converter failed to convert.
if err != nil {
logger.Debug("failed to decode message", zap.Error(err))
}
// If event and error are both nil, then there is nothing to do with this event, it was handled.
if err == nil && event == nil {
logger.Debug("convert function returned (nil, nil)")
err = transport.NewErrTransportMessageConversion("http", "convert function handled request", true, false)
}
return event, err
}
// SetReceiver implements Transport.SetReceiver
func (t *Transport) SetReceiver(r transport.Receiver) {
t.Receiver = r
}
// SetConverter implements Transport.SetConverter
func (t *Transport) SetConverter(c transport.Converter) {
t.Converter = c
}
// HasConverter implements Transport.HasConverter
func (t *Transport) HasConverter() bool {
return t.Converter != nil
}
// StartReceiver implements Transport.StartReceiver
// NOTE: This is a blocking call.
func (t *Transport) StartReceiver(ctx context.Context) error {
t.reMu.Lock()
defer t.reMu.Unlock()
if t.LongPollReq != nil {
go func() { _ = t.longPollStart(ctx) }()
}
if t.Handler == nil {
t.Handler = http.NewServeMux()
}
if !t.handlerRegistered {
// handler.Handle might panic if the user tries to use the same path as the sdk.
t.Handler.Handle(t.GetPath(), t)
t.handlerRegistered = true
}
addr, err := t.listen()
if err != nil {
return err
}
t.server = &http.Server{
Addr: addr.String(),
Handler: attachMiddleware(t.Handler, t.middleware),
}
// Shutdown
defer func() {
t.server.Close()
t.server = nil
}()
errChan := make(chan error, 1)
go func() {
errChan <- t.server.Serve(t.listener)
}()
// wait for the server to return or ctx.Done().
select {
case <-ctx.Done():
// Try a gracefully shutdown.
timeout := DefaultShutdownTimeout
if t.ShutdownTimeout != nil {
timeout = *t.ShutdownTimeout
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
err := t.server.Shutdown(ctx)
<-errChan // Wait for server goroutine to exit
return err
case err := <-errChan:
return err
}
}
func (t *Transport) longPollStart(ctx context.Context) error {
logger := cecontext.LoggerFrom(ctx)
logger.Info("starting long poll receiver")
if t.LongPollClient == nil {
t.crMu.Lock()
t.LongPollClient = &http.Client{}
t.crMu.Unlock()
}
req := &http.Request{
// TODO: decide if it is ok to use HeaderFrom context here.
Header: HeaderFrom(ctx),
}
if t.LongPollReq != nil {
req.Method = t.LongPollReq.Method
req.URL = t.LongPollReq.URL
req.Close = t.LongPollReq.Close
copyHeaders(t.LongPollReq.Header, req.Header)
}
// Override the default request with target from context.
if target := LongPollTargetFrom(ctx); target != nil {
req.URL = target
}
if req.URL == nil {
return errors.New("no long poll target found")
}
req = req.WithContext(ctx)
msgCh := make(chan Message)
defer close(msgCh)
go func(ch chan<- Message) {
for {
if resp, err := t.LongPollClient.Do(req); err != nil {
logger.Errorw("long poll request returned error", err)
uErr := err.(*url.Error)
if uErr.Temporary() || uErr.Timeout() {
continue
}
// TODO: if the transport is throwing errors, we might want to try again. Maybe with a back-off sleep.
// But this error also might be that there was a done on the context.
} else if resp.StatusCode == http.StatusNotModified {
// Keep polling.
continue
} else if resp.StatusCode == http.StatusOK {
body, _ := ioutil.ReadAll(resp.Body)
if err := resp.Body.Close(); err != nil {
logger.Warnw("error closing long poll response body", zap.Error(err))
}
msg := Message{
Header: resp.Header,
Body: body,
}
msgCh <- msg
} else {
// TODO: not sure what to do with upstream errors yet.
logger.Errorw("unhandled long poll response", zap.Any("resp", resp))
}
}
}(msgCh)
// Attach the long poll request context to the context.
ctx = WithTransportContext(ctx, TransportContext{
URI: req.URL.RequestURI(),
Host: req.URL.Host,
Method: req.Method,
})
for {
select {
case <-ctx.Done():
return nil
case msg := <-msgCh:
logger.Debug("got a message", zap.Any("msg", msg))
if event, err := t.MessageToEvent(ctx, &msg); err != nil {
logger.Errorw("could not convert http message to event", zap.Error(err))
} else {
logger.Debugw("got an event", zap.Any("event", event))
// TODO: deliver event.
if _, err := t.invokeReceiver(ctx, *event); err != nil {
logger.Errorw("could not invoke receiver event", zap.Error(err))
}
}
}
}
}
// attachMiddleware attaches the HTTP middleware to the specified handler.
func attachMiddleware(h http.Handler, middleware []Middleware) http.Handler {
for _, m := range middleware {
h = m(h)
}
return h
}
type eventError struct {
ctx context.Context
event *cloudevents.Event
err error
}
func httpDo(ctx context.Context, client *http.Client, req *http.Request, fn func(*http.Response, error) (context.Context, *cloudevents.Event, error)) (context.Context, *cloudevents.Event, error) {
// Run the HTTP request in a goroutine and pass the response to fn.
c := make(chan eventError, 1)
req = req.WithContext(ctx)
go func() {
rctx, event, err := fn(client.Do(req))
c <- eventError{ctx: rctx, event: event, err: err}
}()
select {
case <-ctx.Done():
return ctx, nil, ctx.Err()
case ee := <-c:
return ee.ctx, ee.event, ee.err
}
}
// accepted is a helper method to understand if the response from the target
// accepted the CloudEvent.
func accepted(resp *http.Response) bool {
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return true
}
return false
}
func (t *Transport) invokeReceiver(ctx context.Context, event cloudevents.Event) (*Response, error) {
ctx, r := observability.NewReporter(ctx, reportReceive)
resp, err := t.obsInvokeReceiver(ctx, event)
if err != nil {
r.Error()
} else {
r.OK()
}
return resp, err
}
func (t *Transport) obsInvokeReceiver(ctx context.Context, event cloudevents.Event) (*Response, error) {
logger := cecontext.LoggerFrom(ctx)
if t.Receiver != nil {
// Note: http does not use eventResp.Reason
eventResp := cloudevents.EventResponse{}
resp := Response{}
err := t.Receiver.Receive(ctx, event, &eventResp)
if err != nil {
logger.Warnw("got an error from receiver fn", zap.Error(err))
resp.StatusCode = http.StatusInternalServerError
return &resp, err
}
if eventResp.Event != nil {
if t.loadCodec(ctx) {
if m, err := t.codec.Encode(ctx, *eventResp.Event); err != nil {
logger.Errorw("failed to encode response from receiver fn", zap.Error(err))
} else if msg, ok := m.(*Message); ok {
resp.Message = *msg
}
} else {
logger.Error("failed to load codec")
resp.StatusCode = http.StatusInternalServerError
return &resp, err
}
// Look for a transport response context
var trx *TransportResponseContext
if ptrTrx, ok := eventResp.Context.(*TransportResponseContext); ok {
// found a *TransportResponseContext, use it.
trx = ptrTrx
} else if realTrx, ok := eventResp.Context.(TransportResponseContext); ok {
// found a TransportResponseContext, make it a pointer.
trx = &realTrx
}
// If we found a TransportResponseContext, use it.
if trx != nil && trx.Header != nil && len(trx.Header) > 0 {
copyHeadersEnsure(trx.Header, &resp.Message.Header)
}
}
if eventResp.Status != 0 {
resp.StatusCode = eventResp.Status
} else {
resp.StatusCode = http.StatusAccepted // default is 202 - Accepted
}
return &resp, err
}
return nil, nil
}
// ServeHTTP implements http.Handler
func (t *Transport) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ctx, r := observability.NewReporter(req.Context(), reportServeHTTP)
// Add the transport context to ctx.
ctx = WithTransportContext(ctx, NewTransportContext(req))
logger := cecontext.LoggerFrom(ctx)
body, err := ioutil.ReadAll(req.Body)
if err != nil {
logger.Errorw("failed to handle request", zap.Error(err))
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(`{"error":"Invalid request"}`))
r.Error()
return
}
event, err := t.MessageToEvent(ctx, &Message{
Header: req.Header,
Body: body,
})
if err != nil {
isFatal := true
handled := false
if txerr, ok := err.(*transport.ErrTransportMessageConversion); ok {
isFatal = txerr.IsFatal()
handled = txerr.Handled()
}
if isFatal {
logger.Errorw("failed to convert http message to event", zap.Error(err))
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(fmt.Sprintf(`{"error":%q}`, err.Error())))
r.Error()
return
}
// if handled, do not pass to receiver.
if handled {
w.WriteHeader(http.StatusNoContent)
r.OK()
return
}
}
if event == nil {
logger.Error("failed to get non-nil event from MessageToEvent")
w.WriteHeader(http.StatusBadRequest)
r.Error()
return
}
resp, err := t.invokeReceiver(ctx, *event)
if err != nil {
logger.Warnw("error returned from invokeReceiver", zap.Error(err))
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(fmt.Sprintf(`{"error":%q}`, err.Error())))
r.Error()
return
}
if resp != nil {
if t.Req != nil {
copyHeaders(t.Req.Header, w.Header())
}
if len(resp.Message.Header) > 0 {
copyHeaders(resp.Message.Header, w.Header())
}
status := http.StatusAccepted
if resp.StatusCode >= 200 && resp.StatusCode < 600 {
status = resp.StatusCode
}
w.Header().Add("Content-Length", strconv.Itoa(len(resp.Message.Body)))
w.WriteHeader(status)
if len(resp.Message.Body) > 0 {
if _, err := w.Write(resp.Message.Body); err != nil {
r.Error()
return
}
}
r.OK()
return
}
w.WriteHeader(http.StatusNoContent)
r.OK()
}
// GetPort returns the listening port.
// Returns -1 if there is a listening error.
// Note this will call net.Listen() if the listener is not already started.
func (t *Transport) GetPort() int {
// Ensure we have a listener and therefore a port.
if _, err := t.listen(); err == nil || t.Port != nil {
return *t.Port
}
return -1
}
func (t *Transport) setPort(port int) {
if t.Port == nil {
t.Port = new(int)
}
*t.Port = port
}
// listen if not already listening, update t.Port
func (t *Transport) listen() (net.Addr, error) {
if t.listener == nil {
port := 8080
if t.Port != nil {
port = *t.Port
if port < 0 || port > 65535 {
return nil, fmt.Errorf("invalid port %d", port)
}
}
var err error
if t.listener, err = net.Listen("tcp", fmt.Sprintf(":%d", port)); err != nil {
return nil, err
}
}
addr := t.listener.Addr()
if tcpAddr, ok := addr.(*net.TCPAddr); ok {
t.setPort(tcpAddr.Port)
}
return addr, nil
}
// GetPath returns the path the transport is hosted on. If the path is '/',
// the transport will handle requests on any URI. To discover the true path
// a request was received on, inspect the context from Receive(cxt, ...) with
// TransportContextFrom(ctx).
func (t *Transport) GetPath() string {
path := strings.TrimSpace(t.Path)
if len(path) > 0 {
return path
}
return "/" // default
}

View File

@ -1,9 +0,0 @@
package transport
// Message is the abstract transport message wrapper.
type Message interface {
// CloudEventsVersion returns the version of the CloudEvent.
CloudEventsVersion() string
// TODO maybe get encoding
}

View File

@ -1,44 +0,0 @@
package transport
import (
"context"
"github.com/cloudevents/sdk-go/pkg/cloudevents"
)
// Transport is the interface for transport sender to send the converted Message
// over the underlying transport.
type Transport interface {
Send(context.Context, cloudevents.Event) (context.Context, *cloudevents.Event, error)
SetReceiver(Receiver)
StartReceiver(context.Context) error
// SetConverter sets the delegate to use for converting messages that have
// failed to be decoded from known codecs for this transport.
SetConverter(Converter)
// HasConverter is true when a non-nil converter has been set.
HasConverter() bool
}
// Receiver is an interface to define how a transport will invoke a listener
// of incoming events.
type Receiver interface {
Receive(context.Context, cloudevents.Event, *cloudevents.EventResponse) error
}
// ReceiveFunc wraps a function as a Receiver object.
type ReceiveFunc func(ctx context.Context, e cloudevents.Event, er *cloudevents.EventResponse) error
// Receive implements Receiver.Receive
func (f ReceiveFunc) Receive(ctx context.Context, e cloudevents.Event, er *cloudevents.EventResponse) error {
return f(ctx, e, er)
}
// Converter is an interface to define how a transport delegate to convert an
// non-understood transport message from the internal codecs. Providing a
// Converter allows incoming requests to be bridged to CloudEvents format if
// they have not been sent as an event in CloudEvents format.
type Converter interface {
Convert(context.Context, Message, error) (*cloudevents.Event, error)
}

View File

@ -1,79 +0,0 @@
package types
import (
"encoding/json"
"encoding/xml"
"fmt"
"net/url"
)
// URLRef is a wrapper to url.URL. It is intended to enforce compliance with
// the CloudEvents spec for their definition of URI-Reference. Custom
// marshal methods are implemented to ensure the outbound URLRef object is
// is a flat string.
//
// deprecated: use URIRef.
type URLRef struct {
url.URL
}
// ParseURLRef attempts to parse the given string as a URI-Reference.
func ParseURLRef(u string) *URLRef {
if u == "" {
return nil
}
pu, err := url.Parse(u)
if err != nil {
return nil
}
return &URLRef{URL: *pu}
}
// MarshalJSON implements a custom json marshal method used when this type is
// marshaled using json.Marshal.
func (u URLRef) MarshalJSON() ([]byte, error) {
b := fmt.Sprintf("%q", u.String())
return []byte(b), nil
}
// UnmarshalJSON implements the json unmarshal method used when this type is
// unmarshaled using json.Unmarshal.
func (u *URLRef) UnmarshalJSON(b []byte) error {
var ref string
if err := json.Unmarshal(b, &ref); err != nil {
return err
}
r := ParseURLRef(ref)
if r != nil {
*u = *r
}
return nil
}
// MarshalXML implements a custom xml marshal method used when this type is
// marshaled using xml.Marshal.
func (u URLRef) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.EncodeElement(u.String(), start)
}
// UnmarshalXML implements the xml unmarshal method used when this type is
// unmarshaled using xml.Unmarshal.
func (u *URLRef) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var ref string
if err := d.DecodeElement(&ref, &start); err != nil {
return err
}
r := ParseURLRef(ref)
if r != nil {
*u = *r
}
return nil
}
// String returns the full string representation of the URI-Reference.
func (u *URLRef) String() string {
if u == nil {
return ""
}
return u.URL.String()
}

161
vendor/github.com/cloudevents/sdk-go/v2/alias.go generated vendored Normal file
View File

@ -0,0 +1,161 @@
package v2
// Package cloudevents alias' common functions and types to improve discoverability and reduce
// the number of imports for simple HTTP clients.
import (
"github.com/cloudevents/sdk-go/v2/binding"
"github.com/cloudevents/sdk-go/v2/client"
"github.com/cloudevents/sdk-go/v2/context"
"github.com/cloudevents/sdk-go/v2/event"
"github.com/cloudevents/sdk-go/v2/observability"
"github.com/cloudevents/sdk-go/v2/protocol"
"github.com/cloudevents/sdk-go/v2/protocol/http"
"github.com/cloudevents/sdk-go/v2/types"
)
// Client
type ClientOption client.Option
type Client = client.Client
// Event
type Event = event.Event
type Result = protocol.Result
// Context
type EventContext = event.EventContext
type EventContextV1 = event.EventContextV1
type EventContextV03 = event.EventContextV03
// Custom Types
type Timestamp = types.Timestamp
type URIRef = types.URIRef
// HTTP Protocol
type HTTPOption http.Option
type HTTPProtocol = http.Protocol
// Encoding
type Encoding = binding.Encoding
// Message
type Message = binding.Message
const (
// ReadEncoding
ApplicationXML = event.ApplicationXML
ApplicationJSON = event.ApplicationJSON
TextPlain = event.TextPlain
ApplicationCloudEventsJSON = event.ApplicationCloudEventsJSON
ApplicationCloudEventsBatchJSON = event.ApplicationCloudEventsBatchJSON
Base64 = event.Base64
// Event Versions
VersionV1 = event.CloudEventsVersionV1
VersionV03 = event.CloudEventsVersionV03
// Encoding
EncodingBinary = binding.EncodingBinary
EncodingStructured = binding.EncodingStructured
)
var (
// ContentType Helpers
StringOfApplicationJSON = event.StringOfApplicationJSON
StringOfApplicationXML = event.StringOfApplicationXML
StringOfTextPlain = event.StringOfTextPlain
StringOfApplicationCloudEventsJSON = event.StringOfApplicationCloudEventsJSON
StringOfApplicationCloudEventsBatchJSON = event.StringOfApplicationCloudEventsBatchJSON
StringOfBase64 = event.StringOfBase64
// Client Creation
NewClient = client.New
NewClientObserved = client.NewObserved
NewDefaultClient = client.NewDefault
NewHTTPReceiveHandler = client.NewHTTPReceiveHandler
// Client Options
WithEventDefaulter = client.WithEventDefaulter
WithUUIDs = client.WithUUIDs
WithTimeNow = client.WithTimeNow
WithTracePropagation = client.WithTracePropagation()
// Results
ResultIs = protocol.ResultIs
ResultAs = protocol.ResultAs
// Receipt helpers
NewReceipt = protocol.NewReceipt
ResultACK = protocol.ResultACK
ResultNACK = protocol.ResultNACK
IsACK = protocol.IsACK
IsNACK = protocol.IsNACK
// Event Creation
NewEvent = event.New
NewResult = protocol.NewResult
NewHTTPResult = http.NewResult
// Message Creation
ToMessage = binding.ToMessage
// HTTP Messages
WriteHTTPRequest = http.WriteRequest
// Tracing
EnableTracing = observability.EnableTracing
// Context
ContextWithTarget = context.WithTarget
TargetFromContext = context.TargetFrom
WithEncodingBinary = binding.WithForceBinary
WithEncodingStructured = binding.WithForceStructured
// Custom Types
ParseTimestamp = types.ParseTimestamp
ParseURIRef = types.ParseURIRef
ParseURI = types.ParseURI
// HTTP Protocol
NewHTTP = http.New
// HTTP Protocol Options
WithTarget = http.WithTarget
WithHeader = http.WithHeader
WithShutdownTimeout = http.WithShutdownTimeout
//WithEncoding = http.WithEncoding
//WithStructuredEncoding = http.WithStructuredEncoding // TODO: expose new way
WithPort = http.WithPort
WithPath = http.WithPath
WithMiddleware = http.WithMiddleware
WithListener = http.WithListener
WithRoundTripper = http.WithRoundTripper
)

View File

@ -0,0 +1,39 @@
package binding
import (
"context"
"io"
"github.com/cloudevents/sdk-go/v2/binding/spec"
)
// BinaryWriter is used to visit a binary Message and generate a new representation.
//
// Protocols that supports binary encoding should implement this interface to implement direct
// binary to binary encoding and event to binary encoding.
//
// Start() and End() methods are invoked every time this BinaryWriter implementation is used to visit a Message
type BinaryWriter interface {
// Method invoked at the beginning of the visit. Useful to perform initial memory allocations
Start(ctx context.Context) error
// Set a standard attribute.
//
// The value can either be the correct golang type for the attribute, or a canonical
// string encoding. See package types to perform the needed conversions
SetAttribute(attribute spec.Attribute, value interface{}) error
// Set an extension attribute.
//
// The value can either be the correct golang type for the attribute, or a canonical
// string encoding. See package types to perform the needed conversions
SetExtension(name string, value interface{}) error
// SetData receives an io.Reader for the data attribute.
// io.Reader is not invoked when the data attribute is empty
SetData(data io.Reader) error
// End method is invoked only after the whole encoding process ends successfully.
// If it fails, it's never invoked. It can be used to finalize the message.
End(ctx context.Context) error
}

64
vendor/github.com/cloudevents/sdk-go/v2/binding/doc.go generated vendored Normal file
View File

@ -0,0 +1,64 @@
package binding
/*
Package binding defines interfaces for protocol bindings.
NOTE: Most applications that emit or consume events should use the ../client
package, which provides a simpler API to the underlying binding.
The interfaces in this package provide extra encoding and protocol information
to allow efficient forwarding and end-to-end reliable delivery between a
Receiver and a Sender belonging to different bindings. This is useful for
intermediary applications that route or forward events, but not necessary for
most "endpoint" applications that emit or consume events.
Protocol Bindings
A protocol binding usually implements a Message, a Sender and Receiver, a StructuredWriter and a BinaryWriter (depending on the supported encodings of the protocol) and an Write[ProtocolMessage] method.
Read and write events
The core of this package is the binding.Message interface.
Through binding.MessageReader It defines how to read a protocol specific message for an
encoded event in structured mode or binary mode.
The entity who receives a protocol specific data structure representing a message
(e.g. an HttpRequest) encapsulates it in a binding.Message implementation using a NewMessage method (e.g. http.NewMessage).
Then the entity that wants to send the binding.Message back on the wire,
translates it back to the protocol specific data structure (e.g. a Kafka ConsumerMessage), using
the writers BinaryWriter and StructuredWriter specific to that protocol.
Binding implementations exposes their writers
through a specific Write[ProtocolMessage] function (e.g. kafka.EncodeProducerMessage),
in order to simplify the encoding process.
The encoding process can be customized in order to mutate the final result with binding.TransformerFactory.
A bunch of these are provided directly by the binding/transformer module.
Usually binding.Message implementations can be encoded only one time, because the encoding process drain the message itself.
In order to consume a message several times, the binding/buffering module provides several APIs to buffer the Message.
A message can be converted to an event.Event using binding.ToEvent() method.
An event.Event can be used as Message casting it to binding.EventMessage.
In order to simplify the encoding process for each protocol, this package provide several utility methods like binding.Write and binding.DirectWrite.
The binding.Write method tries to preserve the structured/binary encoding, in order to be as much efficient as possible.
Messages can be eventually wrapped to change their behaviours and binding their lifecycle, like the binding.FinishMessage.
Every Message wrapper implements the MessageWrapper interface
Sender and Receiver
A Receiver receives protocol specific messages and wraps them to into binding.Message implementations.
A Sender converts arbitrary Message implementations to a protocol-specific form using the protocol specific Write method
and sends them.
Message and ExactlyOnceMessage provide methods to allow acknowledgments to
propagate when a reliable messages is forwarded from a Receiver to a Sender.
QoS 0 (unreliable), 1 (at-least-once) and 2 (exactly-once) are supported.
Transport
A binding implementation providing Sender and Receiver implementations can be used as a Transport through the BindingTransport adapter.
*/

View File

@ -0,0 +1,26 @@
package binding
import "errors"
// Encoding enum specifies the type of encodings supported by binding interfaces
type Encoding int
const (
// Binary encoding as specified in https://github.com/cloudevents/spec/blob/master/spec.md#message
EncodingBinary Encoding = iota
// Structured encoding as specified in https://github.com/cloudevents/spec/blob/master/spec.md#message
EncodingStructured
// Message is an instance of EventMessage or it contains EventMessage nested (through MessageWrapper)
EncodingEvent
// When the encoding is unknown (which means that the message is a non-event)
EncodingUnknown
)
// Error to specify that or the Message is not an event or it is encoded with an unknown encoding
var ErrUnknownEncoding = errors.New("unknown Message encoding")
// ErrNotStructured returned by Message.Structured for non-structured messages.
var ErrNotStructured = errors.New("message is not in structured mode")
// ErrNotBinary returned by Message.Binary for non-binary messages.
var ErrNotBinary = errors.New("message is not in binary mode")

View File

@ -0,0 +1,90 @@
package binding
import (
"bytes"
"context"
"github.com/cloudevents/sdk-go/v2/binding/format"
"github.com/cloudevents/sdk-go/v2/binding/spec"
"github.com/cloudevents/sdk-go/v2/event"
)
const (
FORMAT_EVENT_STRUCTURED = "FORMAT_EVENT_STRUCTURED"
)
// EventMessage type-converts a event.Event object to implement Message.
// This allows local event.Event objects to be sent directly via Sender.Send()
// s.Send(ctx, binding.EventMessage(e))
// When an event is wrapped into a EventMessage, the original event could be
// potentially mutated. If you need to use the Event again, after wrapping it into
// an Event message, you should copy it before
type EventMessage event.Event
func ToMessage(e *event.Event) Message {
return (*EventMessage)(e)
}
func (m *EventMessage) ReadEncoding() Encoding {
return EncodingEvent
}
func (m *EventMessage) ReadStructured(ctx context.Context, builder StructuredWriter) error {
f := GetOrDefaultFromCtx(ctx, FORMAT_EVENT_STRUCTURED, format.JSON).(format.Format)
b, err := f.Marshal((*event.Event)(m))
if err != nil {
return err
}
return builder.SetStructuredEvent(ctx, f, bytes.NewReader(b))
}
func (m *EventMessage) ReadBinary(ctx context.Context, b BinaryWriter) (err error) {
err = b.Start(ctx)
if err != nil {
return err
}
err = eventContextToBinaryWriter(m.Context, b)
if err != nil {
return err
}
// Pass the body
body := (*event.Event)(m).Data()
if len(body) > 0 {
err = b.SetData(bytes.NewReader(body))
if err != nil {
return err
}
}
return b.End(ctx)
}
func eventContextToBinaryWriter(c event.EventContext, b BinaryWriter) (err error) {
// Pass all attributes
sv := spec.VS.Version(c.GetSpecVersion())
for _, a := range sv.Attributes() {
value := a.Get(c)
if value != nil {
err = b.SetAttribute(a, value)
}
if err != nil {
return err
}
}
// Pass all extensions
for k, v := range c.GetExtensions() {
err = b.SetExtension(k, v)
if err != nil {
return err
}
}
return nil
}
func (*EventMessage) Finish(error) error { return nil }
var _ Message = (*EventMessage)(nil) // Test it conforms to the interface
// Configure which format to use when marshalling the event to structured mode
func UseFormatForEvent(ctx context.Context, f format.Format) context.Context {
return context.WithValue(ctx, FORMAT_EVENT_STRUCTURED, f)
}

View File

@ -0,0 +1,27 @@
package binding
type finishMessage struct {
Message
finish func(error)
}
func (m *finishMessage) GetWrappedMessage() Message {
return m.Message
}
func (m *finishMessage) Finish(err error) error {
err2 := m.Message.Finish(err) // Finish original message first
if m.finish != nil {
m.finish(err) // Notify callback
}
return err2
}
var _ MessageWrapper = (*finishMessage)(nil)
// WithFinish returns a wrapper for m that calls finish() and
// m.Finish() in its Finish().
// Allows code to be notified when a message is Finished.
func WithFinish(m Message, finish func(error)) Message {
return &finishMessage{Message: m, finish: finish}
}

View File

@ -0,0 +1,8 @@
package format
/*
Package format formats structured events.
The "application/cloudevents+json" format is built-in and always
available. Other formats may be added.
*/

View File

@ -0,0 +1,71 @@
package format
import (
"encoding/json"
"fmt"
"strings"
"github.com/cloudevents/sdk-go/v2/event"
)
// Format marshals and unmarshals structured events to bytes.
type Format interface {
// MediaType identifies the format
MediaType() string
// Marshal event to bytes
Marshal(*event.Event) ([]byte, error)
// Unmarshal bytes to event
Unmarshal([]byte, *event.Event) error
}
// Prefix for event-format media types.
const Prefix = "application/cloudevents"
// IsFormat returns true if mediaType begins with "application/cloudevents"
func IsFormat(mediaType string) bool { return strings.HasPrefix(mediaType, Prefix) }
// JSON is the built-in "application/cloudevents+json" format.
var JSON = jsonFmt{}
type jsonFmt struct{}
func (jsonFmt) MediaType() string { return event.ApplicationCloudEventsJSON }
func (jsonFmt) Marshal(e *event.Event) ([]byte, error) { return json.Marshal(e) }
func (jsonFmt) Unmarshal(b []byte, e *event.Event) error {
return json.Unmarshal(b, e)
}
// built-in formats
var formats map[string]Format
func init() {
formats = map[string]Format{}
Add(JSON)
}
// Lookup returns the format for mediaType, or nil if not found.
func Lookup(mediaType string) Format { return formats[mediaType] }
func unknown(mediaType string) error {
return fmt.Errorf("unknown event format media-type %#v", mediaType)
}
// Add a new Format. It can be retrieved by Lookup(f.MediaType())
func Add(f Format) { formats[f.MediaType()] = f }
// Marshal an event to bytes using the mediaType event format.
func Marshal(mediaType string, e *event.Event) ([]byte, error) {
if f := formats[mediaType]; f != nil {
return f.Marshal(e)
}
return nil, unknown(mediaType)
}
// Unmarshal bytes to an event using the mediaType event format.
func Unmarshal(mediaType string, b []byte, e *event.Event) error {
if f := formats[mediaType]; f != nil {
return f.Unmarshal(b, e)
}
return unknown(mediaType)
}

View File

@ -0,0 +1,99 @@
package binding
import "context"
// The ReadStructured and ReadBinary methods allows to perform an optimized encoding of a Message to a specific data structure.
// A Sender should try each method of interest and fall back to binding.ToEvent() if none are supported.
// An out of the box algorithm is provided for writing a message: binding.Write().
type MessageReader interface {
// Return the type of the message Encoding.
// The encoding should be preferably computed when the message is constructed.
ReadEncoding() Encoding
// ReadStructured transfers a structured-mode event to a StructuredWriter.
// It must return ErrNotStructured if message is not in structured mode.
//
// Returns a different err if something wrong happened while trying to read the structured event.
// In this case, the caller must Finish the message with appropriate error.
//
// This allows Senders to avoid re-encoding messages that are
// already in suitable structured form.
ReadStructured(context.Context, StructuredWriter) error
// ReadBinary transfers a binary-mode event to an BinaryWriter.
// It must return ErrNotBinary if message is not in binary mode.
//
// Returns a different err if something wrong happened while trying to read the binary event
// In this case, the caller must Finish the message with appropriate error
//
// This allows Senders to avoid re-encoding messages that are
// already in suitable binary form.
ReadBinary(context.Context, BinaryWriter) error
}
// Message is the interface to a binding-specific message containing an event.
//
// Reliable Delivery
//
// There are 3 reliable qualities of service for messages:
//
// 0/at-most-once/unreliable: messages can be dropped silently.
//
// 1/at-least-once: messages are not dropped without signaling an error
// to the sender, but they may be duplicated in the event of a re-send.
//
// 2/exactly-once: messages are never dropped (without error) or
// duplicated, as long as both sending and receiving ends maintain
// some binding-specific delivery state. Whether this is persisted
// depends on the configuration of the binding implementations.
//
// The Message interface supports QoS 0 and 1, the ExactlyOnceMessage interface
// supports QoS 2
//
// Message includes the MessageReader interface to read messages. Every binding.Message implementation *must* specify if the message can be accessed one or more times.
//
// When a Message can be forgotten by the entity who produced the message, Message.Finish() *must* be invoked.
type Message interface {
MessageReader
// Finish *must* be called when message from a Receiver can be forgotten by
// the receiver. A QoS 1 sender should not call Finish() until it gets an acknowledgment of
// receipt on the underlying transport. For QoS 2 see ExactlyOnceMessage.
//
// Note that, depending on the Message implementation, forgetting to Finish the message
// could produce memory/resources leaks!
//
// Passing a non-nil err indicates sending or processing failed.
// A non-nil return indicates that the message was not accepted
// by the receivers peer.
Finish(error) error
}
// ExactlyOnceMessage is implemented by received Messages
// that support QoS 2. Only transports that support QoS 2 need to
// implement or use this interface.
type ExactlyOnceMessage interface {
Message
// Received is called by a forwarding QoS2 Sender when it gets
// acknowledgment of receipt (e.g. AMQP 'accept' or MQTT PUBREC)
//
// The receiver must call settle(nil) when it get's the ack-of-ack
// (e.g. AMQP 'settle' or MQTT PUBCOMP) or settle(err) if the
// transfer fails.
//
// Finally the Sender calls Finish() to indicate the message can be
// discarded.
//
// If sending fails, or if the sender does not support QoS 2, then
// Finish() may be called without any call to Received()
Received(settle func(error))
}
// Message Wrapper interface is used to walk through a decorated Message and unwrap it.
type MessageWrapper interface {
Message
// Method to get the wrapped message
GetWrappedMessage() Message
}

View File

@ -0,0 +1,136 @@
package spec
import (
"fmt"
"time"
"github.com/cloudevents/sdk-go/v2/event"
"github.com/cloudevents/sdk-go/v2/types"
)
// Kind is a version-independent identifier for a CloudEvent context attribute.
type Kind uint8
const (
// Required cloudevents attributes
ID Kind = iota
Source
SpecVersion
Type
// Optional cloudevents attributes
DataContentType
DataSchema
Subject
Time
)
const nAttrs = int(Time) + 1
var kindNames = [nAttrs]string{
"id",
"source",
"specversion",
"type",
"datacontenttype",
"dataschema",
"subject",
"time",
}
// String is a human-readable string, for a valid attribute name use Attribute.Name
func (k Kind) String() string { return kindNames[k] }
// IsRequired returns true for attributes defined as "required" by the CE spec.
func (k Kind) IsRequired() bool { return k < DataContentType }
// Attribute is a named attribute accessor.
// The attribute name is specific to a Version.
type Attribute interface {
Kind() Kind
// Name of the attribute with respect to the current spec Version() with prefix
PrefixedName() string
// Name of the attribute with respect to the current spec Version()
Name() string
// Version of the spec that this attribute belongs to
Version() Version
// Get the value of this attribute from an event context
Get(event.EventContextReader) interface{}
// Set the value of this attribute on an event context
Set(event.EventContextWriter, interface{}) error
// Delete this attribute from and event context, when possible
Delete(event.EventContextWriter) error
}
// accessor provides Kind, Get, Set.
type accessor interface {
Kind() Kind
Get(event.EventContextReader) interface{}
Set(event.EventContextWriter, interface{}) error
Delete(event.EventContextWriter) error
}
var acc = [nAttrs]accessor{
&aStr{aKind(ID), event.EventContextReader.GetID, event.EventContextWriter.SetID},
&aStr{aKind(Source), event.EventContextReader.GetSource, event.EventContextWriter.SetSource},
&aStr{aKind(SpecVersion), event.EventContextReader.GetSpecVersion, func(writer event.EventContextWriter, s string) error { return nil }},
&aStr{aKind(Type), event.EventContextReader.GetType, event.EventContextWriter.SetType},
&aStr{aKind(DataContentType), event.EventContextReader.GetDataContentType, event.EventContextWriter.SetDataContentType},
&aStr{aKind(DataSchema), event.EventContextReader.GetDataSchema, event.EventContextWriter.SetDataSchema},
&aStr{aKind(Subject), event.EventContextReader.GetSubject, event.EventContextWriter.SetSubject},
&aTime{aKind(Time), event.EventContextReader.GetTime, event.EventContextWriter.SetTime},
}
// aKind implements Kind()
type aKind Kind
func (kind aKind) Kind() Kind { return Kind(kind) }
type aStr struct {
aKind
get func(event.EventContextReader) string
set func(event.EventContextWriter, string) error
}
func (a *aStr) Get(c event.EventContextReader) interface{} {
if s := a.get(c); s != "" {
return s
}
return nil // Treat blank as missing
}
func (a *aStr) Set(c event.EventContextWriter, v interface{}) error {
s, err := types.ToString(v)
if err != nil {
return fmt.Errorf("invalid value for %s: %#v", a.Kind(), v)
}
return a.set(c, s)
}
func (a *aStr) Delete(c event.EventContextWriter) error {
return a.set(c, "")
}
type aTime struct {
aKind
get func(event.EventContextReader) time.Time
set func(event.EventContextWriter, time.Time) error
}
func (a *aTime) Get(c event.EventContextReader) interface{} {
if v := a.get(c); !v.IsZero() {
return v
}
return nil // Treat zero time as missing.
}
func (a *aTime) Set(c event.EventContextWriter, v interface{}) error {
t, err := types.ToTime(v)
if err != nil {
return fmt.Errorf("invalid value for %s: %#v", a.Kind(), v)
}
return a.set(c, t)
}
func (a *aTime) Delete(c event.EventContextWriter) error {
return a.set(c, time.Time{})
}

View File

@ -0,0 +1,9 @@
package spec
/*
Package spec provides spec-version metadata.
For use by code that maps events using (prefixed) attribute name strings.
Supports handling multiple spec versions uniformly.
*/

View File

@ -0,0 +1,184 @@
package spec
import (
"strings"
"github.com/cloudevents/sdk-go/v2/event"
)
// Version provides meta-data for a single spec-version.
type Version interface {
// String name of the version, e.g. "1.0"
String() string
// Prefix for attribute names.
Prefix() string
// Attribute looks up a prefixed attribute name (case insensitive).
// Returns nil if not found.
Attribute(prefixedName string) Attribute
// Attribute looks up the attribute from kind.
// Returns nil if not found.
AttributeFromKind(kind Kind) Attribute
// Attributes returns all the context attributes for this version.
Attributes() []Attribute
// Convert translates a context to this version.
Convert(event.EventContextConverter) event.EventContext
// NewContext returns a new context for this version.
NewContext() event.EventContext
// SetAttribute sets named attribute to value.
//
// Name is case insensitive.
// Does nothing if name does not start with prefix.
SetAttribute(context event.EventContextWriter, name string, value interface{}) error
}
// Versions contains all known versions with the same attribute prefix.
type Versions struct {
prefix string
all []Version
m map[string]Version
}
// Versions returns the list of all known versions, most recent first.
func (vs *Versions) Versions() []Version { return vs.all }
// Version returns the named version.
func (vs *Versions) Version(name string) Version {
return vs.m[name]
}
// Latest returns the latest Version
func (vs *Versions) Latest() Version { return vs.all[0] }
// PrefixedSpecVersionName returns the specversion attribute PrefixedName
func (vs *Versions) PrefixedSpecVersionName() string { return vs.prefix + "specversion" }
// Prefix is the lowercase attribute name prefix.
func (vs *Versions) Prefix() string { return vs.prefix }
type attribute struct {
accessor
name string
version Version
}
func (a *attribute) PrefixedName() string { return a.version.Prefix() + a.name }
func (a *attribute) Name() string { return a.name }
func (a *attribute) Version() Version { return a.version }
type version struct {
prefix string
context event.EventContext
convert func(event.EventContextConverter) event.EventContext
attrMap map[string]Attribute
attrs []Attribute
}
func (v *version) Attribute(name string) Attribute { return v.attrMap[strings.ToLower(name)] }
func (v *version) Attributes() []Attribute { return v.attrs }
func (v *version) String() string { return v.context.GetSpecVersion() }
func (v *version) Prefix() string { return v.prefix }
func (v *version) NewContext() event.EventContext { return v.context.Clone() }
// HasPrefix is a case-insensitive prefix check.
func (v *version) HasPrefix(name string) bool {
return strings.HasPrefix(strings.ToLower(name), v.prefix)
}
func (v *version) Convert(c event.EventContextConverter) event.EventContext { return v.convert(c) }
func (v *version) SetAttribute(c event.EventContextWriter, name string, value interface{}) error {
if a := v.Attribute(name); a != nil { // Standard attribute
return a.Set(c, value)
}
name = strings.ToLower(name)
var err error
if v.HasPrefix(name) { // Extension attribute
return c.SetExtension(strings.TrimPrefix(name, v.prefix), value)
}
return err
}
func (v *version) AttributeFromKind(kind Kind) Attribute {
for _, a := range v.Attributes() {
if a.Kind() == kind {
return a
}
}
return nil
}
func newVersion(
prefix string,
context event.EventContext,
convert func(event.EventContextConverter) event.EventContext,
attrs ...*attribute,
) *version {
v := &version{
prefix: strings.ToLower(prefix),
context: context,
convert: convert,
attrMap: map[string]Attribute{},
attrs: make([]Attribute, len(attrs)),
}
for i, a := range attrs {
a.version = v
v.attrs[i] = a
v.attrMap[strings.ToLower(a.PrefixedName())] = a
}
return v
}
// WithPrefix returns a set of versions with prefix added to all attribute names.
func WithPrefix(prefix string) *Versions {
attr := func(name string, kind Kind) *attribute {
return &attribute{accessor: acc[kind], name: name}
}
vs := &Versions{
m: map[string]Version{},
prefix: prefix,
all: []Version{
newVersion(prefix, event.EventContextV1{}.AsV1(),
func(c event.EventContextConverter) event.EventContext { return c.AsV1() },
attr("id", ID),
attr("source", Source),
attr("specversion", SpecVersion),
attr("type", Type),
attr("datacontenttype", DataContentType),
attr("dataschema", DataSchema),
attr("subject", Subject),
attr("time", Time),
),
newVersion(prefix, event.EventContextV03{}.AsV03(),
func(c event.EventContextConverter) event.EventContext { return c.AsV03() },
attr("specversion", SpecVersion),
attr("type", Type),
attr("source", Source),
attr("schemaurl", DataSchema),
attr("subject", Subject),
attr("id", ID),
attr("time", Time),
attr("datacontenttype", DataContentType),
),
},
}
for _, v := range vs.all {
vs.m[v.String()] = v
}
return vs
}
// New returns a set of versions
func New() *Versions { return WithPrefix("") }
// Built-in un-prefixed versions.
var (
VS *Versions
V03 Version
V1 Version
)
func init() {
VS = New()
V03 = VS.Version("0.3")
V1 = VS.Version("1.0")
}

View File

@ -0,0 +1,17 @@
package binding
import (
"context"
"io"
"github.com/cloudevents/sdk-go/v2/binding/format"
)
// StructuredWriter is used to visit a structured Message and generate a new representation.
//
// Protocols that supports structured encoding should implement this interface to implement direct
// structured to structured encoding and event to structured encoding.
type StructuredWriter interface {
// Event receives an io.Reader for the whole event.
SetStructuredEvent(ctx context.Context, format format.Format, event io.Reader) error
}

View File

@ -0,0 +1,131 @@
package binding
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"github.com/cloudevents/sdk-go/v2/binding/format"
"github.com/cloudevents/sdk-go/v2/binding/spec"
"github.com/cloudevents/sdk-go/v2/event"
"github.com/cloudevents/sdk-go/v2/types"
)
// Generic error when a conversion of a Message to an Event fails
var ErrCannotConvertToEvent = errors.New("cannot convert message to event")
// Translates a Message with a valid Structured or Binary representation to an Event.
// This function returns the Event generated from the Message and the original encoding of the message or
// an error that points the conversion error.
// transformers can be nil and this function guarantees that they are invoked only once during the encoding process.
func ToEvent(ctx context.Context, message MessageReader, transformers ...TransformerFactory) (*event.Event, error) {
if message == nil {
return nil, nil
}
messageEncoding := message.ReadEncoding()
if messageEncoding == EncodingEvent {
m := message
for m != nil {
if em, ok := m.(*EventMessage); ok {
e := (*event.Event)(em)
var tf TransformerFactories
tf = transformers
if err := tf.EventTransformer()(e); err != nil {
return nil, err
}
return e, nil
}
if mw, ok := m.(MessageWrapper); ok {
m = mw.GetWrappedMessage()
} else {
break
}
}
return nil, ErrCannotConvertToEvent
}
e := event.New()
encoder := &messageToEventBuilder{event: &e}
if _, err := DirectWrite(
context.Background(),
message,
encoder,
encoder,
); err != nil {
return nil, err
}
var tf TransformerFactories
tf = transformers
if err := tf.EventTransformer()(&e); err != nil {
return nil, err
}
return &e, nil
}
type messageToEventBuilder struct {
event *event.Event
}
var _ StructuredWriter = (*messageToEventBuilder)(nil)
var _ BinaryWriter = (*messageToEventBuilder)(nil)
func (b *messageToEventBuilder) SetStructuredEvent(ctx context.Context, format format.Format, event io.Reader) error {
var buf bytes.Buffer
_, err := io.Copy(&buf, event)
if err != nil {
return err
}
return format.Unmarshal(buf.Bytes(), b.event)
}
func (b *messageToEventBuilder) Start(ctx context.Context) error {
return nil
}
func (b *messageToEventBuilder) End(ctx context.Context) error {
return nil
}
func (b *messageToEventBuilder) SetData(data io.Reader) error {
var buf bytes.Buffer
w, err := io.Copy(&buf, data)
if err != nil {
return err
}
if w != 0 {
b.event.DataEncoded = buf.Bytes()
}
return nil
}
func (b *messageToEventBuilder) SetAttribute(attribute spec.Attribute, value interface{}) error {
// If spec version we need to change to right context struct
if attribute.Kind() == spec.SpecVersion {
str, err := types.ToString(value)
if err != nil {
return err
}
switch str {
case event.CloudEventsVersionV03:
b.event.Context = b.event.Context.AsV03()
case event.CloudEventsVersionV1:
b.event.Context = b.event.Context.AsV1()
default:
return fmt.Errorf("unrecognized event version %s", str)
}
return nil
}
return attribute.Set(b.event.Context, value)
}
func (b *messageToEventBuilder) SetExtension(name string, value interface{}) error {
value, err := types.Validate(value)
if err != nil {
return err
}
b.event.SetExtension(name, value)
return nil
}

View File

@ -0,0 +1,73 @@
package binding
import (
"github.com/cloudevents/sdk-go/v2/event"
)
// Implements a transformation process while transferring the event from the Message implementation
// to the provided encoder
//
// A transformer could optionally not provide an implementation for binary and/or structured encodings,
// returning nil to the respective factory method.
type TransformerFactory interface {
// Can return nil if the transformation doesn't support structured encoding directly
StructuredTransformer(writer StructuredWriter) StructuredWriter
// Can return nil if the transformation doesn't support binary encoding directly
BinaryTransformer(writer BinaryWriter) BinaryWriter
// Can return nil if the transformation doesn't support events
EventTransformer() EventTransformer
}
// Utility type alias to manage multiple TransformerFactory
type TransformerFactories []TransformerFactory
func (t TransformerFactories) StructuredTransformer(writer StructuredWriter) StructuredWriter {
if writer == nil {
return nil
}
res := writer
for _, b := range t {
if r := b.StructuredTransformer(res); r != nil {
res = r
} else {
return nil // Structured not supported!
}
}
return res
}
func (t TransformerFactories) BinaryTransformer(writer BinaryWriter) BinaryWriter {
if writer == nil {
return nil
}
res := writer
for i := range t {
b := t[len(t)-i-1]
if r := b.BinaryTransformer(res); r != nil {
res = r
} else {
return nil // Binary not supported!
}
}
return res
}
func (t TransformerFactories) EventTransformer() EventTransformer {
return func(e *event.Event) error {
for _, b := range t {
f := b.EventTransformer()
if f != nil {
err := f(e)
if err != nil {
return err
}
}
}
return nil
}
}
// EventTransformer mutates the provided Event
type EventTransformer func(*event.Event) error

View File

@ -0,0 +1,148 @@
package binding
import (
"context"
"github.com/cloudevents/sdk-go/v2/event"
)
const (
SKIP_DIRECT_STRUCTURED_ENCODING = "SKIP_DIRECT_STRUCTURED_ENCODING"
SKIP_DIRECT_BINARY_ENCODING = "SKIP_DIRECT_BINARY_ENCODING"
PREFERRED_EVENT_ENCODING = "PREFERRED_EVENT_ENCODING"
)
// Invokes the encoders. structuredWriter and binaryWriter could be nil if the protocol doesn't support it.
// transformers can be nil and this function guarantees that they are invoked only once during the encoding process.
//
// Returns:
// * EncodingStructured, nil if message is correctly encoded in structured encoding
// * EncodingBinary, nil if message is correctly encoded in binary encoding
// * EncodingStructured, err if message was structured but error happened during the encoding
// * EncodingBinary, err if message was binary but error happened during the encoding
// * EncodingUnknown, ErrUnknownEncoding if message is not a structured or a binary Message
func DirectWrite(
ctx context.Context,
message MessageReader,
structuredWriter StructuredWriter,
binaryWriter BinaryWriter,
transformers ...TransformerFactory,
) (Encoding, error) {
if structuredWriter != nil && !GetOrDefaultFromCtx(ctx, SKIP_DIRECT_STRUCTURED_ENCODING, false).(bool) {
// Wrap the transformers in the structured builder
structuredWriter = TransformerFactories(transformers).StructuredTransformer(structuredWriter)
// StructuredTransformer could return nil if one of transcoders doesn't support
// direct structured transcoding
if structuredWriter != nil {
if err := message.ReadStructured(ctx, structuredWriter); err == nil {
return EncodingStructured, nil
} else if err != ErrNotStructured {
return EncodingStructured, err
}
}
}
if binaryWriter != nil && !GetOrDefaultFromCtx(ctx, SKIP_DIRECT_BINARY_ENCODING, false).(bool) {
binaryWriter = TransformerFactories(transformers).BinaryTransformer(binaryWriter)
if binaryWriter != nil {
if err := message.ReadBinary(ctx, binaryWriter); err == nil {
return EncodingBinary, nil
} else if err != ErrNotBinary {
return EncodingBinary, err
}
}
}
return EncodingUnknown, ErrUnknownEncoding
}
// This is the full algorithm to encode a Message using transformers:
// 1. It first tries direct encoding using DirectWrite
// 2. If no direct encoding is possible, it uses ToEvent to generate an Event representation
// 3. From the Event, the message is encoded back to the provided structured or binary encoders
// You can tweak the encoding process using the context decorators WithForceStructured, WithForceStructured, etc.
// transformers can be nil and this function guarantees that they are invoked only once during the encoding process.
// Returns:
// * EncodingStructured, nil if message is correctly encoded in structured encoding
// * EncodingBinary, nil if message is correctly encoded in binary encoding
// * EncodingUnknown, ErrUnknownEncoding if message.ReadEncoding() == EncodingUnknown
// * _, err if error happened during the encoding
func Write(
ctx context.Context,
message MessageReader,
structuredWriter StructuredWriter,
binaryWriter BinaryWriter,
transformers ...TransformerFactory,
) (Encoding, error) {
enc := message.ReadEncoding()
var err error
// Skip direct encoding if the event is an event message
if enc != EncodingEvent {
enc, err = DirectWrite(ctx, message, structuredWriter, binaryWriter, transformers...)
if enc != EncodingUnknown {
// Message directly encoded, nothing else to do here
return enc, err
}
}
var e *event.Event
e, err = ToEvent(ctx, message, transformers...)
if err != nil {
return enc, err
}
message = (*EventMessage)(e)
if GetOrDefaultFromCtx(ctx, PREFERRED_EVENT_ENCODING, EncodingBinary).(Encoding) == EncodingStructured {
if structuredWriter != nil {
return EncodingStructured, message.ReadStructured(ctx, structuredWriter)
}
if binaryWriter != nil {
return EncodingBinary, message.ReadBinary(ctx, binaryWriter)
}
} else {
if binaryWriter != nil {
return EncodingBinary, message.ReadBinary(ctx, binaryWriter)
}
if structuredWriter != nil {
return EncodingStructured, message.ReadStructured(ctx, structuredWriter)
}
}
return EncodingUnknown, ErrUnknownEncoding
}
// Skip direct structured to structured encoding during the encoding process
func WithSkipDirectStructuredEncoding(ctx context.Context, skip bool) context.Context {
return context.WithValue(ctx, SKIP_DIRECT_STRUCTURED_ENCODING, skip)
}
// Skip direct binary to binary encoding during the encoding process
func WithSkipDirectBinaryEncoding(ctx context.Context, skip bool) context.Context {
return context.WithValue(ctx, SKIP_DIRECT_BINARY_ENCODING, skip)
}
// Define the preferred encoding from event to message during the encoding process
func WithPreferredEventEncoding(ctx context.Context, enc Encoding) context.Context {
return context.WithValue(ctx, PREFERRED_EVENT_ENCODING, enc)
}
// Force structured encoding during the encoding process
func WithForceStructured(ctx context.Context) context.Context {
return context.WithValue(context.WithValue(ctx, PREFERRED_EVENT_ENCODING, EncodingStructured), SKIP_DIRECT_BINARY_ENCODING, true)
}
// Force binary encoding during the encoding process
func WithForceBinary(ctx context.Context) context.Context {
return context.WithValue(context.WithValue(ctx, PREFERRED_EVENT_ENCODING, EncodingBinary), SKIP_DIRECT_STRUCTURED_ENCODING, true)
}
// Get a configuration value from the provided context
func GetOrDefaultFromCtx(ctx context.Context, key string, def interface{}) interface{} {
if val := ctx.Value(key); val != nil {
return val
} else {
return def
}
}

View File

@ -0,0 +1,222 @@
package client
import (
"context"
"errors"
"fmt"
"io"
"sync"
"go.uber.org/zap"
"github.com/cloudevents/sdk-go/v2/binding"
cecontext "github.com/cloudevents/sdk-go/v2/context"
"github.com/cloudevents/sdk-go/v2/event"
"github.com/cloudevents/sdk-go/v2/protocol"
)
// Client interface defines the runtime contract the CloudEvents client supports.
type Client interface {
// Send will transmit the given event over the client's configured transport.
Send(ctx context.Context, event event.Event) protocol.Result
// Request will transmit the given event over the client's configured
// transport and return any response event.
Request(ctx context.Context, event event.Event) (*event.Event, protocol.Result)
// StartReceiver will register the provided function for callback on receipt
// of a cloudevent. It will also start the underlying protocol as it has
// been configured.
// This call is blocking.
// Valid fn signatures are:
// * func()
// * func() error
// * func(context.Context)
// * func(context.Context) protocol.Result
// * func(event.Event)
// * func(event.Event) protocol.Result
// * func(context.Context, event.Event)
// * func(context.Context, event.Event) protocol.Result
// * func(event.Event) *event.Event
// * func(event.Event) (*event.Event, protocol.Result)
// * func(context.Context, event.Event) *event.Event
// * func(context.Context, event.Event) (*event.Event, protocol.Result)
StartReceiver(ctx context.Context, fn interface{}) error
}
// New produces a new client with the provided transport object and applied
// client options.
func New(obj interface{}, opts ...Option) (Client, error) {
c := &ceClient{}
if p, ok := obj.(protocol.Sender); ok {
c.sender = p
}
if p, ok := obj.(protocol.Requester); ok {
c.requester = p
}
if p, ok := obj.(protocol.Responder); ok {
c.responder = p
}
if p, ok := obj.(protocol.Receiver); ok {
c.receiver = p
}
if p, ok := obj.(protocol.Opener); ok {
c.opener = p
}
if err := c.applyOptions(opts...); err != nil {
return nil, err
}
return c, nil
}
type ceClient struct {
sender protocol.Sender
requester protocol.Requester
receiver protocol.Receiver
responder protocol.Responder
// Optional.
opener protocol.Opener
outboundContextDecorators []func(context.Context) context.Context
invoker Invoker
receiverMu sync.Mutex
eventDefaulterFns []EventDefaulter
}
func (c *ceClient) applyOptions(opts ...Option) error {
for _, fn := range opts {
if err := fn(c); err != nil {
return err
}
}
return nil
}
func (c *ceClient) Send(ctx context.Context, e event.Event) protocol.Result {
if c.sender == nil {
return errors.New("sender not set")
}
for _, f := range c.outboundContextDecorators {
ctx = f(ctx)
}
if len(c.eventDefaulterFns) > 0 {
for _, fn := range c.eventDefaulterFns {
e = fn(ctx, e)
}
}
if err := e.Validate(); err != nil {
return err
}
return c.sender.Send(ctx, (*binding.EventMessage)(&e))
}
func (c *ceClient) Request(ctx context.Context, e event.Event) (*event.Event, protocol.Result) {
if c.requester == nil {
return nil, errors.New("requester not set")
}
for _, f := range c.outboundContextDecorators {
ctx = f(ctx)
}
if len(c.eventDefaulterFns) > 0 {
for _, fn := range c.eventDefaulterFns {
e = fn(ctx, e)
}
}
if err := e.Validate(); err != nil {
return nil, err
}
// If provided a requester, use it to do request/response.
var resp *event.Event
msg, err := c.requester.Request(ctx, (*binding.EventMessage)(&e))
if msg != nil {
defer func() {
if err := msg.Finish(err); err != nil {
cecontext.LoggerFrom(ctx).Warnw("failed calling message.Finish", zap.Error(err))
}
}()
}
// try to turn msg into an event, it might not work and that is ok.
if rs, rserr := binding.ToEvent(ctx, msg); rserr != nil {
cecontext.LoggerFrom(ctx).Debugw("response: failed calling ToEvent", zap.Error(rserr), zap.Any("resp", msg))
if err != nil {
err = fmt.Errorf("%w; failed to convert response into event: %s", err, rserr)
} else {
// If the protocol returns no error, it is an ACK on the request, but we had
// issues turning the response into an event, so make an ACK Result and pass
// down the ToEvent error as well.
err = fmt.Errorf("%w; failed to convert response into event: %s", protocol.ResultACK, rserr)
}
} else {
resp = rs
}
return resp, err
}
// StartReceiver sets up the given fn to handle Receive.
// See Client.StartReceiver for details. This is a blocking call.
func (c *ceClient) StartReceiver(ctx context.Context, fn interface{}) error {
c.receiverMu.Lock()
defer c.receiverMu.Unlock()
if c.invoker != nil {
return fmt.Errorf("client already has a receiver")
}
invoker, err := newReceiveInvoker(fn, c.eventDefaulterFns...) // TODO: this will have to pick between a observed invoker or not.
if err != nil {
return err
}
if invoker.IsReceiver() && c.receiver == nil {
return fmt.Errorf("mismatched receiver callback without protocol.Receiver supported by protocol")
}
if invoker.IsResponder() && c.responder == nil {
return fmt.Errorf("mismatched receiver callback without protocol.Responder supported by protocol")
}
c.invoker = invoker
defer func() {
c.invoker = nil
}()
// Start the opener, if set.
if c.opener != nil {
go func() {
// TODO: handle error correctly here.
if err := c.opener.OpenInbound(ctx); err != nil {
panic(err)
}
}()
}
var msg binding.Message
var respFn protocol.ResponseFn
// Start Polling.
for {
if c.responder != nil {
msg, respFn, err = c.responder.Respond(ctx)
} else if c.receiver != nil {
msg, err = c.receiver.Receive(ctx)
} else {
return errors.New("responder nor receiver set")
}
if err == io.EOF { // Normal close
return nil
}
if err := c.invoker.Invoke(ctx, msg, respFn); err != nil {
return err
}
}
}

View File

@ -0,0 +1,26 @@
package client
import (
"github.com/cloudevents/sdk-go/v2/protocol/http"
)
// NewDefault provides the good defaults for the common case using an HTTP
// Protocol client. The http transport has had WithBinaryEncoding http
// transport option applied to it. The client will always send Binary
// encoding but will inspect the outbound event context and match the version.
// The WithTimeNow, and WithUUIDs client options are also applied to the
// client, all outbound events will have a time and id set if not already
// present.
func NewDefault() (Client, error) {
p, err := http.New()
if err != nil {
return nil, err
}
c, err := NewObserved(p, WithTimeNow(), WithUUIDs())
if err != nil {
return nil, err
}
return c, nil
}

View File

@ -0,0 +1,101 @@
package client
import (
"context"
"github.com/cloudevents/sdk-go/v2/event"
"github.com/cloudevents/sdk-go/v2/extensions"
"github.com/cloudevents/sdk-go/v2/observability"
"github.com/cloudevents/sdk-go/v2/protocol"
"go.opencensus.io/trace"
)
// New produces a new client with the provided transport object and applied
// client options.
func NewObserved(protocol interface{}, opts ...Option) (Client, error) {
client, err := New(protocol, opts...)
if err != nil {
return nil, err
}
c := &obsClient{client: client}
if err := c.applyOptions(opts...); err != nil {
return nil, err
}
return c, nil
}
type obsClient struct {
client Client
addTracing bool
}
func (c *obsClient) applyOptions(opts ...Option) error {
for _, fn := range opts {
if err := fn(c); err != nil {
return err
}
}
return nil
}
// Send transmits the provided event on a preconfigured Protocol. Send returns
// an error if there was an an issue validating the outbound event or the
// transport returns an error.
func (c *obsClient) Send(ctx context.Context, e event.Event) protocol.Result {
ctx, r := observability.NewReporter(ctx, reportSend)
ctx, span := trace.StartSpan(ctx, observability.ClientSpanName, trace.WithSpanKind(trace.SpanKindClient))
defer span.End()
if span.IsRecordingEvents() {
span.AddAttributes(EventTraceAttributes(&e)...)
}
if c.addTracing {
e.Context = e.Context.Clone()
extensions.FromSpanContext(span.SpanContext()).AddTracingAttributes(&e)
}
result := c.client.Send(ctx, e)
if protocol.IsACK(result) {
r.OK()
} else {
r.Error()
}
return result
}
func (c *obsClient) Request(ctx context.Context, e event.Event) (*event.Event, protocol.Result) {
ctx, r := observability.NewReporter(ctx, reportRequest)
ctx, span := trace.StartSpan(ctx, observability.ClientSpanName, trace.WithSpanKind(trace.SpanKindClient))
defer span.End()
if span.IsRecordingEvents() {
span.AddAttributes(EventTraceAttributes(&e)...)
}
resp, result := c.client.Request(ctx, e)
if protocol.IsACK(result) {
r.OK()
} else {
r.Error()
}
return resp, result
}
// StartReceiver sets up the given fn to handle Receive.
// See Client.StartReceiver for details. This is a blocking call.
func (c *obsClient) StartReceiver(ctx context.Context, fn interface{}) error {
ctx, r := observability.NewReporter(ctx, reportStartReceiver)
err := c.client.StartReceiver(ctx, fn)
if err != nil {
r.Error()
} else {
r.OK()
}
return err
}

View File

@ -4,17 +4,18 @@ import (
"context" "context"
"time" "time"
"github.com/cloudevents/sdk-go/pkg/cloudevents" "github.com/cloudevents/sdk-go/v2/event"
"github.com/google/uuid" "github.com/google/uuid"
) )
// EventDefaulter is the function signature for extensions that are able // EventDefaulter is the function signature for extensions that are able
// to perform event defaulting. // to perform event defaulting.
type EventDefaulter func(ctx context.Context, event cloudevents.Event) cloudevents.Event type EventDefaulter func(ctx context.Context, event event.Event) event.Event
// DefaultIDToUUIDIfNotSet will inspect the provided event and assign a UUID to // DefaultIDToUUIDIfNotSet will inspect the provided event and assign a UUID to
// context.ID if it is found to be empty. // context.ID if it is found to be empty.
func DefaultIDToUUIDIfNotSet(ctx context.Context, event cloudevents.Event) cloudevents.Event { func DefaultIDToUUIDIfNotSet(ctx context.Context, event event.Event) event.Event {
if event.Context != nil { if event.Context != nil {
if event.ID() == "" { if event.ID() == "" {
event.Context = event.Context.Clone() event.Context = event.Context.Clone()
@ -26,7 +27,7 @@ func DefaultIDToUUIDIfNotSet(ctx context.Context, event cloudevents.Event) cloud
// DefaultTimeToNowIfNotSet will inspect the provided event and assign a new // DefaultTimeToNowIfNotSet will inspect the provided event and assign a new
// Timestamp to context.Time if it is found to be nil or zero. // Timestamp to context.Time if it is found to be nil or zero.
func DefaultTimeToNowIfNotSet(ctx context.Context, event cloudevents.Event) cloudevents.Event { func DefaultTimeToNowIfNotSet(ctx context.Context, event event.Event) event.Event {
if event.Context != nil { if event.Context != nil {
if event.Time().IsZero() { if event.Time().IsZero() {
event.Context = event.Context.Clone() event.Context = event.Context.Clone()
@ -35,3 +36,17 @@ func DefaultTimeToNowIfNotSet(ctx context.Context, event cloudevents.Event) clou
} }
return event return event
} }
// NewDefaultDataContentTypeIfNotSet returns a defaulter that will inspect the
// provided event and set the provided content type if content type is found
// to be empty.
func NewDefaultDataContentTypeIfNotSet(contentType string) EventDefaulter {
return func(ctx context.Context, event event.Event) event.Event {
if event.Context != nil {
if event.DataContentType() == "" {
event.SetDataContentType(contentType)
}
}
return event
}
}

View File

@ -0,0 +1,39 @@
package client
import (
"context"
"net/http"
thttp "github.com/cloudevents/sdk-go/v2/protocol/http"
)
func NewHTTPReceiveHandler(ctx context.Context, p *thttp.Protocol, fn interface{}) (*EventReceiver, error) {
invoker, err := newReceiveInvoker(fn)
if err != nil {
return nil, err
}
return &EventReceiver{
p: p,
invoker: invoker,
}, nil
}
type EventReceiver struct {
p *thttp.Protocol
invoker Invoker
}
func (r *EventReceiver) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
go func() {
r.p.ServeHTTP(rw, req)
}()
ctx := context.Background()
msg, respFn, err := r.p.Respond(ctx)
if err != nil {
// TODO
} else if err := r.invoker.Invoke(ctx, msg, respFn); err != nil {
// TODO
}
}

View File

@ -0,0 +1,83 @@
package client
import (
"context"
"fmt"
"github.com/cloudevents/sdk-go/v2/binding"
cecontext "github.com/cloudevents/sdk-go/v2/context"
"github.com/cloudevents/sdk-go/v2/protocol"
)
type Invoker interface {
Invoke(context.Context, binding.Message, protocol.ResponseFn) error
IsReceiver() bool
IsResponder() bool
}
var _ Invoker = (*receiveInvoker)(nil)
func newReceiveInvoker(fn interface{}, fns ...EventDefaulter) (Invoker, error) {
r := &receiveInvoker{
eventDefaulterFns: fns,
}
if fn, err := receiver(fn); err != nil {
return nil, err
} else {
r.fn = fn
}
return r, nil
}
type receiveInvoker struct {
fn *receiverFn
eventDefaulterFns []EventDefaulter
}
func (r *receiveInvoker) Invoke(ctx context.Context, m binding.Message, respFn protocol.ResponseFn) (err error) {
defer func() {
if err2 := m.Finish(err); err2 == nil {
err = err2
}
}()
e, err := binding.ToEvent(ctx, m)
if err != nil {
return err
}
if e != nil && r.fn != nil {
resp, result := r.fn.invoke(ctx, *e)
// Apply the defaulter chain to the outgoing event.
if resp != nil && len(r.eventDefaulterFns) > 0 {
for _, fn := range r.eventDefaulterFns {
*resp = fn(ctx, *resp)
}
// Validate the event conforms to the CloudEvents Spec.
if verr := resp.Validate(); verr != nil {
cecontext.LoggerFrom(ctx).Error(fmt.Errorf("cloudevent validation failed on response event: %v, %w", verr, err))
}
}
if respFn != nil {
var rm binding.Message
if resp != nil {
rm = (*binding.EventMessage)(resp)
}
return respFn(ctx, rm, result) // TODO: there is a chance this never gets called. Is that ok?
}
}
return nil
}
func (r *receiveInvoker) IsReceiver() bool {
return !r.fn.hasEventOut
}
func (r *receiveInvoker) IsResponder() bool {
return r.fn.hasEventOut
}

View File

@ -0,0 +1,94 @@
package client
import (
"context"
"github.com/cloudevents/sdk-go/v2/event"
"github.com/cloudevents/sdk-go/v2/extensions"
"github.com/cloudevents/sdk-go/v2/observability"
"go.opencensus.io/stats"
"go.opencensus.io/stats/view"
"go.opencensus.io/trace"
)
var (
// LatencyMs measures the latency in milliseconds for the CloudEvents
// client methods.
LatencyMs = stats.Float64("cloudevents.io/sdk-go/client/latency", "The latency in milliseconds for the CloudEvents client methods.", "ms")
)
var (
// LatencyView is an OpenCensus view that shows client method latency.
LatencyView = &view.View{
Name: "client/latency",
Measure: LatencyMs,
Description: "The distribution of latency inside of client for CloudEvents.",
Aggregation: view.Distribution(0, .01, .1, 1, 10, 100, 1000, 10000),
TagKeys: observability.LatencyTags(),
}
)
type observed int32
// Adheres to Observable
var _ observability.Observable = observed(0)
const (
specversionAttr = "cloudevents.specversion"
typeAttr = "cloudevents.type"
sourceAttr = "cloudevents.source"
subjectAttr = "cloudevents.subject"
datacontenttypeAttr = "cloudevents.datacontenttype"
reportSend observed = iota
reportRequest
reportStartReceiver
)
// MethodName implements Observable.MethodName
func (o observed) MethodName() string {
switch o {
case reportSend:
return "send"
case reportRequest:
return "request"
case reportStartReceiver:
return "start_receiver"
default:
return "unknown"
}
}
// LatencyMs implements Observable.LatencyMs
func (o observed) LatencyMs() *stats.Float64Measure {
return LatencyMs
}
func EventTraceAttributes(e event.EventReader) []trace.Attribute {
as := []trace.Attribute{
trace.StringAttribute(specversionAttr, e.SpecVersion()),
trace.StringAttribute(typeAttr, e.Type()),
trace.StringAttribute(sourceAttr, e.Source()),
}
if sub := e.Subject(); sub != "" {
as = append(as, trace.StringAttribute(subjectAttr, sub))
}
if dct := e.DataContentType(); dct != "" {
as = append(as, trace.StringAttribute(datacontenttypeAttr, dct))
}
return as
}
// TraceSpan returns context and trace.Span based on event. Caller must call span.End()
func TraceSpan(ctx context.Context, e event.Event) (context.Context, *trace.Span) {
var span *trace.Span
if ext, ok := extensions.GetDistributedTracingExtension(e); ok {
ctx, span = ext.StartChildSpan(ctx, observability.ClientSpanName, trace.WithSpanKind(trace.SpanKindServer))
}
if span == nil {
ctx, span = trace.StartSpan(ctx, observability.ClientSpanName, trace.WithSpanKind(trace.SpanKindServer))
}
if span.IsRecordingEvents() {
span.AddAttributes(EventTraceAttributes(&e)...)
}
return ctx, span
}

View File

@ -0,0 +1,73 @@
package client
import (
"fmt"
"github.com/cloudevents/sdk-go/v2/binding"
)
// Option is the function signature required to be considered an client.Option.
type Option func(interface{}) error
// WithEventDefaulter adds an event defaulter to the end of the defaulter chain.
func WithEventDefaulter(fn EventDefaulter) Option {
return func(i interface{}) error {
if c, ok := i.(*ceClient); ok {
if fn == nil {
return fmt.Errorf("client option was given an nil event defaulter")
}
c.eventDefaulterFns = append(c.eventDefaulterFns, fn)
}
return nil
}
}
func WithForceBinary() Option {
return func(i interface{}) error {
if c, ok := i.(*ceClient); ok {
c.outboundContextDecorators = append(c.outboundContextDecorators, binding.WithForceBinary)
}
return nil
}
}
func WithForceStructured() Option {
return func(i interface{}) error {
if c, ok := i.(*ceClient); ok {
c.outboundContextDecorators = append(c.outboundContextDecorators, binding.WithForceStructured)
}
return nil
}
}
// WithUUIDs adds DefaultIDToUUIDIfNotSet event defaulter to the end of the
// defaulter chain.
func WithUUIDs() Option {
return func(i interface{}) error {
if c, ok := i.(*ceClient); ok {
c.eventDefaulterFns = append(c.eventDefaulterFns, DefaultIDToUUIDIfNotSet)
}
return nil
}
}
// WithTimeNow adds DefaultTimeToNowIfNotSet event defaulter to the end of the
// defaulter chain.
func WithTimeNow() Option {
return func(i interface{}) error {
if c, ok := i.(*ceClient); ok {
c.eventDefaulterFns = append(c.eventDefaulterFns, DefaultTimeToNowIfNotSet)
}
return nil
}
}
// WithTracePropagation enables trace propagation via the distributed tracing
// extension.
func WithTracePropagation() Option {
return func(i interface{}) error {
if c, ok := i.(*obsClient); ok {
c.addTracing = true
}
return nil
}
}

View File

@ -0,0 +1,189 @@
package client
import (
"context"
"errors"
"fmt"
"reflect"
"github.com/cloudevents/sdk-go/v2/event"
"github.com/cloudevents/sdk-go/v2/protocol"
)
// Receive is the signature of a fn to be invoked for incoming cloudevents.
type ReceiveFull func(context.Context, event.Event) protocol.Result
type receiverFn struct {
numIn int
numOut int
fnValue reflect.Value
hasContextIn bool
hasEventIn bool
hasEventOut bool
hasResultOut bool
}
const (
inParamUsage = "expected a function taking either no parameters, one or more of (context.Context, event.Event) ordered"
outParamUsage = "expected a function returning one or mode of (*event.Event, protocol.Result) ordered"
)
var (
contextType = reflect.TypeOf((*context.Context)(nil)).Elem()
eventType = reflect.TypeOf((*event.Event)(nil)).Elem()
eventPtrType = reflect.TypeOf((*event.Event)(nil)) // want the ptr type
resultType = reflect.TypeOf((*protocol.Result)(nil)).Elem()
)
// receiver creates a receiverFn wrapper class that is used by the client to
// validate and invoke the provided function.
// Valid fn signatures are:
// * func()
// * func() error
// * func(context.Context)
// * func(context.Context) transport.Result
// * func(event.Event)
// * func(event.Event) transport.Result
// * func(context.Context, event.Event)
// * func(context.Context, event.Event) transport.Result
// * func(event.Event) *event.Event
// * func(event.Event) (*event.Event, transport.Result)
// * func(context.Context, event.Event, *event.Event
// * func(context.Context, event.Event) (*event.Event, transport.Result)
//
func receiver(fn interface{}) (*receiverFn, error) {
fnType := reflect.TypeOf(fn)
if fnType.Kind() != reflect.Func {
return nil, errors.New("must pass a function to handle events")
}
r := &receiverFn{
fnValue: reflect.ValueOf(fn),
numIn: fnType.NumIn(),
numOut: fnType.NumOut(),
}
if err := r.validate(fnType); err != nil {
return nil, err
}
return r, nil
}
func (r *receiverFn) invoke(ctx context.Context, e event.Event) (*event.Event, protocol.Result) {
args := make([]reflect.Value, 0, r.numIn)
if r.numIn > 0 {
if r.hasContextIn {
args = append(args, reflect.ValueOf(ctx))
}
if r.hasEventIn {
args = append(args, reflect.ValueOf(e))
}
}
v := r.fnValue.Call(args)
var respOut protocol.Result
var eOut *event.Event
if r.numOut > 0 {
i := 0
if r.hasEventOut {
if eo, ok := v[i].Interface().(*event.Event); ok {
eOut = eo
}
i++ // <-- note, need to inc i.
}
if r.hasResultOut {
if resp, ok := v[i].Interface().(protocol.Result); ok {
respOut = resp
}
}
}
return eOut, respOut
}
// Verifies that the inputs to a function have a valid signature
// Valid input is to be [0, all] of
// context.Context, event.Event in this order.
func (r *receiverFn) validateInParamSignature(fnType reflect.Type) error {
r.hasContextIn = false
r.hasEventIn = false
switch fnType.NumIn() {
case 2:
// has to be (context.Context, event.Event)
if !fnType.In(1).ConvertibleTo(eventType) {
return fmt.Errorf("%s; cannot convert parameter 2 from %s to event.Event", inParamUsage, fnType.In(1))
} else {
r.hasEventIn = true
}
fallthrough
case 1:
if !fnType.In(0).ConvertibleTo(contextType) {
if !fnType.In(0).ConvertibleTo(eventType) {
return fmt.Errorf("%s; cannot convert parameter 1 from %s to context.Context or event.Event", inParamUsage, fnType.In(0))
} else if r.hasEventIn {
return fmt.Errorf("%s; duplicate parameter of type event.Event", inParamUsage)
} else {
r.hasEventIn = true
}
} else {
r.hasContextIn = true
}
fallthrough
case 0:
return nil
default:
return fmt.Errorf("%s; function has too many parameters (%d)", inParamUsage, fnType.NumIn())
}
}
// Verifies that the outputs of a function have a valid signature
// Valid output signatures to be [0, all] of
// *event.Event, transport.Result in this order
func (r *receiverFn) validateOutParamSignature(fnType reflect.Type) error {
r.hasEventOut = false
r.hasResultOut = false
switch fnType.NumOut() {
case 2:
// has to be (*event.Event, transport.Result)
if !fnType.Out(1).ConvertibleTo(resultType) {
return fmt.Errorf("%s; cannot convert parameter 2 from %s to event.Response", outParamUsage, fnType.Out(1))
} else {
r.hasResultOut = true
}
fallthrough
case 1:
if !fnType.Out(0).ConvertibleTo(resultType) {
if !fnType.Out(0).ConvertibleTo(eventPtrType) {
return fmt.Errorf("%s; cannot convert parameter 1 from %s to *event.Event or transport.Result", outParamUsage, fnType.Out(0))
} else {
r.hasEventOut = true
}
} else if r.hasResultOut {
return fmt.Errorf("%s; duplicate parameter of type event.Response", outParamUsage)
} else {
r.hasResultOut = true
}
fallthrough
case 0:
return nil
default:
return fmt.Errorf("%s; function has too many return types (%d)", outParamUsage, fnType.NumOut())
}
}
// validateReceiverFn validates that a function has the right number of in and
// out params and that they are of allowed types.
func (r *receiverFn) validate(fnType reflect.Type) error {
if err := r.validateInParamSignature(fnType); err != nil {
return err
}
if err := r.validateOutParamSignature(fnType); err != nil {
return err
}
return nil
}

View File

@ -3,7 +3,6 @@ package context
import ( import (
"context" "context"
"net/url" "net/url"
"strings"
) )
// Opaque key type used to store target // Opaque key type used to store target
@ -51,26 +50,3 @@ func TopicFrom(ctx context.Context) string {
} }
return "" return ""
} }
// Opaque key type used to store encoding
type encodingKeyType struct{}
var encodingKey = encodingKeyType{}
// WithEncoding returns back a new context with the given encoding. Encoding is intended to be transport dependent.
// For http transport, `encoding` should be one of [binary, structured] and will be used to override the outbound
// codec encoding setting. If the transport does not understand the encoding, it will be ignored.
func WithEncoding(ctx context.Context, encoding string) context.Context {
return context.WithValue(ctx, encodingKey, strings.ToLower(encoding))
}
// EncodingFrom looks in the given context and returns `target` as a parsed url if found and valid, otherwise nil.
func EncodingFrom(ctx context.Context) string {
c := ctx.Value(encodingKey)
if c != nil {
if s, ok := c.(string); ok && s != "" {
return s
}
}
return ""
}

View File

@ -1,6 +1,7 @@
package cloudevents package event
const ( const (
TextPlain = "text/plain"
TextJSON = "text/json" TextJSON = "text/json"
ApplicationJSON = "application/json" ApplicationJSON = "application/json"
ApplicationXML = "application/xml" ApplicationXML = "application/xml"
@ -20,6 +21,12 @@ func StringOfApplicationXML() *string {
return &a return &a
} }
// StringOfTextPlain returns a string pointer to "text/plain"
func StringOfTextPlain() *string {
a := TextPlain
return &a
}
// StringOfApplicationCloudEventsJSON returns a string pointer to // StringOfApplicationCloudEventsJSON returns a string pointer to
// "application/cloudevents+json" // "application/cloudevents+json"
func StringOfApplicationCloudEventsJSON() *string { func StringOfApplicationCloudEventsJSON() *string {

View File

@ -1,4 +1,4 @@
package cloudevents package event
const ( const (
Base64 = "base64" Base64 = "base64"

View File

@ -4,16 +4,15 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/cloudevents/sdk-go/pkg/cloudevents/datacodec/json" "github.com/cloudevents/sdk-go/v2/event/datacodec/json"
"github.com/cloudevents/sdk-go/pkg/cloudevents/datacodec/text" "github.com/cloudevents/sdk-go/v2/event/datacodec/text"
"github.com/cloudevents/sdk-go/pkg/cloudevents/datacodec/xml" "github.com/cloudevents/sdk-go/v2/event/datacodec/xml"
"github.com/cloudevents/sdk-go/pkg/cloudevents/observability"
) )
// Decoder is the expected function signature for decoding `in` to `out`. What // Decoder is the expected function signature for decoding `in` to `out`.
// `in` is could be decoder dependent. For example, `in` could be bytes, or a // If Event sent the payload as base64, Decoder assumes that `in` is the
// base64 string. // decoded base64 byte array.
type Decoder func(ctx context.Context, in, out interface{}) error type Decoder func(ctx context.Context, in []byte, out interface{}) error
// Encoder is the expected function signature for encoding `in` to bytes. // Encoder is the expected function signature for encoding `in` to bytes.
// Returns an error if the encoder has an issue encoding `in`. // Returns an error if the encoder has an issue encoding `in`.
@ -56,18 +55,7 @@ func AddEncoder(contentType string, fn Encoder) {
// Decode looks up and invokes the decoder registered for the given content // Decode looks up and invokes the decoder registered for the given content
// type. An error is returned if no decoder is registered for the given // type. An error is returned if no decoder is registered for the given
// content type. // content type.
func Decode(ctx context.Context, contentType string, in, out interface{}) error { func Decode(ctx context.Context, contentType string, in []byte, out interface{}) error {
_, r := observability.NewReporter(ctx, reportDecode)
err := obsDecode(ctx, contentType, in, out)
if err != nil {
r.Error()
} else {
r.OK()
}
return err
}
func obsDecode(ctx context.Context, contentType string, in, out interface{}) error {
if fn, ok := decoder[contentType]; ok { if fn, ok := decoder[contentType]; ok {
return fn(ctx, in, out) return fn(ctx, in, out)
} }
@ -78,17 +66,6 @@ func obsDecode(ctx context.Context, contentType string, in, out interface{}) err
// type. An error is returned if no encoder is registered for the given // type. An error is returned if no encoder is registered for the given
// content type. // content type.
func Encode(ctx context.Context, contentType string, in interface{}) ([]byte, error) { func Encode(ctx context.Context, contentType string, in interface{}) ([]byte, error) {
_, r := observability.NewReporter(ctx, reportEncode)
b, err := obsEncode(ctx, contentType, in)
if err != nil {
r.Error()
} else {
r.OK()
}
return b, err
}
func obsEncode(ctx context.Context, contentType string, in interface{}) ([]byte, error) {
if fn, ok := encoder[contentType]; ok { if fn, ok := encoder[contentType]; ok {
return fn(ctx, in) return fn(ctx, in)
} }

View File

@ -0,0 +1,50 @@
package datacodec
import (
"context"
"github.com/cloudevents/sdk-go/v2/event/datacodec/json"
"github.com/cloudevents/sdk-go/v2/event/datacodec/text"
"github.com/cloudevents/sdk-go/v2/event/datacodec/xml"
"github.com/cloudevents/sdk-go/v2/observability"
)
func SetObservedCodecs() {
AddDecoder("", json.DecodeObserved)
AddDecoder("application/json", json.DecodeObserved)
AddDecoder("text/json", json.DecodeObserved)
AddDecoder("application/xml", xml.DecodeObserved)
AddDecoder("text/xml", xml.DecodeObserved)
AddDecoder("text/plain", text.DecodeObserved)
AddEncoder("", json.Encode)
AddEncoder("application/json", json.EncodeObserved)
AddEncoder("text/json", json.EncodeObserved)
AddEncoder("application/xml", xml.EncodeObserved)
AddEncoder("text/xml", xml.EncodeObserved)
AddEncoder("text/plain", text.EncodeObserved)
}
// DecodeObserved calls Decode and records the result.
func DecodeObserved(ctx context.Context, contentType string, in []byte, out interface{}) error {
_, r := observability.NewReporter(ctx, reportDecode)
err := Decode(ctx, contentType, in, out)
if err != nil {
r.Error()
} else {
r.OK()
}
return err
}
// EncodeObserved calls Encode and records the result.
func EncodeObserved(ctx context.Context, contentType string, in interface{}) ([]byte, error) {
_, r := observability.NewReporter(ctx, reportEncode)
b, err := Encode(ctx, contentType, in)
if err != nil {
r.Error()
} else {
r.OK()
}
return b, err
}

View File

@ -0,0 +1,51 @@
package json
import (
"context"
"encoding/json"
"fmt"
"reflect"
)
// Decode takes `in` as []byte.
// If Event sent the payload as base64, Decoder assumes that `in` is the
// decoded base64 byte array.
func Decode(ctx context.Context, in []byte, out interface{}) error {
if in == nil {
return nil
}
if out == nil {
return fmt.Errorf("out is nil")
}
if err := json.Unmarshal(in, out); err != nil {
return fmt.Errorf("[json] found bytes \"%s\", but failed to unmarshal: %s", string(in), err.Error())
}
return nil
}
// Encode attempts to json.Marshal `in` into bytes. Encode will inspect `in`
// and returns `in` unmodified if it is detected that `in` is already a []byte;
// Or json.Marshal errors.
func Encode(ctx context.Context, in interface{}) ([]byte, error) {
if in == nil {
return nil, nil
}
it := reflect.TypeOf(in)
switch it.Kind() {
case reflect.Slice:
if it.Elem().Kind() == reflect.Uint8 {
if b, ok := in.([]byte); ok && len(b) > 0 {
// check to see if it is a pre-encoded byte string.
if b[0] == byte('"') || b[0] == byte('{') || b[0] == byte('[') {
return b, nil
}
}
}
}
return json.Marshal(in)
}

View File

@ -0,0 +1,30 @@
package json
import (
"context"
"github.com/cloudevents/sdk-go/v2/observability"
)
// DecodeObserved calls Decode and records the results.
func DecodeObserved(ctx context.Context, in []byte, out interface{}) error {
_, r := observability.NewReporter(ctx, reportDecode)
err := Decode(ctx, in, out)
if err != nil {
r.Error()
} else {
r.OK()
}
return err
}
// EncodeObserved calls Encode and records the results.
func EncodeObserved(ctx context.Context, in interface{}) ([]byte, error) {
_, r := observability.NewReporter(ctx, reportEncode)
b, err := Encode(ctx, in)
if err != nil {
r.Error()
} else {
r.OK()
}
return b, err
}

View File

@ -1,7 +1,7 @@
package json package json
import ( import (
"github.com/cloudevents/sdk-go/pkg/cloudevents/observability" "github.com/cloudevents/sdk-go/v2/observability"
"go.opencensus.io/stats" "go.opencensus.io/stats"
"go.opencensus.io/stats/view" "go.opencensus.io/stats/view"
) )
@ -33,18 +33,6 @@ const (
reportDecode reportDecode
) )
// TraceName implements Observable.TraceName
func (o observed) TraceName() string {
switch o {
case reportEncode:
return "datacodec/json/encode"
case reportDecode:
return "datacodec/json/decode"
default:
return "datacodec/json/unknown"
}
}
// MethodName implements Observable.MethodName // MethodName implements Observable.MethodName
func (o observed) MethodName() string { func (o observed) MethodName() string {
switch o { switch o {

View File

@ -1,7 +1,7 @@
package datacodec package datacodec
import ( import (
"github.com/cloudevents/sdk-go/pkg/cloudevents/observability" "github.com/cloudevents/sdk-go/v2/observability"
"go.opencensus.io/stats" "go.opencensus.io/stats"
"go.opencensus.io/stats/view" "go.opencensus.io/stats/view"
) )
@ -33,18 +33,6 @@ const (
reportDecode reportDecode
) )
// TraceName implements Observable.TraceName
func (o observed) TraceName() string {
switch o {
case reportEncode:
return "datacodec/encode"
case reportDecode:
return "datacodec/decode"
default:
return "datacodec/unknown"
}
}
// MethodName implements Observable.MethodName // MethodName implements Observable.MethodName
func (o observed) MethodName() string { func (o observed) MethodName() string {
switch o { switch o {

View File

@ -6,21 +6,12 @@ import (
"fmt" "fmt"
) )
func Decode(_ context.Context, in, out interface{}) error { func Decode(_ context.Context, in []byte, out interface{}) error {
p, _ := out.(*string) p, _ := out.(*string)
if p == nil { if p == nil {
return fmt.Errorf("text.Decode out: want *string, got %T", out) return fmt.Errorf("text.Decode out: want *string, got %T", out)
} }
switch s := in.(type) { *p = string(in)
case string:
*p = s
case []byte:
*p = string(s)
case nil: // treat nil like []byte{}
*p = ""
default:
return fmt.Errorf("text.Decode in: want []byte or string, got %T", in)
}
return nil return nil
} }

View File

@ -0,0 +1,30 @@
package text
import (
"context"
"github.com/cloudevents/sdk-go/v2/observability"
)
// DecodeObserved calls Decode and records the results.
func DecodeObserved(ctx context.Context, in []byte, out interface{}) error {
_, r := observability.NewReporter(ctx, reportDecode)
err := Decode(ctx, in, out)
if err != nil {
r.Error()
} else {
r.OK()
}
return err
}
// EncodeObserved calls Encode and records the results.
func EncodeObserved(ctx context.Context, in interface{}) ([]byte, error) {
_, r := observability.NewReporter(ctx, reportEncode)
b, err := Encode(ctx, in)
if err != nil {
r.Error()
} else {
r.OK()
}
return b, err
}

View File

@ -0,0 +1,4 @@
/*
Package text holds the encoder/decoder implementation for `text/plain`.
*/
package text

View File

@ -0,0 +1,51 @@
package text
import (
"github.com/cloudevents/sdk-go/v2/observability"
"go.opencensus.io/stats"
"go.opencensus.io/stats/view"
)
var (
// LatencyMs measures the latency in milliseconds for the CloudEvents xml data
// codec methods.
LatencyMs = stats.Float64("cloudevents.io/sdk-go/datacodec/text/latency", "The latency in milliseconds for the CloudEvents text data codec methods.", "ms")
)
var (
// LatencyView is an OpenCensus view that shows data codec xml method latency.
LatencyView = &view.View{
Name: "datacodec/text/latency",
Measure: LatencyMs,
Description: "The distribution of latency inside of the text data codec for CloudEvents.",
Aggregation: view.Distribution(0, .01, .1, 1, 10, 100, 1000, 10000),
TagKeys: observability.LatencyTags(),
}
)
type observed int32
// Adheres to Observable
var _ observability.Observable = observed(0)
const (
reportEncode observed = iota
reportDecode
)
// MethodName implements Observable.MethodName
func (o observed) MethodName() string {
switch o {
case reportEncode:
return "encode"
case reportDecode:
return "decode"
default:
return "unknown"
}
}
// LatencyMs implements Observable.LatencyMs
func (o observed) LatencyMs() *stats.Float64Measure {
return LatencyMs
}

View File

@ -0,0 +1,35 @@
package xml
import (
"context"
"encoding/xml"
"fmt"
)
// Decode takes `in` as []byte.
// If Event sent the payload as base64, Decoder assumes that `in` is the
// decoded base64 byte array.
func Decode(ctx context.Context, in []byte, out interface{}) error {
if in == nil {
return nil
}
if err := xml.Unmarshal(in, out); err != nil {
return fmt.Errorf("[xml] found bytes, but failed to unmarshal: %s %s", err.Error(), string(in))
}
return nil
}
// Encode attempts to xml.Marshal `in` into bytes. Encode will inspect `in`
// and returns `in` unmodified if it is detected that `in` is already a []byte;
// Or xml.Marshal errors.
func Encode(ctx context.Context, in interface{}) ([]byte, error) {
if b, ok := in.([]byte); ok {
// check to see if it is a pre-encoded byte string.
if len(b) > 0 && b[0] == byte('"') {
return b, nil
}
}
return xml.Marshal(in)
}

View File

@ -0,0 +1,30 @@
package xml
import (
"context"
"github.com/cloudevents/sdk-go/v2/observability"
)
// DecodeObserved calls Decode and records the result.
func DecodeObserved(ctx context.Context, in []byte, out interface{}) error {
_, r := observability.NewReporter(ctx, reportDecode)
err := Decode(ctx, in, out)
if err != nil {
r.Error()
} else {
r.OK()
}
return err
}
// EncodeObserved calls Encode and records the result.
func EncodeObserved(ctx context.Context, in interface{}) ([]byte, error) {
_, r := observability.NewReporter(ctx, reportEncode)
b, err := Encode(ctx, in)
if err != nil {
r.Error()
} else {
r.OK()
}
return b, err
}

View File

@ -1,7 +1,7 @@
package xml package xml
import ( import (
"github.com/cloudevents/sdk-go/pkg/cloudevents/observability" "github.com/cloudevents/sdk-go/v2/observability"
"go.opencensus.io/stats" "go.opencensus.io/stats"
"go.opencensus.io/stats/view" "go.opencensus.io/stats/view"
) )
@ -33,18 +33,6 @@ const (
reportDecode reportDecode
) )
// TraceName implements Observable.TraceName
func (o observed) TraceName() string {
switch o {
case reportEncode:
return "datacodec/xml/encode"
case reportDecode:
return "datacodec/xml/decode"
default:
return "datacodec/xml/unknown"
}
}
// MethodName implements Observable.MethodName // MethodName implements Observable.MethodName
func (o observed) MethodName() string { func (o observed) MethodName() string {
switch o { switch o {

View File

@ -1,4 +1,4 @@
/* /*
Package cloudevents provides primitives to work with CloudEvents specification: https://github.com/cloudevents/spec. Package cloudevents provides primitives to work with CloudEvents specification: https://github.com/cloudevents/spec.
*/ */
package cloudevents package event

158
vendor/github.com/cloudevents/sdk-go/v2/event/event.go generated vendored Normal file
View File

@ -0,0 +1,158 @@
package event
import (
"bytes"
"encoding/json"
"fmt"
"strings"
)
// Event represents the canonical representation of a CloudEvent.
type Event struct {
Context EventContext
DataEncoded []byte
// DataBase64 indicates if the event, when serialized, represents
// the data field using the base64 encoding.
// In v0.3, this field is superseded by DataContentEncoding
DataBase64 bool
FieldErrors map[string]error
}
const (
defaultEventVersion = CloudEventsVersionV1
)
func (e *Event) fieldError(field string, err error) {
if e.FieldErrors == nil {
e.FieldErrors = make(map[string]error, 0)
}
e.FieldErrors[field] = err
}
func (e *Event) fieldOK(field string) {
if e.FieldErrors != nil {
delete(e.FieldErrors, field)
}
}
// New returns a new Event, an optional version can be passed to change the
// default spec version from 1.0 to the provided version.
func New(version ...string) Event {
specVersion := defaultEventVersion
if len(version) >= 1 {
specVersion = version[0]
}
e := &Event{}
e.SetSpecVersion(specVersion)
return *e
}
// DEPRECATED: Access extensions directly via the e.Extensions() map.
// Use functions in the types package to convert extension values.
// For example replace this:
//
// var i int
// err := e.ExtensionAs("foo", &i)
//
// With this:
//
// i, err := types.ToInteger(e.Extensions["foo"])
//
func (e Event) ExtensionAs(name string, obj interface{}) error {
return e.Context.ExtensionAs(name, obj)
}
// Validate performs a spec based validation on this event.
// Validation is dependent on the spec version specified in the event context.
func (e Event) Validate() error {
if e.Context == nil {
return fmt.Errorf("every event conforming to the CloudEvents specification MUST include a context")
}
if e.FieldErrors != nil {
errs := make([]string, 0)
for f, e := range e.FieldErrors {
errs = append(errs, fmt.Sprintf("%q: %s,", f, e))
}
if len(errs) > 0 {
return fmt.Errorf("previous field errors: [%s]", strings.Join(errs, "\n"))
}
}
if err := e.Context.Validate(); err != nil {
return err
}
return nil
}
// String returns a pretty-printed representation of the Event.
func (e Event) String() string {
b := strings.Builder{}
b.WriteString("Validation: ")
valid := e.Validate()
if valid == nil {
b.WriteString("valid\n")
} else {
b.WriteString("invalid\n")
}
if valid != nil {
b.WriteString(fmt.Sprintf("Validation Error: \n%s\n", valid.Error()))
}
b.WriteString(e.Context.String())
if e.DataEncoded != nil {
if e.DataBase64 {
b.WriteString("Data (binary),\n ")
} else {
b.WriteString("Data,\n ")
}
switch e.DataMediaType() {
case ApplicationJSON:
var prettyJSON bytes.Buffer
err := json.Indent(&prettyJSON, e.DataEncoded, " ", " ")
if err != nil {
b.Write(e.DataEncoded)
} else {
b.Write(prettyJSON.Bytes())
}
default:
b.Write(e.DataEncoded)
}
b.WriteString("\n")
}
return b.String()
}
func (e Event) Clone() Event {
out := Event{}
out.Context = e.Context.Clone()
out.DataEncoded = cloneBytes(e.DataEncoded)
out.DataBase64 = e.DataBase64
out.FieldErrors = e.cloneFieldErrors()
return out
}
func cloneBytes(in []byte) []byte {
if in == nil {
return nil
}
out := make([]byte, len(in))
copy(out, in)
return out
}
func (e Event) cloneFieldErrors() map[string]error {
if e.FieldErrors == nil {
return nil
}
newFE := make(map[string]error, len(e.FieldErrors))
for k, v := range e.FieldErrors {
newFE[k] = v
}
return newFE
}

View File

@ -0,0 +1,113 @@
package event
import (
"context"
"encoding/base64"
"fmt"
"strconv"
"github.com/cloudevents/sdk-go/v2/event/datacodec"
)
// SetData encodes the given payload with the given content type.
// If the provided payload is a byte array, when marshalled to json it will be encoded as base64.
// If the provided payload is different from byte array, datacodec.Encode is invoked to attempt a
// marshalling to byte array.
func (e *Event) SetData(contentType string, obj interface{}) error {
e.SetDataContentType(contentType)
if e.SpecVersion() != CloudEventsVersionV1 {
return e.legacySetData(obj)
}
// Version 1.0 and above.
switch obj := obj.(type) {
case []byte:
e.DataEncoded = obj
e.DataBase64 = true
default:
data, err := datacodec.Encode(context.Background(), e.DataMediaType(), obj)
if err != nil {
return err
}
e.DataEncoded = data
e.DataBase64 = false
}
return nil
}
// Deprecated: Delete when we do not have to support Spec v0.3.
func (e *Event) legacySetData(obj interface{}) error {
data, err := datacodec.Encode(context.Background(), e.DataMediaType(), obj)
if err != nil {
return err
}
if e.DeprecatedDataContentEncoding() == Base64 {
buf := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
base64.StdEncoding.Encode(buf, data)
e.DataEncoded = buf
e.DataBase64 = false
} else {
data, err := datacodec.Encode(context.Background(), e.DataMediaType(), obj)
if err != nil {
return err
}
e.DataEncoded = data
e.DataBase64 = false
}
return nil
}
const (
quotes = `"'`
)
func (e Event) Data() []byte {
return e.DataEncoded
}
// DataAs attempts to populate the provided data object with the event payload.
// data should be a pointer type.
func (e Event) DataAs(obj interface{}) error {
data := e.Data()
if len(data) == 0 {
// No data.
return nil
}
if e.SpecVersion() != CloudEventsVersionV1 {
var err error
if data, err = e.legacyConvertData(data); err != nil {
return err
}
}
return datacodec.Decode(context.Background(), e.DataMediaType(), data, obj)
}
func (e Event) legacyConvertData(data []byte) ([]byte, error) {
if e.Context.DeprecatedGetDataContentEncoding() == Base64 {
var bs []byte
// test to see if we need to unquote the data.
if data[0] == quotes[0] || data[0] == quotes[1] {
str, err := strconv.Unquote(string(data))
if err != nil {
return nil, err
}
bs = []byte(str)
} else {
bs = data
}
buf := make([]byte, base64.StdEncoding.DecodedLen(len(bs)))
n, err := base64.StdEncoding.Decode(buf, bs)
if err != nil {
return nil, fmt.Errorf("failed to decode data from base64: %s", err.Error())
}
data = buf[:n]
}
return data, nil
}

View File

@ -1,4 +1,4 @@
package cloudevents package event
import ( import (
"time" "time"
@ -33,19 +33,35 @@ type EventReader interface {
// Extensions use the CloudEvents type system, details in package cloudevents/types. // Extensions use the CloudEvents type system, details in package cloudevents/types.
Extensions() map[string]interface{} Extensions() map[string]interface{}
// DEPRECATED: see event.Context.ExtensionAs
// ExtensionAs returns event.Context.ExtensionAs(name, obj). // ExtensionAs returns event.Context.ExtensionAs(name, obj).
//
// DEPRECATED: Access extensions directly via the e.Extensions() map.
// Use functions in the types package to convert extension values.
// For example replace this:
//
// var i int
// err := e.ExtensionAs("foo", &i)
//
// With this:
//
// i, err := types.ToInteger(e.Extensions["foo"])
//
ExtensionAs(string, interface{}) error ExtensionAs(string, interface{}) error
// Data Attribute // Data Attribute
// Data returns the raw data buffer
// If the event was encoded with base64 encoding, Data returns the already decoded
// byte array
Data() []byte
// DataAs attempts to populate the provided data object with the event payload. // DataAs attempts to populate the provided data object with the event payload.
// data should be a pointer type.
DataAs(interface{}) error DataAs(interface{}) error
} }
// EventWriter is the interface for writing through an event onto attributes. // EventWriter is the interface for writing through an event onto attributes.
// If an error is thrown by a sub-component, EventWriter panics. // If an error is thrown by a sub-component, EventWriter caches the error
// internally and exposes errors with a call to event.Validate().
type EventWriter interface { type EventWriter interface {
// Context Attributes // Context Attributes
@ -73,6 +89,9 @@ type EventWriter interface {
// SetExtension performs event.Context.SetExtension. // SetExtension performs event.Context.SetExtension.
SetExtension(string, interface{}) SetExtension(string, interface{})
// SetData encodes the given payload with the current encoding settings. // SetData encodes the given payload with the given content type.
SetData(interface{}) error // If the provided payload is a byte array, when marshalled to json it will be encoded as base64.
// If the provided payload is different from byte array, datacodec.Encode is invoked to attempt a
// marshalling to byte array.
SetData(string, interface{}) error
} }

View File

@ -1,15 +1,15 @@
package cloudevents package event
import ( import (
"context" "context"
"encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"strconv"
"strings" "strings"
"github.com/cloudevents/sdk-go/pkg/cloudevents/observability" errors2 "github.com/pkg/errors"
"github.com/cloudevents/sdk-go/v2/observability"
) )
// MarshalJSON implements a custom json marshal method used when this type is // MarshalJSON implements a custom json marshal method used when this type is
@ -26,7 +26,7 @@ func (e Event) MarshalJSON() ([]byte, error) {
var err error var err error
switch e.SpecVersion() { switch e.SpecVersion() {
case CloudEventsVersionV01, CloudEventsVersionV02, CloudEventsVersionV03: case CloudEventsVersionV03:
b, err = JsonEncodeLegacy(e) b, err = JsonEncodeLegacy(e)
case CloudEventsVersionV1: case CloudEventsVersionV1:
b, err = JsonEncode(e) b, err = JsonEncode(e)
@ -59,10 +59,6 @@ func (e *Event) UnmarshalJSON(b []byte) error {
var err error var err error
switch version { switch version {
case CloudEventsVersionV01:
err = e.JsonDecodeV01(b, raw)
case CloudEventsVersionV02:
err = e.JsonDecodeV02(b, raw)
case CloudEventsVersionV03: case CloudEventsVersionV03:
err = e.JsonDecodeV03(b, raw) err = e.JsonDecodeV03(b, raw)
case CloudEventsVersionV1: case CloudEventsVersionV1:
@ -82,15 +78,6 @@ func (e *Event) UnmarshalJSON(b []byte) error {
} }
func versionFromRawMessage(raw map[string]json.RawMessage) string { func versionFromRawMessage(raw map[string]json.RawMessage) string {
// v0.1
if v, ok := raw["cloudEventsVersion"]; ok {
var version string
if err := json.Unmarshal(v, &version); err != nil {
return ""
}
return version
}
// v0.2 and after // v0.2 and after
if v, ok := raw["specversion"]; ok { if v, ok := raw["specversion"]; ok {
var version string var version string
@ -104,68 +91,51 @@ func versionFromRawMessage(raw map[string]json.RawMessage) string {
// JsonEncode // JsonEncode
func JsonEncode(e Event) ([]byte, error) { func JsonEncode(e Event) ([]byte, error) {
data, err := e.DataBytes() return jsonEncode(e.Context, e.DataEncoded, e.DataBase64)
if err != nil {
return nil, err
}
return jsonEncode(e.Context, data, e.DataBinary)
} }
// JsonEncodeLegacy // JsonEncodeLegacy
func JsonEncodeLegacy(e Event) ([]byte, error) { func JsonEncodeLegacy(e Event) ([]byte, error) {
var data []byte
isBase64 := e.Context.DeprecatedGetDataContentEncoding() == Base64 isBase64 := e.Context.DeprecatedGetDataContentEncoding() == Base64
var err error return jsonEncode(e.Context, e.DataEncoded, isBase64)
data, err = e.DataBytes()
if err != nil {
return nil, err
}
return jsonEncode(e.Context, data, isBase64)
} }
func jsonEncode(ctx EventContextReader, data []byte, isBase64 bool) ([]byte, error) { func jsonEncode(ctx EventContextReader, data []byte, shouldEncodeToBase64 bool) ([]byte, error) {
var b map[string]json.RawMessage var b map[string]json.RawMessage
var err error var err error
if ctx.GetSpecVersion() == CloudEventsVersionV01 {
b, err = marshalEventLegacy(ctx)
} else {
b, err = marshalEvent(ctx, ctx.GetExtensions()) b, err = marshalEvent(ctx, ctx.GetExtensions())
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
if data != nil { if data != nil {
// data is passed in as an encoded []byte. That slice might be any // data here is a serialized version of whatever payload.
// number of things but for json encoding of the envelope all we care // If we need to write the payload as base64, shouldEncodeToBase64 is true.
// is if the payload is either a string or a json object. If it is a
// json object, it can be inserted into the body without modification.
// Otherwise we need to quote it if not already quoted.
mediaType, err := ctx.GetDataMediaType() mediaType, err := ctx.GetDataMediaType()
if err != nil { if err != nil {
return nil, err return nil, err
} }
isJson := mediaType == "" || mediaType == ApplicationJSON || mediaType == TextJSON isJson := mediaType == "" || mediaType == ApplicationJSON || mediaType == TextJSON
// TODO(#60): we do not support json values at the moment, only objects and lists. // If isJson and no encoding to base64, we don't need to perform additional steps
if isJson && !isBase64 { if isJson && !shouldEncodeToBase64 {
b["data"] = data b["data"] = data
} else { } else {
var dataKey string var dataKey = "data"
if ctx.GetSpecVersion() == CloudEventsVersionV1 { if ctx.GetSpecVersion() == CloudEventsVersionV1 && shouldEncodeToBase64 {
dataKey = "data_base64" dataKey = "data_base64"
buf := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
base64.StdEncoding.Encode(buf, data)
data = buf
} else {
dataKey = "data"
} }
if data[0] != byte('"') { var dataPointer []byte
b[dataKey] = []byte(strconv.QuoteToASCII(string(data))) if shouldEncodeToBase64 {
dataPointer, err = json.Marshal(data)
} else { } else {
// already quoted dataPointer, err = json.Marshal(string(data))
b[dataKey] = data
} }
if err != nil {
return nil, err
}
b[dataKey] = dataPointer
} }
} }
@ -177,65 +147,6 @@ func jsonEncode(ctx EventContextReader, data []byte, isBase64 bool) ([]byte, err
return body, nil return body, nil
} }
// JsonDecodeV01 takes in the byte representation of a version 0.1 structured json CloudEvent and returns a
// cloudevent.Event or an error if there are parsing errors.
func (e *Event) JsonDecodeV01(body []byte, raw map[string]json.RawMessage) error {
ec := EventContextV01{}
if err := json.Unmarshal(body, &ec); err != nil {
return err
}
var data interface{}
if d, ok := raw["data"]; ok {
data = []byte(d)
}
e.Context = &ec
e.Data = data
e.DataEncoded = data != nil
return nil
}
// JsonDecodeV02 takes in the byte representation of a version 0.2 structured json CloudEvent and returns a
// cloudevent.Event or an error if there are parsing errors.
func (e *Event) JsonDecodeV02(body []byte, raw map[string]json.RawMessage) error {
ec := EventContextV02{}
if err := json.Unmarshal(body, &ec); err != nil {
return err
}
// TODO: could use reflection to get these.
delete(raw, "specversion")
delete(raw, "type")
delete(raw, "source")
delete(raw, "id")
delete(raw, "time")
delete(raw, "schemaurl")
delete(raw, "contenttype")
var data interface{}
if d, ok := raw["data"]; ok {
data = []byte(d)
}
delete(raw, "data")
if len(raw) > 0 {
extensions := make(map[string]interface{}, len(raw))
for k, v := range raw {
k = strings.ToLower(k)
extensions[k] = v
}
ec.Extensions = extensions
}
e.Context = &ec
e.Data = data
e.DataEncoded = data != nil
return nil
}
// JsonDecodeV03 takes in the byte representation of a version 0.3 structured json CloudEvent and returns a // JsonDecodeV03 takes in the byte representation of a version 0.3 structured json CloudEvent and returns a
// cloudevent.Event or an error if there are parsing errors. // cloudevent.Event or an error if there are parsing errors.
func (e *Event) JsonDecodeV03(body []byte, raw map[string]json.RawMessage) error { func (e *Event) JsonDecodeV03(body []byte, raw map[string]json.RawMessage) error {
@ -244,7 +155,6 @@ func (e *Event) JsonDecodeV03(body []byte, raw map[string]json.RawMessage) error
return err return err
} }
// TODO: could use reflection to get these.
delete(raw, "specversion") delete(raw, "specversion")
delete(raw, "type") delete(raw, "type")
delete(raw, "source") delete(raw, "source")
@ -255,24 +165,53 @@ func (e *Event) JsonDecodeV03(body []byte, raw map[string]json.RawMessage) error
delete(raw, "datacontenttype") delete(raw, "datacontenttype")
delete(raw, "datacontentencoding") delete(raw, "datacontentencoding")
var data interface{} var data []byte
if d, ok := raw["data"]; ok { if d, ok := raw["data"]; ok {
data = []byte(d) data = d
// Decode the Base64 if we have a base64 payload
if ec.DeprecatedGetDataContentEncoding() == Base64 {
var tmp []byte
if err := json.Unmarshal(d, &tmp); err != nil {
return err
}
e.DataBase64 = true
e.DataEncoded = tmp
} else {
if ec.DataContentType != nil {
ct := *ec.DataContentType
if ct != ApplicationJSON && ct != TextJSON {
var dataStr string
err := json.Unmarshal(d, &dataStr)
if err != nil {
return err
}
data = []byte(dataStr)
}
}
e.DataEncoded = data
e.DataBase64 = false
}
} }
delete(raw, "data") delete(raw, "data")
if len(raw) > 0 { if len(raw) > 0 {
extensions := make(map[string]interface{}, len(raw)) extensions := make(map[string]interface{}, len(raw))
ec.Extensions = extensions
for k, v := range raw { for k, v := range raw {
k = strings.ToLower(k) k = strings.ToLower(k)
extensions[k] = v var tmp interface{}
if err := json.Unmarshal(v, &tmp); err != nil {
return err
}
if err := ec.SetExtension(k, tmp); err != nil {
return errors2.Wrap(err, "Cannot set extension with key "+k)
}
} }
ec.Extensions = extensions
} }
e.Context = &ec e.Context = &ec
e.Data = data
e.DataEncoded = data != nil
return nil return nil
} }
@ -294,9 +233,21 @@ func (e *Event) JsonDecodeV1(body []byte, raw map[string]json.RawMessage) error
delete(raw, "dataschema") delete(raw, "dataschema")
delete(raw, "datacontenttype") delete(raw, "datacontenttype")
var data interface{} var data []byte
if d, ok := raw["data"]; ok { if d, ok := raw["data"]; ok {
data = []byte(d) data = d
if ec.DataContentType != nil {
ct := *ec.DataContentType
if ct != ApplicationJSON && ct != TextJSON {
var dataStr string
err := json.Unmarshal(d, &dataStr)
if err != nil {
return err
}
data = []byte(dataStr)
}
}
} }
delete(raw, "data") delete(raw, "data")
@ -307,38 +258,43 @@ func (e *Event) JsonDecodeV1(body []byte, raw map[string]json.RawMessage) error
return err return err
} }
dataBase64 = tmp dataBase64 = tmp
} }
delete(raw, "data_base64") delete(raw, "data_base64")
if len(raw) > 0 {
extensions := make(map[string]interface{}, len(raw))
for k, v := range raw {
k = strings.ToLower(k)
var tmp string
if err := json.Unmarshal(v, &tmp); err != nil {
return err
}
extensions[k] = tmp
}
ec.Extensions = extensions
}
e.Context = &ec
if data != nil && dataBase64 != nil { if data != nil && dataBase64 != nil {
return errors.New("parsing error: JSON decoder found both 'data', and 'data_base64' in JSON payload") return errors.New("parsing error: JSON decoder found both 'data', and 'data_base64' in JSON payload")
} }
if data != nil { if data != nil {
e.Data = data e.DataEncoded = data
e.DataBase64 = false
} else if dataBase64 != nil { } else if dataBase64 != nil {
e.Data = dataBase64 e.DataEncoded = dataBase64
e.DataBase64 = true
} }
e.DataEncoded = data != nil
if len(raw) > 0 {
extensions := make(map[string]interface{}, len(raw))
ec.Extensions = extensions
for k, v := range raw {
k = strings.ToLower(k)
var tmp interface{}
if err := json.Unmarshal(v, &tmp); err != nil {
return err
}
if err := ec.SetExtension(k, tmp); err != nil {
return errors2.Wrap(err, "Cannot set extension with key "+k)
}
}
}
e.Context = &ec
return nil return nil
} }
func marshalEventLegacy(event interface{}) (map[string]json.RawMessage, error) { func marshalEvent(eventCtx EventContextReader, extensions map[string]interface{}) (map[string]json.RawMessage, error) {
b, err := json.Marshal(event) b, err := json.Marshal(eventCtx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -348,19 +304,12 @@ func marshalEventLegacy(event interface{}) (map[string]json.RawMessage, error) {
return nil, err return nil, err
} }
return brm, nil sv, err := json.Marshal(eventCtx.GetSpecVersion())
}
func marshalEvent(event interface{}, extensions map[string]interface{}) (map[string]json.RawMessage, error) {
b, err := json.Marshal(event)
if err != nil { if err != nil {
return nil, err return nil, err
} }
brm := map[string]json.RawMessage{} brm["specversion"] = sv
if err := json.Unmarshal(b, &brm); err != nil {
return nil, err
}
for k, v := range extensions { for k, v := range extensions {
k = strings.ToLower(k) k = strings.ToLower(k)

View File

@ -1,9 +1,9 @@
package cloudevents package event
import ( import (
"fmt" "fmt"
"github.com/cloudevents/sdk-go/pkg/cloudevents/observability" "github.com/cloudevents/sdk-go/v2/observability"
"go.opencensus.io/stats" "go.opencensus.io/stats"
"go.opencensus.io/stats/view" "go.opencensus.io/stats/view"
) )
@ -38,18 +38,6 @@ const (
reportUnmarshal reportUnmarshal
) )
// TraceName implements Observable.TraceName
func (o observed) TraceName() string {
switch o {
case reportMarshal:
return "cloudevents/event/marshaljson"
case reportUnmarshal:
return "cloudevents/event/unmarshaljson"
default:
return "cloudevents/event/unknwown"
}
}
// MethodName implements Observable.MethodName // MethodName implements Observable.MethodName
func (o observed) MethodName() string { func (o observed) MethodName() string {
switch o { switch o {
@ -78,11 +66,6 @@ type eventJSONObserved struct {
// Adheres to Observable // Adheres to Observable
var _ observability.Observable = (*eventJSONObserved)(nil) var _ observability.Observable = (*eventJSONObserved)(nil)
// TraceName implements Observable.TraceName
func (c eventJSONObserved) TraceName() string {
return fmt.Sprintf("%s/%s", c.o.TraceName(), c.v)
}
// MethodName implements Observable.MethodName // MethodName implements Observable.MethodName
func (c eventJSONObserved) MethodName() string { func (c eventJSONObserved) MethodName() string {
return fmt.Sprintf("%s/%s", c.o.MethodName(), c.v) return fmt.Sprintf("%s/%s", c.o.MethodName(), c.v)

View File

@ -1,4 +1,4 @@
package cloudevents package event
import ( import (
"time" "time"

View File

@ -1,4 +1,4 @@
package cloudevents package event
import ( import (
"fmt" "fmt"
@ -9,86 +9,105 @@ var _ EventWriter = (*Event)(nil)
// SetSpecVersion implements EventWriter.SetSpecVersion // SetSpecVersion implements EventWriter.SetSpecVersion
func (e *Event) SetSpecVersion(v string) { func (e *Event) SetSpecVersion(v string) {
if e.Context == nil {
switch v { switch v {
case CloudEventsVersionV01:
e.Context = EventContextV01{}.AsV01()
case CloudEventsVersionV02:
e.Context = EventContextV02{}.AsV02()
case CloudEventsVersionV03: case CloudEventsVersionV03:
e.Context = EventContextV03{}.AsV03() if e.Context == nil {
case CloudEventsVersionV1: e.Context = &EventContextV03{}
e.Context = EventContextV1{}.AsV1() } else {
default: e.Context = e.Context.AsV03()
panic(fmt.Errorf("a valid spec version is required: [%s, %s, %s, %s]",
CloudEventsVersionV01, CloudEventsVersionV02, CloudEventsVersionV03, CloudEventsVersionV1))
} }
case CloudEventsVersionV1:
if e.Context == nil {
e.Context = &EventContextV1{}
} else {
e.Context = e.Context.AsV1()
}
default:
e.fieldError("specversion", fmt.Errorf("a valid spec version is required: [%s, %s]",
CloudEventsVersionV03, CloudEventsVersionV1))
return return
} }
if err := e.Context.SetSpecVersion(v); err != nil { e.fieldOK("specversion")
panic(err) return
}
} }
// SetType implements EventWriter.SetType // SetType implements EventWriter.SetType
func (e *Event) SetType(t string) { func (e *Event) SetType(t string) {
if err := e.Context.SetType(t); err != nil { if err := e.Context.SetType(t); err != nil {
panic(err) e.fieldError("type", err)
} else {
e.fieldOK("type")
} }
} }
// SetSource implements EventWriter.SetSource // SetSource implements EventWriter.SetSource
func (e *Event) SetSource(s string) { func (e *Event) SetSource(s string) {
if err := e.Context.SetSource(s); err != nil { if err := e.Context.SetSource(s); err != nil {
panic(err) e.fieldError("source", err)
} else {
e.fieldOK("source")
} }
} }
// SetSubject implements EventWriter.SetSubject // SetSubject implements EventWriter.SetSubject
func (e *Event) SetSubject(s string) { func (e *Event) SetSubject(s string) {
if err := e.Context.SetSubject(s); err != nil { if err := e.Context.SetSubject(s); err != nil {
panic(err) e.fieldError("subject", err)
} else {
e.fieldOK("subject")
} }
} }
// SetID implements EventWriter.SetID // SetID implements EventWriter.SetID
func (e *Event) SetID(id string) { func (e *Event) SetID(id string) {
if err := e.Context.SetID(id); err != nil { if err := e.Context.SetID(id); err != nil {
panic(err) e.fieldError("id", err)
} else {
e.fieldOK("id")
} }
} }
// SetTime implements EventWriter.SetTime // SetTime implements EventWriter.SetTime
func (e *Event) SetTime(t time.Time) { func (e *Event) SetTime(t time.Time) {
if err := e.Context.SetTime(t); err != nil { if err := e.Context.SetTime(t); err != nil {
panic(err) e.fieldError("time", err)
} else {
e.fieldOK("time")
} }
} }
// SetDataSchema implements EventWriter.SetDataSchema // SetDataSchema implements EventWriter.SetDataSchema
func (e *Event) SetDataSchema(s string) { func (e *Event) SetDataSchema(s string) {
if err := e.Context.SetDataSchema(s); err != nil { if err := e.Context.SetDataSchema(s); err != nil {
panic(err) e.fieldError("dataschema", err)
} else {
e.fieldOK("dataschema")
} }
} }
// SetDataContentType implements EventWriter.SetDataContentType // SetDataContentType implements EventWriter.SetDataContentType
func (e *Event) SetDataContentType(ct string) { func (e *Event) SetDataContentType(ct string) {
if err := e.Context.SetDataContentType(ct); err != nil { if err := e.Context.SetDataContentType(ct); err != nil {
panic(err) e.fieldError("datacontenttype", err)
} else {
e.fieldOK("datacontenttype")
} }
} }
// DeprecatedSetDataContentEncoding implements EventWriter.DeprecatedSetDataContentEncoding // DeprecatedSetDataContentEncoding implements EventWriter.DeprecatedSetDataContentEncoding
func (e *Event) SetDataContentEncoding(enc string) { func (e *Event) SetDataContentEncoding(enc string) {
if err := e.Context.DeprecatedSetDataContentEncoding(enc); err != nil { if err := e.Context.DeprecatedSetDataContentEncoding(enc); err != nil {
panic(err) e.fieldError("datacontentencoding", err)
} else {
e.fieldOK("datacontentencoding")
} }
} }
// SetExtension implements EventWriter.SetExtension // SetExtension implements EventWriter.SetExtension
func (e *Event) SetExtension(name string, obj interface{}) { func (e *Event) SetExtension(name string, obj interface{}) {
if err := e.Context.SetExtension(name, obj); err != nil { if err := e.Context.SetExtension(name, obj); err != nil {
panic(err) e.fieldError("extension:"+name, err)
} else {
e.fieldOK("extension:" + name)
} }
} }

View File

@ -1,4 +1,4 @@
package cloudevents package event
import "time" import "time"
@ -57,8 +57,6 @@ type EventContextReader interface {
// EventContextWriter are the methods required to be a writer of context // EventContextWriter are the methods required to be a writer of context
// attributes. // attributes.
type EventContextWriter interface { type EventContextWriter interface {
// SetSpecVersion sets the spec version of the context.
SetSpecVersion(string) error
// SetType sets the type of the context. // SetType sets the type of the context.
SetType(string) error SetType(string) error
// SetSource sets the source of the context. // SetSource sets the source of the context.
@ -79,6 +77,8 @@ type EventContextWriter interface {
// SetExtension sets the given interface onto the extension attributes // SetExtension sets the given interface onto the extension attributes
// determined by the provided name. // determined by the provided name.
// //
// This function fails in V1 if the name doesn't respect the regex ^[a-zA-Z0-9]+$
//
// Package ./types documents the types that are allowed as extension values. // Package ./types documents the types that are allowed as extension values.
SetExtension(string, interface{}) error SetExtension(string, interface{}) error
} }
@ -86,16 +86,6 @@ type EventContextWriter interface {
// EventContextConverter are the methods that allow for event version // EventContextConverter are the methods that allow for event version
// conversion. // conversion.
type EventContextConverter interface { type EventContextConverter interface {
// AsV01 provides a translation from whatever the "native" encoding of the
// CloudEvent was to the equivalent in v0.1 field names, moving fields to or
// from extensions as necessary.
AsV01() *EventContextV01
// AsV02 provides a translation from whatever the "native" encoding of the
// CloudEvent was to the equivalent in v0.2 field names, moving fields to or
// from extensions as necessary.
AsV02() *EventContextV02
// AsV03 provides a translation from whatever the "native" encoding of the // AsV03 provides a translation from whatever the "native" encoding of the
// CloudEvent was to the equivalent in v0.3 field names, moving fields to or // CloudEvent was to the equivalent in v0.3 field names, moving fields to or
// from extensions as necessary. // from extensions as necessary.

View File

@ -1,12 +1,13 @@
package cloudevents package event
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"mime"
"sort" "sort"
"strings" "strings"
"github.com/cloudevents/sdk-go/pkg/cloudevents/types" "github.com/cloudevents/sdk-go/v2/types"
) )
const ( const (
@ -17,12 +18,10 @@ const (
// EventContextV03 represents the non-data attributes of a CloudEvents v0.3 // EventContextV03 represents the non-data attributes of a CloudEvents v0.3
// event. // event.
type EventContextV03 struct { type EventContextV03 struct {
// SpecVersion - The version of the CloudEvents specification used by the event.
SpecVersion string `json:"specversion"`
// Type - The type of the occurrence which has happened. // Type - The type of the occurrence which has happened.
Type string `json:"type"` Type string `json:"type"`
// Source - A URI describing the event producer. // Source - A URI describing the event producer.
Source types.URLRef `json:"source"` Source types.URIRef `json:"source"`
// Subject - The subject of the event in the context of the event producer // Subject - The subject of the event in the context of the event producer
// (identified by `source`). // (identified by `source`).
Subject *string `json:"subject,omitempty"` Subject *string `json:"subject,omitempty"`
@ -31,9 +30,8 @@ type EventContextV03 struct {
// Time - A Timestamp when the event happened. // Time - A Timestamp when the event happened.
Time *types.Timestamp `json:"time,omitempty"` Time *types.Timestamp `json:"time,omitempty"`
// DataSchema - A link to the schema that the `data` attribute adheres to. // DataSchema - A link to the schema that the `data` attribute adheres to.
SchemaURL *types.URLRef `json:"schemaurl,omitempty"` SchemaURL *types.URIRef `json:"schemaurl,omitempty"`
// GetDataMediaType - A MIME (RFC2046) string describing the media type of `data`. // GetDataMediaType - A MIME (RFC2046) string describing the media type of `data`.
// TODO: Should an empty string assume `application/json`, `application/octet-stream`, or auto-detect the content?
DataContentType *string `json:"datacontenttype,omitempty"` DataContentType *string `json:"datacontenttype,omitempty"`
// DeprecatedDataContentEncoding describes the content encoding for the `data` attribute. Valid: nil, `Base64`. // DeprecatedDataContentEncoding describes the content encoding for the `data` attribute. Valid: nil, `Base64`.
DataContentEncoding *string `json:"datacontentencoding,omitempty"` DataContentEncoding *string `json:"datacontentencoding,omitempty"`
@ -82,6 +80,10 @@ func (ec *EventContextV03) SetExtension(name string, value interface{}) error {
} }
if value == nil { if value == nil {
delete(ec.Extensions, name) delete(ec.Extensions, name)
if len(ec.Extensions) == 0 {
ec.Extensions = nil
}
return nil
} else { } else {
v, err := types.Validate(value) v, err := types.Validate(value)
if err == nil { if err == nil {
@ -89,61 +91,42 @@ func (ec *EventContextV03) SetExtension(name string, value interface{}) error {
} }
return err return err
} }
return nil
} }
// Clone implements EventContextConverter.Clone // Clone implements EventContextConverter.Clone
func (ec EventContextV03) Clone() EventContext { func (ec EventContextV03) Clone() EventContext {
return ec.AsV03() ec03 := ec.AsV03()
ec03.Source = types.Clone(ec.Source).(types.URIRef)
if ec.Time != nil {
ec03.Time = types.Clone(ec.Time).(*types.Timestamp)
}
if ec.SchemaURL != nil {
ec03.SchemaURL = types.Clone(ec.SchemaURL).(*types.URIRef)
}
ec03.Extensions = ec.cloneExtensions()
return ec03
} }
// AsV01 implements EventContextConverter.AsV01 func (ec *EventContextV03) cloneExtensions() map[string]interface{} {
func (ec EventContextV03) AsV01() *EventContextV01 { old := ec.Extensions
ecv2 := ec.AsV02() if old == nil {
return ecv2.AsV01() return nil
}
// AsV02 implements EventContextConverter.AsV02
func (ec EventContextV03) AsV02() *EventContextV02 {
ret := EventContextV02{
SpecVersion: CloudEventsVersionV02,
ID: ec.ID,
Time: ec.Time,
Type: ec.Type,
SchemaURL: ec.SchemaURL,
ContentType: ec.DataContentType,
Source: ec.Source,
Extensions: make(map[string]interface{}),
} }
// Subject was introduced in 0.3, so put it in an extension for 0.2. new := make(map[string]interface{}, len(ec.Extensions))
if ec.Subject != nil { for k, v := range old {
_ = ret.SetExtension(SubjectKey, *ec.Subject) new[k] = types.Clone(v)
} }
// DeprecatedDataContentEncoding was introduced in 0.3, so put it in an extension for 0.2. return new
if ec.DataContentEncoding != nil {
_ = ret.SetExtension(DataContentEncodingKey, *ec.DataContentEncoding)
}
if ec.Extensions != nil {
for k, v := range ec.Extensions {
ret.Extensions[k] = v
}
}
if len(ret.Extensions) == 0 {
ret.Extensions = nil
}
return &ret
} }
// AsV03 implements EventContextConverter.AsV03 // AsV03 implements EventContextConverter.AsV03
func (ec EventContextV03) AsV03() *EventContextV03 { func (ec EventContextV03) AsV03() *EventContextV03 {
ec.SpecVersion = CloudEventsVersionV03
return &ec return &ec
} }
// AsV04 implements EventContextConverter.AsV04 // AsV04 implements EventContextConverter.AsV04
func (ec EventContextV03) AsV1() *EventContextV1 { func (ec EventContextV03) AsV1() *EventContextV1 {
ret := EventContextV1{ ret := EventContextV1{
SpecVersion: CloudEventsVersionV1,
ID: ec.ID, ID: ec.ID,
Time: ec.Time, Time: ec.Time,
Type: ec.Type, Type: ec.Type,
@ -192,16 +175,6 @@ func (ec EventContextV03) Validate() error {
errors = append(errors, "type: MUST be a non-empty string") errors = append(errors, "type: MUST be a non-empty string")
} }
// specversion
// Type: String
// Constraints:
// REQUIRED
// MUST be a non-empty string
specVersion := strings.TrimSpace(ec.SpecVersion)
if specVersion == "" {
errors = append(errors, "specversion: MUST be a non-empty string")
}
// source // source
// Type: URI-reference // Type: URI-reference
// Constraints: // Constraints:
@ -264,8 +237,12 @@ func (ec EventContextV03) Validate() error {
if ec.DataContentType != nil { if ec.DataContentType != nil {
dataContentType := strings.TrimSpace(*ec.DataContentType) dataContentType := strings.TrimSpace(*ec.DataContentType)
if dataContentType == "" { if dataContentType == "" {
// TODO: need to test for RFC 2046
errors = append(errors, "datacontenttype: if present, MUST adhere to the format specified in RFC 2046") errors = append(errors, "datacontenttype: if present, MUST adhere to the format specified in RFC 2046")
} else {
_, _, err := mime.ParseMediaType(dataContentType)
if err != nil {
errors = append(errors, fmt.Sprintf("datacontenttype: failed to parse RFC 2046 media type, %s", err.Error()))
}
} }
} }
@ -278,7 +255,6 @@ func (ec EventContextV03) Validate() error {
if ec.DataContentEncoding != nil { if ec.DataContentEncoding != nil {
dataContentEncoding := strings.ToLower(strings.TrimSpace(*ec.DataContentEncoding)) dataContentEncoding := strings.ToLower(strings.TrimSpace(*ec.DataContentEncoding))
if dataContentEncoding != Base64 { if dataContentEncoding != Base64 {
// TODO: need to test for RFC 2046
errors = append(errors, "datacontentencoding: if present, MUST adhere to RFC 2045 Section 6.1") errors = append(errors, "datacontentencoding: if present, MUST adhere to RFC 2045 Section 6.1")
} }
} }
@ -295,7 +271,7 @@ func (ec EventContextV03) String() string {
b.WriteString("Context Attributes,\n") b.WriteString("Context Attributes,\n")
b.WriteString(" specversion: " + ec.SpecVersion + "\n") b.WriteString(" specversion: " + CloudEventsVersionV03 + "\n")
b.WriteString(" type: " + ec.Type + "\n") b.WriteString(" type: " + ec.Type + "\n")
b.WriteString(" source: " + ec.Source.String() + "\n") b.WriteString(" source: " + ec.Source.String() + "\n")
if ec.Subject != nil { if ec.Subject != nil {

View File

@ -1,4 +1,4 @@
package cloudevents package event
import ( import (
"fmt" "fmt"
@ -8,9 +8,6 @@ import (
// GetSpecVersion implements EventContextReader.GetSpecVersion // GetSpecVersion implements EventContextReader.GetSpecVersion
func (ec EventContextV03) GetSpecVersion() string { func (ec EventContextV03) GetSpecVersion() string {
if ec.SpecVersion != "" {
return ec.SpecVersion
}
return CloudEventsVersionV03 return CloudEventsVersionV03
} }

View File

@ -1,27 +1,17 @@
package cloudevents package event
import ( import (
"errors" "errors"
"fmt"
"net/url" "net/url"
"strings" "strings"
"time" "time"
"github.com/cloudevents/sdk-go/pkg/cloudevents/types" "github.com/cloudevents/sdk-go/v2/types"
) )
// Adhere to EventContextWriter // Adhere to EventContextWriter
var _ EventContextWriter = (*EventContextV03)(nil) var _ EventContextWriter = (*EventContextV03)(nil)
// SetSpecVersion implements EventContextWriter.SetSpecVersion
func (ec *EventContextV03) SetSpecVersion(v string) error {
if v != CloudEventsVersionV03 {
return fmt.Errorf("invalid version %q, expecting %q", v, CloudEventsVersionV03)
}
ec.SpecVersion = CloudEventsVersionV03
return nil
}
// SetDataContentType implements EventContextWriter.SetDataContentType // SetDataContentType implements EventContextWriter.SetDataContentType
func (ec *EventContextV03) SetDataContentType(ct string) error { func (ec *EventContextV03) SetDataContentType(ct string) error {
ct = strings.TrimSpace(ct) ct = strings.TrimSpace(ct)
@ -46,7 +36,7 @@ func (ec *EventContextV03) SetSource(u string) error {
if err != nil { if err != nil {
return err return err
} }
ec.Source = types.URLRef{URL: *pu} ec.Source = types.URIRef{URL: *pu}
return nil return nil
} }
@ -92,7 +82,7 @@ func (ec *EventContextV03) SetDataSchema(u string) error {
if err != nil { if err != nil {
return err return err
} }
ec.SchemaURL = &types.URLRef{URL: *pu} ec.SchemaURL = &types.URIRef{URL: *pu}
return nil return nil
} }

View File

@ -1,4 +1,4 @@
package cloudevents package event
import ( import (
"errors" "errors"
@ -7,7 +7,7 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/cloudevents/sdk-go/pkg/cloudevents/types" "github.com/cloudevents/sdk-go/v2/types"
) )
// WIP: AS OF SEP 20, 2019 // WIP: AS OF SEP 20, 2019
@ -26,9 +26,6 @@ type EventContextV1 struct {
// Source - A URI describing the event producer. // Source - A URI describing the event producer.
// +required // +required
Source types.URIRef `json:"source"` Source types.URIRef `json:"source"`
// SpecVersion - The version of the CloudEvents specification used by the event.
// +required
SpecVersion string `json:"specversion"`
// Type - The type of the occurrence which has happened. // Type - The type of the occurrence which has happened.
// +required // +required
Type string `json:"type"` Type string `json:"type"`
@ -73,8 +70,9 @@ func (ec EventContextV1) ExtensionAs(name string, obj interface{}) error {
} }
// SetExtension adds the extension 'name' with value 'value' to the CloudEvents context. // SetExtension adds the extension 'name' with value 'value' to the CloudEvents context.
// This function fails if the name doesn't respect the regex ^[a-zA-Z0-9]+$
func (ec *EventContextV1) SetExtension(name string, value interface{}) error { func (ec *EventContextV1) SetExtension(name string, value interface{}) error {
if !IsAlphaNumericLowercaseLetters(name) { if !IsAlphaNumeric(name) {
return errors.New("bad key, CloudEvents attribute names MUST consist of lower-case letters ('a' to 'z') or digits ('0' to '9') from the ASCII character set") return errors.New("bad key, CloudEvents attribute names MUST consist of lower-case letters ('a' to 'z') or digits ('0' to '9') from the ASCII character set")
} }
@ -84,6 +82,9 @@ func (ec *EventContextV1) SetExtension(name string, value interface{}) error {
} }
if value == nil { if value == nil {
delete(ec.Extensions, name) delete(ec.Extensions, name)
if len(ec.Extensions) == 0 {
ec.Extensions = nil
}
return nil return nil
} else { } else {
v, err := types.Validate(value) // Ensure it's a legal CE attribute value v, err := types.Validate(value) // Ensure it's a legal CE attribute value
@ -96,39 +97,46 @@ func (ec *EventContextV1) SetExtension(name string, value interface{}) error {
// Clone implements EventContextConverter.Clone // Clone implements EventContextConverter.Clone
func (ec EventContextV1) Clone() EventContext { func (ec EventContextV1) Clone() EventContext {
return ec.AsV1() ec1 := ec.AsV1()
ec1.Source = types.Clone(ec.Source).(types.URIRef)
if ec.Time != nil {
ec1.Time = types.Clone(ec.Time).(*types.Timestamp)
}
if ec.DataSchema != nil {
ec1.DataSchema = types.Clone(ec.DataSchema).(*types.URI)
}
ec1.Extensions = ec.cloneExtensions()
return ec1
} }
// AsV01 implements EventContextConverter.AsV01 func (ec *EventContextV1) cloneExtensions() map[string]interface{} {
func (ec EventContextV1) AsV01() *EventContextV01 { old := ec.Extensions
ecv2 := ec.AsV02() if old == nil {
return ecv2.AsV01() return nil
} }
new := make(map[string]interface{}, len(ec.Extensions))
// AsV02 implements EventContextConverter.AsV02 for k, v := range old {
func (ec EventContextV1) AsV02() *EventContextV02 { new[k] = types.Clone(v)
ecv3 := ec.AsV03() }
return ecv3.AsV02() return new
} }
// AsV03 implements EventContextConverter.AsV03 // AsV03 implements EventContextConverter.AsV03
func (ec EventContextV1) AsV03() *EventContextV03 { func (ec EventContextV1) AsV03() *EventContextV03 {
ret := EventContextV03{ ret := EventContextV03{
SpecVersion: CloudEventsVersionV03,
ID: ec.ID, ID: ec.ID,
Time: ec.Time, Time: ec.Time,
Type: ec.Type, Type: ec.Type,
DataContentType: ec.DataContentType, DataContentType: ec.DataContentType,
Source: types.URLRef{URL: ec.Source.URL}, Source: types.URIRef{URL: ec.Source.URL},
Subject: ec.Subject, Subject: ec.Subject,
Extensions: make(map[string]interface{}), Extensions: make(map[string]interface{}),
} }
if ec.DataSchema != nil { if ec.DataSchema != nil {
ret.SchemaURL = &types.URLRef{URL: ec.DataSchema.URL} ret.SchemaURL = &types.URIRef{URL: ec.DataSchema.URL}
} }
// TODO: DeprecatedDataContentEncoding needs to be moved to extensions.
if ec.Extensions != nil { if ec.Extensions != nil {
for k, v := range ec.Extensions { for k, v := range ec.Extensions {
k = strings.ToLower(k) k = strings.ToLower(k)
@ -151,7 +159,6 @@ func (ec EventContextV1) AsV03() *EventContextV03 {
// AsV04 implements EventContextConverter.AsV04 // AsV04 implements EventContextConverter.AsV04
func (ec EventContextV1) AsV1() *EventContextV1 { func (ec EventContextV1) AsV1() *EventContextV1 {
ec.SpecVersion = CloudEventsVersionV1
return &ec return &ec
} }
@ -183,16 +190,6 @@ func (ec EventContextV1) Validate() error {
errors = append(errors, "source: REQUIRED") errors = append(errors, "source: REQUIRED")
} }
// specversion
// Type: String
// Constraints:
// REQUIRED
// MUST be a non-empty string
specVersion := strings.TrimSpace(ec.SpecVersion)
if specVersion == "" {
errors = append(errors, "specversion: MUST be a non-empty string")
}
// type // type
// Type: String // Type: String
// Constraints: // Constraints:
@ -218,7 +215,7 @@ func (ec EventContextV1) Validate() error {
} else { } else {
_, _, err := mime.ParseMediaType(dataContentType) _, _, err := mime.ParseMediaType(dataContentType)
if err != nil { if err != nil {
errors = append(errors, fmt.Sprintf("datacontenttype: failed to parse media type, %s", err.Error())) errors = append(errors, fmt.Sprintf("datacontenttype: failed to parse RFC 2046 media type, %s", err.Error()))
} }
} }
} }
@ -267,7 +264,7 @@ func (ec EventContextV1) String() string {
b.WriteString("Context Attributes,\n") b.WriteString("Context Attributes,\n")
b.WriteString(" specversion: " + ec.SpecVersion + "\n") b.WriteString(" specversion: " + CloudEventsVersionV1 + "\n")
b.WriteString(" type: " + ec.Type + "\n") b.WriteString(" type: " + ec.Type + "\n")
b.WriteString(" source: " + ec.Source.String() + "\n") b.WriteString(" source: " + ec.Source.String() + "\n")
if ec.Subject != nil { if ec.Subject != nil {

Some files were not shown because too many files have changed in this diff Show More