diff --git a/.circleci/config.yml b/.circleci/config.yml index 25122d772..5039115d0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -55,7 +55,7 @@ jobs: - run: go get honnef.co/go/tools/cmd/staticcheck - run: name: "Run staticcheck" - command: pushd v2; staticcheck ./...; popd + command: pushd v2; staticcheck -tags conformance ./...; popd - run: name: Run unit tests command: | diff --git a/hack/conformance-test.sh b/hack/conformance-test.sh index cdb024f1d..445dd4c4f 100755 --- a/hack/conformance-test.sh +++ b/hack/conformance-test.sh @@ -7,7 +7,7 @@ set -o pipefail # v2 only pushd ./v2/test/conformance/ -go test -v -timeout 15s +go test --tags=conformance -v -timeout 15s # Remove test only deps. go mod tidy diff --git a/hack/unit-test.sh b/hack/unit-test.sh index f8a3b8a95..f952803dc 100755 --- a/hack/unit-test.sh +++ b/hack/unit-test.sh @@ -4,21 +4,6 @@ set -o errexit set -o nounset set -o pipefail -# v1 -pushd ./v1 -touch ./coverage.tmp -echo 'mode: atomic' > ./coverage.txt -COVERPKG=$(go list ./... | grep -v /vendor | tr "\n" ",") -for gomodule in $(go list ./... | grep -v /cmd | grep -v /vendor) -do - go test -v -timeout 15s -covermode=atomic -coverprofile=coverage.tmp -coverpkg "$COVERPKG" "$gomodule" 2>&1 | sed 's/ of statements in.*//; /warning: no packages being tested depend on matches for pattern /d' - tail -n +2 coverage.tmp >> ./coverage.txt -done -rm coverage.tmp -# Remove test only deps. -go mod tidy -popd - # v2 pushd ./v2 touch ./coverage.tmp diff --git a/v1/README.md b/v1/README.md deleted file mode 100644 index 93d28c03d..000000000 --- a/v1/README.md +++ /dev/null @@ -1,76 +0,0 @@ -# NOTE: The v1 directory will be removed for v2.0.0. - -We will make a final migration release that will match the v2.0.0 release with -the addition of this legacy directory for migration support. - -# Migration Guide - -To enable an easier migration, this directory holds a copy of the code in the -branch `release-1.y.z`. - -Switch your imports from `github.com/cloudevents/sdk-go` to -`github.com/cloudevents/sdk-go/legacy` and your code should compile again, -letting you get on with the task of migrating to v2. - -## Background - -In the migration from v1 to v2 of the SDK, there are a lot of API breaking -changes. It is shorter to define what is not a breaking change: - -- The `Event` object marshaling results in the same json. -- cloudevents.NewDefault should get an http server working out of the box. -- Most of alias.go file remains with some exceptions. -- Most of the original demos remain in the cmd dir to see how the new - integrations should be. - -Large breaking changes have to do with a strategy change inside the SDK to shift -the control to the integrator, allowing more direct access to the knobs required -to make choices over plumbing those knobs through the SDK down to the original -transports that implement the features integrators are really trying to control. - -If you implemented a custom transport, the migration to how protocol bindings -work is covered in the document [TBD](TODO). - -## Architectural Changes - -New Architectural Layout: - -``` -Client <-- Operates on event.Event - | - v -Protocol <-- Operates on binding.Message - | - v -3rd Party <-- Operates out of our control -``` - -Some Architectural changes that are worth noting: - -- Client still exists but it has a new API. - - client.Request allows for responses from outbound events. - - client.StartReceiver has a mode that will test for underlying support if the - receiver function is allowed to produce responses to inbound events. -- Client interface is event.Event focused. -- Protocol layer operates on `binding.Message` - - This is a change from v1, `transport.Transport` mixed up `event.Event` - objects into the interface. With the thinking that codecs were specific to a - transport. But as we implemented bindings, it became clear that there are - many cases where the cost to convert a 3rd Party message into a - `event.Event` is too high and it is better to stay in the intermediate state - of a `binding.Message` (similar to a `transport.Message` but - `transport.Message` was never exposed in the v1 architecture). -- Setting a transport to emit a specific version of cloudevents is an - anti-pattern. If a version is required, the burden should be on the integrator - to implement what they need. The edge cases the SDK had to handle made that - code unrulely. It is simpler if the SDK does simple things. So outbound event - encoding is based on the `event.Event` that is passed in. - -## Moves and renames - -Note these are based on internal packages unless noted as from alias. - -- `cloudevents.Event` --> `event.Event` -- `transport.Codec` --> Deleted, the binding concept replaced it. -- `transport.Transport` --> Deleted, the - protocol.Sender/Receiver/Requester/Responder interfaces replaced it. diff --git a/v1/alias.go b/v1/alias.go deleted file mode 100644 index 885c37797..000000000 --- a/v1/alias.go +++ /dev/null @@ -1,241 +0,0 @@ -package v1 - -// 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/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/client" - "github.com/cloudevents/sdk-go/v1/cloudevents/context" - "github.com/cloudevents/sdk-go/v1/cloudevents/observability" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport/http" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -// Client - -// Deprecated: legacy. -type ClientOption client.Option - -// Deprecated: legacy. -type Client = client.Client - -// Deprecated: legacy. -type ConvertFn = client.ConvertFn - -// Event - -// Deprecated: legacy. -type Event = cloudevents.Event - -// Deprecated: legacy. -type EventResponse = cloudevents.EventResponse - -// Context - -// Deprecated: legacy. -type EventContext = cloudevents.EventContext - -// Deprecated: legacy. -type EventContextV1 = cloudevents.EventContextV1 - -// Deprecated: legacy. -type EventContextV01 = cloudevents.EventContextV01 - -// Deprecated: legacy. -type EventContextV02 = cloudevents.EventContextV02 - -// Deprecated: legacy. -type EventContextV03 = cloudevents.EventContextV03 - -// Custom Types - -// Deprecated: legacy. -type Timestamp = types.Timestamp - -// Deprecated: legacy. -type URLRef = types.URLRef - -// HTTP Transport - -// Deprecated: legacy. -type HTTPOption http.Option - -// Deprecated: legacy. -type HTTPTransport = http.Transport - -// Deprecated: legacy. -type HTTPTransportContext = http.TransportContext - -// Deprecated: legacy. -type HTTPTransportResponseContext = http.TransportResponseContext - -// Deprecated: legacy. -type HTTPEncoding = http.Encoding - -const ( - // Encoding - // Deprecated: legacy. - ApplicationXML = cloudevents.ApplicationXML - // Deprecated: legacy. - ApplicationJSON = cloudevents.ApplicationJSON - // Deprecated: legacy. - ApplicationCloudEventsJSON = cloudevents.ApplicationCloudEventsJSON - // Deprecated: legacy. - ApplicationCloudEventsBatchJSON = cloudevents.ApplicationCloudEventsBatchJSON - // Deprecated: legacy. - Base64 = cloudevents.Base64 - - // Event Versions - - // Deprecated: legacy. - VersionV1 = cloudevents.CloudEventsVersionV1 - // Deprecated: legacy. - VersionV01 = cloudevents.CloudEventsVersionV01 - // Deprecated: legacy. - VersionV02 = cloudevents.CloudEventsVersionV02 - // Deprecated: legacy. - VersionV03 = cloudevents.CloudEventsVersionV03 - - // HTTP Transport Encodings - - // Deprecated: legacy. - HTTPBinaryV1 = http.BinaryV1 - // Deprecated: legacy. - HTTPStructuredV1 = http.StructuredV1 - // Deprecated: legacy. - HTTPBatchedV1 = http.BatchedV1 - // Deprecated: legacy. - HTTPBinaryV01 = http.BinaryV01 - // Deprecated: legacy. - HTTPStructuredV01 = http.StructuredV01 - // Deprecated: legacy. - HTTPBinaryV02 = http.BinaryV02 - // Deprecated: legacy. - HTTPStructuredV02 = http.StructuredV02 - // Deprecated: legacy. - HTTPBinaryV03 = http.BinaryV03 - // Deprecated: legacy. - HTTPStructuredV03 = http.StructuredV03 - // Deprecated: legacy. - HTTPBatchedV03 = http.BatchedV03 - - // Context HTTP Transport Encodings - - Binary = http.Binary - Structured = http.Structured -) - -var ( - // ContentType Helpers - - // Deprecated: legacy. - StringOfApplicationJSON = cloudevents.StringOfApplicationJSON - // Deprecated: legacy. - StringOfApplicationXML = cloudevents.StringOfApplicationXML - // Deprecated: legacy. - StringOfApplicationCloudEventsJSON = cloudevents.StringOfApplicationCloudEventsJSON - // Deprecated: legacy. - StringOfApplicationCloudEventsBatchJSON = cloudevents.StringOfApplicationCloudEventsBatchJSON - // Deprecated: legacy. - StringOfBase64 = cloudevents.StringOfBase64 - - // Client Creation - - // Deprecated: legacy. - NewClient = client.New - // Deprecated: legacy. - NewDefaultClient = client.NewDefault - - // Client Options - - // Deprecated: legacy. - WithEventDefaulter = client.WithEventDefaulter - // Deprecated: legacy. - WithUUIDs = client.WithUUIDs - // Deprecated: legacy. - WithTimeNow = client.WithTimeNow - // Deprecated: legacy. - WithConverterFn = client.WithConverterFn - // Deprecated: legacy. - WithDataContentType = client.WithDataContentType - // Deprecated: legacy. - WithoutTracePropagation = client.WithoutTracePropagation - - // Event Creation - - // Deprecated: legacy. - NewEvent = cloudevents.New - - // Tracing - - // Deprecated: legacy. - EnableTracing = observability.EnableTracing - - // Context - - // Deprecated: legacy. - ContextWithTarget = context.WithTarget - // Deprecated: legacy. - TargetFromContext = context.TargetFrom - // Deprecated: legacy. - ContextWithEncoding = context.WithEncoding - // Deprecated: legacy. - EncodingFromContext = context.EncodingFrom - - // Custom Types - - // Deprecated: legacy. - ParseTimestamp = types.ParseTimestamp - // Deprecated: legacy. - ParseURLRef = types.ParseURLRef - // Deprecated: legacy. - ParseURIRef = types.ParseURIRef - // Deprecated: legacy. - ParseURI = types.ParseURI - - // HTTP Transport - - // Deprecated: legacy. - NewHTTPTransport = http.New - - // HTTP Transport Options - - // Deprecated: legacy. - WithTarget = http.WithTarget - // Deprecated: legacy. - WithMethod = http.WithMethod - // Deprecated: legacy. - WitHHeader = http.WithHeader - // Deprecated: legacy. - WithShutdownTimeout = http.WithShutdownTimeout - // Deprecated: legacy. - WithEncoding = http.WithEncoding - // Deprecated: legacy. - WithContextBasedEncoding = http.WithContextBasedEncoding - // Deprecated: legacy. - WithBinaryEncoding = http.WithBinaryEncoding - // Deprecated: legacy. - WithStructuredEncoding = http.WithStructuredEncoding - // Deprecated: legacy. - WithPort = http.WithPort - // Deprecated: legacy. - WithPath = http.WithPath - // Deprecated: legacy. - WithMiddleware = http.WithMiddleware - // Deprecated: legacy. - WithLongPollTarget = http.WithLongPollTarget - // Deprecated: legacy. - WithListener = http.WithListener - // Deprecated: legacy. - WithHTTPTransport = http.WithHTTPTransport - - // HTTP Context - - // Deprecated: legacy. - HTTPTransportContextFrom = http.TransportContextFrom - // Deprecated: legacy. - ContextWithHeader = http.ContextWithHeader - // Deprecated: legacy. - SetContextHeaders = http.SetContextHeaders -) diff --git a/v1/binding/buffering/acks_before_finish_message.go b/v1/binding/buffering/acks_before_finish_message.go deleted file mode 100644 index 47983e2f9..000000000 --- a/v1/binding/buffering/acks_before_finish_message.go +++ /dev/null @@ -1,33 +0,0 @@ -package buffering - -import ( - "sync/atomic" - - "github.com/cloudevents/sdk-go/v1/binding" -) - -type acksMessage struct { - binding.Message - requiredAcks int32 -} - -func (m *acksMessage) GetWrappedMessage() binding.Message { - return m.Message -} - -func (m *acksMessage) Finish(err error) error { - remainingAcks := atomic.AddInt32(&m.requiredAcks, -1) - if remainingAcks == 0 { - return m.Message.Finish(err) - } - return nil -} - -var _ binding.MessageWrapper = (*acksMessage)(nil) - -// WithAcksBeforeFinish returns a wrapper for m that calls m.Finish() -// only after the specified number of acks are received. -// Use it when you need to route a Message to more Sender instances -func WithAcksBeforeFinish(m binding.Message, requiredAcks int) binding.Message { - return &acksMessage{Message: m, requiredAcks: int32(requiredAcks)} -} diff --git a/v1/binding/buffering/acks_before_finish_message_test.go b/v1/binding/buffering/acks_before_finish_message_test.go deleted file mode 100644 index 31a9baeed..000000000 --- a/v1/binding/buffering/acks_before_finish_message_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package buffering - -import ( - "context" - "net/url" - "sync" - "testing" - - "github.com/stretchr/testify/assert" - - cloudevents "github.com/cloudevents/sdk-go/v1" - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -func TestWithAcksBeforeFinish(t *testing.T) { - var testEvent = cloudevents.Event{ - Data: []byte(`"data"`), - DataEncoded: true, - Context: cloudevents.EventContextV1{ - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: types.URIRef{URL: url.URL{Path: "source"}}, - ID: "id", - Type: "type"}.AsV1(), - } - - finishCalled := false - finishMessage := binding.WithFinish(binding.EventMessage(testEvent), func(err error) { - finishCalled = true - }) - - wg := sync.WaitGroup{} - - messageToTest := WithAcksBeforeFinish(finishMessage, 1000) - for i := 0; i < 1000; i++ { - wg.Add(1) - go func(m binding.Message) { - ch := make(chan binding.Message, 1) - assert.NoError(t, binding.ChanSender(ch).Send(context.Background(), m)) - <-ch - wg.Done() - }(messageToTest) - } - - wg.Wait() - assert.True(t, finishCalled) -} - -func TestCopyAndWithAcksBeforeFinish(t *testing.T) { - var testEvent = cloudevents.Event{ - Data: []byte(`"data"`), - DataEncoded: true, - Context: cloudevents.EventContextV1{ - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: types.URIRef{URL: url.URL{Path: "source"}}, - ID: "id", - Type: "type"}.AsV1(), - } - - finishCalled := false - finishMessage := binding.WithFinish(binding.EventMessage(testEvent), func(err error) { - finishCalled = true - }) - - copiedMessage, err := BufferMessage(context.Background(), finishMessage) - assert.NoError(t, err) - - wg := sync.WaitGroup{} - - messageToTest := WithAcksBeforeFinish(copiedMessage, 1000) - for i := 0; i < 1000; i++ { - wg.Add(1) - go func(m binding.Message) { - ch := make(chan binding.Message, 1) - assert.NoError(t, binding.ChanSender(ch).Send(context.Background(), m)) - <-ch - wg.Done() - }(messageToTest) - } - - wg.Wait() - assert.True(t, finishCalled) -} diff --git a/v1/binding/buffering/binary_buffer_message.go b/v1/binding/buffering/binary_buffer_message.go deleted file mode 100644 index 12e95ecaf..000000000 --- a/v1/binding/buffering/binary_buffer_message.go +++ /dev/null @@ -1,106 +0,0 @@ -package buffering - -import ( - "bytes" - "context" - "io" - - "github.com/valyala/bytebufferpool" - - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/spec" -) - -var binaryMessagePool bytebufferpool.Pool - -// binaryBufferedMessage implements a binary-mode message as a simple struct. -// This message implementation is used by CopyMessage and BufferMessage -type binaryBufferedMessage struct { - metadata map[spec.Attribute]interface{} - extensions map[string]interface{} - body *bytebufferpool.ByteBuffer -} - -func (m *binaryBufferedMessage) Start(ctx context.Context) error { - m.metadata = make(map[spec.Attribute]interface{}, 4) - m.extensions = make(map[string]interface{}) - return nil -} - -func (m *binaryBufferedMessage) End() error { - return nil -} - -func (m *binaryBufferedMessage) GetParent() binding.Message { - return nil -} - -func (m *binaryBufferedMessage) Encoding() binding.Encoding { - return binding.EncodingBinary -} - -func (m *binaryBufferedMessage) Structured(context.Context, binding.StructuredEncoder) error { - return binding.ErrNotStructured -} - -func (m *binaryBufferedMessage) Binary(ctx context.Context, b binding.BinaryEncoder) (err error) { - err = b.Start(ctx) - if err != nil { - return - } - for k, v := range m.metadata { - err = b.SetAttribute(k, v) - if err != nil { - return - } - } - for k, v := range m.extensions { - err = b.SetExtension(k, v) - if err != nil { - return - } - } - if m.body != nil { - err = b.SetData(bytes.NewReader(m.body.Bytes())) - if err != nil { - return - } - } - return b.End() -} - -func (b *binaryBufferedMessage) Finish(error) error { - if b.body != nil { - binaryMessagePool.Put(b.body) - } - return nil -} - -// Binary Encoder -func (b *binaryBufferedMessage) SetData(data io.Reader) error { - buf := binaryMessagePool.Get() - w, err := io.Copy(buf, data) - if err != nil { - return err - } - if w == 0 { - binaryMessagePool.Put(buf) - return nil - } - b.body = buf - return nil -} - -func (b *binaryBufferedMessage) SetAttribute(attribute spec.Attribute, value interface{}) error { - // If spec version we need to change to right context struct - b.metadata[attribute] = value - return nil -} - -func (b *binaryBufferedMessage) SetExtension(name string, value interface{}) error { - b.extensions[name] = value - return nil -} - -var _ binding.Message = (*binaryBufferedMessage)(nil) // Test it conforms to the interface -var _ binding.BinaryEncoder = (*binaryBufferedMessage)(nil) diff --git a/v1/binding/buffering/copy_message.go b/v1/binding/buffering/copy_message.go deleted file mode 100644 index d683267af..000000000 --- a/v1/binding/buffering/copy_message.go +++ /dev/null @@ -1,51 +0,0 @@ -package buffering - -import ( - "context" - - "github.com/cloudevents/sdk-go/v1/binding" -) - -// BufferMessage does the same than CopyMessage and it also bounds the original Message -// lifecycle to the newly created message: calling Finish() on the returned message calls m.Finish() -func BufferMessage(ctx context.Context, m binding.Message, transformers ...binding.TransformerFactory) (binding.Message, error) { - result, err := CopyMessage(ctx, m, transformers...) - if err != nil { - return nil, err - } - return binding.WithFinish(result, func(err error) { _ = m.Finish(err) }), nil -} - -// CopyMessage reads m once and creates an in-memory copy depending on the encoding of m. -// The returned copy is not dependent on any transport and can be read many times. -// When the copy can be forgot, the copied message must be finished with Finish() message to release the memory -func CopyMessage(ctx context.Context, m binding.Message, transformers ...binding.TransformerFactory) (binding.Message, error) { - originalMessageEncoding := m.Encoding() - - if originalMessageEncoding == binding.EncodingUnknown { - return nil, binding.ErrUnknownEncoding - } - if originalMessageEncoding == binding.EncodingEvent { - e, _, err := binding.ToEvent(ctx, m, transformers...) - if err != nil { - return nil, err - } - return binding.EventMessage(e), nil - } - - sm := structBufferedMessage{} - bm := binaryBufferedMessage{} - - encoding, err := binding.RunDirectEncoding(ctx, m, &sm, &bm, transformers) - if encoding == binding.EncodingStructured { - return &sm, err - } else if encoding == binding.EncodingBinary { - return &bm, err - } else { - e, _, err := binding.ToEvent(ctx, m, transformers...) - if err != nil { - return nil, err - } - return binding.EventMessage(e), nil - } -} diff --git a/v1/binding/buffering/copy_message_benchmark_test.go b/v1/binding/buffering/copy_message_benchmark_test.go deleted file mode 100644 index e9c4bf9e5..000000000 --- a/v1/binding/buffering/copy_message_benchmark_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package buffering - -import ( - "context" - "testing" - - "github.com/cloudevents/sdk-go/v1/binding/test" -) - -var err error - -func BenchmarkBufferMessageFromStructured(b *testing.B) { - e := test.FullEvent() - input := test.NewMockStructuredMessage(e) - ctx := context.Background() - for i := 0; i < b.N; i++ { - outputMessage, _ := BufferMessage(ctx, input) - err = outputMessage.Finish(nil) - } -} - -func BenchmarkBufferMessageFromBinary(b *testing.B) { - e := test.FullEvent() - input := test.NewMockBinaryMessage(e) - ctx := context.Background() - for i := 0; i < b.N; i++ { - outputMessage, _ := BufferMessage(ctx, input) - err = outputMessage.Finish(nil) - } -} diff --git a/v1/binding/buffering/copy_message_test.go b/v1/binding/buffering/copy_message_test.go deleted file mode 100644 index 58612d5e6..000000000 --- a/v1/binding/buffering/copy_message_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package buffering - -import ( - "context" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/test" - "github.com/cloudevents/sdk-go/v1/cloudevents" -) - -type copyMessageTestCase struct { - name string - encoding binding.Encoding - message binding.Message - want cloudevents.Event -} - -func TestCopyMessage(t *testing.T) { - tests := []copyMessageTestCase{} - - for _, v := range test.Events() { - tests = append(tests, []copyMessageTestCase{ - { - name: "From structured with payload/" + test.NameOf(v), - encoding: binding.EncodingStructured, - message: test.NewMockStructuredMessage(v), - want: v, - }, - { - name: "From structured without payload/" + test.NameOf(v), - encoding: binding.EncodingStructured, - message: test.NewMockStructuredMessage(v), - want: v, - }, - { - name: "From binary with payload/" + test.NameOf(v), - encoding: binding.EncodingBinary, - message: test.NewMockBinaryMessage(v), - want: v, - }, - { - name: "From binary without payload/" + test.NameOf(v), - encoding: binding.EncodingBinary, - message: test.NewMockBinaryMessage(v), - want: v, - }, - { - name: "From event with payload/" + test.NameOf(v), - encoding: binding.EncodingEvent, - message: binding.EventMessage(v), - want: v, - }, - { - name: "From event without payload/" + test.NameOf(v), - encoding: binding.EncodingEvent, - message: binding.EventMessage(v), - want: v, - }, - }...) - } - for _, tt := range tests { - tt := tt // Don't use range variable in Run() scope - t.Run(fmt.Sprintf("CopyMessage: %s", tt.name), func(t *testing.T) { - finished := false - message := binding.WithFinish(tt.message, func(err error) { - finished = true - }) - cpy, err := CopyMessage(context.Background(), message) - require.NoError(t, err) - // The copy can be read any number of times - for i := 0; i < 3; i++ { - got, encoding, err := binding.ToEvent(context.Background(), cpy) - assert.NoError(t, err) - require.Equal(t, tt.encoding, encoding) - test.AssertEventEquals(t, test.ExToStr(t, tt.want), test.ExToStr(t, got)) - } - require.NoError(t, cpy.Finish(nil)) - require.Equal(t, false, finished) - }) - t.Run(fmt.Sprintf("BufferMessage: %s", tt.name), func(t *testing.T) { - finished := false - message := binding.WithFinish(tt.message, func(err error) { - finished = true - }) - cpy, err := BufferMessage(context.Background(), message) - require.NoError(t, err) - // The copy can be read any number of times - for i := 0; i < 3; i++ { - got, encoding, err := binding.ToEvent(context.Background(), cpy) - assert.NoError(t, err) - require.Equal(t, tt.encoding, encoding) - test.AssertEventEquals(t, test.ExToStr(t, tt.want), test.ExToStr(t, got)) - } - require.NoError(t, cpy.Finish(nil)) - require.Equal(t, true, finished) - }) - } -} diff --git a/v1/binding/buffering/struct_buffer_message.go b/v1/binding/buffering/struct_buffer_message.go deleted file mode 100644 index 8058f9acf..000000000 --- a/v1/binding/buffering/struct_buffer_message.go +++ /dev/null @@ -1,57 +0,0 @@ -package buffering - -import ( - "bytes" - "context" - "io" - - "github.com/valyala/bytebufferpool" - - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/format" -) - -var structMessagePool bytebufferpool.Pool - -// structBufferedMessage implements a structured-mode message as a simple struct. -// This message implementation is used by CopyMessage and BufferMessage -type structBufferedMessage struct { - Format format.Format - Bytes *bytebufferpool.ByteBuffer -} - -func (m *structBufferedMessage) GetParent() binding.Message { - return nil -} - -func (m *structBufferedMessage) Encoding() binding.Encoding { - return binding.EncodingStructured -} - -// Structured copies structured data to a StructuredEncoder -func (m *structBufferedMessage) Structured(ctx context.Context, enc binding.StructuredEncoder) error { - return enc.SetStructuredEvent(ctx, m.Format, bytes.NewReader(m.Bytes.B)) -} - -// Binary returns ErrNotBinary -func (m structBufferedMessage) Binary(context.Context, binding.BinaryEncoder) error { - return binding.ErrNotBinary -} - -func (m *structBufferedMessage) Finish(error) error { - structMessagePool.Put(m.Bytes) - return nil -} - -func (m *structBufferedMessage) SetStructuredEvent(ctx context.Context, format format.Format, event io.Reader) error { - m.Bytes = structMessagePool.Get() - _, err := io.Copy(m.Bytes, event) - if err != nil { - return err - } - m.Format = format - return nil -} - -var _ binding.Message = (*structBufferedMessage)(nil) // Test it conforms to the interface -var _ binding.StructuredEncoder = (*structBufferedMessage)(nil) // Test it conforms to the interface diff --git a/v1/binding/chan.go b/v1/binding/chan.go deleted file mode 100644 index fdda2d69a..000000000 --- a/v1/binding/chan.go +++ /dev/null @@ -1,52 +0,0 @@ -package binding - -import ( - "context" - "errors" - "io" -) - -// ChanSender implements Sender by sending on a channel. -type ChanSender chan<- Message - -// ChanReceiver implements Receiver by receiving from a channel. -type ChanReceiver <-chan Message - -func (s ChanSender) Send(ctx context.Context, m Message) (err error) { - defer func() { - err2 := m.Finish(err) - if err == nil { - err = err2 - } - }() - select { - case <-ctx.Done(): - return ctx.Err() - case s <- m: - return nil - } -} - -func (s ChanSender) Close(ctx context.Context) (err error) { - defer func() { - if recover() != nil { - err = errors.New("send on closed channel") - } - }() - close(s) - return nil -} - -func (r ChanReceiver) Receive(ctx context.Context) (Message, error) { - select { - case <-ctx.Done(): - return nil, ctx.Err() - case m, ok := <-r: - if !ok { - return nil, io.EOF - } - return m, nil - } -} - -func (r ChanReceiver) Close(ctx context.Context) error { return nil } diff --git a/v1/binding/doc.go b/v1/binding/doc.go deleted file mode 100644 index 6038a3ecf..000000000 --- a/v1/binding/doc.go +++ /dev/null @@ -1,43 +0,0 @@ -/* - -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 implements at least Message, Sender and Receiver, and usually -Encoder. - -Receiver: receives protocol messages and wraps them to implement the Message interface. - -Message: interface that defines the visitors for an encoded event in structured mode, -binary mode or event mode. A method is provided to read the Encoding of the message - -Sender: converts arbitrary Message implementations to a protocol-specific form -and sends them. A protocol Sender should preserve the spec-version and -structured/binary mode of sent messages as far as possible. This package -provides generic Sender wrappers to pre-process messages into a specific -spec-version or structured/binary mode when the user requires that. - -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. - - -Intermediaries - -Intermediaries can forward Messages from a Receiver to a Sender without -knowledge of the underlying protocols. The Message interface allows structured -messages to be forwarded without decoding and re-encoding. It also allows any -Message to be fully decoded and examined as needed. - -*/ -package binding diff --git a/v1/binding/event_message.go b/v1/binding/event_message.go deleted file mode 100644 index e4118ec29..000000000 --- a/v1/binding/event_message.go +++ /dev/null @@ -1,91 +0,0 @@ -package binding - -import ( - "bytes" - "context" - - cloudevents "github.com/cloudevents/sdk-go/v1" - "github.com/cloudevents/sdk-go/v1/binding/format" - "github.com/cloudevents/sdk-go/v1/binding/spec" - ce "github.com/cloudevents/sdk-go/v1/cloudevents" -) - -// EventMessage type-converts a cloudevents.Event object to implement Message. -// This allows local cloudevents.Event objects to be sent directly via Sender.Send() -// s.Send(ctx, binding.EventMessage(e)) -type EventMessage ce.Event - -func (m EventMessage) GetParent() Message { - return nil -} - -func (m EventMessage) Encoding() Encoding { - return EncodingEvent -} - -func (m EventMessage) Structured(ctx context.Context, builder StructuredEncoder) error { - // TODO here only json is supported, should we support other message encodings? - b, err := format.JSON.Marshal(ce.Event(m)) - if err != nil { - return err - } - return builder.SetStructuredEvent(ctx, format.JSON, bytes.NewReader(b)) -} - -func (m EventMessage) Binary(ctx context.Context, b BinaryEncoder) (err error) { - err = b.Start(ctx) - if err != nil { - return err - } - err = EventContextToBinaryEncoder(m.Context, b) - if err != nil { - return err - } - // Pass the body - body, err := (*ce.Event)(&m).DataBytes() - if err != nil { - return err - } - if len(body) > 0 { - err = b.SetData(bytes.NewReader(body)) - if err != nil { - return err - } - } - return b.End() -} - -func (EventMessage) Finish(error) error { return nil } - -func (m *EventMessage) SetEvent(e ce.Event) error { - *m = EventMessage(e) - return nil -} - -var _ Message = (*EventMessage)(nil) // Test it conforms to the interface - -func EventContextToBinaryEncoder(c cloudevents.EventContext, b BinaryEncoder) (err error) { - // Pass all attributes - var sv spec.Version - sv, err = spec.VS.Version(c.GetSpecVersion()) - if err != nil { - return err - } - 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 -} diff --git a/v1/binding/example_implementing_test.go b/v1/binding/example_implementing_test.go deleted file mode 100644 index f4b79cc7f..000000000 --- a/v1/binding/example_implementing_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package binding_test - -import ( - "bytes" - "context" - "encoding/json" - "io" - "io/ioutil" - - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/format" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport" -) - -// ExMessage is a json.RawMessage, a byte slice containing a JSON encoded event. -// It implements binding.MockStructuredMessage -// -// Note: a good binding implementation should provide an easy way to convert -// between the Message implementation and the "native" message format. -// In this case it's as simple as: -// -// native = ExMessage(impl) -// impl = json.RawMessage(native) -// -// For example in a HTTP binding it should be easy to convert between -// the HTTP binding.Message implementation and net/http.Request and -// Response types. There are no interfaces for this conversion as it -// requires the use of unknown types. -type ExMessage json.RawMessage - -func (m ExMessage) GetParent() binding.Message { - return nil -} - -func (m ExMessage) Encoding() binding.Encoding { - return binding.EncodingStructured -} - -func (m ExMessage) Structured(ctx context.Context, b binding.StructuredEncoder) error { - return b.SetStructuredEvent(ctx, format.JSON, bytes.NewReader(m)) -} - -func (m ExMessage) Binary(context.Context, binding.BinaryEncoder) error { - return binding.ErrNotBinary -} - -func (m ExMessage) Finish(error) error { return nil } - -var _ binding.Message = (*ExMessage)(nil) - -// ExSender sends by writing JSON encoded events to an io.Writer -// ExSender supports transcoding -// ExSender implements directly StructuredEncoder & EventEncoder -type ExSender struct { - encoder *json.Encoder - transformers binding.TransformerFactories -} - -func NewExSender(w io.Writer, factories ...binding.TransformerFactory) binding.Sender { - return &ExSender{encoder: json.NewEncoder(w), transformers: factories} -} - -func (s *ExSender) Send(ctx context.Context, m binding.Message) error { - // Encode tries the various encodings, starting with provided root encoder factories. - // If a sender doesn't support a specific encoding, a null root encoder factory could be provided. - _, err := binding.Encode( - ctx, - m, - s, - nil, - s.transformers, - ) - - return err -} - -func (s *ExSender) SetStructuredEvent(ctx context.Context, f format.Format, event io.Reader) error { - if f == format.JSON { - b, err := ioutil.ReadAll(event) - if err != nil { - return err - } - return s.encoder.Encode(json.RawMessage(b)) - } else { - return binding.ErrNotStructured - } -} - -func (s *ExSender) Close(context.Context) error { return nil } - -var _ binding.Sender = (*ExSender)(nil) -var _ binding.StructuredEncoder = (*ExSender)(nil) - -// ExReceiver receives by reading JSON encoded events from an io.Reader -type ExReceiver struct{ decoder *json.Decoder } - -func NewExReceiver(r io.Reader) binding.Receiver { return &ExReceiver{json.NewDecoder(r)} } - -func (r *ExReceiver) Receive(context.Context) (binding.Message, error) { - var rm json.RawMessage - err := r.decoder.Decode(&rm) // This is just a byte copy. - return ExMessage(rm), err -} -func (r *ExReceiver) Close(context.Context) error { return nil } - -// NewExTransport returns a transport.Transport which is implemented by -// an ExSender and an ExReceiver -func NewExTransport(r io.Reader, w io.Writer) transport.Transport { - return binding.NewTransportAdapter(NewExSender(w), NewExReceiver(r), []func(ctx context.Context) context.Context{}) -} - -// Example of implementing a transport including a simple message type, -// and a transport sender and receiver. -func Example_implementing() {} diff --git a/v1/binding/example_using_test.go b/v1/binding/example_using_test.go deleted file mode 100644 index 24281386b..000000000 --- a/v1/binding/example_using_test.go +++ /dev/null @@ -1,126 +0,0 @@ -package binding_test - -import ( - "context" - "fmt" - "io" - "strconv" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/client" -) - -const count = 3 // Example ends after this many events. - -// The sender uses the cloudevents.Client API, not the transport APIs directly. -func runSender(w io.Writer) error { - c, err := client.New(NewExTransport(nil, w), client.WithoutTracePropagation()) - if err != nil { - return err - } - for i := 0; i < count; i++ { - e := cloudevents.New() - e.SetType("example.com/event") - e.SetSource("example.com/source") - e.SetID(strconv.Itoa(i)) - if err := e.SetData(fmt.Sprintf("hello %d", i)); err != nil { - return err - } - if _, _, err := c.Send(context.TODO(), e); err != nil { - return err - } - } - return nil -} - -// The receiver uses the cloudevents.Client API, not the transport APIs directly. -func runReceiver(r io.Reader) error { - i := 0 - process := func(e cloudevents.Event) error { - fmt.Printf("%s\n", e) - i++ - if i == count { - return io.EOF - } - return nil - } - c, err := client.New(NewExTransport(r, nil), client.WithoutTracePropagation()) - if err != nil { - return err - } - return c.StartReceiver(context.TODO(), process) -} - -// The intermediary receives events and forwards them to another -// process using ExReceiver and ExSender directly. -// -// By forwarding a transport.Message instead of a cloudevents.Event, -// it allows the transports to avoid un-necessary decoding of -// structured events, and to exchange delivery status between reliable -// transports. Even transports using different protocols can ensure -// reliable delivery. -// -func runIntermediary(r io.Reader, w io.WriteCloser) error { - defer w.Close() - for { - receiver := NewExReceiver(r) - sender := NewExSender(w) - for i := 0; i < count; i++ { - if m, err := receiver.Receive(context.TODO()); err != nil { - return err - } else if err := sender.Send(context.TODO(), m); err != nil { - return err - } - } - } -} - -// This example shows how to use a transport in sender, receiver, -// and intermediary processes. -// -// The sender and receiver use the client.Client API to send and -// receive messages. the transport. Only the intermediary example -// actually uses the transport APIs for efficiency and reliability in -// forwarding events. -func Example_using() { - r1, w1 := io.Pipe() // The sender-to-intermediary pipe - r2, w2 := io.Pipe() // The intermediary-to-receiver pipe - - done := make(chan error) - go func() { done <- runReceiver(r2) }() - go func() { done <- runIntermediary(r1, w2) }() - go func() { done <- runSender(w1) }() - for i := 0; i < 2; i++ { - if err := <-done; err != nil && err != io.EOF { - fmt.Println(err) - } - } - - // Output: - // Validation: valid - // Context Attributes, - // specversion: 1.0 - // type: example.com/event - // source: example.com/source - // id: 0 - // Data, - // "hello 0" - // - // Validation: valid - // Context Attributes, - // specversion: 1.0 - // type: example.com/event - // source: example.com/source - // id: 1 - // Data, - // "hello 1" - // - // Validation: valid - // Context Attributes, - // specversion: 1.0 - // type: example.com/event - // source: example.com/source - // id: 2 - // Data, - // "hello 2" -} diff --git a/v1/binding/finish_message.go b/v1/binding/finish_message.go deleted file mode 100644 index 3c4efc5c0..000000000 --- a/v1/binding/finish_message.go +++ /dev/null @@ -1,27 +0,0 @@ -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} -} diff --git a/v1/binding/finish_message_test.go b/v1/binding/finish_message_test.go deleted file mode 100644 index df294282a..000000000 --- a/v1/binding/finish_message_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package binding_test - -import ( - "context" - "net/url" - "testing" - - "github.com/stretchr/testify/assert" - - cloudevents "github.com/cloudevents/sdk-go/v1" - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -func TestWithFinish(t *testing.T) { - var testEvent = cloudevents.Event{ - Data: []byte(`"data"`), - DataEncoded: true, - Context: cloudevents.EventContextV1{ - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: types.URIRef{URL: url.URL{Path: "source"}}, - ID: "id", - Type: "type"}.AsV1(), - } - - done := make(chan error, 1) - m := binding.WithFinish(binding.EventMessage(testEvent), func(err error) { - done <- err - }) - select { - case <-done: - assert.Fail(t, "done early") - default: - } - ch := make(chan binding.Message, 1) - assert.NoError(t, binding.ChanSender(ch).Send(context.Background(), m)) - assert.NoError(t, <-done) -} diff --git a/v1/binding/format/format.go b/v1/binding/format/format.go deleted file mode 100644 index 4070acdcc..000000000 --- a/v1/binding/format/format.go +++ /dev/null @@ -1,103 +0,0 @@ -/* -Package format formats structured events. - -The "application/cloudevents+json" format is built-in and always -available. Other formats may be added. -*/ -package format - -import ( - "encoding/json" - "fmt" - "strings" - - ce "github.com/cloudevents/sdk-go/v1/cloudevents" -) - -// Format marshals and unmarshals structured events to bytes. -type Format interface { - // MediaType identifies the format - MediaType() string - // Marshal event to bytes - Marshal(ce.Event) ([]byte, error) - // Unmarshal bytes to event - Unmarshal([]byte, *ce.Event) error -} - -// UnknownFormat allows an event with an unknown format string to be forwarded, -// but Marshal() and Unmarshal will always fail. -type UnknownFormat string - -func (uf UnknownFormat) MediaType() string { return string(uf) } -func (uf UnknownFormat) Marshal(ce.Event) ([]byte, error) { return nil, unknown(uf.MediaType()) } -func (uf UnknownFormat) Unmarshal([]byte, *ce.Event) error { return unknown(uf.MediaType()) } - -// 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 ce.ApplicationCloudEventsJSON } - -func (jsonFmt) Marshal(e ce.Event) ([]byte, error) { return json.Marshal(e) } -func (jsonFmt) Unmarshal(b []byte, e *ce.Event) error { - err := json.Unmarshal(b, e) - if err != nil { - return err - } - - // Extensions to go types when unparsed - for k, v := range e.Extensions() { - var vParsed interface{} - switch v.(type) { - case json.RawMessage: - err = json.Unmarshal(v.(json.RawMessage), &vParsed) - if err != nil { - return err - } - e.SetExtension(k, vParsed) - } - } - - return nil -} - -// 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 ce.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 *ce.Event) error { - if f := formats[mediaType]; f != nil { - return f.Unmarshal(b, e) - } - return unknown(mediaType) -} diff --git a/v1/binding/format/format_test.go b/v1/binding/format/format_test.go deleted file mode 100644 index fb4632e80..000000000 --- a/v1/binding/format/format_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package format_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/cloudevents/sdk-go/v1/binding/format" - ce "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -func TestJSON(t *testing.T) { - assert := assert.New(t) - e := ce.Event{ - Context: ce.EventContextV03{ - Type: "type", - ID: "id", - Source: *types.ParseURLRef("source"), - }.AsV03(), - } - e.SetExtension("ex", "val") - assert.NoError(e.SetData("foo")) - b, err := format.JSON.Marshal(e) - assert.NoError(err) - assert.Equal(`{"data":"foo","ex":"val","id":"id","source":"source","specversion":"0.3","type":"type"}`, string(b)) - - var e2 ce.Event - assert.NoError(format.JSON.Unmarshal(b, &e2)) - assert.Equal(e, e2) -} - -func TestLookup(t *testing.T) { - assert := assert.New(t) - assert.Nil(format.Lookup("nosuch")) - - f := format.Lookup(ce.ApplicationCloudEventsJSON) - assert.Equal(f.MediaType(), ce.ApplicationCloudEventsJSON) - assert.Equal(format.JSON, f) -} - -func TestMarshalUnmarshal(t *testing.T) { - assert := assert.New(t) - e := ce.Event{ - Context: ce.EventContextV03{ - Type: "type", - ID: "id", - Source: *types.ParseURLRef("source"), - }.AsV03(), - } - assert.NoError(e.SetData("foo")) - b, err := format.Marshal(format.JSON.MediaType(), e) - assert.NoError(err) - assert.Equal(`{"data":"foo","id":"id","source":"source","specversion":"0.3","type":"type"}`, string(b)) - - var e2 ce.Event - assert.NoError(format.Unmarshal(format.JSON.MediaType(), b, &e2)) - assert.Equal(e, e2) - - _, err = format.Marshal("nosuchformat", e) - assert.EqualError(err, "unknown event format media-type \"nosuchformat\"") - err = format.Unmarshal("nosuchformat", nil, &e) - assert.EqualError(err, "unknown event format media-type \"nosuchformat\"") -} - -type dummyFormat struct{} - -func (dummyFormat) MediaType() string { return "dummy" } -func (dummyFormat) Marshal(ce.Event) ([]byte, error) { return []byte("dummy!"), nil } -func (dummyFormat) Unmarshal(b []byte, e *ce.Event) error { e.Data = "undummy!"; return nil } - -func TestAdd(t *testing.T) { - assert := assert.New(t) - format.Add(dummyFormat{}) - assert.Equal(dummyFormat{}, format.Lookup("dummy")) - - e := ce.Event{} - b, err := format.Marshal("dummy", e) - assert.NoError(err) - assert.Equal("dummy!", string(b)) - err = format.Unmarshal("dummy", b, &e) - assert.NoError(err) - assert.Equal("undummy!", e.Data) -} diff --git a/v1/binding/interfaces.go b/v1/binding/interfaces.go deleted file mode 100644 index 5b9f4088c..000000000 --- a/v1/binding/interfaces.go +++ /dev/null @@ -1,214 +0,0 @@ -package binding - -import ( - "context" - "errors" - "io" - - "github.com/cloudevents/sdk-go/v1/binding/format" - "github.com/cloudevents/sdk-go/v1/binding/spec" -) - -// 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 it 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") - -// 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 -// -// The Structured and Binary methods provide optional optimized transfer of an event -// to a Sender, they may not be implemented by all Message instances. A Sender should -// try each method of interest and fall back to ToEvent() if none are supported. -// -type Message interface { - // Return the type of the message Encoding. - // The encoding should be preferably computed when the message is constructed. - Encoding() Encoding - - // Structured transfers a structured-mode event to a StructuredEncoder. - // Returns 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. - Structured(context.Context, StructuredEncoder) error - - // Binary transfers a binary-mode event to an BinaryEncoder. - // Returns 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 - // - // Allows Senders to forward a binary message without allocating an - // intermediate Event. - Binary(context.Context, BinaryEncoder) error - - // Finish *must* be called when message from a Receiver can be forgotten by - // the receiver. Sender.Send() calls Finish() when the message is sent. A QoS - // 1 sender should not call Finish() until it gets an acknowledgment of - // receipt on the underlying transport. For QoS 2 see ExactlyOnceMessage. - // - // 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 -} - -// 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") - -// StructuredEncoder 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 -> structured transfer and event -> binary. -type StructuredEncoder interface { - // Event receives an io.Reader for the whole event. - SetStructuredEvent(ctx context.Context, format format.Format, event io.Reader) error -} - -// BinaryEncoder 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 -> binary transfer and event -> binary. -// -// Start() and End() methods are invoked every time this BinaryEncoder implementation is used to visit a Message -type BinaryEncoder 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 cloudevents/types - 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 cloudevents/types - SetExtension(name string, value interface{}) error - - // SetData receives an io.Reader for the data attribute. - // io.Reader could be empty, meaning that message payload 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() 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 -} - -// Receiver receives messages. -type Receiver interface { - // Receive blocks till a message is received or ctx expires. - // - // A non-nil error means the receiver is closed. - // io.EOF means it closed cleanly, any other value indicates an error. - Receive(ctx context.Context) (Message, error) -} - -// Sender sends messages. -type Sender interface { - // Send a message. - // - // Send returns when the "outbound" message has been sent. The Sender may - // still be expecting acknowledgment or holding other state for the message. - // - // m.Finish() is called when sending is finished: expected acknowledgments (or - // errors) have been received, the Sender is no longer holding any state for - // the message. m.Finish() may be called during or after Send(). - // - // To support optimized forwading of structured-mode messages, Send() - // should use the encoding returned by m.Structured() if there is one. - // Otherwise m.Event() can be encoded as per the binding's rules. - Send(ctx context.Context, m Message) error -} - -// Requester sends a message and receives a response -// -// Optional interface that may be implemented by protocols that support -// request/response correlation. -type Requester interface { - // Request sends m like Sender.Send() but also arranges to receive a response. - // The returned Receiver is used to receive the response. - Request(ctx context.Context, m Message) (Receiver, error) -} - -// Closer is the common interface for things that can be closed -type Closer interface { - Close(ctx context.Context) error -} - -// ReceiveCloser is a Receiver that can be closed. -type ReceiveCloser interface { - Receiver - Closer -} - -// SendCloser is a Sender that can be closed. -type SendCloser interface { - Sender - Closer -} diff --git a/v1/binding/spec/attributes.go b/v1/binding/spec/attributes.go deleted file mode 100644 index 511a131af..000000000 --- a/v1/binding/spec/attributes.go +++ /dev/null @@ -1,133 +0,0 @@ -package spec - -import ( - "fmt" - "time" - - ce "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/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() - Name() string - // Version of the spec that this attribute belongs to - Version() Version - // Get the value of this attribute from an event context - Get(ce.EventContextReader) interface{} - // Set the value of this attribute on an event context - Set(ce.EventContextWriter, interface{}) error - // Delete this attribute from and event context, when possible - Delete(ce.EventContextWriter) error -} - -// accessor provides Kind, Get, Set. -type accessor interface { - Kind() Kind - Get(ce.EventContextReader) interface{} - Set(ce.EventContextWriter, interface{}) error - Delete(ce.EventContextWriter) error -} - -var acc = [nAttrs]accessor{ - &aStr{aKind(ID), ce.EventContextReader.GetID, ce.EventContextWriter.SetID}, - &aStr{aKind(Source), ce.EventContextReader.GetSource, ce.EventContextWriter.SetSource}, - &aStr{aKind(SpecVersion), ce.EventContextReader.GetSpecVersion, ce.EventContextWriter.SetSpecVersion}, - &aStr{aKind(Type), ce.EventContextReader.GetType, ce.EventContextWriter.SetType}, - &aStr{aKind(DataContentType), ce.EventContextReader.GetDataContentType, ce.EventContextWriter.SetDataContentType}, - &aStr{aKind(DataSchema), ce.EventContextReader.GetDataSchema, ce.EventContextWriter.SetDataSchema}, - &aStr{aKind(Subject), ce.EventContextReader.GetSubject, ce.EventContextWriter.SetSubject}, - &aTime{aKind(Time), ce.EventContextReader.GetTime, ce.EventContextWriter.SetTime}, -} - -// aKind implements Kind() -type aKind Kind - -func (kind aKind) Kind() Kind { return Kind(kind) } - -type aStr struct { - aKind - get func(ce.EventContextReader) string - set func(ce.EventContextWriter, string) error -} - -func (a *aStr) Get(c ce.EventContextReader) interface{} { - if s := a.get(c); s != "" { - return s - } - return nil // Treat blank as missing -} - -func (a *aStr) Set(c ce.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 ce.EventContextWriter) error { - return a.set(c, "") -} - -type aTime struct { - aKind - get func(ce.EventContextReader) time.Time - set func(ce.EventContextWriter, time.Time) error -} - -func (a *aTime) Get(c ce.EventContextReader) interface{} { - if v := a.get(c); !v.IsZero() { - return v - } - return nil // Treat zero time as missing. -} - -func (a *aTime) Set(c ce.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 ce.EventContextWriter) error { - return a.set(c, time.Time{}) -} diff --git a/v1/binding/spec/attributes_test.go b/v1/binding/spec/attributes_test.go deleted file mode 100644 index 05d79b369..000000000 --- a/v1/binding/spec/attributes_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package spec_test - -import ( - "testing" - "time" - - "github.com/cloudevents/sdk-go/v1/binding/spec" - "github.com/google/go-cmp/cmp" - "github.com/stretchr/testify/assert" -) - -func TestAttributes03(t *testing.T) { - assert := assert.New(t) - v, err := spec.WithPrefix("x:").Version("0.3") - assert.NoError(err) - subject := v.Attribute("x:subject") - assert.Equal(spec.Subject, subject.Kind()) - assert.Equal("x:subject", subject.Name()) - - c := v.NewContext() - assert.Equal("0.3", c.GetSpecVersion()) - assert.NoError(subject.Set(c, "foobar")) - got := subject.Get(c) - assert.Equal("foobar", got) - assert.Equal("foobar", c.GetSubject()) - - now := time.Now() - atime := v.Attribute("x:time") - assert.Equal(spec.Time, atime.Kind()) - assert.NoError(atime.Set(c, now)) - tm := atime.Get(c) - assert.Empty(cmp.Diff(now, tm)) - assert.Empty(cmp.Diff(now, c.GetTime())) - - nosuch := v.Attribute("nosuch") - assert.Nil(nosuch) - - err = subject.Set(c, 1) - assert.EqualError(err, "invalid value for subject: 1") - err = atime.Set(c, "foo") - assert.EqualError(err, `invalid value for time: "foo"`) -} - -func TestAttributes02(t *testing.T) { - assert := assert.New(t) - v, err := spec.WithPrefix("x:").Version("0.2") - assert.NoError(err) - c := v.NewContext() - id := v.Attribute("x:id") - assert.NoError(id.Set(c, "foobar")) - s := id.Get(c) - assert.Equal("foobar", s) - assert.Equal("foobar", c.GetID()) - - now := time.Now() - atime := v.Attribute("x:time") - assert.Equal(spec.Time, atime.Kind()) - assert.NoError(atime.Set(c, now)) - tm := atime.Get(c) - assert.Empty(cmp.Diff(now, tm)) - assert.Empty(cmp.Diff(now, c.GetTime())) - - nosuch := v.Attribute("nosuch") - assert.Nil(nosuch) - - err = id.Set(c, 1) - assert.EqualError(err, "invalid value for id: 1") - err = atime.Set(c, "foo") - assert.EqualError(err, "invalid value for time: \"foo\"") -} - -func TestAttributes01(t *testing.T) { - assert := assert.New(t) - v, err := spec.WithPrefix("x:").Version("0.1") - assert.NoError(err) - c := v.NewContext() - contentType := v.Attribute("x:contentType") - assert.Equal(spec.DataContentType, contentType.Kind()) - assert.NoError(contentType.Set(c, "foobar")) - s := contentType.Get(c) - assert.Equal("foobar", s) - assert.Equal("foobar", c.GetDataContentType()) - - nosuch := v.Attribute("x:subject") - assert.Nil(nosuch) -} - -func TestAttributesBadVersions(t *testing.T) { - assert := assert.New(t) - v, err := spec.WithPrefix("x:").Version("0.x") - assert.Nil(v) - assert.EqualError(err, `invalid spec version "0.x"`) -} diff --git a/v1/binding/spec/doc.go b/v1/binding/spec/doc.go deleted file mode 100644 index 38d6fddf9..000000000 --- a/v1/binding/spec/doc.go +++ /dev/null @@ -1,8 +0,0 @@ -/* -Package spec provides spec-version metadata. - -For use by code that maps events using (prefixed) attribute name strings. -Supports handling multiple spec versions uniformly. - -*/ -package spec diff --git a/v1/binding/spec/spec.go b/v1/binding/spec/spec.go deleted file mode 100644 index 53663af03..000000000 --- a/v1/binding/spec/spec.go +++ /dev/null @@ -1,233 +0,0 @@ -package spec - -import ( - "fmt" - "strings" - - ce "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -// 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(name string) Attribute - // Attributes returns all the context attributes for this version. - Attributes() []Attribute - // NewContext returns a new context for this version. - NewContext() ce.EventContext - // Convert translates a context to this version. - Convert(ce.EventContextConverter) ce.EventContext - // SetAttribute sets named attribute to value. - // - // Name is case insensitive. - // Does nothing if name does not start with prefix. - SetAttribute(context ce.EventContextWriter, name string, value interface{}) error - // Attribute looks up the attribute from kind. - // Returns nil if not found. - AttributeFromKind(kind Kind) Attribute -} - -// Versions contains all known versions with the same attribute prefix. -type Versions struct { - prefix string - all []Version - m map[string]Version - svnames []string -} - -// 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, error) { - if v := vs.m[name]; v != nil { - return v, nil - } - return nil, fmt.Errorf("invalid spec version %#v", name) -} - -// Latest returns the latest Version -func (vs *Versions) Latest() Version { return vs.all[0] } - -// SpecVersionNames returns distinct names of the specversion -// attribute used in all versions, newest first. -// Names are prefixed. -func (vs *Versions) SpecVersionNames() []string { return vs.svnames } - -// Prefix is the lowercase attribute name prefix. -func (vs *Versions) Prefix() string { return vs.prefix } - -// FindVersion calls getAttr with known (prefixed) spec-version attribute names -// till it finds a valid version. -func (vs *Versions) FindVersion(getAttr func(string) string) (Version, error) { - for _, sv := range vs.svnames { - if v, err := vs.Version(getAttr(sv)); err == nil { - return v, nil - } - } - return nil, fmt.Errorf("CloudEvents spec-version not found") -} - -type attribute struct { - accessor - name string - version Version -} - -func (a *attribute) Name() string { return a.name } -func (a *attribute) Version() Version { return a.version } - -type version struct { - prefix string - context ce.EventContext - convert func(ce.EventContextConverter) ce.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() ce.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 ce.EventContextConverter) ce.EventContext { return v.convert(c) } - -func (v *version) SetAttribute(c ce.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 strings.HasPrefix(name, v.prefix) { // Extension attribute - value, err = types.Validate(value) - if err == nil { - err = 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 ce.EventContext, - convert func(ce.EventContextConverter) ce.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.name = prefix + a.name - a.version = v - v.attrs[i] = a - v.attrMap[strings.ToLower(a.name)] = 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{}, - svnames: []string{ - prefix + "specversion", - prefix + "cloudEventsVersion", - }, - all: []Version{ - newVersion(prefix, ce.EventContextV1{}.AsV1(), - func(c ce.EventContextConverter) ce.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, ce.EventContextV03{}.AsV03(), - func(c ce.EventContextConverter) ce.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), - ), - newVersion(prefix, ce.EventContextV02{}.AsV02(), - func(c ce.EventContextConverter) ce.EventContext { return c.AsV02() }, - attr("specversion", SpecVersion), - attr("type", Type), - attr("source", Source), - attr("schemaurl", DataSchema), - attr("id", ID), - attr("time", Time), - attr("contenttype", DataContentType), - ), - newVersion(prefix, ce.EventContextV01{}.AsV01(), - func(c ce.EventContextConverter) ce.EventContext { return c.AsV01() }, - attr("cloudEventsVersion", SpecVersion), - attr("eventType", Type), - attr("source", Source), - attr("schemaURL", DataSchema), - attr("eventID", ID), - attr("eventTime", Time), - attr("contentType", 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 - V01 Version - V02 Version - V03 Version - V1 Version -) - -func init() { - VS = New() - V01, _ = VS.Version("0.1") - V02, _ = VS.Version("0.2") - V03, _ = VS.Version("0.3") - V1, _ = VS.Version("1.0") -} diff --git a/v1/binding/spec/spec_test.go b/v1/binding/spec/spec_test.go deleted file mode 100644 index 84f53f93a..000000000 --- a/v1/binding/spec/spec_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package spec_test - -import ( - "testing" - - "github.com/cloudevents/sdk-go/v1/binding/spec" - ce "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/stretchr/testify/assert" -) - -func TestVersions(t *testing.T) { - assert := assert.New(t) - versions := spec.New() - assert.Equal([]string{"specversion", "cloudEventsVersion"}, versions.SpecVersionNames()) - - want := []string{"1.0", "0.3", "0.2", "0.1"} - all := versions.Versions() - assert.Equal(len(want), len(all)) - for i, s := range want { - assert.Equal(s, all[i].String()) - assert.Equal(s, all[i].NewContext().GetSpecVersion()) - converted := all[i].Convert(ce.EventContextV01{}.AsV01()) - assert.Equal(s, converted.GetSpecVersion(), "%v %v %v", i, s, converted) - } - assert.Equal(want[0], versions.Latest().NewContext().GetSpecVersion()) -} diff --git a/v1/binding/test/benchmark.go b/v1/binding/test/benchmark.go deleted file mode 100644 index 0f358390e..000000000 --- a/v1/binding/test/benchmark.go +++ /dev/null @@ -1,44 +0,0 @@ -package test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "golang.org/x/sync/errgroup" - - "github.com/cloudevents/sdk-go/v1/binding" -) - -// Simple send/receive benchmark. -// Requires a sender and receiver that are connected to each other. -func BenchmarkSendReceive(b *testing.B, s binding.Sender, r binding.Receiver) { - m := binding.EventMessage(FullEvent()) - ctx := context.Background() - b.ResetTimer() // Don't count setup. - for i := 0; i < b.N; i++ { - n := 10 // Messages to send async. - g := errgroup.Group{} - g.Go(func() error { - for j := 0; j < n; j++ { - if err := s.Send(ctx, m); err != nil { - return err - } - } - return nil - }) - g.Go(func() error { - for j := 0; j < n; j++ { - m, err := r.Receive(ctx) - if err != nil { - return err - } - if err := m.Finish(nil); err != nil { - return err - } - } - return nil - }) - assert.NoError(b, g.Wait()) - } -} diff --git a/v1/binding/test/events.go b/v1/binding/test/events.go deleted file mode 100644 index b826b0ad2..000000000 --- a/v1/binding/test/events.go +++ /dev/null @@ -1,97 +0,0 @@ -// Package test contains test data and generic tests for testing bindings. -package test - -import ( - "fmt" - "net/url" - "reflect" - "time" - - "github.com/cloudevents/sdk-go/v1/binding/spec" - ce "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -func strptr(s string) *string { return &s } - -var ( - Source = types.URIRef{URL: url.URL{Scheme: "http", Host: "example.com", Path: "/source"}} - Timestamp = types.Timestamp{Time: time.Date(2020, 03, 21, 12, 34, 56, 780000000, time.UTC)} - Schema = types.URI{URL: url.URL{Scheme: "http", Host: "example.com", Path: "/schema"}} -) - -// FullEvent has all context attributes set and JSON string data. -func FullEvent() ce.Event { - e := ce.Event{ - Context: ce.EventContextV1{ - Type: "com.example.FullEvent", - Source: Source, - ID: "full-event", - Time: &Timestamp, - DataSchema: &Schema, - DataContentType: strptr("text/json"), - Subject: strptr("topic"), - }.AsV1(), - } - - e.SetExtension("exbool", true) - e.SetExtension("exint", 42) - e.SetExtension("exstring", "exstring") - e.SetExtension("exbinary", []byte{0, 1, 2, 3}) - e.SetExtension("exurl", Source) - e.SetExtension("extime", Timestamp) - - if err := e.SetData("hello"); err != nil { - panic(err) - } - return e -} - -// MinEvent has only required attributes set. -func MinEvent() ce.Event { - return ce.Event{ - Context: ce.EventContextV1{ - Type: "com.example.MinEvent", - Source: Source, - ID: "min-event", - }.AsV1(), - } -} - -// AllVersions returns all versions of each event in events. -// ID gets a -number suffix so IDs are unique. -func AllVersions(events []ce.Event) []ce.Event { - versions := spec.New() - all := versions.Versions() - result := make([]ce.Event, len(events)*len(all)) - i := 0 - for _, e := range events { - for _, v := range all { - result[i] = e - result[i].Context = v.Convert(e.Context) - result[i].SetID(fmt.Sprintf("%v-%v", e.ID(), i)) // Unique IDs - i++ - } - } - return result -} - -// Events is a set of test events that should be handled correctly by -// all event-processing code. -func Events() []ce.Event { - return AllVersions([]ce.Event{FullEvent(), MinEvent()}) -} - -// NoExtensions returns a copy of events with no Extensions. -// Use for testing where extensions are not supported. -func NoExtensions(events []ce.Event) []ce.Event { - result := make([]ce.Event, len(events)) - for i, e := range events { - result[i] = e - result[i].Context = e.Context.Clone() - ctx := reflect.ValueOf(result[i].Context).Elem() - ext := ctx.FieldByName("Extensions") - ext.Set(reflect.Zero(ext.Type())) - } - return result -} diff --git a/v1/binding/test/mock_binary_message.go b/v1/binding/test/mock_binary_message.go deleted file mode 100644 index 465e2ad3c..000000000 --- a/v1/binding/test/mock_binary_message.go +++ /dev/null @@ -1,119 +0,0 @@ -package test - -import ( - "bytes" - "context" - "io" - "io/ioutil" - - cloudevents "github.com/cloudevents/sdk-go/v1" - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/spec" -) - -// MockBinaryMessage implements a binary-mode message as a simple struct. -type MockBinaryMessage struct { - Metadata map[spec.Attribute]interface{} - Extensions map[string]interface{} - Body []byte -} - -func (bm *MockBinaryMessage) Start(ctx context.Context) error { - bm.Metadata = make(map[spec.Attribute]interface{}) - bm.Extensions = make(map[string]interface{}) - return nil -} - -func (bm *MockBinaryMessage) SetAttribute(attribute spec.Attribute, value interface{}) error { - bm.Metadata[attribute] = value - return nil -} - -func (bm *MockBinaryMessage) SetExtension(name string, value interface{}) error { - bm.Extensions[name] = value - return nil -} - -func (bm *MockBinaryMessage) SetData(data io.Reader) (err error) { - bm.Body, err = ioutil.ReadAll(data) - return err -} - -func (bm *MockBinaryMessage) End() error { - return nil -} - -var versions = spec.New() - -func NewMockBinaryMessage(e cloudevents.Event) binding.Message { - version, err := versions.Version(e.SpecVersion()) - if err != nil { - panic(err) - } - - m := MockBinaryMessage{ - Metadata: make(map[spec.Attribute]interface{}), - Extensions: make(map[string]interface{}), - } - - for _, attribute := range version.Attributes() { - val := attribute.Get(e.Context) - if val != nil { - m.Metadata[attribute] = val - } - } - - for k, v := range e.Extensions() { - m.Extensions[k] = v - } - - m.Body, err = e.DataBytes() - if err != nil { - panic(err) - } - - return &m -} - -func (bm *MockBinaryMessage) GetParent() binding.Message { - return nil -} - -func (bm *MockBinaryMessage) Structured(context.Context, binding.StructuredEncoder) error { - return binding.ErrNotStructured -} - -func (bm *MockBinaryMessage) Binary(ctx context.Context, b binding.BinaryEncoder) error { - err := b.Start(ctx) - if err != nil { - return err - } - for k, v := range bm.Metadata { - err = b.SetAttribute(k, v) - if err != nil { - return err - } - } - for k, v := range bm.Extensions { - err = b.SetExtension(k, v) - if err != nil { - return err - } - } - if len(bm.Body) != 0 { - err = b.SetData(bytes.NewReader(bm.Body)) - if err != nil { - return err - } - } - return b.End() -} - -func (bm *MockBinaryMessage) Encoding() binding.Encoding { - return binding.EncodingBinary -} - -func (bm *MockBinaryMessage) Finish(error) error { return nil } - -var _ binding.Message = (*MockBinaryMessage)(nil) // Test it conforms to the interface -var _ binding.BinaryEncoder = (*MockBinaryMessage)(nil) diff --git a/v1/binding/test/mock_structured_message.go b/v1/binding/test/mock_structured_message.go deleted file mode 100644 index 8fcc082ed..000000000 --- a/v1/binding/test/mock_structured_message.go +++ /dev/null @@ -1,56 +0,0 @@ -package test - -import ( - "bytes" - "context" - "io" - "io/ioutil" - - cloudevents "github.com/cloudevents/sdk-go/v1" - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/format" -) - -// MockStructuredMessage implements a structured-mode message as a simple struct. -type MockStructuredMessage struct { - Format format.Format - Bytes []byte -} - -func (s *MockStructuredMessage) SetStructuredEvent(ctx context.Context, format format.Format, event io.Reader) (err error) { - s.Format = format - s.Bytes, err = ioutil.ReadAll(event) - if err != nil { - return - } - - return nil -} - -func NewMockStructuredMessage(e cloudevents.Event) binding.Message { - testEventSerialized, err := format.JSON.Marshal(e) - if err != nil { - panic(err) - } - return &MockStructuredMessage{ - Bytes: testEventSerialized, - Format: format.JSON, - } -} - -func (s *MockStructuredMessage) Structured(ctx context.Context, b binding.StructuredEncoder) error { - return b.SetStructuredEvent(ctx, s.Format, bytes.NewReader(s.Bytes)) -} - -func (s *MockStructuredMessage) Binary(context.Context, binding.BinaryEncoder) error { - return binding.ErrNotBinary -} - -func (bm *MockStructuredMessage) Encoding() binding.Encoding { - return binding.EncodingStructured -} - -func (s *MockStructuredMessage) Finish(error) error { return nil } - -var _ binding.Message = (*MockStructuredMessage)(nil) // Test it conforms to the interface -var _ binding.StructuredEncoder = (*MockStructuredMessage)(nil) diff --git a/v1/binding/test/test.go b/v1/binding/test/test.go deleted file mode 100644 index 6700af08f..000000000 --- a/v1/binding/test/test.go +++ /dev/null @@ -1,153 +0,0 @@ -// Package test provides re-usable functions for binding tests. -package test - -import ( - "context" - "encoding/json" - "fmt" - "reflect" - "sync" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - cloudevents "github.com/cloudevents/sdk-go/v1" - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/format" - "github.com/cloudevents/sdk-go/v1/binding/spec" - ce "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -// NameOf generates a string test name from x, esp. for ce.Event and ce.Message. -func NameOf(x interface{}) string { - switch x := x.(type) { - case ce.Event: - b, err := json.Marshal(x) - if err == nil { - return fmt.Sprintf("Event%s", b) - } - case binding.Message: - return fmt.Sprintf("Message{%s}", reflect.TypeOf(x).String()) - } - return fmt.Sprintf("%T(%#v)", x, x) -} - -// Run f as a test for each event in events -func EachEvent(t *testing.T, events []ce.Event, f func(*testing.T, ce.Event)) { - for _, e := range events { - in := e - t.Run(NameOf(in), func(t *testing.T) { f(t, in) }) - } -} - -// Run f as a test for each message in messages -func EachMessage(t *testing.T, messages []binding.Message, f func(*testing.T, binding.Message)) { - for _, m := range messages { - in := m - t.Run(NameOf(in), func(t *testing.T) { f(t, in) }) - } -} - -// Canonical converts all attributes to canonical string form for comparisons. -func Canonical(t *testing.T, c ce.EventContext) { - t.Helper() - for k, v := range c.GetExtensions() { - s, err := types.Format(v) - require.NoError(t, err, "extension[%q]=%#v: %v", k, v, err) - assert.NoError(t, c.SetExtension(k, s)) - } -} - -// SendReceive does, s.Send(in) and returns r.Receive(). -// Halt test on error. -func SendReceive(t *testing.T, ctx context.Context, in binding.Message, s binding.Sender, r binding.Receiver, outAssert func(binding.Message)) { - t.Helper() - wg := sync.WaitGroup{} - wg.Add(2) - - go func() { - defer wg.Done() - out, recvErr := r.Receive(ctx) - require.NoError(t, recvErr) - outAssert(out) - finishErr := out.Finish(nil) - require.NoError(t, finishErr) - }() - - go func() { - defer wg.Done() - err := s.Send(ctx, in) - require.NoError(t, err) - }() - - wg.Wait() -} - -func AssertEventContextEquals(t *testing.T, want cloudevents.EventContext, have cloudevents.EventContext) { - wantVersion, err := spec.VS.Version(want.GetSpecVersion()) - require.NoError(t, err) - haveVersion, err := spec.VS.Version(have.GetSpecVersion()) - require.NoError(t, err) - require.Equal(t, wantVersion, haveVersion) - - for _, a := range wantVersion.Attributes() { - require.Equal(t, a.Get(want), a.Get(have), "Attribute %s does not match: %v != %v", a.Name(), a.Get(want), a.Get(have)) - } - - require.Equal(t, want.GetExtensions(), have.GetExtensions(), "Extensions") -} - -func AssertEventEquals(t *testing.T, want cloudevents.Event, have cloudevents.Event) { - AssertEventContextEquals(t, want.Context, have.Context) - wantPayload, err := want.DataBytes() - assert.NoError(t, err) - havePayload, err := have.DataBytes() - assert.NoError(t, err) - assert.Equal(t, wantPayload, havePayload) -} - -func ExToStr(t *testing.T, e ce.Event) ce.Event { - for k, v := range e.Extensions() { - var vParsed interface{} - var err error - - switch v.(type) { - case json.RawMessage: - err = json.Unmarshal(v.(json.RawMessage), &vParsed) - assert.NoError(t, err) - default: - vParsed, err = types.Format(v) - require.NoError(t, err) - } - e.SetExtension(k, vParsed) - } - return e -} - -func MustJSON(e ce.Event) []byte { - b, err := format.JSON.Marshal(e) - if err != nil { - panic(err) - } - return b -} - -func MustToEvent(ctx context.Context, m binding.Message) (e ce.Event, encoding binding.Encoding) { - var err error - e, encoding, err = binding.ToEvent(ctx, m) - if err != nil { - panic(err) - } - return -} - -func CopyEventContext(e ce.Event) ce.Event { - newE := ce.Event{} - newE.Context = e.Context.Clone() - newE.DataEncoded = e.DataEncoded - newE.Data = e.Data - newE.DataBinary = e.DataBinary - return newE -} diff --git a/v1/binding/test/test_test.go b/v1/binding/test/test_test.go deleted file mode 100644 index 7baa80fc0..000000000 --- a/v1/binding/test/test_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package test_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/test" -) - -func TestEvent(t *testing.T) { - assert := assert.New(t) - - e := test.FullEvent() - assert.Equal("1.0", e.SpecVersion()) - assert.Equal("com.example.FullEvent", e.Type()) - assert.Equal(true, e.DataEncoded) - var s string - err := e.DataAs(&s) - assert.NoError(err) - assert.Equal("hello", s) - - e = test.MinEvent() - assert.Equal("1.0", e.SpecVersion()) - assert.Equal("com.example.MinEvent", e.Type()) - assert.Nil(e.Data) - assert.Empty(e.DataContentType()) -} - -type dummySR chan binding.Message - -func (d dummySR) Send(ctx context.Context, m binding.Message) (err error) { d <- m; return nil } -func (d dummySR) Receive(ctx context.Context) (binding.Message, error) { return <-d, nil } - -func TestSendReceive(t *testing.T) { - sr := make(dummySR) - allIn := []binding.Message{} - for _, e := range test.Events() { - allIn = append(allIn, binding.EventMessage(e)) - } - - var allOut []binding.Message - test.EachMessage(t, allIn, func(t *testing.T, in binding.Message) { - test.SendReceive(t, context.Background(), in, sr, sr, func(out binding.Message) { - assert.Equal(t, in, out) - allOut = append(allOut, out) - }) - }) - assert.Equal(t, allIn, allOut) -} diff --git a/v1/binding/test/transcoder.go b/v1/binding/test/transcoder.go deleted file mode 100644 index 9e21a7d37..000000000 --- a/v1/binding/test/transcoder.go +++ /dev/null @@ -1,45 +0,0 @@ -package test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - - cloudevents "github.com/cloudevents/sdk-go/v1" - "github.com/cloudevents/sdk-go/v1/binding" -) - -type TranscoderTestArgs struct { - Name string - InputMessage binding.Message - WantEvent cloudevents.Event - Transformers []binding.TransformerFactory -} - -func RunTranscoderTests(t *testing.T, ctx context.Context, tests []TranscoderTestArgs) { - for _, tt := range tests { - tt := tt // Don't use range variable inside scope - t.Run(tt.Name, func(t *testing.T) { - - mockStructured := MockStructuredMessage{} - mockBinary := MockBinaryMessage{} - - enc, err := binding.Encode(ctx, tt.InputMessage, &mockStructured, &mockBinary, tt.Transformers) - require.NoError(t, err) - - var e cloudevents.Event - if enc == binding.EncodingStructured { - e, _, err = binding.ToEvent(ctx, &mockStructured) - require.NoError(t, err) - } else if enc == binding.EncodingBinary { - e, _, err = binding.ToEvent(ctx, &mockBinary) - require.NoError(t, err) - } else { - t.Fatalf("Unexpected encoding %v", enc) - } - require.NoError(t, err) - AssertEventEquals(t, tt.WantEvent, e) - }) - } -} diff --git a/v1/binding/to_event.go b/v1/binding/to_event.go deleted file mode 100644 index ff28a7ed7..000000000 --- a/v1/binding/to_event.go +++ /dev/null @@ -1,123 +0,0 @@ -package binding - -import ( - "bytes" - "context" - "errors" - "io" - - cloudevents "github.com/cloudevents/sdk-go/v1" - "github.com/cloudevents/sdk-go/v1/binding/format" - "github.com/cloudevents/sdk-go/v1/binding/spec" - ce "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -var ErrCannotConvertToEvent = errors.New("cannot convert message to event") - -// Translates a Message with a valid Structured or Binary representation to an Event -// The TransformerFactories **aren't invoked** during the transformation to event, -// but after the event instance is generated -func ToEvent(ctx context.Context, message Message, transformers ...TransformerFactory) (e ce.Event, encoding Encoding, err error) { - e = cloudevents.NewEvent() - - messageEncoding := message.Encoding() - if messageEncoding == EncodingEvent { - for m := message; m != nil; { - if em, ok := m.(EventMessage); ok { - e = ce.Event(em) - encoding = EncodingEvent - err = TransformerFactories(transformers).EventTransformer()(&e) - return - } - if mw, ok := m.(MessageWrapper); ok { - m = mw.GetWrappedMessage() - } else { - break - } - } - err = ErrCannotConvertToEvent - return - } - - encoder := &messageToEventBuilder{event: &e} - encoding, err = RunDirectEncoding( - context.TODO(), - message, - encoder, - encoder, - []TransformerFactory{}, - ) - if err != nil { - return e, encoding, err - } - err = TransformerFactories(transformers).EventTransformer()(&e) - return -} - -type messageToEventBuilder struct { - event *ce.Event -} - -var _ StructuredEncoder = (*messageToEventBuilder)(nil) -var _ BinaryEncoder = (*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() 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 { - return b.event.SetData(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 cloudevents.VersionV01: - b.event.Context = b.event.Context.AsV01() - case cloudevents.VersionV02: - b.event.Context = b.event.Context.AsV02() - case cloudevents.VersionV03: - b.event.Context = b.event.Context.AsV03() - case cloudevents.VersionV1: - b.event.Context = b.event.Context.AsV1() - } - 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 -} diff --git a/v1/binding/to_event_test.go b/v1/binding/to_event_test.go deleted file mode 100644 index 7bb583fb0..000000000 --- a/v1/binding/to_event_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package binding_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/test" - "github.com/cloudevents/sdk-go/v1/cloudevents" -) - -type toEventTestCase struct { - name string - encoding binding.Encoding - message binding.Message - want cloudevents.Event -} - -func TestToEvent(t *testing.T) { - tests := []toEventTestCase{} - - for _, v := range test.Events() { - tests = append(tests, []toEventTestCase{ - { - name: "From mock structured with payload/" + test.NameOf(v), - encoding: binding.EncodingStructured, - message: test.NewMockStructuredMessage(v), - want: v, - }, - { - name: "From mock structured without payload/" + test.NameOf(v), - encoding: binding.EncodingStructured, - message: test.NewMockStructuredMessage(v), - want: v, - }, - { - name: "From mock binary with payload/" + test.NameOf(v), - encoding: binding.EncodingBinary, - message: test.NewMockBinaryMessage(v), - want: v, - }, - { - name: "From mock binary without payload/" + test.NameOf(v), - encoding: binding.EncodingBinary, - message: test.NewMockBinaryMessage(v), - want: v, - }, - { - name: "From event with payload/" + test.NameOf(v), - encoding: binding.EncodingEvent, - message: binding.EventMessage(v), - want: v, - }, - { - name: "From event without payload/" + test.NameOf(v), - encoding: binding.EncodingEvent, - message: binding.EventMessage(v), - want: v, - }, - }...) - } - for _, tt := range tests { - tt := tt // Don't use range variable in Run() scope - t.Run(tt.name, func(t *testing.T) { - got, encoding, err := binding.ToEvent(context.Background(), tt.message) - require.NoError(t, err) - require.Equal(t, tt.encoding, encoding) - test.AssertEventEquals(t, test.ExToStr(t, tt.want), test.ExToStr(t, got)) - }) - } -} diff --git a/v1/binding/transcoder.go b/v1/binding/transcoder.go deleted file mode 100644 index 9ff08f0fb..000000000 --- a/v1/binding/transcoder.go +++ /dev/null @@ -1,78 +0,0 @@ -package binding - -import ce "github.com/cloudevents/sdk-go/v1" - -// 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(encoder StructuredEncoder) StructuredEncoder - - // Can return nil if the transformation doesn't support binary encoding directly - BinaryTransformer(encoder BinaryEncoder) BinaryEncoder - - // 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(encoder StructuredEncoder) StructuredEncoder { - if encoder == nil { - return nil - } - res := encoder - for _, b := range t { - if b == nil { - continue - } - if r := b.StructuredTransformer(res); r != nil { - res = r - } else { - return nil // Structured not supported! - } - } - return res -} - -func (t TransformerFactories) BinaryTransformer(encoder BinaryEncoder) BinaryEncoder { - if encoder == nil { - return nil - } - res := encoder - for _, b := range t { - if b == nil { - continue - } - 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 *ce.Event) error { - for _, factory := range t { - if factory == nil { - continue - } - f := factory.EventTransformer() - if f != nil { - err := f(e) - if err != nil { - return err - } - } - } - return nil - } -} - -type EventTransformer func(*ce.Event) error diff --git a/v1/binding/transcoder/add_metadata.go b/v1/binding/transcoder/add_metadata.go deleted file mode 100644 index dc08a3db9..000000000 --- a/v1/binding/transcoder/add_metadata.go +++ /dev/null @@ -1,119 +0,0 @@ -package transcoder - -import ( - cloudevents "github.com/cloudevents/sdk-go/v1" - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/spec" -) - -// Add cloudevents attribute (if missing) during the encoding process -func AddAttribute(attributeKind spec.Kind, value interface{}) binding.TransformerFactory { - return setAttributeTranscoderFactory{attributeKind: attributeKind, value: value} -} - -// Add cloudevents extension (if missing) during the encoding process -func AddExtension(name string, value interface{}) binding.TransformerFactory { - return setExtensionTranscoderFactory{name: name, value: value} -} - -type setAttributeTranscoderFactory struct { - attributeKind spec.Kind - value interface{} -} - -func (a setAttributeTranscoderFactory) StructuredTransformer(binding.StructuredEncoder) binding.StructuredEncoder { - return nil -} - -func (a setAttributeTranscoderFactory) BinaryTransformer(encoder binding.BinaryEncoder) binding.BinaryEncoder { - return &setAttributeTransformer{ - BinaryEncoder: encoder, - attributeKind: a.attributeKind, - value: a.value, - found: false, - } -} - -func (a setAttributeTranscoderFactory) EventTransformer() binding.EventTransformer { - return func(event *cloudevents.Event) error { - v, err := spec.VS.Version(event.SpecVersion()) - if err != nil { - return err - } - if v.AttributeFromKind(a.attributeKind).Get(event.Context) == nil { - return v.AttributeFromKind(a.attributeKind).Set(event.Context, a.value) - } - return nil - } -} - -type setExtensionTranscoderFactory struct { - name string - value interface{} -} - -func (a setExtensionTranscoderFactory) StructuredTransformer(binding.StructuredEncoder) binding.StructuredEncoder { - return nil -} - -func (a setExtensionTranscoderFactory) BinaryTransformer(encoder binding.BinaryEncoder) binding.BinaryEncoder { - return &setExtensionTransformer{ - BinaryEncoder: encoder, - name: a.name, - value: a.value, - found: false, - } -} - -func (a setExtensionTranscoderFactory) EventTransformer() binding.EventTransformer { - return func(event *cloudevents.Event) error { - if _, ok := event.Extensions()[a.name]; !ok { - return event.Context.SetExtension(a.name, a.value) - } - return nil - } -} - -type setAttributeTransformer struct { - binding.BinaryEncoder - attributeKind spec.Kind - value interface{} - version spec.Version - found bool -} - -func (b *setAttributeTransformer) SetAttribute(attribute spec.Attribute, value interface{}) error { - if attribute.Kind() == b.attributeKind { - b.found = true - } - b.version = attribute.Version() - return b.BinaryEncoder.SetAttribute(attribute, value) -} - -func (b *setAttributeTransformer) End() error { - if !b.found { - return b.BinaryEncoder.SetAttribute(b.version.AttributeFromKind(b.attributeKind), b.value) - } - return nil -} - -type setExtensionTransformer struct { - binding.BinaryEncoder - name string - value interface{} - found bool -} - -func (b *setExtensionTransformer) SetExtension(name string, value interface{}) error { - if name == b.name { - b.found = true - } - return b.BinaryEncoder.SetExtension(name, value) -} - -func (b *setExtensionTransformer) End() error { - if !b.found { - return b.BinaryEncoder.SetExtension(b.name, b.value) - } - return nil -} diff --git a/v1/binding/transcoder/add_metadata_test.go b/v1/binding/transcoder/add_metadata_test.go deleted file mode 100644 index 92d8c3e22..000000000 --- a/v1/binding/transcoder/add_metadata_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package transcoder - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/spec" - "github.com/cloudevents/sdk-go/v1/binding/test" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -func TestAddAttribute(t *testing.T) { - e := test.MinEvent() - e.Context = e.Context.AsV1() - - subject := "aaa" - expectedEventWithSubject := test.CopyEventContext(e) - require.NoError(t, expectedEventWithSubject.Context.SetSubject(subject)) - - timestamp, err := types.ToTime(time.Now()) - require.NoError(t, err) - expectedEventWithTime := test.CopyEventContext(e) - require.NoError(t, expectedEventWithTime.Context.SetTime(timestamp)) - - test.RunTranscoderTests(t, context.Background(), []test.TranscoderTestArgs{ - { - Name: "No change to id to Mock Structured message", - InputMessage: test.NewMockStructuredMessage(test.CopyEventContext(e)), - WantEvent: test.CopyEventContext(e), - Transformers: binding.TransformerFactories{AddAttribute(spec.ID, "new-id")}, - }, - { - Name: "No change to id to Mock Binary message", - InputMessage: test.NewMockBinaryMessage(test.CopyEventContext(e)), - WantEvent: test.CopyEventContext(e), - Transformers: binding.TransformerFactories{AddAttribute(spec.ID, "new-id")}, - }, - { - Name: "No change to id to Event message", - InputMessage: binding.EventMessage(test.CopyEventContext(e)), - WantEvent: test.CopyEventContext(e), - Transformers: binding.TransformerFactories{AddAttribute(spec.ID, "new-id")}, - }, - { - Name: "Add subject to Mock Structured message", - InputMessage: test.NewMockStructuredMessage(test.CopyEventContext(e)), - WantEvent: expectedEventWithSubject, - Transformers: binding.TransformerFactories{AddAttribute(spec.Subject, subject)}, - }, - { - Name: "Add subject to Mock Binary message", - InputMessage: test.NewMockBinaryMessage(test.CopyEventContext(e)), - WantEvent: expectedEventWithSubject, - Transformers: binding.TransformerFactories{AddAttribute(spec.Subject, subject)}, - }, - { - Name: "Add subject to Event message", - InputMessage: binding.EventMessage(test.CopyEventContext(e)), - WantEvent: expectedEventWithSubject, - Transformers: binding.TransformerFactories{AddAttribute(spec.Subject, subject)}, - }, - { - Name: "Add time to Mock Structured message", - InputMessage: test.NewMockStructuredMessage(test.CopyEventContext(e)), - WantEvent: expectedEventWithTime, - Transformers: binding.TransformerFactories{AddAttribute(spec.Time, timestamp)}, - }, - { - Name: "Add time to Mock Binary message", - InputMessage: test.NewMockBinaryMessage(test.CopyEventContext(e)), - WantEvent: expectedEventWithTime, - Transformers: binding.TransformerFactories{AddAttribute(spec.Time, timestamp)}, - }, - { - Name: "Add time to Event message", - InputMessage: binding.EventMessage(test.CopyEventContext(e)), - WantEvent: expectedEventWithTime, - Transformers: binding.TransformerFactories{AddAttribute(spec.Time, timestamp)}, - }, - }) -} - -func TestAddExtension(t *testing.T) { - e := test.MinEvent() - e.Context = e.Context.AsV1() - - extName := "aaa" - extValue := "bbb" - expectedEventWithExtension := test.CopyEventContext(e) - require.NoError(t, expectedEventWithExtension.Context.SetExtension(extName, extValue)) - - test.RunTranscoderTests(t, context.Background(), []test.TranscoderTestArgs{ - { - Name: "No change to extension 'aaa' to Mock Structured message", - InputMessage: test.NewMockStructuredMessage(test.CopyEventContext(expectedEventWithExtension)), - WantEvent: test.CopyEventContext(expectedEventWithExtension), - Transformers: binding.TransformerFactories{AddExtension(extName, extValue)}, - }, - { - Name: "No change to extension 'aaa' to Mock Binary message", - InputMessage: test.NewMockBinaryMessage(test.CopyEventContext(expectedEventWithExtension)), - WantEvent: test.CopyEventContext(expectedEventWithExtension), - Transformers: binding.TransformerFactories{AddExtension(extName, extValue)}, - }, - { - Name: "No change to extension 'aaa' to Event message", - InputMessage: binding.EventMessage(test.CopyEventContext(expectedEventWithExtension)), - WantEvent: test.CopyEventContext(expectedEventWithExtension), - Transformers: binding.TransformerFactories{AddExtension(extName, extValue)}, - }, - { - Name: "Add extension 'aaa' to Mock Structured message", - InputMessage: test.NewMockStructuredMessage(test.CopyEventContext(e)), - WantEvent: test.CopyEventContext(expectedEventWithExtension), - Transformers: binding.TransformerFactories{AddExtension(extName, extValue)}, - }, - { - Name: "Add extension 'aaa' to Mock Binary message", - InputMessage: test.NewMockBinaryMessage(test.CopyEventContext(e)), - WantEvent: test.CopyEventContext(expectedEventWithExtension), - Transformers: binding.TransformerFactories{AddExtension(extName, extValue)}, - }, - { - Name: "Add extension 'aaa' to Event message", - InputMessage: binding.EventMessage(test.CopyEventContext(e)), - WantEvent: test.CopyEventContext(expectedEventWithExtension), - Transformers: binding.TransformerFactories{AddExtension(extName, extValue)}, - }, - }) -} diff --git a/v1/binding/transcoder/delete_metadata.go b/v1/binding/transcoder/delete_metadata.go deleted file mode 100644 index d87b3be56..000000000 --- a/v1/binding/transcoder/delete_metadata.go +++ /dev/null @@ -1,90 +0,0 @@ -package transcoder - -import ( - cloudevents "github.com/cloudevents/sdk-go/v1" - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/spec" -) - -// Delete cloudevents attribute during the encoding process -func DeleteAttribute(attributeKind spec.Kind) binding.TransformerFactory { - return deleteAttributeTranscoderFactory{attributeKind: attributeKind} -} - -// Delete cloudevents extension during the encoding process -func DeleteExtension(name string) binding.TransformerFactory { - return deleteExtensionTranscoderFactory{name: name} -} - -type deleteAttributeTranscoderFactory struct { - attributeKind spec.Kind -} - -func (a deleteAttributeTranscoderFactory) StructuredTransformer(binding.StructuredEncoder) binding.StructuredEncoder { - return nil -} - -func (a deleteAttributeTranscoderFactory) BinaryTransformer(encoder binding.BinaryEncoder) binding.BinaryEncoder { - return &deleteAttributeTransformer{ - BinaryEncoder: encoder, - attributeKind: a.attributeKind, - } -} - -func (a deleteAttributeTranscoderFactory) EventTransformer() binding.EventTransformer { - return func(event *cloudevents.Event) error { - v, err := spec.VS.Version(event.SpecVersion()) - if err != nil { - return err - } - if v.AttributeFromKind(a.attributeKind).Get(event.Context) != nil { - return v.AttributeFromKind(a.attributeKind).Delete(event.Context) - } - return nil - } -} - -type deleteExtensionTranscoderFactory struct { - name string -} - -func (a deleteExtensionTranscoderFactory) StructuredTransformer(binding.StructuredEncoder) binding.StructuredEncoder { - return nil -} - -func (a deleteExtensionTranscoderFactory) BinaryTransformer(encoder binding.BinaryEncoder) binding.BinaryEncoder { - return &deleteExtensionTransformer{ - BinaryEncoder: encoder, - name: a.name, - } -} - -func (a deleteExtensionTranscoderFactory) EventTransformer() binding.EventTransformer { - return func(event *cloudevents.Event) error { - return event.Context.SetExtension(a.name, nil) - } -} - -type deleteAttributeTransformer struct { - binding.BinaryEncoder - attributeKind spec.Kind -} - -func (b *deleteAttributeTransformer) SetAttribute(attribute spec.Attribute, value interface{}) error { - if attribute.Kind() == b.attributeKind { - return nil - } - return b.BinaryEncoder.SetAttribute(attribute, value) -} - -type deleteExtensionTransformer struct { - binding.BinaryEncoder - name string -} - -func (b *deleteExtensionTransformer) SetExtension(name string, value interface{}) error { - if b.name == name { - return nil - } - return b.BinaryEncoder.SetExtension(name, value) -} diff --git a/v1/binding/transcoder/delete_metadata_test.go b/v1/binding/transcoder/delete_metadata_test.go deleted file mode 100644 index f69e4c8da..000000000 --- a/v1/binding/transcoder/delete_metadata_test.go +++ /dev/null @@ -1,131 +0,0 @@ -package transcoder - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/spec" - "github.com/cloudevents/sdk-go/v1/binding/test" -) - -func TestDeleteAttribute(t *testing.T) { - withSubjectEvent := test.MinEvent() - withSubjectEvent.Context = withSubjectEvent.Context.AsV1() - require.NoError(t, withSubjectEvent.Context.SetSubject("aaa")) - - withTimeEvent := test.CopyEventContext(withSubjectEvent) - require.NoError(t, withTimeEvent.Context.SetTime(time.Now())) - - noSubjectEvent := test.CopyEventContext(withSubjectEvent) - require.NoError(t, noSubjectEvent.Context.SetSubject("")) - - test.RunTranscoderTests(t, context.Background(), []test.TranscoderTestArgs{ - { - Name: "Remove subject from Mock Structured message", - InputMessage: test.NewMockStructuredMessage(test.CopyEventContext(withSubjectEvent)), - WantEvent: noSubjectEvent, - Transformers: binding.TransformerFactories{DeleteAttribute(spec.Subject)}, - }, - { - Name: "Remove subject from Mock Binary message", - InputMessage: test.NewMockBinaryMessage(test.CopyEventContext(withSubjectEvent)), - WantEvent: noSubjectEvent, - Transformers: binding.TransformerFactories{DeleteAttribute(spec.Subject)}, - }, - { - Name: "Remove subject from Event message", - InputMessage: binding.EventMessage(test.CopyEventContext(withSubjectEvent)), - WantEvent: noSubjectEvent, - Transformers: binding.TransformerFactories{DeleteAttribute(spec.Subject)}, - }, - { - Name: "Remove time from Mock Structured message", - InputMessage: test.NewMockStructuredMessage(test.CopyEventContext(withTimeEvent)), - WantEvent: test.CopyEventContext(withSubjectEvent), - Transformers: binding.TransformerFactories{DeleteAttribute(spec.Time)}, - }, - { - Name: "Remove time from Mock Binary message", - InputMessage: test.NewMockBinaryMessage(test.CopyEventContext(withTimeEvent)), - WantEvent: test.CopyEventContext(withSubjectEvent), - Transformers: binding.TransformerFactories{DeleteAttribute(spec.Time)}, - }, - { - Name: "Remove time from Event message", - InputMessage: binding.EventMessage(test.CopyEventContext(withTimeEvent)), - WantEvent: test.CopyEventContext(withSubjectEvent), - Transformers: binding.TransformerFactories{DeleteAttribute(spec.Time)}, - }, - { - Name: "Do nothing with Mock Structured message", - InputMessage: test.NewMockStructuredMessage(test.CopyEventContext(withSubjectEvent)), - WantEvent: test.CopyEventContext(withSubjectEvent), - Transformers: binding.TransformerFactories{DeleteAttribute(spec.Time)}, - }, - { - Name: "Do nothing with Mock Binary message", - InputMessage: test.NewMockBinaryMessage(test.CopyEventContext(withSubjectEvent)), - WantEvent: test.CopyEventContext(withSubjectEvent), - Transformers: binding.TransformerFactories{DeleteAttribute(spec.Time)}, - }, - { - Name: "Do nothing with Event message", - InputMessage: binding.EventMessage(test.CopyEventContext(withSubjectEvent)), - WantEvent: test.CopyEventContext(withSubjectEvent), - Transformers: binding.TransformerFactories{DeleteAttribute(spec.Time)}, - }, - }) -} - -func TestDeleteExtension(t *testing.T) { - e := test.MinEvent() - e.Context = e.Context.AsV1() - - extName := "aaa" - extValue := "bbb" - expectedEventWithExtension := test.CopyEventContext(e) - require.NoError(t, expectedEventWithExtension.Context.SetExtension(extName, extValue)) - - test.RunTranscoderTests(t, context.Background(), []test.TranscoderTestArgs{ - { - Name: "No change to Mock Structured message", - InputMessage: test.NewMockStructuredMessage(test.CopyEventContext(expectedEventWithExtension)), - WantEvent: test.CopyEventContext(expectedEventWithExtension), - Transformers: binding.TransformerFactories{DeleteExtension("ccc")}, - }, - { - Name: "No change to Mock Binary message", - InputMessage: test.NewMockBinaryMessage(test.CopyEventContext(expectedEventWithExtension)), - WantEvent: test.CopyEventContext(expectedEventWithExtension), - Transformers: binding.TransformerFactories{DeleteExtension("ccc")}, - }, - { - Name: "No change to Event message", - InputMessage: binding.EventMessage(test.CopyEventContext(expectedEventWithExtension)), - WantEvent: test.CopyEventContext(expectedEventWithExtension), - Transformers: binding.TransformerFactories{DeleteExtension("ccc")}, - }, - { - Name: "Delete extension 'aaa' from Mock Structured message", - InputMessage: test.NewMockStructuredMessage(test.CopyEventContext(expectedEventWithExtension)), - WantEvent: test.CopyEventContext(e), - Transformers: binding.TransformerFactories{DeleteExtension(extName)}, - }, - { - Name: "Delete extension 'aaa' from Mock Binary message", - InputMessage: test.NewMockBinaryMessage(test.CopyEventContext(expectedEventWithExtension)), - WantEvent: test.CopyEventContext(e), - Transformers: binding.TransformerFactories{DeleteExtension(extName)}, - }, - { - Name: "Delete extension 'aaa' from Event message", - InputMessage: binding.EventMessage(test.CopyEventContext(expectedEventWithExtension)), - WantEvent: test.CopyEventContext(e), - Transformers: binding.TransformerFactories{DeleteExtension(extName)}, - }, - }) -} diff --git a/v1/binding/transcoder/update_metadata.go b/v1/binding/transcoder/update_metadata.go deleted file mode 100644 index d93c9d0a5..000000000 --- a/v1/binding/transcoder/update_metadata.go +++ /dev/null @@ -1,125 +0,0 @@ -package transcoder - -import ( - cloudevents "github.com/cloudevents/sdk-go/v1" - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/spec" -) - -// Update cloudevents attribute (if present) using the provided function during the encoding process -func UpdateAttribute(attributeKind spec.Kind, updater func(interface{}) (interface{}, error)) binding.TransformerFactory { - return updateAttributeTranscoderFactory{attributeKind: attributeKind, updater: updater} -} - -// Update cloudevents extension (if present) using the provided function during the encoding process -func UpdateExtension(name string, updater func(interface{}) (interface{}, error)) binding.TransformerFactory { - return updateExtensionTranscoderFactory{name: name, updater: updater} -} - -type updateAttributeTranscoderFactory struct { - attributeKind spec.Kind - updater func(interface{}) (interface{}, error) -} - -func (a updateAttributeTranscoderFactory) StructuredTransformer(binding.StructuredEncoder) binding.StructuredEncoder { - return nil -} - -func (a updateAttributeTranscoderFactory) BinaryTransformer(encoder binding.BinaryEncoder) binding.BinaryEncoder { - return &updateAttributeTransformer{ - BinaryEncoder: encoder, - attributeKind: a.attributeKind, - updater: a.updater, - } -} - -func (a updateAttributeTranscoderFactory) EventTransformer() binding.EventTransformer { - return func(event *cloudevents.Event) error { - v, err := spec.VS.Version(event.SpecVersion()) - if err != nil { - return err - } - if val := v.AttributeFromKind(a.attributeKind).Get(event.Context); val != nil { - newVal, err := a.updater(val) - if err != nil { - return err - } - if newVal == nil { - return v.AttributeFromKind(a.attributeKind).Delete(event.Context) - } else { - return v.AttributeFromKind(a.attributeKind).Set(event.Context, newVal) - } - } - return nil - } -} - -type updateExtensionTranscoderFactory struct { - name string - updater func(interface{}) (interface{}, error) -} - -func (a updateExtensionTranscoderFactory) StructuredTransformer(binding.StructuredEncoder) binding.StructuredEncoder { - return nil -} - -func (a updateExtensionTranscoderFactory) BinaryTransformer(encoder binding.BinaryEncoder) binding.BinaryEncoder { - return &updateExtensionTransformer{ - BinaryEncoder: encoder, - name: a.name, - updater: a.updater, - } -} - -func (a updateExtensionTranscoderFactory) EventTransformer() binding.EventTransformer { - return func(event *cloudevents.Event) error { - if val, ok := event.Extensions()[a.name]; ok { - newVal, err := a.updater(val) - if err != nil { - return err - } - return event.Context.SetExtension(a.name, newVal) - } - return nil - } -} - -type updateAttributeTransformer struct { - binding.BinaryEncoder - attributeKind spec.Kind - updater func(interface{}) (interface{}, error) -} - -func (b *updateAttributeTransformer) SetAttribute(attribute spec.Attribute, value interface{}) error { - if attribute.Kind() == b.attributeKind { - newVal, err := b.updater(value) - if err != nil { - return err - } - if newVal != nil { - return b.BinaryEncoder.SetAttribute(attribute, newVal) - } - return nil - } - return b.BinaryEncoder.SetAttribute(attribute, value) -} - -type updateExtensionTransformer struct { - binding.BinaryEncoder - name string - updater func(interface{}) (interface{}, error) -} - -func (b *updateExtensionTransformer) SetExtension(name string, value interface{}) error { - if name == b.name { - newVal, err := b.updater(value) - if err != nil { - return err - } - if newVal != nil { - return b.BinaryEncoder.SetExtension(name, newVal) - } - return nil - } - return b.BinaryEncoder.SetExtension(name, value) -} diff --git a/v1/binding/transcoder/update_metadata_test.go b/v1/binding/transcoder/update_metadata_test.go deleted file mode 100644 index b362de14b..000000000 --- a/v1/binding/transcoder/update_metadata_test.go +++ /dev/null @@ -1,152 +0,0 @@ -package transcoder - -import ( - "context" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/spec" - "github.com/cloudevents/sdk-go/v1/binding/test" -) - -func TestUpdateAttribute(t *testing.T) { - withSubjectEvent := test.MinEvent() - withSubjectEvent.Context = withSubjectEvent.Context.AsV1() - require.NoError(t, withSubjectEvent.Context.SetSubject("abc")) - - subjectUpdateFunc := func(v interface{}) (interface{}, error) { - return strings.ToUpper(v.(string)), nil - } - updatedSubjectEvent := test.CopyEventContext(withSubjectEvent) - require.NoError(t, updatedSubjectEvent.Context.SetSubject(strings.ToUpper("abc"))) - - location, err := time.LoadLocation("UTC") - require.NoError(t, err) - timestamp := time.Now().In(location) - withTimeEvent := test.CopyEventContext(withSubjectEvent) - require.NoError(t, withTimeEvent.Context.SetTime(timestamp)) - - timeUpdateFunc := func(v interface{}) (interface{}, error) { - return v.(time.Time).Add(3 * time.Hour), nil - } - updatedTimeEvent := test.CopyEventContext(withTimeEvent) - require.NoError(t, updatedTimeEvent.Context.SetTime(timestamp.Add(3*time.Hour))) - - test.RunTranscoderTests(t, context.Background(), []test.TranscoderTestArgs{ - { - Name: "Update subject in Mock Structured message", - InputMessage: test.NewMockStructuredMessage(test.CopyEventContext(withSubjectEvent)), - WantEvent: test.CopyEventContext(updatedSubjectEvent), - Transformers: binding.TransformerFactories{UpdateAttribute(spec.Subject, subjectUpdateFunc)}, - }, - { - Name: "Update subject in Mock Binary message", - InputMessage: test.NewMockBinaryMessage(test.CopyEventContext(withSubjectEvent)), - WantEvent: test.CopyEventContext(updatedSubjectEvent), - Transformers: binding.TransformerFactories{UpdateAttribute(spec.Subject, subjectUpdateFunc)}, - }, - { - Name: "Update subject in Event message", - InputMessage: binding.EventMessage(test.CopyEventContext(withSubjectEvent)), - WantEvent: test.CopyEventContext(updatedSubjectEvent), - Transformers: binding.TransformerFactories{UpdateAttribute(spec.Subject, subjectUpdateFunc)}, - }, - { - Name: "Update time in Mock Structured message", - InputMessage: test.NewMockStructuredMessage(test.CopyEventContext(withTimeEvent)), - WantEvent: test.CopyEventContext(updatedTimeEvent), - Transformers: binding.TransformerFactories{UpdateAttribute(spec.Time, timeUpdateFunc)}, - }, - { - Name: "Update time in Mock Binary message", - InputMessage: test.NewMockBinaryMessage(test.CopyEventContext(withTimeEvent)), - WantEvent: test.CopyEventContext(updatedTimeEvent), - Transformers: binding.TransformerFactories{UpdateAttribute(spec.Time, timeUpdateFunc)}, - }, - { - Name: "Update time in Event message", - InputMessage: binding.EventMessage(test.CopyEventContext(withTimeEvent)), - WantEvent: test.CopyEventContext(updatedTimeEvent), - Transformers: binding.TransformerFactories{UpdateAttribute(spec.Time, timeUpdateFunc)}, - }, - { - Name: "Do nothing with Mock Structured message", - InputMessage: test.NewMockStructuredMessage(test.CopyEventContext(withSubjectEvent)), - WantEvent: test.CopyEventContext(withSubjectEvent), - Transformers: binding.TransformerFactories{UpdateAttribute(spec.DataContentType, func(i interface{}) (interface{}, error) { - return "text/plain", nil - })}, - }, - { - Name: "Do nothing with Mock Binary message", - InputMessage: test.NewMockBinaryMessage(test.CopyEventContext(withSubjectEvent)), - WantEvent: test.CopyEventContext(withSubjectEvent), - Transformers: binding.TransformerFactories{UpdateAttribute(spec.DataContentType, func(i interface{}) (interface{}, error) { - return "text/plain", nil - })}, - }, - { - Name: "Do nothing with Event message", - InputMessage: binding.EventMessage(test.CopyEventContext(withSubjectEvent)), - WantEvent: test.CopyEventContext(withSubjectEvent), - Transformers: binding.TransformerFactories{UpdateAttribute(spec.DataContentType, func(i interface{}) (interface{}, error) { - return "text/plain", nil - })}, - }, - }) -} - -func TestUpdateExtension(t *testing.T) { - e := test.MinEvent() - e.Context = e.Context.AsV1() - require.NoError(t, e.Context.SetExtension("aaa", "bbb")) - - updateFunc := func(v interface{}) (interface{}, error) { - return strings.ToUpper(v.(string)), nil - } - updatedExtensionEvent := test.CopyEventContext(e) - require.NoError(t, updatedExtensionEvent.Context.SetExtension("aaa", strings.ToUpper("bbb"))) - - test.RunTranscoderTests(t, context.Background(), []test.TranscoderTestArgs{ - { - Name: "No change in Mock Structured message", - InputMessage: test.NewMockStructuredMessage(test.CopyEventContext(e)), - WantEvent: test.CopyEventContext(e), - Transformers: binding.TransformerFactories{UpdateExtension("ccc", updateFunc)}, - }, - { - Name: "No change in Mock Binary message", - InputMessage: test.NewMockBinaryMessage(test.CopyEventContext(e)), - WantEvent: test.CopyEventContext(e), - Transformers: binding.TransformerFactories{UpdateExtension("ccc", updateFunc)}, - }, - { - Name: "No change in Event message", - InputMessage: binding.EventMessage(test.CopyEventContext(e)), - WantEvent: test.CopyEventContext(e), - Transformers: binding.TransformerFactories{UpdateExtension("ccc", updateFunc)}, - }, - { - Name: "Update extension 'aaa' in Mock Structured message", - InputMessage: test.NewMockStructuredMessage(test.CopyEventContext(e)), - WantEvent: test.CopyEventContext(updatedExtensionEvent), - Transformers: binding.TransformerFactories{UpdateExtension("aaa", updateFunc)}, - }, - { - Name: "Update extension 'aaa' in Mock Binary message", - InputMessage: test.NewMockBinaryMessage(test.CopyEventContext(e)), - WantEvent: test.CopyEventContext(updatedExtensionEvent), - Transformers: binding.TransformerFactories{UpdateExtension("aaa", updateFunc)}, - }, - { - Name: "Update extension 'aaa' in Event message", - InputMessage: binding.EventMessage(test.CopyEventContext(e)), - WantEvent: test.CopyEventContext(updatedExtensionEvent), - Transformers: binding.TransformerFactories{UpdateExtension("aaa", updateFunc)}, - }, - }) -} diff --git a/v1/binding/transcoder/version.go b/v1/binding/transcoder/version.go deleted file mode 100644 index 7690c8483..000000000 --- a/v1/binding/transcoder/version.go +++ /dev/null @@ -1,44 +0,0 @@ -package transcoder - -import ( - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/spec" - ce "github.com/cloudevents/sdk-go/v1/cloudevents" -) - -// Returns a TransformerFactory that converts the event context version to the specified one. -func Version(version spec.Version) binding.TransformerFactory { - return versionTranscoderFactory{version: version} -} - -type versionTranscoderFactory struct { - version spec.Version -} - -func (v versionTranscoderFactory) StructuredTransformer(binding.StructuredEncoder) binding.StructuredEncoder { - return nil // Not supported, must fallback to EventTransformer! -} - -func (v versionTranscoderFactory) BinaryTransformer(encoder binding.BinaryEncoder) binding.BinaryEncoder { - return binaryVersionTransformer{BinaryEncoder: encoder, version: v.version} -} - -func (v versionTranscoderFactory) EventTransformer() binding.EventTransformer { - return func(e *ce.Event) error { - e.Context = v.version.Convert(e.Context) - return nil - } -} - -type binaryVersionTransformer struct { - binding.BinaryEncoder - version spec.Version -} - -func (b binaryVersionTransformer) SetAttribute(attribute spec.Attribute, value interface{}) error { - if attribute.Kind() == spec.SpecVersion { - return b.BinaryEncoder.SetAttribute(b.version.AttributeFromKind(spec.SpecVersion), b.version.String()) - } - attributeInDifferentVersion := b.version.AttributeFromKind(attribute.Kind()) - return b.BinaryEncoder.SetAttribute(attributeInDifferentVersion, value) -} diff --git a/v1/binding/transcoder/version_test.go b/v1/binding/transcoder/version_test.go deleted file mode 100644 index 518fcafbf..000000000 --- a/v1/binding/transcoder/version_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package transcoder - -import ( - "context" - "net/url" - "testing" - - "github.com/stretchr/testify/require" - - cloudevents "github.com/cloudevents/sdk-go/v1" - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/spec" - "github.com/cloudevents/sdk-go/v1/binding/test" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -func TestVersionTranscoder(t *testing.T) { - var testEventV02 = cloudevents.Event{ - Context: cloudevents.EventContextV02{ - Source: types.URLRef{URL: url.URL{Path: "source"}}, - ContentType: cloudevents.StringOfApplicationJSON(), - ID: "id", - Type: "type", - }.AsV02(), - } - - var testEventV1 = testEventV02 - testEventV1.Context = testEventV02.Context.AsV1() - - data := []byte("\"data\"") - err := testEventV02.SetData(data) - require.NoError(t, err) - err = testEventV1.SetData(data) - require.NoError(t, err) - - test.RunTranscoderTests(t, context.Background(), []test.TranscoderTestArgs{ - { - Name: "V02 -> V1 with Mock Structured message", - InputMessage: test.NewMockStructuredMessage(test.CopyEventContext(testEventV02)), - WantEvent: test.CopyEventContext(testEventV1), - Transformers: binding.TransformerFactories{Version(spec.V1)}, - }, - { - Name: "V02 -> V1 with Mock Binary message", - InputMessage: test.NewMockBinaryMessage(test.CopyEventContext(testEventV02)), - WantEvent: test.CopyEventContext(testEventV1), - Transformers: binding.TransformerFactories{Version(spec.V1)}, - }, - { - Name: "V02 -> V1 with Event message", - InputMessage: binding.EventMessage(test.CopyEventContext(testEventV02)), - WantEvent: test.CopyEventContext(testEventV1), - Transformers: binding.TransformerFactories{Version(spec.V1)}, - }, - }) -} diff --git a/v1/binding/translate.go b/v1/binding/translate.go deleted file mode 100644 index 7cc30d994..000000000 --- a/v1/binding/translate.go +++ /dev/null @@ -1,148 +0,0 @@ -package binding - -import ( - "context" - "errors" - - ce "github.com/cloudevents/sdk-go/v1" -) - -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. createRootStructuredEncoder and createRootBinaryEncoder could be null if the protocol doesn't support it -// -// Returns: -// * EncodingStructured, nil if message was structured and correctly translated to Event -// * EncodingBinary, nil if message was binary and correctly translated to Event -// * EncodingStructured, err if message was structured but error happened during translation -// * BinaryEncoding, err if message was binary but error happened during translation -// * EncodingUnknown, ErrUnknownEncoding if message is not recognized -func RunDirectEncoding( - ctx context.Context, - message Message, - structuredEncoder StructuredEncoder, - binaryEncoder BinaryEncoder, - factories TransformerFactories, -) (Encoding, error) { - if structuredEncoder != nil && !GetOrDefaultFromCtx(ctx, SKIP_DIRECT_STRUCTURED_ENCODING, false).(bool) { - // Wrap the transformers in the structured builder - structuredEncoder = factories.StructuredTransformer(structuredEncoder) - - // StructuredTransformer could return nil if one of transcoders doesn't support - // direct structured transcoding - if structuredEncoder != nil { - if err := message.Structured(ctx, structuredEncoder); err == nil { - return EncodingStructured, nil - } else if err != ErrNotStructured { - return EncodingStructured, err - } - } - } - - if binaryEncoder != nil && !GetOrDefaultFromCtx(ctx, SKIP_DIRECT_BINARY_ENCODING, false).(bool) { - binaryEncoder = factories.BinaryTransformer(binaryEncoder) - if binaryEncoder != nil { - if err := message.Binary(ctx, binaryEncoder); 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 RunEncoders -// 2. If no direct encoding is possible, it goes through ToEvent to generate an event representation -// 3. Using the encoders previously defined -// You can tweak the encoding process using the context decorators WithForceStructured, WithForceStructured, etc. -// This function guarantees that transformers are invoked only one time during the encoding process. -// Returns: -// * EncodingStructured, nil if message was structured and correctly translated to Event -// * EncodingBinary, nil if message was binary and correctly translated to Event -// * EncodingStructured, err if message was structured but error happened during translation -// * BinaryEncoding, err if message was binary but error happened during translation -// * EncodingUnknown, ErrUnknownEncoding if message is not recognized -func Encode( - ctx context.Context, - message Message, - structuredEncoder StructuredEncoder, - binaryEncoder BinaryEncoder, - transformers TransformerFactories, -) (Encoding, error) { - enc := message.Encoding() - var err error - // Skip direct encoding if the event is an event message - if enc != EncodingEvent { - enc, err = RunDirectEncoding(ctx, message, structuredEncoder, binaryEncoder, transformers) - if enc != EncodingUnknown { - // Message directly encoded, nothing else to do here - return enc, err - } - } - - var e ce.Event - e, enc, err = ToEvent(ctx, message, transformers) - if err != nil { - return enc, err - } - - message = EventMessage(e) - - if GetOrDefaultFromCtx(ctx, PREFERRED_EVENT_ENCODING, EncodingBinary).(Encoding) == EncodingStructured { - if structuredEncoder != nil { - return EncodingStructured, message.Structured(ctx, structuredEncoder) - } - if binaryEncoder != nil { - return EncodingBinary, message.Binary(ctx, binaryEncoder) - } - } else { - if binaryEncoder != nil { - return EncodingBinary, message.Binary(ctx, binaryEncoder) - } - if structuredEncoder != nil { - return EncodingStructured, message.Structured(ctx, structuredEncoder) - } - } - - return enc, errors.New("cannot find a suitable encoder to use from EventMessage") -} - -// 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) -} - -func GetOrDefaultFromCtx(ctx context.Context, key string, def interface{}) interface{} { - if val := ctx.Value(key); val != nil { - return val - } else { - return def - } -} diff --git a/v1/binding/transport.go b/v1/binding/transport.go deleted file mode 100644 index cc17123a7..000000000 --- a/v1/binding/transport.go +++ /dev/null @@ -1,66 +0,0 @@ -package binding - -import ( - "context" - "io" - - ce "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport" -) - -// BindingTransport implements transport.Transport using a Sender and Receiver. -type BindingTransport struct { - Sender Sender - Receiver Receiver - SenderContextDecorators []func(context.Context) context.Context - handler transport.Receiver -} - -var _ transport.Transport = (*BindingTransport)(nil) // Conforms to the interface - -func NewTransportAdapter(s Sender, r Receiver, senderContextDecorators []func(context.Context) context.Context) *BindingTransport { - if senderContextDecorators == nil { - senderContextDecorators = []func(ctx context.Context) context.Context{} - } - return &BindingTransport{Sender: s, Receiver: r, SenderContextDecorators: senderContextDecorators} -} - -func (t *BindingTransport) Send(ctx context.Context, e ce.Event) (context.Context, *ce.Event, error) { - for _, f := range t.SenderContextDecorators { - ctx = f(ctx) - } - return ctx, nil, t.Sender.Send(ctx, EventMessage(e)) -} - -func (t *BindingTransport) SetReceiver(r transport.Receiver) { t.handler = r } - -func (t *BindingTransport) StartReceiver(ctx context.Context) error { - for { - m, err := t.Receiver.Receive(ctx) - if err == io.EOF { // Normal close - return nil - } else if err != nil { - return err - } - if err := t.handle(ctx, m); err != nil { - return err - } - } -} - -func (t *BindingTransport) handle(ctx context.Context, m Message) (err error) { - defer func() { - if err2 := m.Finish(err); err == nil { - err = err2 - } - }() - e, _, err := ToEvent(ctx, m) - if err != nil { - return err - } - return t.handler.Receive(ctx, e, nil) -} - -func (t *BindingTransport) SetConverter(transport.Converter) {} -func (t *BindingTransport) HasConverter() bool { return false } -func (t *BindingTransport) HasTracePropagation() bool { return false } diff --git a/v1/binding/transport_test.go b/v1/binding/transport_test.go deleted file mode 100644 index 40b7fa30b..000000000 --- a/v1/binding/transport_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package binding_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - - cloudevents "github.com/cloudevents/sdk-go/v1" - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/test" -) - -func TestTransportSend(t *testing.T) { - messageChannel := make(chan binding.Message, 1) - transport := binding.NewTransportAdapter(binding.ChanSender(messageChannel), binding.ChanReceiver(messageChannel), nil) - ev := test.MinEvent() - - client, err := cloudevents.NewClient(transport, cloudevents.WithoutTracePropagation()) - require.NoError(t, err) - - _, _, err = client.Send(context.Background(), ev) - require.NoError(t, err) - - result := <-messageChannel - - test.AssertEventEquals(t, ev, cloudevents.Event(result.(binding.EventMessage))) -} - -func TestTransportReceive(t *testing.T) { - messageChannel := make(chan binding.Message, 1) - eventReceivedChannel := make(chan cloudevents.Event, 1) - transport := binding.NewTransportAdapter(binding.ChanSender(messageChannel), binding.ChanReceiver(messageChannel), nil) - ev := test.MinEvent() - - client, err := cloudevents.NewClient(transport) - require.NoError(t, err) - - messageChannel <- binding.EventMessage(ev) - - go func() { - err = client.StartReceiver(context.Background(), func(event cloudevents.Event) { - eventReceivedChannel <- event - }) - require.NoError(t, err) - }() - - result := <-eventReceivedChannel - - test.AssertEventEquals(t, ev, result) -} diff --git a/v1/bindings/amqp/amqp_test.go b/v1/bindings/amqp/amqp_test.go deleted file mode 100644 index 9fb90d3d2..000000000 --- a/v1/bindings/amqp/amqp_test.go +++ /dev/null @@ -1,115 +0,0 @@ -// +build amqp - -package amqp - -import ( - "context" - "io" - "net/url" - "os" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "pack.ag/amqp" - - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/test" - ce "github.com/cloudevents/sdk-go/v1/cloudevents" -) - -func TestSendSkipBinary(t *testing.T) { - c, s, r := testSenderReceiver(t) - defer c.Close() - test.EachEvent(t, test.Events(), func(t *testing.T, e ce.Event) { - eventIn := test.ExToStr(t, e) - in := test.NewMockBinaryMessage(eventIn) - test.SendReceive(t, binding.WithSkipDirectBinaryEncoding(binding.WithPreferredEventEncoding(context.Background(), binding.EncodingStructured), true), in, s, r, func(out binding.Message) { - eventOut, encoding := test.MustToEvent(context.TODO(), out) - assert.Equal(t, encoding, binding.EncodingStructured) - test.AssertEventEquals(t, eventIn, test.ExToStr(t, eventOut)) - }) - }) -} - -func TestSendSkipStructured(t *testing.T) { - c, s, r := testSenderReceiver(t) - defer c.Close() - test.EachEvent(t, test.Events(), func(t *testing.T, e ce.Event) { - eventIn := test.ExToStr(t, e) - in := test.NewMockStructuredMessage(eventIn) - test.SendReceive(t, binding.WithSkipDirectStructuredEncoding(context.Background(), true), in, s, r, func(out binding.Message) { - eventOut, encoding := test.MustToEvent(context.Background(), out) - assert.Equal(t, encoding, binding.EncodingBinary) - test.AssertEventEquals(t, eventIn, test.ExToStr(t, eventOut)) - }) - }) -} - -func TestSendEventReceiveStruct(t *testing.T) { - c, s, r := testSenderReceiver(t) - defer c.Close() - test.EachEvent(t, test.Events(), func(t *testing.T, e ce.Event) { - eventIn := test.ExToStr(t, e) - in := binding.EventMessage(eventIn) - test.SendReceive(t, binding.WithPreferredEventEncoding(context.TODO(), binding.EncodingStructured), in, s, r, func(out binding.Message) { - eventOut, encoding := test.MustToEvent(context.Background(), out) - assert.Equal(t, encoding, binding.EncodingStructured) - test.AssertEventEquals(t, eventIn, test.ExToStr(t, eventOut)) - }) - }) -} - -func TestSendEventReceiveBinary(t *testing.T) { - c, s, r := testSenderReceiver(t) - defer c.Close() - test.EachEvent(t, test.Events(), func(t *testing.T, e ce.Event) { - eventIn := test.ExToStr(t, e) - in := binding.EventMessage(eventIn) - test.SendReceive(t, context.Background(), in, s, r, func(out binding.Message) { - eventOut, encoding := test.MustToEvent(context.Background(), out) - assert.Equal(t, encoding, binding.EncodingBinary) - test.AssertEventEquals(t, eventIn, test.ExToStr(t, eventOut)) - }) - }) -} - -// Ideally add AMQP server support to the binding. - -// Some test require an AMQP broker or router. If the connection fails -// the tests are skipped. The env variable TEST_AMQP_URL can be set to the -// test URL, otherwise the default is "/test" -// -// On option is http://qpid.apache.org/components/dispatch-router/indexthtml. -// It can be installed from source or from RPMs, see https://qpid.apache.org/packages.html -// Run `qdrouterd` and the tests will work with no further config. -func testClient(t testing.TB) (client *amqp.Client, session *amqp.Session, addr string) { - t.Helper() - addr = "test" - s := os.Getenv("TEST_AMQP_URL") - if u, err := url.Parse(s); err == nil && u.Path != "" { - addr = u.Path - } - client, err := amqp.Dial(s) - if err != nil { - t.Skipf("ampq.Dial(%#v): %v", s, err) - } - session, err = client.NewSession() - require.NoError(t, err) - return client, session, addr -} - -func testSenderReceiver(t testing.TB, senderOptions ...SenderOptionFunc) (io.Closer, binding.Sender, binding.Receiver) { - c, ss, a := testClient(t) - r, err := ss.NewReceiver(amqp.LinkSourceAddress(a)) - require.NoError(t, err) - s, err := ss.NewSender(amqp.LinkTargetAddress(a)) - require.NoError(t, err) - return c, NewSender(s, senderOptions...), &Receiver{r} -} - -func BenchmarkSendReceive(b *testing.B) { - c, s, r := testSenderReceiver(b) - defer func() { require.NoError(b, c.Close()) }() - test.BenchmarkSendReceive(b, s, r) -} diff --git a/v1/bindings/amqp/doc.go b/v1/bindings/amqp/doc.go deleted file mode 100644 index e2062e4e8..000000000 --- a/v1/bindings/amqp/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -/* -Package amqp implements an AMQP binding. -*/ -package amqp - -// TODO(slinkydeveloper) diff --git a/v1/bindings/amqp/encoder.go b/v1/bindings/amqp/encoder.go deleted file mode 100644 index 016bf2047..000000000 --- a/v1/bindings/amqp/encoder.go +++ /dev/null @@ -1,90 +0,0 @@ -package amqp - -import ( - "context" - "io" - "io/ioutil" - - "pack.ag/amqp" - - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/format" - "github.com/cloudevents/sdk-go/v1/binding/spec" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -// Fill the provided amqpMessage with the message m. -// Using context you can tweak the encoding processing (more details on binding.Translate documentation). -func EncodeAMQPMessage(ctx context.Context, m binding.Message, amqpMessage *amqp.Message, transformerFactories ...binding.TransformerFactory) error { - structuredEncoder := (*amqpMessageEncoder)(amqpMessage) - binaryEncoder := (*amqpMessageEncoder)(amqpMessage) - - _, err := binding.Encode( - ctx, - m, - structuredEncoder, - binaryEncoder, - transformerFactories, - ) - return err -} - -type amqpMessageEncoder amqp.Message - -func (b *amqpMessageEncoder) SetStructuredEvent(ctx context.Context, format format.Format, event io.Reader) error { - val, err := ioutil.ReadAll(event) - if err != nil { - return err - } - b.Data = [][]byte{val} - b.Properties = &amqp.MessageProperties{ContentType: format.MediaType()} - return nil -} - -func (b *amqpMessageEncoder) Start(ctx context.Context) error { - b.Properties = &amqp.MessageProperties{} - b.ApplicationProperties = make(map[string]interface{}) - return nil -} - -func (b *amqpMessageEncoder) End() error { - return nil -} - -func (b *amqpMessageEncoder) SetData(reader io.Reader) error { - data, err := ioutil.ReadAll(reader) - if err != nil { - return err - } - b.Data = [][]byte{data} - return nil -} - -func (b *amqpMessageEncoder) SetAttribute(attribute spec.Attribute, value interface{}) error { - if attribute.Kind() == spec.DataContentType { - s, err := types.Format(value) - if err != nil { - return err - } - b.Properties.ContentType = s - } else { - v, err := safeAMQPPropertiesUnwrap(value) - if err != nil { - return err - } - b.ApplicationProperties[prefix+attribute.Name()] = v - } - return nil -} - -func (b *amqpMessageEncoder) SetExtension(name string, value interface{}) error { - v, err := safeAMQPPropertiesUnwrap(value) - if err != nil { - return err - } - b.ApplicationProperties[prefix+name] = v - return nil -} - -var _ binding.BinaryEncoder = (*amqpMessageEncoder)(nil) // Test it conforms to the interface -var _ binding.StructuredEncoder = (*amqpMessageEncoder)(nil) // Test it conforms to the interface diff --git a/v1/bindings/amqp/message.go b/v1/bindings/amqp/message.go deleted file mode 100644 index 897f4a3b5..000000000 --- a/v1/bindings/amqp/message.go +++ /dev/null @@ -1,114 +0,0 @@ -package amqp - -import ( - "bytes" - "context" - "errors" - "reflect" - "strings" - - "pack.ag/amqp" - - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/format" - "github.com/cloudevents/sdk-go/v1/binding/spec" -) - -const prefix = "cloudEvents:" // Name prefix for AMQP properties that hold CE attributes. - -var ( - // Use the package path as AMQP error condition name - condition = amqp.ErrorCondition(reflect.TypeOf(Message{}).PkgPath()) - specs = spec.WithPrefix(prefix) -) - -// Message implements binding.Message by wrapping an *amqp.Message. -type Message struct { - AMQP *amqp.Message - encoding binding.Encoding -} - -func NewMessage(message *amqp.Message) *Message { - if message.Properties != nil && format.IsFormat(message.Properties.ContentType) { - return &Message{AMQP: message, encoding: binding.EncodingStructured} - } else if _, err := specs.FindVersion(func(k string) string { - s, _ := message.ApplicationProperties[k].(string) - return s - }); err == nil { - return &Message{AMQP: message, encoding: binding.EncodingBinary} - } else { - return &Message{AMQP: message, encoding: binding.EncodingUnknown} - } -} - -// Check if amqp.Message implements binding.Message -var _ binding.Message = (*Message)(nil) - -func (m *Message) Encoding() binding.Encoding { - return m.encoding -} - -func (m *Message) Structured(ctx context.Context, encoder binding.StructuredEncoder) error { - if m.AMQP.Properties != nil && format.IsFormat(m.AMQP.Properties.ContentType) { - return encoder.SetStructuredEvent(ctx, format.Lookup(m.AMQP.Properties.ContentType), bytes.NewReader(m.AMQP.GetData())) - } - return binding.ErrNotStructured -} - -func (m *Message) Binary(ctx context.Context, encoder binding.BinaryEncoder) error { - if len(m.AMQP.ApplicationProperties) == 0 { - return errors.New("AMQP CloudEvents message has no application properties") - } - version, err := specs.FindVersion(func(k string) string { - s, _ := m.AMQP.ApplicationProperties[k].(string) - return s - }) - if err != nil { - return err - } - - err = encoder.Start(ctx) - if err != nil { - return err - } - - if m.AMQP.Properties != nil && m.AMQP.Properties.ContentType != "" { - err = encoder.SetAttribute(version.AttributeFromKind(spec.DataContentType), m.AMQP.Properties.ContentType) - if err != nil { - return err - } - } - - for k, v := range m.AMQP.ApplicationProperties { - if strings.HasPrefix(k, prefix) { - attr := version.Attribute(k) - if attr != nil { - err = encoder.SetAttribute(attr, v) - } else { - err = encoder.SetExtension(strings.ToLower(strings.TrimPrefix(k, prefix)), v) - } - } - if err != nil { - return err - } - } - - data := m.AMQP.GetData() - if len(data) != 0 { // Some data - err = encoder.SetData(bytes.NewReader(data)) - if err != nil { - return err - } - } - return encoder.End() -} - -func (m *Message) Finish(err error) error { - if err != nil { - return m.AMQP.Reject(&amqp.Error{ - Condition: condition, - Description: err.Error(), - }) - } - return m.AMQP.Accept() -} diff --git a/v1/bindings/amqp/option.go b/v1/bindings/amqp/option.go deleted file mode 100644 index 448b8323d..000000000 --- a/v1/bindings/amqp/option.go +++ /dev/null @@ -1,11 +0,0 @@ -package amqp - -import "github.com/cloudevents/sdk-go/v1/binding" - -type SenderOptionFunc func(sender *Sender) - -func WithTranscoder(factory binding.TransformerFactory) SenderOptionFunc { - return func(sender *Sender) { - sender.transformerFactories = append(sender.transformerFactories, factory) - } -} diff --git a/v1/bindings/amqp/receiver.go b/v1/bindings/amqp/receiver.go deleted file mode 100644 index 287dbb667..000000000 --- a/v1/bindings/amqp/receiver.go +++ /dev/null @@ -1,22 +0,0 @@ -package amqp - -import ( - "context" - - "github.com/cloudevents/sdk-go/v1/binding" - "pack.ag/amqp" -) - -// Receiver wraps an amqp.Receiver as a binding.Receiver -type Receiver struct{ AMQP *amqp.Receiver } - -func (r *Receiver) Receive(ctx context.Context) (binding.Message, error) { - m, err := r.AMQP.Receive(ctx) - if err != nil { - return nil, err - } - - return NewMessage(m), nil -} - -func (r *Receiver) Close(ctx context.Context) error { return r.AMQP.Close(ctx) } diff --git a/v1/bindings/amqp/sender.go b/v1/bindings/amqp/sender.go deleted file mode 100644 index b3eaaaa58..000000000 --- a/v1/bindings/amqp/sender.go +++ /dev/null @@ -1,42 +0,0 @@ -package amqp - -import ( - "context" - - "pack.ag/amqp" - - "github.com/cloudevents/sdk-go/v1/binding" -) - -// Sender wraps an amqp.Sender as a binding.Sender -type Sender struct { - AMQP *amqp.Sender - - transformerFactories binding.TransformerFactories -} - -func (s *Sender) Send(ctx context.Context, in binding.Message) error { - var err error - defer func() { _ = in.Finish(err) }() - if m, ok := in.(*Message); ok { // Already an AMQP message. - return s.AMQP.Send(ctx, m.AMQP) - } - - var amqpMessage amqp.Message - err = EncodeAMQPMessage(ctx, in, &amqpMessage, s.transformerFactories) - if err != nil { - return err - } - - return s.AMQP.Send(ctx, &amqpMessage) -} - -func (s *Sender) Close(ctx context.Context) error { return s.AMQP.Close(ctx) } - -func NewSender(amqpClient *amqp.Sender, options ...SenderOptionFunc) binding.Sender { - s := &Sender{AMQP: amqpClient, transformerFactories: make(binding.TransformerFactories, 0)} - for _, o := range options { - o(s) - } - return s -} diff --git a/v1/bindings/amqp/types.go b/v1/bindings/amqp/types.go deleted file mode 100644 index 02174cd10..000000000 --- a/v1/bindings/amqp/types.go +++ /dev/null @@ -1,24 +0,0 @@ -package amqp - -import "github.com/cloudevents/sdk-go/v1/cloudevents/types" - -func safeAMQPPropertiesUnwrap(val interface{}) (interface{}, error) { - v, err := types.Validate(val) - if err != nil { - return nil, err - } - switch t := v.(type) { - case types.URI: // Use string form of URLs. - v = t.String() - case types.URIRef: // Use string form of URLs. - v = t.String() - case types.URLRef: // Use string form of URLs. - v = t.String() - case types.Timestamp: // Use string form of URLs. - v = t.Time - case int32: // Use AMQP long for Integer as per CE spec. - v = int64(t) - } - - return v, nil -} diff --git a/v1/bindings/doc.go b/v1/bindings/doc.go deleted file mode 100644 index b0a91eefc..000000000 --- a/v1/bindings/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package bindings contains packages that implement different protocol bindings. -// -// Package ../binding provides interfaces and helper functions for implementing -// and using protocol bindings in a uniform way. -package bindings diff --git a/v1/bindings/http/doc.go b/v1/bindings/http/doc.go deleted file mode 100644 index 312fc21d4..000000000 --- a/v1/bindings/http/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Package http implements an HTTP CloudEvents binding. -package http - -// TODO(alanconway, slinkydeveloper) -// - different kinds of sender/receiver: long poll, event as response, etc. -// - review blocking, error handling, closing. diff --git a/v1/bindings/http/http_test.go b/v1/bindings/http/http_test.go deleted file mode 100644 index 402d55446..000000000 --- a/v1/bindings/http/http_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package http_test - -import ( - "context" - nethttp "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/test" - "github.com/cloudevents/sdk-go/v1/bindings/http" - ce "github.com/cloudevents/sdk-go/v1/cloudevents" -) - -func TestSendSkipBinary(t *testing.T) { - close, s, r := testSenderReceiver(t) - defer close() - test.EachEvent(t, test.Events(), func(t *testing.T, eventIn ce.Event) { - eventIn = test.ExToStr(t, eventIn) - in := test.NewMockBinaryMessage(eventIn) - test.SendReceive(t, binding.WithSkipDirectBinaryEncoding(binding.WithPreferredEventEncoding(context.Background(), binding.EncodingStructured), true), in, s, r, func(out binding.Message) { - eventOut, encoding := test.MustToEvent(context.Background(), out) - assert.Equal(t, encoding, binding.EncodingStructured) - test.AssertEventEquals(t, eventIn, test.ExToStr(t, eventOut)) - }) - }) -} - -func TestSendSkipStructured(t *testing.T) { - close, s, r := testSenderReceiver(t) - defer close() - test.EachEvent(t, test.Events(), func(t *testing.T, eventIn ce.Event) { - eventIn = test.ExToStr(t, eventIn) - in := test.NewMockStructuredMessage(eventIn) - test.SendReceive(t, binding.WithSkipDirectStructuredEncoding(context.Background(), true), in, s, r, func(out binding.Message) { - eventOut, encoding := test.MustToEvent(context.Background(), out) - assert.Equal(t, encoding, binding.EncodingBinary) - test.AssertEventEquals(t, eventIn, test.ExToStr(t, eventOut)) - }) - }) -} - -func TestSendBinaryReceiveBinary(t *testing.T) { - close, s, r := testSenderReceiver(t) - defer close() - test.EachEvent(t, test.Events(), func(t *testing.T, eventIn ce.Event) { - eventIn = test.ExToStr(t, eventIn) - in := test.NewMockBinaryMessage(eventIn) - test.SendReceive(t, context.Background(), in, s, r, func(out binding.Message) { - eventOut, encoding := test.MustToEvent(context.Background(), out) - assert.Equal(t, encoding, binding.EncodingBinary) - test.AssertEventEquals(t, eventIn, test.ExToStr(t, eventOut)) - }) - }) -} - -func TestSendStructReceiveStruct(t *testing.T) { - close, s, r := testSenderReceiver(t) - defer close() - test.EachEvent(t, test.Events(), func(t *testing.T, eventIn ce.Event) { - eventIn = test.ExToStr(t, eventIn) - in := test.NewMockStructuredMessage(eventIn) - test.SendReceive(t, context.Background(), in, s, r, func(out binding.Message) { - eventOut, encoding := test.MustToEvent(context.Background(), out) - assert.Equal(t, encoding, binding.EncodingStructured) - test.AssertEventEquals(t, eventIn, test.ExToStr(t, eventOut)) - }) - }) -} - -func TestSendEventReceiveBinary(t *testing.T) { - close, s, r := testSenderReceiver(t) - defer close() - test.EachEvent(t, test.Events(), func(t *testing.T, eventIn ce.Event) { - eventIn = test.ExToStr(t, eventIn) - in := binding.EventMessage(eventIn) - test.SendReceive(t, context.Background(), in, s, r, func(out binding.Message) { - eventOut, encoding := test.MustToEvent(context.Background(), out) - assert.Equal(t, encoding, binding.EncodingBinary) - test.AssertEventEquals(t, eventIn, test.ExToStr(t, eventOut)) - }) - }) -} - -func testSenderReceiver(t testing.TB, options ...http.SenderOptionFunc) (func(), binding.Sender, binding.Receiver) { - r := http.NewReceiver() // Parameters? Capacity, sync. - srv := httptest.NewServer(r) - u, err := url.Parse(srv.URL) - require.NoError(t, err) - s := http.NewSender(&nethttp.Client{}, u, options...) // Capacity, sync etc. - return func() { srv.Close() }, s, r -} - -func BenchmarkSendReceive(b *testing.B) { - c, s, r := testSenderReceiver(b) - defer c() // Cleanup - test.BenchmarkSendReceive(b, s, r) -} diff --git a/v1/bindings/http/message.go b/v1/bindings/http/message.go deleted file mode 100644 index 943c3864e..000000000 --- a/v1/bindings/http/message.go +++ /dev/null @@ -1,105 +0,0 @@ -package http - -import ( - "context" - "io" - nethttp "net/http" - "strings" - - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/format" - "github.com/cloudevents/sdk-go/v1/binding/spec" -) - -const prefix = "Ce-" - -var specs = spec.WithPrefix(prefix) - -const ContentType = "Content-Type" - -// Message holds the Header and Body of a HTTP Request or Response. -type Message struct { - Header nethttp.Header - BodyReader io.ReadCloser - encoding binding.Encoding - OnFinish func(error) error -} - -// Check if http.Message implements binding.Message -var _ binding.Message = (*Message)(nil) - -// NewMessage returns a Message with header and data from body. -// Reads and closes body. -func NewMessage(header nethttp.Header, body io.ReadCloser) (*Message, error) { - m := Message{Header: header} - if body != nil { - m.BodyReader = body - } - if ft := format.Lookup(header.Get(ContentType)); ft == nil { - m.encoding = binding.EncodingStructured - } else if _, err := specs.FindVersion(m.Header.Get); err != nil { - m.encoding = binding.EncodingBinary - } else { - m.encoding = binding.EncodingUnknown - } - return &m, nil -} - -func (m *Message) Encoding() binding.Encoding { - return m.encoding -} - -func (m *Message) Structured(ctx context.Context, encoder binding.StructuredEncoder) error { - if ft := format.Lookup(m.Header.Get(ContentType)); ft == nil { - return binding.ErrNotStructured - } else { - return encoder.SetStructuredEvent(ctx, ft, m.BodyReader) - } -} - -func (m *Message) Binary(ctx context.Context, encoder binding.BinaryEncoder) error { - version, err := specs.FindVersion(m.Header.Get) - if err != nil { - return binding.ErrNotBinary - } - - err = encoder.Start(ctx) - if err != nil { - return err - } - - for k, v := range m.Header { - if strings.HasPrefix(k, prefix) { - attr := version.Attribute(k) - if attr != nil { - err = encoder.SetAttribute(attr, v[0]) - } else { - err = encoder.SetExtension(strings.ToLower(strings.TrimPrefix(k, prefix)), v[0]) - } - } else if k == ContentType { - err = encoder.SetAttribute(version.AttributeFromKind(spec.DataContentType), v[0]) - } - if err != nil { - return err - } - } - - if m.BodyReader != nil { - err = encoder.SetData(m.BodyReader) - if err != nil { - return err - } - } - - return encoder.End() -} - -func (m *Message) Finish(err error) error { - if m.BodyReader != nil { - _ = m.BodyReader.Close() - } - if m.OnFinish != nil { - return m.OnFinish(err) - } - return nil -} diff --git a/v1/bindings/http/option.go b/v1/bindings/http/option.go deleted file mode 100644 index 032562513..000000000 --- a/v1/bindings/http/option.go +++ /dev/null @@ -1,11 +0,0 @@ -package http - -import "github.com/cloudevents/sdk-go/v1/binding" - -type SenderOptionFunc func(sender *Sender) - -func WithTranscoder(factory binding.TransformerFactory) SenderOptionFunc { - return func(sender *Sender) { - sender.transformerFactories = append(sender.transformerFactories, factory) - } -} diff --git a/v1/bindings/http/receiver.go b/v1/bindings/http/receiver.go deleted file mode 100644 index 3531173ee..000000000 --- a/v1/bindings/http/receiver.go +++ /dev/null @@ -1,53 +0,0 @@ -package http - -import ( - "context" - "fmt" - "io" - "net/http" - nethttp "net/http" - - "github.com/cloudevents/sdk-go/v1/binding" -) - -type msgErr struct { - msg *Message - err error -} - -// Receiver for CloudEvents as HTTP requests. -// Implements http.Handler, To receive messages, associate it with a http.Server. -type Receiver struct { - incoming chan msgErr -} - -// ServeHTTP implements http.Handler. -// Blocks until Message.Finish is called. -func (r *Receiver) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - m, err := NewMessage(req.Header, req.Body) - if err != nil { - r.incoming <- msgErr{nil, err} - } - done := make(chan error) - m.OnFinish = func(err error) error { done <- err; return nil } - r.incoming <- msgErr{m, err} // Send to Receive() - if err = <-done; err != nil { - nethttp.Error(rw, fmt.Sprintf("cannot forward CloudEvent: %v", err), http.StatusInternalServerError) - } -} - -// NewReceiver creates a receiver -func NewReceiver() *Receiver { - return &Receiver{incoming: make(chan msgErr)} -} - -// Receive the next incoming HTTP request as a CloudEvent. -// Returns non-nil error if the incoming HTTP request fails to parse as a CloudEvent -// Returns io.EOF if the receiver is closed. -func (r *Receiver) Receive(ctx context.Context) (binding.Message, error) { - msgErr, ok := <-r.incoming - if !ok { - return nil, io.EOF - } - return msgErr.msg, msgErr.err -} diff --git a/v1/bindings/http/request_encoder.go b/v1/bindings/http/request_encoder.go deleted file mode 100644 index 1124aac14..000000000 --- a/v1/bindings/http/request_encoder.go +++ /dev/null @@ -1,78 +0,0 @@ -package http - -import ( - "context" - "io" - "io/ioutil" - "net/http" - - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/format" - "github.com/cloudevents/sdk-go/v1/binding/spec" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -// Fill the provided req with the message m. -// Using context you can tweak the encoding processing (more details on binding.Translate documentation). -func EncodeHttpRequest(ctx context.Context, m binding.Message, req *http.Request, transformerFactories binding.TransformerFactories) error { - structuredEncoder := (*httpRequestEncoder)(req) - binaryEncoder := (*httpRequestEncoder)(req) - - _, err := binding.Encode( - ctx, - m, - structuredEncoder, - binaryEncoder, - transformerFactories, - ) - return err -} - -type httpRequestEncoder http.Request - -func (b *httpRequestEncoder) SetStructuredEvent(ctx context.Context, format format.Format, event io.Reader) error { - b.Header.Set(ContentType, format.MediaType()) - b.Body = ioutil.NopCloser(event) - return nil -} - -func (b *httpRequestEncoder) Start(ctx context.Context) error { - return nil -} - -func (b *httpRequestEncoder) End() error { - return nil -} - -func (b *httpRequestEncoder) SetData(reader io.Reader) error { - b.Body = ioutil.NopCloser(reader) - return nil -} - -func (b *httpRequestEncoder) SetAttribute(attribute spec.Attribute, value interface{}) error { - // Http headers, everything is a string! - s, err := types.Format(value) - if err != nil { - return err - } - - if attribute.Kind() == spec.DataContentType { - b.Header.Add(ContentType, s) - } else { - b.Header.Add(prefix+attribute.Name(), s) - } - return nil -} - -func (b *httpRequestEncoder) SetExtension(name string, value interface{}) error { - // Http headers, everything is a string! - s, err := types.Format(value) - if err != nil { - return err - } - b.Header.Add(prefix+name, s) - return nil -} - -var _ binding.StructuredEncoder = (*httpRequestEncoder)(nil) // Test it conforms to the interface -var _ binding.BinaryEncoder = (*httpRequestEncoder)(nil) // Test it conforms to the interface diff --git a/v1/bindings/http/response_encoder.go b/v1/bindings/http/response_encoder.go deleted file mode 100644 index 47165ef99..000000000 --- a/v1/bindings/http/response_encoder.go +++ /dev/null @@ -1,78 +0,0 @@ -package http - -import ( - "context" - "io" - "io/ioutil" - "net/http" - - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/format" - "github.com/cloudevents/sdk-go/v1/binding/spec" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -// Fill the provided res with the message m. -// Using context you can tweak the encoding processing (more details on binding.Translate documentation). -func EncodeHttpResponse(ctx context.Context, m binding.Message, res *http.Response, transformerFactories binding.TransformerFactories) error { - structuredEncoder := (*httpResponseEncoder)(res) - binaryEncoder := (*httpResponseEncoder)(res) - - _, err := binding.Encode( - ctx, - m, - structuredEncoder, - binaryEncoder, - transformerFactories, - ) - return err -} - -type httpResponseEncoder http.Response - -func (b *httpResponseEncoder) SetStructuredEvent(ctx context.Context, format format.Format, event io.Reader) error { - b.Header.Set(ContentType, format.MediaType()) - b.Body = ioutil.NopCloser(event) - return nil -} - -func (b *httpResponseEncoder) Start(ctx context.Context) error { - return nil -} - -func (b *httpResponseEncoder) End() error { - return nil -} - -func (b *httpResponseEncoder) SetData(reader io.Reader) error { - b.Body = ioutil.NopCloser(reader) - return nil -} - -func (b *httpResponseEncoder) SetAttribute(attribute spec.Attribute, value interface{}) error { - // Http headers, everything is a string! - s, err := types.Format(value) - if err != nil { - return err - } - - if attribute.Kind() == spec.DataContentType { - b.Header.Add(ContentType, s) - } else { - b.Header.Add(prefix+attribute.Name(), s) - } - return nil -} - -func (b *httpResponseEncoder) SetExtension(name string, value interface{}) error { - // Http headers, everything is a string! - s, err := types.Format(value) - if err != nil { - return err - } - b.Header.Add(prefix+name, s) - return nil -} - -var _ binding.StructuredEncoder = (*httpResponseEncoder)(nil) // Test it conforms to the interface -var _ binding.BinaryEncoder = (*httpResponseEncoder)(nil) // Test it conforms to the interface diff --git a/v1/bindings/http/response_encoder_test.go b/v1/bindings/http/response_encoder_test.go deleted file mode 100644 index 6015069ac..000000000 --- a/v1/bindings/http/response_encoder_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package http - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - - "net/http" - - cloudevents "github.com/cloudevents/sdk-go/v1" - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/test" - ce "github.com/cloudevents/sdk-go/v1/cloudevents" -) - -func TestEncodeHttpResponse(t *testing.T) { - tests := []struct { - name string - context context.Context - messageFactory func(e cloudevents.Event) binding.Message - expectedEncoding binding.Encoding - }{ - { - name: "Structured to Structured", - context: context.TODO(), - messageFactory: test.NewMockStructuredMessage, - expectedEncoding: binding.EncodingStructured, - }, - { - name: "Binary to Binary", - context: context.TODO(), - messageFactory: test.NewMockBinaryMessage, - expectedEncoding: binding.EncodingBinary, - }, - { - name: "Event to Structured", - context: binding.WithPreferredEventEncoding(context.TODO(), binding.EncodingStructured), - messageFactory: func(e cloudevents.Event) binding.Message { return binding.EventMessage(e) }, - expectedEncoding: binding.EncodingStructured, - }, - { - name: "Event to Binary", - context: binding.WithPreferredEventEncoding(context.TODO(), binding.EncodingBinary), - messageFactory: func(e cloudevents.Event) binding.Message { return binding.EventMessage(e) }, - expectedEncoding: binding.EncodingBinary, - }, - } - for _, tt := range tests { - test.EachEvent(t, test.Events(), func(t *testing.T, eventIn ce.Event) { - t.Run(tt.name, func(t *testing.T) { - res := &http.Response{ - Header: make(http.Header), - } - - eventIn = test.ExToStr(t, eventIn) - messageIn := tt.messageFactory(eventIn) - - err := EncodeHttpResponse(tt.context, messageIn, res, binding.TransformerFactories{}) - require.NoError(t, err) - - //Little hack to go back to Message - messageOut, err := NewMessage(res.Header, res.Body) - require.NoError(t, err) - - eventOut, encoding, err := binding.ToEvent(context.TODO(), messageOut) - require.Equal(t, encoding, tt.expectedEncoding) - test.AssertEventEquals(t, eventIn, eventOut) - }) - }) - } -} diff --git a/v1/bindings/http/sender.go b/v1/bindings/http/sender.go deleted file mode 100644 index 4c638d954..000000000 --- a/v1/bindings/http/sender.go +++ /dev/null @@ -1,54 +0,0 @@ -package http - -import ( - "context" - "fmt" - "net/http" - nethttp "net/http" - "net/url" - - "github.com/cloudevents/sdk-go/v1/binding" -) - -type Sender struct { - // Client is the HTTP client used to send events as HTTP requests - Client *http.Client - // Target is the URL to send event requests to. - Target *url.URL - - transformerFactories binding.TransformerFactories -} - -func (s *Sender) Send(ctx context.Context, m binding.Message) (err error) { - defer func() { _ = m.Finish(err) }() - if s.Client == nil || s.Target == nil { - return fmt.Errorf("not initialized: %#v", s) - } - - var req *http.Request - req, err = http.NewRequest("POST", s.Target.String(), nil) - if err != nil { - return - } - req = req.WithContext(ctx) - - if err = EncodeHttpRequest(ctx, m, req, s.transformerFactories); err != nil { - return - } - resp, err := s.Client.Do(req) - if err != nil { - return - } - if resp.StatusCode/100 != 2 { - return fmt.Errorf("%d %s", resp.StatusCode, nethttp.StatusText(resp.StatusCode)) - } - return -} - -func NewSender(client *http.Client, target *url.URL, options ...SenderOptionFunc) binding.Sender { - s := &Sender{Client: client, Target: target, transformerFactories: make(binding.TransformerFactories, 0)} - for _, o := range options { - o(s) - } - return s -} diff --git a/v1/bindings/kafka_sarama/doc.go b/v1/bindings/kafka_sarama/doc.go deleted file mode 100644 index 96f40f4f5..000000000 --- a/v1/bindings/kafka_sarama/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -// Package kafka implements the Kafka CloudEvents binding. -package kafka_sarama - -// TODO(slinkydeveloper) diff --git a/v1/bindings/kafka_sarama/kafka_test.go b/v1/bindings/kafka_sarama/kafka_test.go deleted file mode 100644 index a7e8f3692..000000000 --- a/v1/bindings/kafka_sarama/kafka_test.go +++ /dev/null @@ -1,188 +0,0 @@ -// +build kafka - -package kafka_sarama - -import ( - "context" - "os" - "strings" - "testing" - - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/Shopify/sarama" - - cloudevents "github.com/cloudevents/sdk-go/v1" - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/test" -) - -const ( - TEST_GROUP_ID = "test_group_id" -) - -var ( - e = test.FullEvent() - StructuredConsumerMessageWithoutKey = &sarama.ConsumerMessage{ - Value: test.MustJSON(e), - Headers: []*sarama.RecordHeader{{ - Key: []byte(ContentType), - Value: []byte(cloudevents.ApplicationCloudEventsJSON), - }}, - } - StructuredConsumerMessageWithKey = &sarama.ConsumerMessage{ - Key: []byte("aaa"), - Value: test.MustJSON(e), - Headers: []*sarama.RecordHeader{{ - Key: []byte(ContentType), - Value: []byte(cloudevents.ApplicationCloudEventsJSON), - }}, - } - BinaryConsumerMessageWithoutKey = &sarama.ConsumerMessage{ - Value: []byte("hello world!"), - Headers: mustToSaramaConsumerHeaders(map[string]string{ - "ce_type": e.Type(), - "ce_source": e.Source(), - "ce_id": e.ID(), - "ce_time": test.Timestamp.String(), - "ce_specversion": "1.0", - "ce_dataschema": test.Schema.String(), - "ce_datacontenttype": "text/json", - "ce_subject": "topic", - "ce_exta": "someext", - }), - } - BinaryConsumerMessageWithKey = &sarama.ConsumerMessage{ - Key: []byte("akey"), - Value: []byte("hello world!"), - Headers: mustToSaramaConsumerHeaders(map[string]string{ - "ce_type": e.Type(), - "ce_source": e.Source(), - "ce_id": e.ID(), - "ce_time": test.Timestamp.String(), - "ce_specversion": "1.0", - "ce_dataschema": test.Schema.String(), - "ce_datacontenttype": "text/json", - "ce_subject": "topic", - "ce_exta": "someext", - }), - } -) - -func TestSendStructuredMessageToStructuredWithKey(t *testing.T) { - close, s, r := testSenderReceiver(t) - defer close() - test.EachEvent(t, test.Events(), func(t *testing.T, eventIn cloudevents.Event) { - eventIn = test.ExToStr(t, eventIn) - require.NoError(t, eventIn.Context.SetExtension("key", "aaa")) - - in := test.NewMockStructuredMessage(eventIn) - test.SendReceive(t, binding.WithPreferredEventEncoding(context.TODO(), binding.EncodingStructured), in, s, r, func(out binding.Message) { - eventOut, encoding := test.MustToEvent(context.Background(), out) - assert.Equal(t, binding.EncodingEvent, encoding) - test.AssertEventEquals(t, eventIn, test.ExToStr(t, eventOut)) - }) - }) -} - -func TestSendStructuredMessageToStructuredWithoutKey(t *testing.T) { - close, s, r := testSenderReceiver(t) - defer close() - test.EachEvent(t, test.Events(), func(t *testing.T, eventIn cloudevents.Event) { - eventIn = test.ExToStr(t, eventIn) - - in := test.NewMockStructuredMessage(eventIn) - test.SendReceive(t, binding.WithPreferredEventEncoding(context.TODO(), binding.EncodingStructured), in, s, r, func(out binding.Message) { - eventOut, encoding := test.MustToEvent(context.Background(), out) - assert.Equal(t, binding.EncodingStructured, encoding) - test.AssertEventEquals(t, eventIn, test.ExToStr(t, eventOut)) - }) - }) -} - -func TestSendBinaryMessageToBinaryWithKey(t *testing.T) { - close, s, r := testSenderReceiver(t) - defer close() - test.EachEvent(t, test.Events(), func(t *testing.T, eventIn cloudevents.Event) { - eventIn = test.ExToStr(t, eventIn) - require.NoError(t, eventIn.Context.SetExtension("key", "aaa")) - - in := test.NewMockBinaryMessage(eventIn) - test.SendReceive(t, binding.WithPreferredEventEncoding(context.TODO(), binding.EncodingBinary), in, s, r, func(out binding.Message) { - eventOut, encoding := test.MustToEvent(context.Background(), out) - assert.Equal(t, binding.EncodingBinary, encoding) - test.AssertEventEquals(t, eventIn, test.ExToStr(t, eventOut)) - }) - }) -} - -func TestSendBinaryMessageToBinaryWithoutKey(t *testing.T) { - close, s, r := testSenderReceiver(t) - defer close() - test.EachEvent(t, test.Events(), func(t *testing.T, eventIn cloudevents.Event) { - eventIn = test.ExToStr(t, eventIn) - - in := test.NewMockBinaryMessage(eventIn) - test.SendReceive(t, binding.WithPreferredEventEncoding(context.TODO(), binding.EncodingBinary), in, s, r, func(out binding.Message) { - eventOut, encoding := test.MustToEvent(context.Background(), out) - assert.Equal(t, binding.EncodingBinary, encoding) - test.AssertEventEquals(t, eventIn, test.ExToStr(t, eventOut)) - }) - }) -} - -func testClient(t testing.TB) sarama.Client { - t.Helper() - s := os.Getenv("TEST_KAFKA_BOOTSTRAP_SERVER") - if s == "" { - s = "localhost:9092" - } - - config := sarama.NewConfig() - config.Version = sarama.V2_0_0_0 - config.Producer.Return.Successes = true - config.Producer.Return.Errors = true - config.Consumer.Offsets.Initial = sarama.OffsetOldest - client, err := sarama.NewClient(strings.Split(s, ","), config) - if err != nil { - t.Skipf("Cannot create sarama client to servers [%s]: %v", s, err) - } - - return client -} - -func testSenderReceiver(t testing.TB, options ...SenderOptionFunc) (func(), binding.Sender, binding.Receiver) { - client := testClient(t) - - topicName := "test-ce-client-" + uuid.New().String() - r := NewReceiver(client, TEST_GROUP_ID, topicName) - s, err := NewSender(client, topicName, options...) - require.NoError(t, err) - - return func() { - err = r.Close(context.TODO()) - require.NoError(t, err) - err = s.Close(context.TODO()) - require.NoError(t, err) - err = client.Close() - require.NoError(t, err) - }, s, r -} - -func BenchmarkSendReceive(b *testing.B) { - c, s, r := testSenderReceiver(b) - defer c() // Cleanup - test.BenchmarkSendReceive(b, s, r) -} - -func mustToSaramaConsumerHeaders(m map[string]string) []*sarama.RecordHeader { - res := make([]*sarama.RecordHeader, len(m)) - i := 0 - for k, v := range m { - res[i] = &sarama.RecordHeader{Key: []byte(k), Value: []byte(v)} - i++ - } - return res -} diff --git a/v1/bindings/kafka_sarama/message.go b/v1/bindings/kafka_sarama/message.go deleted file mode 100644 index a36241c50..000000000 --- a/v1/bindings/kafka_sarama/message.go +++ /dev/null @@ -1,153 +0,0 @@ -package kafka_sarama - -import ( - "bytes" - "context" - "strings" - - ce "github.com/cloudevents/sdk-go/v1" - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/format" - "github.com/cloudevents/sdk-go/v1/binding/spec" - - "github.com/Shopify/sarama" -) - -const prefix = "ce_" - -var specs = spec.WithPrefix(prefix) - -const ContentType = "content-type" - -// Message holds a sarama ConsumerMessage. -type Message struct { - Key, Value []byte - Headers map[string][]byte - ContentType string - format format.Format - version spec.Version -} - -// Check if http.Message implements binding.Message -var _ binding.Message = (*Message)(nil) - -// NewMessage returns a Message with data from body. -// Reads and closes body. -func NewMessage(cm *sarama.ConsumerMessage) (binding.Message, error) { - var contentType string - headers := make(map[string][]byte, len(cm.Headers)) - for _, r := range cm.Headers { - k := strings.ToLower(string(r.Key)) - if k == ContentType { - contentType = string(r.Value) - } - headers[strings.ToLower(string(r.Key))] = r.Value - } - return NewMessageFromRaw(cm.Key, cm.Value, contentType, headers) -} - -func NewMessageFromRaw(key []byte, value []byte, contentType string, headers map[string][]byte) (binding.Message, error) { - if ft := format.Lookup(contentType); ft != nil { - // if the message is structured and has a key, - // then it's cheaper to go through event message - // because we need to add the key as extension - if key != nil { - event := ce.Event{} - err := ft.Unmarshal(value, &event) - if err != nil { - return nil, err - } - event.SetExtension("key", string(key)) - return binding.EventMessage(event), nil - } else { - return &Message{ - Key: key, - Value: value, - ContentType: contentType, - Headers: headers, - format: ft, - }, nil - } - } else if v, err := specs.FindVersion(func(s string) string { - return string(headers[strings.ToLower(s)]) - }); err == nil { - return &Message{ - Key: key, - Value: value, - ContentType: contentType, - Headers: headers, - version: v, - }, nil - } - - return &Message{ - Key: key, - Value: value, - ContentType: contentType, - Headers: headers, - }, nil -} - -func (m *Message) Encoding() binding.Encoding { - if m.version != nil { - return binding.EncodingBinary - } - if m.format != nil { - return binding.EncodingStructured - } - return binding.EncodingUnknown -} - -func (m *Message) Structured(ctx context.Context, encoder binding.StructuredEncoder) error { - if m.format != nil { - return encoder.SetStructuredEvent(ctx, m.format, bytes.NewReader(m.Value)) - } - return binding.ErrNotStructured -} - -func (m *Message) Binary(ctx context.Context, encoder binding.BinaryEncoder) error { - if m.version == nil { - return binding.ErrNotBinary - } - - err := encoder.Start(ctx) - if err != nil { - return err - } - - for k, v := range m.Headers { - if strings.HasPrefix(k, prefix) { - attr := m.version.Attribute(k) - if attr != nil { - err = encoder.SetAttribute(attr, string(v)) - } else { - err = encoder.SetExtension(strings.ToLower(strings.TrimPrefix(k, prefix)), string(v)) - } - } else if k == ContentType { - err = encoder.SetAttribute(m.version.AttributeFromKind(spec.DataContentType), string(v)) - } - if err != nil { - return err - } - } - - if m.Key != nil { - err = encoder.SetExtension("key", string(m.Key)) - if err != nil { - return err - } - } - - if m.Value != nil { - err = encoder.SetData(bytes.NewReader(m.Value)) - if err != nil { - return err - } - } - - return encoder.End() -} - -func (m *Message) Finish(error) error { - return nil -} diff --git a/v1/bindings/kafka_sarama/message_benchmark_test.go b/v1/bindings/kafka_sarama/message_benchmark_test.go deleted file mode 100644 index 6fe6bd2a3..000000000 --- a/v1/bindings/kafka_sarama/message_benchmark_test.go +++ /dev/null @@ -1,69 +0,0 @@ -// +build kafka - -package kafka_sarama_test - -import ( - "context" - "testing" - - cloudevents "github.com/cloudevents/sdk-go/v1" - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/bindings/kafka_sarama" -) - -// Avoid DCE -var M binding.Message -var Event cloudevents.Event -var Err error - -func BenchmarkNewStructuredMessageWithoutKey(b *testing.B) { - for i := 0; i < b.N; i++ { - M, Err = kafka_sarama.NewMessage(kafka_sarama.StructuredConsumerMessageWithoutKey) - } -} - -func BenchmarkNewStructuredMessageWithKey(b *testing.B) { - for i := 0; i < b.N; i++ { - M, Err = kafka_sarama.NewMessage(kafka_sarama.StructuredConsumerMessageWithKey) - } -} - -func BenchmarkNewBinaryMessageWithoutKey(b *testing.B) { - for i := 0; i < b.N; i++ { - M, Err = kafka_sarama.NewMessage(kafka_sarama.BinaryConsumerMessageWithoutKey) - } -} - -func BenchmarkNewBinaryMessageWithKey(b *testing.B) { - for i := 0; i < b.N; i++ { - M, Err = kafka_sarama.NewMessage(kafka_sarama.BinaryConsumerMessageWithKey) - } -} - -func BenchmarkNewStructuredMessageWithoutKeyToEvent(b *testing.B) { - for i := 0; i < b.N; i++ { - M, Err = kafka_sarama.NewMessage(kafka_sarama.StructuredConsumerMessageWithoutKey) - Event, _, Err = binding.ToEvent(context.TODO(), M) - } -} - -func BenchmarkNewStructuredMessageWithKeyToEvent(b *testing.B) { - for i := 0; i < b.N; i++ { - M, Err = kafka_sarama.NewMessage(kafka_sarama.StructuredConsumerMessageWithKey) - Event, _, Err = binding.ToEvent(context.TODO(), M) - } -} - -func BenchmarkNewBinaryMessageWithoutKeyToEvent(b *testing.B) { - for i := 0; i < b.N; i++ { - M, Err = kafka_sarama.NewMessage(kafka_sarama.BinaryConsumerMessageWithoutKey) - Event, _, Err = binding.ToEvent(context.TODO(), M) - } -} - -func BenchmarkNewBinaryMessageWithKeyToEvent(b *testing.B) { - for i := 0; i < b.N; i++ { - M, Err = kafka_sarama.NewMessage(kafka_sarama.BinaryConsumerMessageWithKey) - Event, _, Err = binding.ToEvent(context.TODO(), M) - } -} diff --git a/v1/bindings/kafka_sarama/option.go b/v1/bindings/kafka_sarama/option.go deleted file mode 100644 index 076b1d057..000000000 --- a/v1/bindings/kafka_sarama/option.go +++ /dev/null @@ -1,11 +0,0 @@ -package kafka_sarama - -import "github.com/cloudevents/sdk-go/v1/binding" - -type SenderOptionFunc func(sender *Sender) - -func WithTranscoder(factory binding.TransformerFactory) SenderOptionFunc { - return func(sender *Sender) { - sender.transformerFactories = append(sender.transformerFactories, factory) - } -} diff --git a/v1/bindings/kafka_sarama/producer_message_encoder.go b/v1/bindings/kafka_sarama/producer_message_encoder.go deleted file mode 100644 index edc12fb54..000000000 --- a/v1/bindings/kafka_sarama/producer_message_encoder.go +++ /dev/null @@ -1,170 +0,0 @@ -package kafka_sarama - -import ( - "bytes" - "context" - "io" - - "github.com/Shopify/sarama" - - ce "github.com/cloudevents/sdk-go/v1" - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/format" - "github.com/cloudevents/sdk-go/v1/binding/spec" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -const ( - SKIP_KEY_EXTENSION = "SKIP_KEY_EXTENSION" -) - -// Fill the provided producerMessage with the message m. -// Using context you can tweak the encoding processing (more details on binding.Translate documentation). -func EncodeKafkaProducerMessage(ctx context.Context, m binding.Message, producerMessage *sarama.ProducerMessage, transformerFactories binding.TransformerFactories) error { - skipKey := binding.GetOrDefaultFromCtx(ctx, SKIP_KEY_EXTENSION, false).(bool) - - if skipKey { - enc := &kafkaProducerMessageEncoder{ - producerMessage, - skipKey, - } - - _, err := binding.Encode( - ctx, - m, - enc, - enc, - transformerFactories, - ) - return err - } - - enc := m.Encoding() - var err error - // Skip direct encoding if the event is an event message - if enc == binding.EncodingBinary { - encoder := &kafkaProducerMessageEncoder{ - producerMessage, - skipKey, - } - enc, err = binding.RunDirectEncoding(ctx, m, nil, encoder, transformerFactories) - if enc != binding.EncodingUnknown { - // Message directly encoded binary -> binary, nothing else to do here - return err - } - } - - var e ce.Event - e, _, err = binding.ToEvent(ctx, m, transformerFactories) - if err != nil { - return err - } - - if val, ok := e.Extensions()["key"]; ok { - s, err := types.Format(val) - if err != nil { - return err - } - - producerMessage.Key = sarama.StringEncoder(s) - } - - eventMessage := binding.EventMessage(e) - - encoder := &kafkaProducerMessageEncoder{ - producerMessage, - skipKey, - } - - if binding.GetOrDefaultFromCtx(ctx, binding.PREFERRED_EVENT_ENCODING, binding.EncodingBinary).(binding.Encoding) == binding.EncodingStructured { - return eventMessage.Structured(ctx, encoder) - } else { - return eventMessage.Binary(ctx, encoder) - } -} - -type kafkaProducerMessageEncoder struct { - *sarama.ProducerMessage - skipKey bool -} - -func (b *kafkaProducerMessageEncoder) SetStructuredEvent(ctx context.Context, format format.Format, event io.Reader) error { - b.Headers = []sarama.RecordHeader{{ - Key: []byte(ContentType), - Value: []byte(format.MediaType()), - }} - - var buf bytes.Buffer - _, err := io.Copy(&buf, event) - if err != nil { - return err - } - - b.Value = sarama.ByteEncoder(buf.Bytes()) - return nil -} - -func (b *kafkaProducerMessageEncoder) Start(ctx context.Context) error { - b.Headers = []sarama.RecordHeader{} - return nil -} - -func (b *kafkaProducerMessageEncoder) End() error { - return nil -} - -func (b *kafkaProducerMessageEncoder) SetData(reader io.Reader) error { - var buf bytes.Buffer - _, err := io.Copy(&buf, reader) - if err != nil { - return err - } - - b.Value = sarama.ByteEncoder(buf.Bytes()) - return nil -} - -func (b *kafkaProducerMessageEncoder) SetAttribute(attribute spec.Attribute, value interface{}) error { - // Everything is a string here - s, err := types.Format(value) - if err != nil { - return err - } - - if attribute.Kind() == spec.DataContentType { - b.Headers = append(b.Headers, sarama.RecordHeader{Key: []byte(ContentType), Value: []byte(s)}) - } else { - b.Headers = append(b.Headers, sarama.RecordHeader{Key: []byte(prefix + attribute.Name()), Value: []byte(s)}) - } - return nil -} - -func (b *kafkaProducerMessageEncoder) SetExtension(name string, value interface{}) error { - if !b.skipKey && name == "key" { - if v, ok := value.([]byte); ok { - b.Key = sarama.ByteEncoder(v) - } else { - s, err := types.Format(value) - if err != nil { - return err - } - b.Key = sarama.ByteEncoder(s) - } - return nil - } - - // Http headers, everything is a string! - s, err := types.Format(value) - if err != nil { - return err - } - b.Headers = append(b.Headers, sarama.RecordHeader{Key: []byte(prefix + name), Value: []byte(s)}) - return nil -} - -var _ binding.StructuredEncoder = (*kafkaProducerMessageEncoder)(nil) // Test it conforms to the interface -var _ binding.BinaryEncoder = (*kafkaProducerMessageEncoder)(nil) // Test it conforms to the interface - -func WithSkipKeyExtension(ctx context.Context) context.Context { - return context.WithValue(ctx, SKIP_KEY_EXTENSION, true) -} diff --git a/v1/bindings/kafka_sarama/producer_message_encoder_benchmark_test.go b/v1/bindings/kafka_sarama/producer_message_encoder_benchmark_test.go deleted file mode 100644 index 4cb847fe7..000000000 --- a/v1/bindings/kafka_sarama/producer_message_encoder_benchmark_test.go +++ /dev/null @@ -1,71 +0,0 @@ -// +build kafka - -package kafka_sarama_test - -import ( - "context" - "testing" - - "github.com/Shopify/sarama" - - cloudevents "github.com/cloudevents/sdk-go/v1" - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/test" - "github.com/cloudevents/sdk-go/v1/bindings/kafka_sarama" -) - -// Avoid DCE -var ProducerMessage *sarama.ProducerMessage - -var ( - ctxSkipKey context.Context - ctx context.Context - eventWithoutKey cloudevents.Event - eventWithKey cloudevents.Event - structuredMessageWithoutKey binding.Message - structuredMessageWithKey binding.Message - binaryMessageWithoutKey binding.Message - binaryMessageWithKey binding.Message -) - -func init() { - ctxSkipKey = kafka_sarama.WithSkipKeyExtension(context.TODO()) - ctx = context.TODO() - - eventWithoutKey = test.FullEvent() - eventWithKey = test.FullEvent() - eventWithKey.SetExtension("key", "aaaaaa") - - structuredMessageWithoutKey = test.NewMockStructuredMessage(eventWithoutKey) - structuredMessageWithKey = test.NewMockStructuredMessage(eventWithKey) - binaryMessageWithoutKey = test.NewMockBinaryMessage(eventWithoutKey) - binaryMessageWithKey = test.NewMockBinaryMessage(eventWithKey) -} - -func BenchmarkEncodeStructuredMessageSkipKey(b *testing.B) { - for i := 0; i < b.N; i++ { - ProducerMessage = &sarama.ProducerMessage{} - Err = kafka_sarama.EncodeKafkaProducerMessage(ctxSkipKey, structuredMessageWithoutKey, ProducerMessage, binding.TransformerFactories{}) - } -} - -func BenchmarkEncodeStructuredMessage(b *testing.B) { - for i := 0; i < b.N; i++ { - ProducerMessage = &sarama.ProducerMessage{} - Err = kafka_sarama.EncodeKafkaProducerMessage(ctx, structuredMessageWithKey, ProducerMessage, binding.TransformerFactories{}) - } -} - -func BenchmarkEncodeBinaryMessageSkipKey(b *testing.B) { - for i := 0; i < b.N; i++ { - ProducerMessage = &sarama.ProducerMessage{} - Err = kafka_sarama.EncodeKafkaProducerMessage(ctxSkipKey, binaryMessageWithoutKey, ProducerMessage, binding.TransformerFactories{}) - } -} - -func BenchmarkEncodeBinaryMessage(b *testing.B) { - for i := 0; i < b.N; i++ { - ProducerMessage = &sarama.ProducerMessage{} - Err = kafka_sarama.EncodeKafkaProducerMessage(ctx, binaryMessageWithKey, ProducerMessage, binding.TransformerFactories{}) - } -} diff --git a/v1/bindings/kafka_sarama/producer_message_encoder_test.go b/v1/bindings/kafka_sarama/producer_message_encoder_test.go deleted file mode 100644 index 3cb89ea68..000000000 --- a/v1/bindings/kafka_sarama/producer_message_encoder_test.go +++ /dev/null @@ -1,137 +0,0 @@ -// +build kafka - -package kafka_sarama - -import ( - "context" - "strings" - "testing" - - "github.com/Shopify/sarama" - "github.com/stretchr/testify/require" - - cloudevents "github.com/cloudevents/sdk-go/v1" - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/test" - ce "github.com/cloudevents/sdk-go/v1/cloudevents" -) - -func TestEncodeKafkaProducerMessage(t *testing.T) { - tests := []struct { - name string - context context.Context - messageFactory func(e cloudevents.Event) binding.Message - expectedEncoding binding.Encoding - skipKey bool - }{ - { - name: "Structured to Structured with Skip key", - context: context.TODO(), - messageFactory: func(e cloudevents.Event) binding.Message { return test.NewMockStructuredMessage(e) }, - expectedEncoding: binding.EncodingStructured, - skipKey: true, - }, - { - name: "Binary to Binary with Skip key", - context: context.TODO(), - messageFactory: func(e cloudevents.Event) binding.Message { return test.NewMockBinaryMessage(e) }, - expectedEncoding: binding.EncodingBinary, - skipKey: true, - }, - { - name: "Event to Structured with Skip key", - context: binding.WithPreferredEventEncoding(context.TODO(), binding.EncodingStructured), - messageFactory: func(e cloudevents.Event) binding.Message { return binding.EventMessage(e) }, - expectedEncoding: binding.EncodingStructured, - skipKey: true, - }, - { - name: "Event to Binary with Skip key", - context: binding.WithPreferredEventEncoding(context.TODO(), binding.EncodingBinary), - messageFactory: func(e cloudevents.Event) binding.Message { return binding.EventMessage(e) }, - expectedEncoding: binding.EncodingBinary, - skipKey: true, - }, - { - name: "Structured to Structured", - context: binding.WithPreferredEventEncoding(context.TODO(), binding.EncodingStructured), - messageFactory: func(e cloudevents.Event) binding.Message { return test.NewMockStructuredMessage(e) }, - expectedEncoding: binding.EncodingEvent, - skipKey: false, - }, - { - name: "Binary to Binary", - context: context.TODO(), - messageFactory: func(e cloudevents.Event) binding.Message { return test.NewMockBinaryMessage(e) }, - expectedEncoding: binding.EncodingBinary, - skipKey: false, - }, - { - name: "Event to Structured", - context: binding.WithPreferredEventEncoding(context.TODO(), binding.EncodingStructured), - messageFactory: func(e cloudevents.Event) binding.Message { return binding.EventMessage(e) }, - expectedEncoding: binding.EncodingEvent, - skipKey: false, - }, - { - name: "Event to Binary", - context: binding.WithPreferredEventEncoding(context.TODO(), binding.EncodingBinary), - messageFactory: func(e cloudevents.Event) binding.Message { return binding.EventMessage(e) }, - expectedEncoding: binding.EncodingBinary, - skipKey: false, - }, - } - for _, tt := range tests { - test.EachEvent(t, test.Events(), func(t *testing.T, eventIn ce.Event) { - t.Run(tt.name, func(t *testing.T) { - ctx := tt.context - - if tt.skipKey { - ctx = WithSkipKeyExtension(ctx) - } else { - eventIn.SetExtension("key", "bla") - } - - kafkaMessage := &sarama.ProducerMessage{ - Topic: "aaa", - } - - eventIn = test.ExToStr(t, eventIn) - messageIn := tt.messageFactory(eventIn) - - err := EncodeKafkaProducerMessage(ctx, messageIn, kafkaMessage, binding.TransformerFactories{}) - require.NoError(t, err) - - //Little hack to go back to Message - headers := make(map[string][]byte) - for _, h := range kafkaMessage.Headers { - headers[strings.ToLower(string(h.Key))] = h.Value - } - - var key []byte - if kafkaMessage.Key != nil { - key, err = kafkaMessage.Key.Encode() - require.NoError(t, err) - } - - var value []byte - if kafkaMessage.Value != nil { - value, err = kafkaMessage.Value.Encode() - require.NoError(t, err) - } - - if !tt.skipKey { - require.Equal(t, []byte("bla"), key) - } - - messageOut, err := NewMessageFromRaw(key, value, string(headers[ContentType]), headers) - require.NoError(t, err) - - eventOut, encoding, err := binding.ToEvent(context.TODO(), messageOut) - require.NoError(t, err) - require.Equal(t, tt.expectedEncoding, encoding) - test.AssertEventEquals(t, eventIn, eventOut) - }) - }) - } -} diff --git a/v1/bindings/kafka_sarama/receiver.go b/v1/bindings/kafka_sarama/receiver.go deleted file mode 100644 index 95587f747..000000000 --- a/v1/bindings/kafka_sarama/receiver.go +++ /dev/null @@ -1,87 +0,0 @@ -package kafka_sarama - -import ( - "context" - "io" - - "github.com/Shopify/sarama" - - "github.com/cloudevents/sdk-go/v1/binding" -) - -type msgErr struct { - msg binding.Message - err error -} - -type Receiver struct { - incoming chan msgErr - - client sarama.Client - topic string - groupId string - saramaConsumerGroup sarama.ConsumerGroup -} - -func (r *Receiver) Setup(sess sarama.ConsumerGroupSession) error { - return nil -} - -func (r *Receiver) Cleanup(sarama.ConsumerGroupSession) error { - return nil -} - -func (r *Receiver) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error { - for message := range claim.Messages() { - m, err := NewMessage(message) - - if err != nil { - r.incoming <- msgErr{err: err} - } else { - r.incoming <- msgErr{ - msg: binding.WithFinish(m, func(err error) { session.MarkMessage(message, "") }), - } - } - - } - return nil -} - -func NewReceiver(client sarama.Client, groupId string, topic string) *Receiver { - return &Receiver{ - incoming: make(chan msgErr), - client: client, - groupId: groupId, - topic: topic, - } -} - -func (r *Receiver) Receive(ctx context.Context) (binding.Message, error) { - // Consumer Group not started! - if r.saramaConsumerGroup == nil { - cg, err := sarama.NewConsumerGroupFromClient(r.groupId, r.client) - if err != nil { - return nil, err - } - r.saramaConsumerGroup = cg - - go func() { - if err := cg.Consume(ctx, []string{r.topic}, r); err != nil { - r.incoming <- msgErr{err: err} - } - }() - } - - msgErr, ok := <-r.incoming - if !ok { - return nil, io.EOF - } - return msgErr.msg, msgErr.err -} - -func (r *Receiver) Close(ctx context.Context) error { - if r.saramaConsumerGroup != nil { - return r.saramaConsumerGroup.Close() - } - return nil -} diff --git a/v1/bindings/kafka_sarama/sender.go b/v1/bindings/kafka_sarama/sender.go deleted file mode 100644 index 58eda4b52..000000000 --- a/v1/bindings/kafka_sarama/sender.go +++ /dev/null @@ -1,48 +0,0 @@ -package kafka_sarama - -import ( - "context" - - "github.com/Shopify/sarama" - - "github.com/cloudevents/sdk-go/v1/binding" -) - -type Sender struct { - topic string - syncProducer sarama.SyncProducer - - transformerFactories binding.TransformerFactories -} - -func (s *Sender) Send(ctx context.Context, m binding.Message) error { - kafkaMessage := sarama.ProducerMessage{Topic: s.topic} - - if err := EncodeKafkaProducerMessage(ctx, m, &kafkaMessage, s.transformerFactories); err != nil { - return err - } - - _, _, err := s.syncProducer.SendMessage(&kafkaMessage) - return err -} - -func (s *Sender) Close(ctx context.Context) error { - return s.syncProducer.Close() -} - -func NewSender(client sarama.Client, topic string, options ...SenderOptionFunc) (*Sender, error) { - producer, err := sarama.NewSyncProducerFromClient(client) - if err != nil { - return nil, err - } - - s := &Sender{ - topic: topic, - syncProducer: producer, - transformerFactories: make(binding.TransformerFactories, 0), - } - for _, o := range options { - o(s) - } - return s, nil -} diff --git a/v1/cloudevents/client/client.go b/v1/cloudevents/client/client.go deleted file mode 100644 index 55721a024..000000000 --- a/v1/cloudevents/client/client.go +++ /dev/null @@ -1,226 +0,0 @@ -package client - -import ( - "context" - "fmt" - "sync" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/extensions" - "github.com/cloudevents/sdk-go/v1/cloudevents/observability" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport/http" - "go.opencensus.io/trace" -) - -// 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, WithUUIDs and WithDataContentType("application/json") -// 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(), WithDataContentType(cloudevents.ApplicationJSON)) - if err != nil { - return nil, err - } - return c, nil -} - -type ceClient struct { - transport transport.Transport - fn *receiverFn - - convertFn ConvertFn - - receiverMu sync.Mutex - eventDefaulterFns []EventDefaulter - - disableTracePropagation bool -} - -// 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) - - ctx, span := trace.StartSpan(ctx, clientSpanName, trace.WithSpanKind(trace.SpanKindClient)) - defer span.End() - if span.IsRecordingEvents() { - span.AddAttributes(eventTraceAttributes(event.Context)...) - } - - 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) - } - } - - // Set distributed tracing extension. - if !c.disableTracePropagation { - if span := trace.FromContext(ctx); span != nil { - event.Context = event.Context.Clone() - if err := extensions.FromSpanContext(span.SpanContext()).AddTracingAttributes(event.Context); err != nil { - return ctx, nil, fmt.Errorf("error setting distributed tracing extension: %w", err) - } - } - } - - // 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) - - var span *trace.Span - if !c.transport.HasTracePropagation() { - if ext, ok := extensions.GetDistributedTracingExtension(event); ok { - ctx, span = ext.StartChildSpan(ctx, clientSpanName, trace.WithSpanKind(trace.SpanKindServer)) - } - } - if span == nil { - ctx, span = trace.StartSpan(ctx, clientSpanName, trace.WithSpanKind(trace.SpanKindServer)) - } - defer span.End() - if span.IsRecordingEvents() { - span.AddAttributes(eventTraceAttributes(event.Context)...) - } - - 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 { - err := c.fn.invoke(ctx, event, resp) - - // 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 -} diff --git a/v1/cloudevents/client/client_test.go b/v1/cloudevents/client/client_test.go deleted file mode 100644 index bb09baade..000000000 --- a/v1/cloudevents/client/client_test.go +++ /dev/null @@ -1,873 +0,0 @@ -package client_test - -import ( - "bytes" - "context" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "net/url" - "strings" - "testing" - "time" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/client" - cehttp "github.com/cloudevents/sdk-go/v1/cloudevents/transport/http" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/lightstep/tracecontext.go/traceparent" - "go.opencensus.io/trace" - - "github.com/google/go-cmp/cmp" -) - -var ( - // Headers that are added to the response, but we don't want to check in our assertions. - unimportantHeaders = []string{ - "accept-encoding", - "content-length", - "user-agent", - "connection", - "traceparent", - "tracestate", - } -) - -func simpleBinaryClient(target string) client.Client { - t, err := cehttp.New( - cehttp.WithTarget(target), - cehttp.WithBinaryEncoding(), - ) - if err != nil { - return nil - } - - c, err := client.New(t, client.WithoutTracePropagation()) - if err != nil { - return nil - } - return c -} - -func simpleTracingBinaryClient(target string) client.Client { - t, err := cehttp.New( - cehttp.WithTarget(target), - cehttp.WithBinaryEncoding(), - ) - if err != nil { - return nil - } - - c, err := client.New(t) - if err != nil { - return nil - } - return c -} - -func simpleStructuredClient(target string) client.Client { - t, err := cehttp.New( - cehttp.WithTarget(target), - cehttp.WithStructuredEncoding(), - ) - if err != nil { - return nil - } - - c, err := client.New(t, client.WithoutTracePropagation()) - if err != nil { - return nil - } - return c -} - -func TestClientSend(t *testing.T) { - now := time.Now() - - testCases := map[string]struct { - c func(target string) client.Client - event cloudevents.Event - resp *http.Response - want *requestValidation - wantErr string - }{ - "binary simple v0.1": { - c: simpleBinaryClient, - event: cloudevents.Event{ - Context: cloudevents.EventContextV01{ - EventType: "unit.test.client", - Source: *types.ParseURLRef("/unit/test/client"), - EventTime: &types.Timestamp{Time: now}, - EventID: "AABBCCDDEE", - }.AsV01(), - Data: &map[string]interface{}{ - "sq": 42, - "msg": "hello", - }, - }, - resp: &http.Response{ - StatusCode: http.StatusAccepted, - }, - want: &requestValidation{ - Headers: map[string][]string{ - "ce-cloudeventsversion": {"0.1"}, - "ce-eventid": {"AABBCCDDEE"}, - "ce-eventtime": {now.UTC().Format(time.RFC3339Nano)}, - "ce-eventtype": {"unit.test.client"}, - "ce-source": {"/unit/test/client"}, - }, - Body: `{"msg":"hello","sq":42}`, - }, - }, - "binary simple v0.2": { - c: simpleBinaryClient, - event: cloudevents.Event{ - Context: cloudevents.EventContextV02{ - Type: "unit.test.client", - Source: *types.ParseURLRef("/unit/test/client"), - Time: &types.Timestamp{Time: now}, - ID: "AABBCCDDEE", - }.AsV02(), - Data: &map[string]interface{}{ - "sq": 42, - "msg": "hello", - }, - }, - resp: &http.Response{ - StatusCode: http.StatusAccepted, - }, - want: &requestValidation{ - Headers: map[string][]string{ - "ce-specversion": {"0.2"}, - "ce-id": {"AABBCCDDEE"}, - "ce-time": {now.UTC().Format(time.RFC3339Nano)}, - "ce-type": {"unit.test.client"}, - "ce-source": {"/unit/test/client"}, - }, - Body: `{"msg":"hello","sq":42}`, - }, - }, - "binary simple v0.3": { - c: simpleBinaryClient, - event: cloudevents.Event{ - Context: cloudevents.EventContextV03{ - Type: "unit.test.client", - Source: *types.ParseURLRef("/unit/test/client"), - Time: &types.Timestamp{Time: now}, - ID: "AABBCCDDEE", - }.AsV03(), - Data: &map[string]interface{}{ - "sq": 42, - "msg": "hello", - }, - }, - resp: &http.Response{ - StatusCode: http.StatusAccepted, - }, - want: &requestValidation{ - Headers: map[string][]string{ - "ce-specversion": {"0.3"}, - "ce-id": {"AABBCCDDEE"}, - "ce-time": {now.UTC().Format(time.RFC3339Nano)}, - "ce-type": {"unit.test.client"}, - "ce-source": {"/unit/test/client"}, - }, - Body: `{"msg":"hello","sq":42}`, - }, - }, - "structured simple v0.1": { - c: simpleStructuredClient, - event: cloudevents.Event{ - Context: cloudevents.EventContextV01{ - EventType: "unit.test.client", - Source: *types.ParseURLRef("/unit/test/client"), - EventTime: &types.Timestamp{Time: now}, - EventID: "AABBCCDDEE", - }.AsV01(), - Data: &map[string]interface{}{ - "sq": 42, - "msg": "hello", - }, - }, - resp: &http.Response{ - StatusCode: http.StatusAccepted, - }, - want: &requestValidation{ - Headers: map[string][]string{ - "content-type": {"application/cloudevents+json"}, - }, - Body: fmt.Sprintf(`{"cloudEventsVersion":"0.1","data":{"msg":"hello","sq":42},"eventID":"AABBCCDDEE","eventTime":%q,"eventType":"unit.test.client","source":"/unit/test/client"}`, - now.UTC().Format(time.RFC3339Nano), - ), - }, - }, - "structured simple v0.2": { - c: simpleStructuredClient, - event: cloudevents.Event{ - Context: cloudevents.EventContextV02{ - Type: "unit.test.client", - Source: *types.ParseURLRef("/unit/test/client"), - Time: &types.Timestamp{Time: now}, - ID: "AABBCCDDEE", - }.AsV02(), - Data: &map[string]interface{}{ - "sq": 42, - "msg": "hello", - }, - }, - resp: &http.Response{ - StatusCode: http.StatusAccepted, - }, - want: &requestValidation{ - Headers: map[string][]string{ - "content-type": {"application/cloudevents+json"}, - }, - Body: fmt.Sprintf(`{"data":{"msg":"hello","sq":42},"id":"AABBCCDDEE","source":"/unit/test/client","specversion":"0.2","time":%q,"type":"unit.test.client"}`, - now.UTC().Format(time.RFC3339Nano), - ), - }, - }, - "structured simple v0.3": { - c: simpleStructuredClient, - event: cloudevents.Event{ - Context: cloudevents.EventContextV03{ - Type: "unit.test.client", - Source: *types.ParseURLRef("/unit/test/client"), - Time: &types.Timestamp{Time: now}, - ID: "AABBCCDDEE", - }.AsV03(), - Data: &map[string]interface{}{ - "sq": 42, - "msg": "hello", - }, - }, - resp: &http.Response{ - StatusCode: http.StatusAccepted, - }, - want: &requestValidation{ - Headers: map[string][]string{ - "content-type": {"application/cloudevents+json"}, - }, - Body: fmt.Sprintf(`{"data":{"msg":"hello","sq":42},"id":"AABBCCDDEE","source":"/unit/test/client","specversion":"0.3","time":%q,"type":"unit.test.client"}`, - now.UTC().Format(time.RFC3339Nano), - ), - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - handler := &fakeHandler{ - t: t, - response: tc.resp, - requests: make([]requestValidation, 0), - } - server := httptest.NewServer(handler) - defer server.Close() - - c := tc.c(server.URL) - - _, _, err := c.Send(context.TODO(), tc.event) // TODO: update test with new returned event and returned context - if tc.wantErr != "" { - if err == nil { - t.Fatalf("failed to return expected error, got nil") - } - want := tc.wantErr - got := err.Error() - if !strings.Contains(got, want) { - t.Fatalf("failed to return expected error, got %q, want %q", err, want) - } - return - } else { - if err != nil { - t.Fatalf("failed to send event: %s", err) - } - } - - rv := handler.popRequest(t) - - assertEquality(t, server.URL, *tc.want, rv) - }) - } -} - -func TestTracingClientSend(t *testing.T) { - now := time.Now() - - testCases := map[string]struct { - c func(target string) client.Client - event cloudevents.Event - resp *http.Response - tpHeader string - sample bool - }{ - "send unsampled": { - c: simpleTracingBinaryClient, - event: cloudevents.Event{ - Context: cloudevents.EventContextV01{ - EventType: "unit.test.client", - Source: *types.ParseURLRef("/unit/test/client"), - EventTime: &types.Timestamp{Time: now}, - EventID: "AABBCCDDEE", - }.AsV1(), - Data: &map[string]interface{}{ - "sq": 42, - "msg": "hello", - }, - }, - resp: &http.Response{ - StatusCode: http.StatusAccepted, - }, - tpHeader: "ce-traceparent", - }, - "send sampled": { - c: simpleTracingBinaryClient, - event: cloudevents.Event{ - Context: cloudevents.EventContextV01{ - EventType: "unit.test.client", - Source: *types.ParseURLRef("/unit/test/client"), - EventTime: &types.Timestamp{Time: now}, - EventID: "AABBCCDDEE", - }.AsV1(), - Data: &map[string]interface{}{ - "sq": 42, - "msg": "hello", - }, - }, - resp: &http.Response{ - StatusCode: http.StatusAccepted, - }, - sample: true, - tpHeader: "ce-traceparent", - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - handler := &fakeHandler{ - t: t, - response: tc.resp, - requests: make([]requestValidation, 0), - } - server := httptest.NewServer(handler) - defer server.Close() - - c := tc.c(server.URL) - - var sampler trace.Sampler - if tc.sample { - sampler = trace.AlwaysSample() - } else { - sampler = trace.NeverSample() - } - ctx, span := trace.StartSpan(context.TODO(), "test-span", trace.WithSampler(sampler)) - sc := span.SpanContext() - - _, _, err := c.Send(ctx, tc.event) - span.End() - - if err != nil { - t.Fatalf("failed to send event: %s", err) - } - - rv := handler.popRequest(t) - - var got traceparent.TraceParent - if tp := rv.Headers.Get(tc.tpHeader); tp == "" { - t.Fatal("missing traceparent header") - } else { - got, err = traceparent.ParseString(tp) - if err != nil { - t.Fatalf("invalid traceparent: %s", err) - } - } - if got.TraceID != sc.TraceID { - t.Errorf("unexpected trace id: want %s got %s", sc.TraceID, got.TraceID) - } - if got.Flags.Recorded != tc.sample { - t.Errorf("unexpected recorded flag: want %t got %t", tc.sample, got.Flags.Recorded) - } - }) - } -} - -func simpleBinaryOptions(port int, path string) []cehttp.Option { - opts := []cehttp.Option{ - cehttp.WithPort(port), - cehttp.WithBinaryEncoding(), - } - if len(path) > 0 { - opts = append(opts, cehttp.WithPath(path)) - } - return opts -} - -func simpleStructuredOptions(port int, path string) []cehttp.Option { - opts := []cehttp.Option{ - cehttp.WithPort(port), - cehttp.WithStructuredEncoding(), - } - if len(path) > 0 { - opts = append(opts, cehttp.WithPath(path)) - } - return opts -} - -func TestClientReceive(t *testing.T) { - now := time.Now() - - testCases := map[string]struct { - optsFn func(port int, path string) []cehttp.Option - req *requestValidation - want cloudevents.Event - wantErr string - }{ - "binary simple v0.1": { - optsFn: simpleBinaryOptions, - req: &requestValidation{ - Headers: map[string][]string{ - "ce-cloudeventsversion": {"0.1"}, - "ce-eventid": {"AABBCCDDEE"}, - "ce-eventtime": {now.UTC().Format(time.RFC3339Nano)}, - "ce-eventtype": {"unit.test.client"}, - "ce-source": {"/unit/test/client"}, - "content-type": {"application/json"}, - }, - Body: `{"msg":"hello","sq":"42"}`, - }, - want: cloudevents.Event{ - Context: cloudevents.EventContextV01{ - EventType: "unit.test.client", - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *types.ParseURLRef("/unit/test/client"), - EventTime: &types.Timestamp{Time: now}, - EventID: "AABBCCDDEE", - }.AsV01(), - Data: &map[string]string{ - "sq": "42", - "msg": "hello", - }, - }, - }, - "binary simple v0.2": { - optsFn: simpleBinaryOptions, - req: &requestValidation{ - Headers: map[string][]string{ - "ce-specversion": {"0.2"}, - "ce-id": {"AABBCCDDEE"}, - "ce-time": {now.UTC().Format(time.RFC3339Nano)}, - "ce-type": {"unit.test.client"}, - "ce-source": {"/unit/test/client"}, - "content-type": {"application/json"}, - }, - Body: `{"msg":"hello","sq":"42"}`, - }, - want: cloudevents.Event{ - Context: cloudevents.EventContextV02{ - Type: "unit.test.client", - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *types.ParseURLRef("/unit/test/client"), - Time: &types.Timestamp{Time: now}, - ID: "AABBCCDDEE", - }.AsV02(), - Data: &map[string]string{ - "sq": "42", - "msg": "hello", - }, - }, - }, - "binary simple v0.3": { - optsFn: simpleBinaryOptions, - req: &requestValidation{ - Headers: map[string][]string{ - "ce-specversion": {"0.3"}, - "ce-id": {"AABBCCDDEE"}, - "ce-time": {now.UTC().Format(time.RFC3339Nano)}, - "ce-type": {"unit.test.client"}, - "ce-source": {"/unit/test/client"}, - "content-type": {"application/json"}, - }, - Body: `{"msg":"hello","sq":"42"}`, - }, - want: cloudevents.Event{ - Context: cloudevents.EventContextV03{ - Type: "unit.test.client", - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *types.ParseURLRef("/unit/test/client"), - Time: &types.Timestamp{Time: now}, - ID: "AABBCCDDEE", - }.AsV03(), - Data: &map[string]string{ - "sq": "42", - "msg": "hello", - }, - }, - }, - "structured simple v0.1": { - optsFn: simpleStructuredOptions, - req: &requestValidation{ - Headers: map[string][]string{ - "content-type": {"application/cloudevents+json"}, - }, - Body: fmt.Sprintf(`{"cloudEventsVersion":"0.1","contentType":"application/json","data":{"msg":"hello","sq":"42"},"eventID":"AABBCCDDEE","eventTime":%q,"eventType":"unit.test.client","source":"/unit/test/client"}`, - now.UTC().Format(time.RFC3339Nano), - ), - }, - want: cloudevents.Event{ - Context: cloudevents.EventContextV01{ - EventType: "unit.test.client", - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *types.ParseURLRef("/unit/test/client"), - EventTime: &types.Timestamp{Time: now}, - EventID: "AABBCCDDEE", - }.AsV01(), - Data: &map[string]string{ - "sq": "42", - "msg": "hello", - }, - }, - }, - "structured simple v0.2": { - optsFn: simpleStructuredOptions, - req: &requestValidation{ - Headers: map[string][]string{ - "content-type": {"application/cloudevents+json"}, - }, - Body: fmt.Sprintf(`{"contenttype":"application/json","data":{"msg":"hello","sq":"42"},"id":"AABBCCDDEE","source":"/unit/test/client","specversion":"0.2","time":%q,"type":"unit.test.client"}`, - now.UTC().Format(time.RFC3339Nano), - ), - }, - want: cloudevents.Event{ - Context: cloudevents.EventContextV02{ - Type: "unit.test.client", - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *types.ParseURLRef("/unit/test/client"), - Time: &types.Timestamp{Time: now}, - ID: "AABBCCDDEE", - }.AsV02(), - Data: &map[string]string{ - "sq": "42", - "msg": "hello", - }, - }, - }, - "structured simple v0.3": { - optsFn: simpleStructuredOptions, - req: &requestValidation{ - Headers: map[string][]string{ - "content-type": {"application/cloudevents+json"}, - }, - Body: fmt.Sprintf(`{"data":{"msg":"hello","sq":"42"},"datacontenttype":"application/json","id":"AABBCCDDEE","source":"/unit/test/client","specversion":"0.3","time":%q,"type":"unit.test.client"}`, - now.UTC().Format(time.RFC3339Nano), - ), - }, - want: cloudevents.Event{ - Context: cloudevents.EventContextV03{ - Type: "unit.test.client", - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *types.ParseURLRef("/unit/test/client"), - Time: &types.Timestamp{Time: now}, - ID: "AABBCCDDEE", - }.AsV03(), - Data: &map[string]string{ - "sq": "42", - "msg": "hello", - }, - }, - }, - } - for n, tc := range testCases { - for _, path := range []string{"", "/", "/unittest/"} { - t.Run(n+" at path "+path, func(t *testing.T) { - - events := make(chan cloudevents.Event) - - tp, err := cehttp.New(tc.optsFn(0, path)...) - if err != nil { - t.Errorf("failed to make http transport %s", err.Error()) - } - - c, err := client.New(tp) - if err != nil { - t.Errorf("failed to make client %s", err.Error()) - } - - ctx, cancel := context.WithCancel(context.TODO()) - go func() { - err = c.StartReceiver(ctx, func(ctx context.Context, event cloudevents.Event, resp *cloudevents.EventResponse) error { - go func() { - events <- event - }() - return nil - }) - if err != nil { - t.Errorf("failed to start receiver %s", err.Error()) - } - }() - time.Sleep(5 * time.Millisecond) // let the server start - - target, _ := url.Parse(fmt.Sprintf("http://localhost:%d%s", tp.GetPort(), tp.GetPath())) - - if tc.wantErr != "" { - if err == nil { - t.Fatalf("failed to return expected error, got nil") - } - want := tc.wantErr - got := err.Error() - if !strings.Contains(got, want) { - t.Fatalf("failed to return expected error, got %q, want %q", err, want) - } - cancel() - return - } else { - if err != nil { - t.Fatalf("failed to send event %s", err) - } - } - - req := &http.Request{ - Method: "POST", - URL: target, - Header: tc.req.Headers, - Body: ioutil.NopCloser(strings.NewReader(tc.req.Body)), - ContentLength: int64(len([]byte(tc.req.Body))), - } - - _, err = http.DefaultClient.Do(req) - - //// Make a copy of the request. - //body, err := ioutil.ReadAll(resp.Body) - //if err != nil { - // t.Error("failed to read the request body") - //} - //gotResp := requestValidation{ - // Headers: resp.Header, - // Body: string(body), - //} - // - //_ = gotResp // TODO: check response - - got := <-events - - if diff := cmp.Diff(tc.want.Context, got.Context); diff != "" { - t.Errorf("unexpected events.Context (-want, +got) = %v", diff) - } - - data := &map[string]string{} - err = got.DataAs(data) - if err != nil { - t.Fatalf("returned unexpected error, got %s", err.Error()) - } - - if diff := cmp.Diff(tc.want.Data, data); diff != "" { - t.Errorf("unexpected events.Data (-want, +got) = %v", diff) - } - - // Now stop the client - cancel() - - // try the request again, expecting an error: - - if _, err = http.DefaultClient.Do(req); err == nil { - t.Fatalf("expected error to when sending request to stopped client") - } - }) - } - } -} - -func TestTracedClientReceive(t *testing.T) { - now := time.Now() - - testCases := map[string]struct { - optsFn func(port int, path string) []cehttp.Option - event cloudevents.Event - }{ - "simple binary v1.0": { - optsFn: simpleBinaryOptions, - event: cloudevents.Event{ - Context: cloudevents.EventContextV01{ - EventType: "unit.test.client", - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *types.ParseURLRef("/unit/test/client"), - EventTime: &types.Timestamp{Time: now}, - EventID: "AABBCCDDEE", - }.AsV1(), - Data: &map[string]string{ - "sq": "42", - "msg": "hello", - }, - }, - }, - "simple binary v0.2": { - optsFn: simpleBinaryOptions, - event: cloudevents.Event{ - Context: cloudevents.EventContextV02{ - Type: "unit.test.client", - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *types.ParseURLRef("/unit/test/client"), - Time: &types.Timestamp{Time: now}, - ID: "AABBCCDDEE", - }.AsV02(), - Data: &map[string]string{ - "sq": "42", - "msg": "hello", - }, - }, - }, - "simple binary v0.3": { - optsFn: simpleBinaryOptions, - event: cloudevents.Event{ - Context: cloudevents.EventContextV03{ - Type: "unit.test.client", - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *types.ParseURLRef("/unit/test/client"), - Time: &types.Timestamp{Time: now}, - ID: "AABBCCDDEE", - }.AsV03(), - Data: &map[string]string{ - "sq": "42", - "msg": "hello", - }, - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - spanContexts := make(chan trace.SpanContext) - - tp, err := cehttp.New(tc.optsFn(0, "")...) - if err != nil { - t.Errorf("failed to make http transport %s", err.Error()) - } - - c, err := client.New(tp) - if err != nil { - t.Errorf("failed to make client %s", err.Error()) - } - - ctx, cancel := context.WithCancel(context.TODO()) - go func() { - err = c.StartReceiver(ctx, func(ctx context.Context, event cloudevents.Event, resp *cloudevents.EventResponse) error { - go func() { - spanContexts <- trace.FromContext(ctx).SpanContext() - }() - return nil - }) - if err != nil { - t.Errorf("failed to start receiver %s", err.Error()) - } - }() - time.Sleep(5 * time.Millisecond) // let the server start - - target := fmt.Sprintf("http://localhost:%d", tp.GetPort()) - client := simpleBinaryClient(target) - - ctx, span := trace.StartSpan(context.TODO(), "test-span") - _, _, err = client.Send(ctx, tc.event) - span.End() - - if err != nil { - t.Fatalf("failed to send event %s", err) - } - - got := <-spanContexts - - if span.SpanContext().TraceID != got.TraceID { - t.Errorf("unexpected traceID. want: %s, got %s", span.SpanContext().TraceID, got.TraceID) - } - - // Now stop the client - cancel() - }) - } -} - -type requestValidation struct { - Host string - Headers http.Header - Body string -} - -type fakeHandler struct { - t *testing.T - response *http.Response - requests []requestValidation -} - -func (f *fakeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - - // Make a copy of the request. - body, err := ioutil.ReadAll(r.Body) - if err != nil { - f.t.Error("failed to read the request body") - } - f.requests = append(f.requests, requestValidation{ - Host: r.Host, - Headers: r.Header, - Body: string(body), - }) - - // Write the response. - if f.response != nil { - for h, vs := range f.response.Header { - for _, v := range vs { - w.Header().Add(h, v) - } - } - w.WriteHeader(f.response.StatusCode) - var buf bytes.Buffer - if f.response.ContentLength > 0 { - _, _ = buf.ReadFrom(f.response.Body) - _, _ = w.Write(buf.Bytes()) - } - } else { - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("")) - } -} - -func (f *fakeHandler) popRequest(t *testing.T) requestValidation { - if len(f.requests) == 0 { - t.Error("Unable to pop request") - } - rv := f.requests[0] - f.requests = f.requests[1:] - return rv -} - -func assertEquality(t *testing.T, replacementURL string, expected, actual requestValidation) { - server, err := url.Parse(replacementURL) - if err != nil { - t.Errorf("Bad replacement URL: %q", replacementURL) - } - expected.Host = server.Host - canonicalizeHeaders(expected, actual) - if diff := cmp.Diff(expected, actual); diff != "" { - t.Errorf("Unexpected difference (-want, +got): %v", diff) - } -} - -func canonicalizeHeaders(rvs ...requestValidation) { - // HTTP header names are case-insensitive, so normalize them to lower case for comparison. - for _, rv := range rvs { - headers := rv.Headers - for n, v := range headers { - delete(headers, n) - ln := strings.ToLower(n) - - if isImportantHeader(ln) { - headers[ln] = v - } - } - } -} - -func isImportantHeader(h string) bool { - for _, v := range unimportantHeaders { - if v == h { - return false - } - } - return true -} diff --git a/v1/cloudevents/client/defaulters.go b/v1/cloudevents/client/defaulters.go deleted file mode 100644 index 8ddd8bf28..000000000 --- a/v1/cloudevents/client/defaulters.go +++ /dev/null @@ -1,51 +0,0 @@ -package client - -import ( - "context" - "time" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/google/uuid" -) - -// EventDefaulter is the function signature for extensions that are able -// to perform event defaulting. -type EventDefaulter func(ctx context.Context, event cloudevents.Event) cloudevents.Event - -// DefaultIDToUUIDIfNotSet will inspect the provided event and assign a UUID to -// context.ID if it is found to be empty. -func DefaultIDToUUIDIfNotSet(ctx context.Context, event cloudevents.Event) cloudevents.Event { - if event.Context != nil { - if event.ID() == "" { - event.Context = event.Context.Clone() - event.SetID(uuid.New().String()) - } - } - return event -} - -// DefaultTimeToNowIfNotSet will inspect the provided event and assign a new -// Timestamp to context.Time if it is found to be nil or zero. -func DefaultTimeToNowIfNotSet(ctx context.Context, event cloudevents.Event) cloudevents.Event { - if event.Context != nil { - if event.Time().IsZero() { - event.Context = event.Context.Clone() - event.SetTime(time.Now()) - } - } - 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 cloudevents.Event) cloudevents.Event { - if event.Context != nil { - if event.DataContentType() == "" { - event.SetDataContentType(contentType) - } - } - return event - } -} diff --git a/v1/cloudevents/client/defaulters_test.go b/v1/cloudevents/client/defaulters_test.go deleted file mode 100644 index 30ff3404b..000000000 --- a/v1/cloudevents/client/defaulters_test.go +++ /dev/null @@ -1,159 +0,0 @@ -package client - -import ( - "context" - "testing" - "time" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/google/go-cmp/cmp" -) - -var versions = []string{"0.1", "0.2", "0.3", "1.0"} - -func TestDefaultIDToUUIDIfNotSet_empty(t *testing.T) { - for _, tc := range versions { - t.Run(tc, func(t *testing.T) { - got := DefaultIDToUUIDIfNotSet(context.TODO(), cloudevents.New(tc)) - - if got.Context != nil && got.ID() == "" { - t.Errorf("failed to generate an id for event") - } - }) - } -} - -func TestDefaultIDToUUIDIfNotSet_set(t *testing.T) { - for _, tc := range versions { - t.Run(tc, func(t *testing.T) { - event := cloudevents.New(tc) - event.SetID("abc-123") - - got := DefaultIDToUUIDIfNotSet(context.TODO(), event) - - if got.ID() != "abc-123" { - t.Errorf("id was defaulted when already set") - } - }) - } -} - -func TestDefaultIDToUUIDIfNotSet_nil(t *testing.T) { - got := DefaultIDToUUIDIfNotSet(context.TODO(), cloudevents.Event{}) - - if got.Context != nil && got.ID() == "" { - t.Errorf("failed to generate time for nil context event") - } -} - -func TestDefaultIDToUUIDIfNotSetImmutable(t *testing.T) { - event := cloudevents.Event{ - Context: &cloudevents.EventContextV01{}, - } - - got := DefaultIDToUUIDIfNotSet(context.TODO(), event) - - want := "0.1" - - if diff := cmp.Diff(want, got.SpecVersion()); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - - if event.Context.AsV01().EventID != "" { - t.Errorf("modified the original event") - } - - if got.Context.AsV01().EventID == "" { - t.Errorf("failed to generate an id for event") - } -} - -func TestDefaultTimeToNowIfNotSet_empty(t *testing.T) { - for _, tc := range versions { - t.Run(tc, func(t *testing.T) { - got := DefaultTimeToNowIfNotSet(context.TODO(), cloudevents.New(tc)) - - if got.Time().IsZero() { - t.Errorf("failed to generate time for event") - } - }) - } -} - -func TestDefaultTimeToNowIfNotSet_set(t *testing.T) { - for _, tc := range versions { - t.Run(tc, func(t *testing.T) { - event := cloudevents.New(tc) - now := time.Now() - - event.SetTime(now) - - got := DefaultTimeToNowIfNotSet(context.TODO(), event) - - if !got.Time().Equal(now) { - t.Errorf("time was defaulted when already set") - } - }) - } -} - -func TestDefaultTimeToNowIfNotSet_nil(t *testing.T) { - got := DefaultTimeToNowIfNotSet(context.TODO(), cloudevents.Event{}) - - if got.Context != nil && got.Time().IsZero() { - t.Errorf("failed to generate time for nil context event") - } -} - -func TestDefaultTimeToNowIfNotSetImmutable(t *testing.T) { - event := cloudevents.Event{ - Context: &cloudevents.EventContextV01{}, - } - - got := DefaultTimeToNowIfNotSet(context.TODO(), event) - - want := "0.1" - - if diff := cmp.Diff(want, got.SpecVersion()); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - - if event.Context.AsV01().EventTime != nil { - t.Errorf("modified the original event") - } - - if got.Context.AsV01().EventTime.IsZero() { - t.Errorf("failed to generate a time for event") - } -} - -func TestNewDefaultDataContentTypeIfNotSet_empty(t *testing.T) { - ct := "a/b" - for _, tc := range versions { - t.Run(tc, func(t *testing.T) { - fn := NewDefaultDataContentTypeIfNotSet(ct) - got := fn(context.TODO(), cloudevents.New(tc)) - - if got.DataContentType() != ct { - t.Errorf("failed to default data content type for event") - } - }) - } -} - -func TestNewDefaultDataContentTypeIfNotSet_set(t *testing.T) { - ct := "a/b" - for _, tc := range versions { - t.Run(tc, func(t *testing.T) { - event := cloudevents.New(tc) - event.SetDataContentType(ct) - - fn := NewDefaultDataContentTypeIfNotSet("b/c") - got := fn(context.TODO(), event) - - if got.DataContentType() != ct { - t.Errorf("failed to preserve data content type for event") - } - }) - } -} diff --git a/v1/cloudevents/client/doc.go b/v1/cloudevents/client/doc.go deleted file mode 100644 index a6a602bb4..000000000 --- a/v1/cloudevents/client/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -/* -Package client holds the recommended entry points for interacting with the CloudEvents Golang SDK. The client wraps -a selected transport. The client adds validation and defaulting for sending events, and flexible receiver method -registration. For full details, read the `client.Client` documentation. -*/ -package client diff --git a/v1/cloudevents/client/observability.go b/v1/cloudevents/client/observability.go deleted file mode 100644 index 110c78d22..000000000 --- a/v1/cloudevents/client/observability.go +++ /dev/null @@ -1,76 +0,0 @@ -package client - -import ( - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/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 ( - clientSpanName = "cloudevents.client" - - specversionAttr = "cloudevents.specversion" - typeAttr = "cloudevents.type" - sourceAttr = "cloudevents.source" - subjectAttr = "cloudevents.subject" - datacontenttypeAttr = "cloudevents.datacontenttype" - - reportSend observed = iota - reportReceive -) - -// MethodName implements Observable.MethodName -func (o observed) MethodName() string { - switch o { - case reportSend: - return "send" - case reportReceive: - return "receive" - default: - return "unknown" - } -} - -// LatencyMs implements Observable.LatencyMs -func (o observed) LatencyMs() *stats.Float64Measure { - return LatencyMs -} - -func eventTraceAttributes(e cloudevents.EventContextReader) []trace.Attribute { - as := []trace.Attribute{ - trace.StringAttribute(specversionAttr, e.GetSpecVersion()), - trace.StringAttribute(typeAttr, e.GetType()), - trace.StringAttribute(sourceAttr, e.GetSource()), - } - if sub := e.GetSubject(); sub != "" { - as = append(as, trace.StringAttribute(subjectAttr, sub)) - } - if dct := e.GetDataContentType(); dct != "" { - as = append(as, trace.StringAttribute(datacontenttypeAttr, dct)) - } - return as -} diff --git a/v1/cloudevents/client/options.go b/v1/cloudevents/client/options.go deleted file mode 100644 index a9364f6ad..000000000 --- a/v1/cloudevents/client/options.go +++ /dev/null @@ -1,72 +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 - } -} - -// WithDataContentType adds the resulting defaulter from -// NewDefaultDataContentTypeIfNotSet event defaulter to the end of the -// defaulter chain. -func WithDataContentType(contentType string) Option { - return func(c *ceClient) error { - c.eventDefaulterFns = append(c.eventDefaulterFns, NewDefaultDataContentTypeIfNotSet(contentType)) - 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 - } -} - -// WithoutTracePropagation disables automatic trace propagation via -// the distributed tracing extension. -func WithoutTracePropagation() Option { - return func(c *ceClient) error { - c.disableTracePropagation = true - return nil - } -} diff --git a/v1/cloudevents/client/options_test.go b/v1/cloudevents/client/options_test.go deleted file mode 100644 index 7488ca102..000000000 --- a/v1/cloudevents/client/options_test.go +++ /dev/null @@ -1,137 +0,0 @@ -package client - -import ( - "context" - "testing" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/google/go-cmp/cmp" -) - -func TestWithEventDefaulter(t *testing.T) { - - v1 := func(ctx context.Context, event cloudevents.Event) cloudevents.Event { - event.Context = event.Context.AsV01() - return event - } - - v2 := func(ctx context.Context, event cloudevents.Event) cloudevents.Event { - event.Context = event.Context.AsV02() - return event - } - - v3 := func(ctx context.Context, event cloudevents.Event) cloudevents.Event { - event.Context = event.Context.AsV03() - return event - } - - testCases := map[string]struct { - c *ceClient - fns []EventDefaulter - want int // number of defaulters - wantErr string - }{ - "none": { - c: &ceClient{}, - want: 0, - }, - "one": { - c: &ceClient{}, - fns: []EventDefaulter{v1}, - want: 1, - }, - "three": { - c: &ceClient{}, - fns: []EventDefaulter{v1, v2, v3}, - want: 3, - }, - "nil fn": { - c: &ceClient{}, - fns: []EventDefaulter{nil}, - wantErr: "client option was given an nil event defaulter", - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - var err error - for _, fn := range tc.fns { - err = tc.c.applyOptions(WithEventDefaulter(fn)) - if err != nil { - break - } - } - - if tc.wantErr != "" || err != nil { - var gotErr string - if err != nil { - gotErr = err.Error() - } - if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - got := len(tc.c.eventDefaulterFns) - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} - -func TestWith_Defaulters(t *testing.T) { - - testCases := map[string]struct { - c *ceClient - opts []Option - want int // number of defaulters - wantErr string - }{ - "none": { - c: &ceClient{}, - want: 0, - }, - "uuid": { - c: &ceClient{}, - opts: []Option{WithUUIDs()}, - want: 1, - }, - "time": { - c: &ceClient{}, - opts: []Option{WithTimeNow()}, - want: 1, - }, - "uuid and time": { - c: &ceClient{}, - opts: []Option{WithUUIDs(), WithTimeNow()}, - want: 2, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - var err error - if len(tc.opts) > 0 { - err = tc.c.applyOptions(tc.opts...) - } - - if tc.wantErr != "" || err != nil { - var gotErr string - if err != nil { - gotErr = err.Error() - } - if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - got := len(tc.c.eventDefaulterFns) - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} diff --git a/v1/cloudevents/client/receiver.go b/v1/cloudevents/client/receiver.go deleted file mode 100644 index d63ec13a3..000000000 --- a/v1/cloudevents/client/receiver.go +++ /dev/null @@ -1,193 +0,0 @@ -package client - -import ( - "context" - "errors" - "fmt" - "reflect" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/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 -} diff --git a/v1/cloudevents/client/receiver_test.go b/v1/cloudevents/client/receiver_test.go deleted file mode 100644 index 3beb1f764..000000000 --- a/v1/cloudevents/client/receiver_test.go +++ /dev/null @@ -1,237 +0,0 @@ -package client - -import ( - "context" - "errors" - "testing" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/google/go-cmp/cmp" -) - -func TestReceiverFnValidTypes(t *testing.T) { - for name, fn := range map[string]interface{}{ - "no in, no out": func() {}, - "no in, error out": func() error { return nil }, - "ctx in, no out": func(context.Context) {}, - "ctx, Event in, no out": func(context.Context, cloudevents.Event) {}, - "ctx, EventResponse in, no out": func(context.Context, *cloudevents.EventResponse) {}, - "ctx, Event, EventResponse in, no out": func(context.Context, cloudevents.Event, *cloudevents.EventResponse) {}, - "ctx in, error out": func(context.Context) error { return nil }, - "ctx, Event in, error out": func(context.Context, cloudevents.Event) error { return nil }, - "ctx, EventResponse in, error out": func(context.Context, *cloudevents.EventResponse) error { return nil }, - "ctx, Event, EventResponse in, error out": func(context.Context, cloudevents.Event, *cloudevents.EventResponse) error { return nil }, - "Event in, no out": func(cloudevents.Event) {}, - "EventResponse in, no out": func(*cloudevents.EventResponse) {}, - "Event, EventResponse in, no out": func(cloudevents.Event, *cloudevents.EventResponse) {}, - "Event in, error out": func(cloudevents.Event) error { return nil }, - "EventResponse in, error out": func(*cloudevents.EventResponse) error { return nil }, - "Event, EventResponse in, error out": func(cloudevents.Event, *cloudevents.EventResponse) error { return nil }, - } { - t.Run(name, func(t *testing.T) { - if _, err := receiver(fn); err != nil { - t.Errorf("%q failed: %v", name, err) - } - }) - } -} - -func TestReceiverFnInvalidTypes(t *testing.T) { - for name, fn := range map[string]interface{}{ - "wrong type in": func(string) {}, - "wrong type out": func() string { return "" }, - "extra in": func(context.Context, cloudevents.Event, *cloudevents.EventResponse, map[string]string) {}, - "extra out": func(context.Context, *cloudevents.EventResponse) (error, int) { return nil, 0 }, - "context dup EventResponse in": func(context.Context, *cloudevents.EventResponse, *cloudevents.EventResponse) {}, - "dup EventResponse in": func(*cloudevents.EventResponse, *cloudevents.EventResponse) {}, - "context dup Event in": func(context.Context, cloudevents.Event, cloudevents.Event) {}, - "dup Event in": func(cloudevents.Event, cloudevents.Event) {}, - "wrong order, context3 in": func(*cloudevents.EventResponse, *cloudevents.EventResponse, context.Context) {}, - "wrong order, event in": func(context.Context, *cloudevents.EventResponse, cloudevents.Event) {}, - "wrong order, resp in": func(*cloudevents.EventResponse, cloudevents.Event) {}, - "wrong order, context2 in": func(*cloudevents.EventResponse, context.Context) {}, - "Event as ptr in": func(*cloudevents.Event) {}, - "EventResponse as non-ptr in": func(cloudevents.EventResponse) {}, - "extra Event in": func(cloudevents.Event, *cloudevents.EventResponse, cloudevents.Event) {}, - "not a function": map[string]string(nil), - } { - t.Run(name, func(t *testing.T) { - if _, err := receiver(fn); err == nil { - t.Errorf("%q failed to catch the issue", name) - } - }) - } -} - -func TestReceiverFnInvoke_1(t *testing.T) { - wantErr := errors.New("UNIT TEST") - key := struct{}{} - wantCtx := context.WithValue(context.TODO(), key, "UNIT TEST") - wantEvent := cloudevents.Event{ - Context: &cloudevents.EventContextV02{ - ID: "UNIT TEST", - }, - } - wantResp := &cloudevents.EventResponse{Reason: "UNIT TEST"} - - fn, err := receiver(func(ctx context.Context, event cloudevents.Event, resp *cloudevents.EventResponse) error { - if diff := cmp.Diff(wantCtx.Value(key), ctx.Value(key)); diff != "" { - t.Errorf("unexpected context (-want, +got) = %v", diff) - } - - if diff := cmp.Diff(wantEvent, event); diff != "" { - t.Errorf("unexpected event (-want, +got) = %v", diff) - } - - if diff := cmp.Diff(wantResp, resp); diff != "" { - t.Errorf("unexpected response (-want, +got) = %v", diff) - } - return wantErr - }) - if err != nil { - t.Errorf("unexpected error, wanted nil got = %v", err) - } - - err = fn.invoke(wantCtx, wantEvent, wantResp) - - if diff := cmp.Diff(wantErr.Error(), err.Error()); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } -} - -func TestReceiverFnInvoke_2(t *testing.T) { - wantErr := errors.New("UNIT TEST") - key := struct{}{} - ctx := context.WithValue(context.TODO(), key, "UNIT TEST") - wantEvent := cloudevents.Event{ - Context: &cloudevents.EventContextV02{ - ID: "UNIT TEST", - }, - } - wantResp := &cloudevents.EventResponse{Reason: "UNIT TEST"} - - fn, err := receiver(func(event cloudevents.Event, resp *cloudevents.EventResponse) error { - if diff := cmp.Diff(wantEvent, event); diff != "" { - t.Errorf("unexpected event (-want, +got) = %v", diff) - } - - if diff := cmp.Diff(wantResp, resp); diff != "" { - t.Errorf("unexpected response (-want, +got) = %v", diff) - } - return wantErr - }) - if err != nil { - t.Errorf("unexpected error, wanted nil got = %v", err) - } - - err = fn.invoke(ctx, wantEvent, wantResp) - - if diff := cmp.Diff(wantErr.Error(), err.Error()); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } -} - -func TestReceiverFnInvoke_3(t *testing.T) { - key := struct{}{} - ctx := context.WithValue(context.TODO(), key, "UNIT TEST") - wantEvent := cloudevents.Event{ - Context: &cloudevents.EventContextV02{ - ID: "UNIT TEST", - }, - } - wantResp := &cloudevents.EventResponse{Reason: "UNIT TEST"} - - fn, err := receiver(func(event cloudevents.Event, resp *cloudevents.EventResponse) { - if diff := cmp.Diff(wantEvent, event); diff != "" { - t.Errorf("unexpected event (-want, +got) = %v", diff) - } - - if diff := cmp.Diff(wantResp, resp); diff != "" { - t.Errorf("unexpected response (-want, +got) = %v", diff) - } - }) - if err != nil { - t.Errorf("unexpected error, wanted nil got = %v", err) - } - - err = fn.invoke(ctx, wantEvent, wantResp) - - if err != nil { - t.Errorf("unexpected error, want nil got got = %v", err.Error()) - } -} - -func TestReceiverFnInvoke_4(t *testing.T) { - wantErr := errors.New("UNIT TEST") - key := struct{}{} - ctx := context.WithValue(context.TODO(), key, "UNIT TEST") - event := cloudevents.Event{ - Context: &cloudevents.EventContextV02{ - ID: "UNIT TEST", - }, - } - wantResp := &cloudevents.EventResponse{Reason: "UNIT TEST"} - - fn, err := receiver(func(resp *cloudevents.EventResponse) error { - if diff := cmp.Diff(wantResp, resp); diff != "" { - t.Errorf("unexpected response (-want, +got) = %v", diff) - } - return wantErr - }) - if err != nil { - t.Errorf("unexpected error, wanted nil got = %v", err) - } - - err = fn.invoke(ctx, event, wantResp) - - if diff := cmp.Diff(wantErr.Error(), err.Error()); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } -} - -func TestReceiverFnInvoke_5(t *testing.T) { - wantErr := errors.New("UNIT TEST") - key := struct{}{} - ctx := context.WithValue(context.TODO(), key, "UNIT TEST") - event := cloudevents.Event{ - Context: &cloudevents.EventContextV02{ - ID: "UNIT TEST", - }, - } - resp := &cloudevents.EventResponse{Reason: "UNIT TEST"} - - fn, err := receiver(func() error { - return wantErr - }) - if err != nil { - t.Errorf("unexpected error, wanted nil got = %v", err) - } - - err = fn.invoke(ctx, event, resp) - - if diff := cmp.Diff(wantErr.Error(), err.Error()); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } -} - -func TestReceiverFnInvoke_6(t *testing.T) { - key := struct{}{} - ctx := context.WithValue(context.TODO(), key, "UNIT TEST") - event := cloudevents.Event{ - Context: &cloudevents.EventContextV02{ - ID: "UNIT TEST", - }, - } - resp := &cloudevents.EventResponse{Reason: "UNIT TEST"} - - fn, err := receiver(func() {}) - if err != nil { - t.Errorf("unexpected error, wanted nil got = %v", err) - } - - err = fn.invoke(ctx, event, resp) - - if err != nil { - t.Errorf("unexpected error, want nil got got = %v", err.Error()) - } -} diff --git a/v1/cloudevents/content_type.go b/v1/cloudevents/content_type.go deleted file mode 100644 index e4e0e17f2..000000000 --- a/v1/cloudevents/content_type.go +++ /dev/null @@ -1,35 +0,0 @@ -package cloudevents - -const ( - TextJSON = "text/json" - ApplicationJSON = "application/json" - ApplicationXML = "application/xml" - ApplicationCloudEventsJSON = "application/cloudevents+json" - ApplicationCloudEventsBatchJSON = "application/cloudevents-batch+json" -) - -// StringOfApplicationJSON returns a string pointer to "application/json" -func StringOfApplicationJSON() *string { - a := ApplicationJSON - return &a -} - -// StringOfApplicationXML returns a string pointer to "application/xml" -func StringOfApplicationXML() *string { - a := ApplicationXML - return &a -} - -// StringOfApplicationCloudEventsJSON returns a string pointer to -// "application/cloudevents+json" -func StringOfApplicationCloudEventsJSON() *string { - a := ApplicationCloudEventsJSON - return &a -} - -// StringOfApplicationCloudEventsBatchJSON returns a string pointer to -// "application/cloudevents-batch+json" -func StringOfApplicationCloudEventsBatchJSON() *string { - a := ApplicationCloudEventsBatchJSON - return &a -} diff --git a/v1/cloudevents/content_type_test.go b/v1/cloudevents/content_type_test.go deleted file mode 100644 index 2f2f10d7f..000000000 --- a/v1/cloudevents/content_type_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package cloudevents_test - -import ( - "testing" - - ce "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/google/go-cmp/cmp" -) - -func TestStringOfApplicationJSON(t *testing.T) { - want := strptr("application/json") - got := ce.StringOfApplicationJSON() - - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("unexpected string (-want, +got) = %v", diff) - } -} - -func TestStringOfApplicationXML(t *testing.T) { - want := strptr("application/xml") - got := ce.StringOfApplicationXML() - - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("unexpected string (-want, +got) = %v", diff) - } -} - -func TestStringOfApplicationCloudEventsJSON(t *testing.T) { - want := strptr("application/cloudevents+json") - got := ce.StringOfApplicationCloudEventsJSON() - - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("unexpected string (-want, +got) = %v", diff) - } -} - -func TestStringOfApplicationCloudEventsBatchJSON(t *testing.T) { - want := strptr("application/cloudevents-batch+json") - got := ce.StringOfApplicationCloudEventsBatchJSON() - - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("unexpected string (-want, +got) = %v", diff) - } -} diff --git a/v1/cloudevents/context/context.go b/v1/cloudevents/context/context.go deleted file mode 100644 index e580360f1..000000000 --- a/v1/cloudevents/context/context.go +++ /dev/null @@ -1,76 +0,0 @@ -package context - -import ( - "context" - "net/url" - "strings" -) - -// Opaque key type used to store target -type targetKeyType struct{} - -var targetKey = targetKeyType{} - -// WithTarget returns back a new context with the given target. Target is intended to be transport dependent. -// For http transport, `target` should be a full URL and will be injected into the outbound http request. -func WithTarget(ctx context.Context, target string) context.Context { - return context.WithValue(ctx, targetKey, target) -} - -// TargetFrom looks in the given context and returns `target` as a parsed url if found and valid, otherwise nil. -func TargetFrom(ctx context.Context) *url.URL { - c := ctx.Value(targetKey) - if c != nil { - if s, ok := c.(string); ok && s != "" { - if target, err := url.Parse(s); err == nil { - return target - } - } - } - return nil -} - -// Opaque key type used to store topic -type topicKeyType struct{} - -var topicKey = topicKeyType{} - -// WithTopic returns back a new context with the given topic. Topic is intended to be transport dependent. -// For pubsub transport, `topic` should be a Pub/Sub Topic ID. -func WithTopic(ctx context.Context, topic string) context.Context { - return context.WithValue(ctx, topicKey, topic) -} - -// TopicFrom looks in the given context and returns `topic` as a string if found and valid, otherwise "". -func TopicFrom(ctx context.Context) string { - c := ctx.Value(topicKey) - if c != nil { - if s, ok := c.(string); ok { - return s - } - } - 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 "" -} diff --git a/v1/cloudevents/context/context_test.go b/v1/cloudevents/context/context_test.go deleted file mode 100644 index b12a5eccf..000000000 --- a/v1/cloudevents/context/context_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package context_test - -import ( - "context" - "net/url" - "testing" - - cecontext "github.com/cloudevents/sdk-go/v1/cloudevents/context" - "github.com/google/go-cmp/cmp" -) - -func TestTargetContext(t *testing.T) { - exampleDotCom, _ := url.Parse("http://example.com") - - testCases := map[string]struct { - target string - ctx context.Context - want *url.URL - }{ - "nil context": {}, - "nil context, set url": { - target: "http://example.com", - want: exampleDotCom, - }, - "todo context, set url": { - ctx: context.TODO(), - target: "http://example.com", - want: exampleDotCom, - }, - "bad url": { - ctx: context.TODO(), - target: "%", - }, - "already set target": { - ctx: cecontext.WithTarget(context.TODO(), "http://example2.com"), - target: "http://example.com", - want: exampleDotCom, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - ctx := cecontext.WithTarget(tc.ctx, tc.target) - - got := cecontext.TargetFrom(ctx) - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} - -func TestEncodingContext(t *testing.T) { - testCases := map[string]struct { - encoding string - ctx context.Context - want string - }{ - "nil context": {}, - "nil context, set encoding": { - encoding: "foo", - want: "foo", - }, - "todo context, set encoding": { - ctx: context.TODO(), - encoding: "foo", - want: "foo", - }, - "already set encoding": { - ctx: cecontext.WithTarget(context.TODO(), "foo"), - encoding: "bar", - want: "bar", - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - ctx := cecontext.WithEncoding(tc.ctx, tc.encoding) - - got := cecontext.EncodingFrom(ctx) - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} diff --git a/v1/cloudevents/context/doc.go b/v1/cloudevents/context/doc.go deleted file mode 100644 index 377cab850..000000000 --- a/v1/cloudevents/context/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -/* -Package context holds the last resort overrides and fyi objects that can be passed to clients and transports added to -context.Context objects. -*/ -package context diff --git a/v1/cloudevents/context/logger.go b/v1/cloudevents/context/logger.go deleted file mode 100644 index 996f72057..000000000 --- a/v1/cloudevents/context/logger.go +++ /dev/null @@ -1,43 +0,0 @@ -package context - -import ( - "context" - - "go.uber.org/zap" -) - -// Opaque key type used to store logger -type loggerKeyType struct{} - -var loggerKey = loggerKeyType{} - -// fallbackLogger is the logger is used when there is no logger attached to the context. -var fallbackLogger *zap.SugaredLogger - -func init() { - if logger, err := zap.NewProduction(); err != nil { - // We failed to create a fallback logger. - fallbackLogger = zap.NewNop().Sugar() - } else { - fallbackLogger = logger.Named("fallback").Sugar() - } -} - -// WithLogger returns a new context with the logger injected into the given context. -func WithLogger(ctx context.Context, logger *zap.SugaredLogger) context.Context { - if logger == nil { - return context.WithValue(ctx, loggerKey, fallbackLogger) - } - return context.WithValue(ctx, loggerKey, logger) -} - -// LoggerFrom returns the logger stored in context. -func LoggerFrom(ctx context.Context) *zap.SugaredLogger { - l := ctx.Value(loggerKey) - if l != nil { - if logger, ok := l.(*zap.SugaredLogger); ok { - return logger - } - } - return fallbackLogger -} diff --git a/v1/cloudevents/context/logger_test.go b/v1/cloudevents/context/logger_test.go deleted file mode 100644 index ee369cfae..000000000 --- a/v1/cloudevents/context/logger_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package context - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "go.uber.org/zap" -) - -func TestLoggerContext(t *testing.T) { - var namedLogger *zap.SugaredLogger - if logger, err := zap.NewProduction(); err != nil { - t.Fatal(err) - } else { - namedLogger = logger.Named("unittest").Sugar() - } - - nopLogger := zap.NewNop().Sugar() - - testCases := map[string]struct { - logger *zap.SugaredLogger - ctx context.Context - want *zap.SugaredLogger - }{ - "nil context": { - want: fallbackLogger, - }, - "nil context, set nop logger": { - logger: nopLogger, - want: nopLogger, - }, - "todo context, set logger": { - ctx: context.TODO(), - logger: namedLogger, - want: namedLogger, - }, - "already set logger": { - ctx: WithLogger(context.TODO(), nopLogger), - logger: namedLogger, - want: namedLogger, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - ctx := WithLogger(tc.ctx, tc.logger) - got := LoggerFrom(ctx) - - if diff := cmp.Diff(tc.want, got, cmpopts.IgnoreUnexported(zap.SugaredLogger{})); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} diff --git a/v1/cloudevents/data_content_encoding.go b/v1/cloudevents/data_content_encoding.go deleted file mode 100644 index 180102ee3..000000000 --- a/v1/cloudevents/data_content_encoding.go +++ /dev/null @@ -1,11 +0,0 @@ -package cloudevents - -const ( - Base64 = "base64" -) - -// StringOfBase64 returns a string pointer to "Base64" -func StringOfBase64() *string { - a := Base64 - return &a -} diff --git a/v1/cloudevents/data_content_encoding_test.go b/v1/cloudevents/data_content_encoding_test.go deleted file mode 100644 index ab62ba8a6..000000000 --- a/v1/cloudevents/data_content_encoding_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package cloudevents_test - -import ( - "testing" - - ce "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/google/go-cmp/cmp" -) - -func TestStringOfBase64(t *testing.T) { - want := strptr("base64") - got := ce.StringOfBase64() - - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("unexpected string (-want, +got) = %v", diff) - } -} diff --git a/v1/cloudevents/datacodec/codec.go b/v1/cloudevents/datacodec/codec.go deleted file mode 100644 index 12243addb..000000000 --- a/v1/cloudevents/datacodec/codec.go +++ /dev/null @@ -1,96 +0,0 @@ -package datacodec - -import ( - "context" - "fmt" - - "github.com/cloudevents/sdk-go/v1/cloudevents/datacodec/json" - "github.com/cloudevents/sdk-go/v1/cloudevents/datacodec/text" - "github.com/cloudevents/sdk-go/v1/cloudevents/datacodec/xml" - "github.com/cloudevents/sdk-go/v1/cloudevents/observability" -) - -// Decoder is the expected function signature for decoding `in` to `out`. What -// `in` is could be decoder dependent. For example, `in` could be bytes, or a -// base64 string. -type Decoder func(ctx context.Context, in, out interface{}) error - -// Encoder is the expected function signature for encoding `in` to bytes. -// Returns an error if the encoder has an issue encoding `in`. -type Encoder func(ctx context.Context, in interface{}) ([]byte, error) - -var decoder map[string]Decoder -var encoder map[string]Encoder - -func init() { - decoder = make(map[string]Decoder, 10) - encoder = make(map[string]Encoder, 10) - - AddDecoder("", json.Decode) - AddDecoder("application/json", json.Decode) - AddDecoder("text/json", json.Decode) - AddDecoder("application/xml", xml.Decode) - AddDecoder("text/xml", xml.Decode) - AddDecoder("text/plain", text.Decode) - - AddEncoder("", json.Encode) - AddEncoder("application/json", json.Encode) - AddEncoder("text/json", json.Encode) - AddEncoder("application/xml", xml.Encode) - AddEncoder("text/xml", xml.Encode) - AddEncoder("text/plain", text.Encode) -} - -// AddDecoder registers a decoder for a given content type. The codecs will use -// these to decode the data payload from a cloudevent.Event object. -func AddDecoder(contentType string, fn Decoder) { - decoder[contentType] = fn -} - -// AddEncoder registers an encoder for a given content type. The codecs will -// use these to encode the data payload for a cloudevent.Event object. -func AddEncoder(contentType string, fn Encoder) { - encoder[contentType] = fn -} - -// 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 -// content type. -func Decode(ctx context.Context, contentType string, in, 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 { - return fn(ctx, in, out) - } - return fmt.Errorf("[decode] unsupported content type: %q", contentType) -} - -// Encode looks up and invokes the encoder registered for the given content -// type. An error is returned if no encoder is registered for the given -// content type. -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 { - return fn(ctx, in) - } - return nil, fmt.Errorf("[encode] unsupported content type: %q", contentType) -} diff --git a/v1/cloudevents/datacodec/codec_test.go b/v1/cloudevents/datacodec/codec_test.go deleted file mode 100644 index 831041e77..000000000 --- a/v1/cloudevents/datacodec/codec_test.go +++ /dev/null @@ -1,239 +0,0 @@ -package datacodec_test - -import ( - "context" - "fmt" - "strings" - "testing" - - "github.com/cloudevents/sdk-go/v1/cloudevents/datacodec" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/google/go-cmp/cmp" -) - -func strptr(s string) *string { return &s } - -type Example struct { - Sequence int `json:"id"` - Message string `json:"message"` -} - -func TestCodecDecode(t *testing.T) { - testCases := map[string]struct { - contentType string - decoder datacodec.Decoder - in interface{} - want interface{} - wantErr string - }{ - "empty": {}, - "invalid content type": { - contentType: "unit/testing-invalid", - wantErr: `[decode] unsupported content type: "unit/testing-invalid"`, - }, - - "text/plain": { - contentType: "text/plain", - in: "hello😀", - want: strptr("hello😀"), // Test unicode outiside UTF-8 - }, - "application/json": { - contentType: "application/json", - in: []byte(`{"a":"apple","b":"banana"}`), - want: &map[string]string{ - "a": "apple", - "b": "banana", - }, - }, - "application/xml": { - contentType: "application/xml", - in: []byte(`7Hello, Structured Encoding v0.2!`), - want: &Example{Sequence: 7, Message: "Hello, Structured Encoding v0.2!"}, - }, - - "custom content type": { - contentType: "unit/testing", - in: []byte("Hello, Testing"), - decoder: func(ctx context.Context, in, out interface{}) error { - if b, ok := in.([]byte); ok { - if s, k := out.(*map[string]string); k { - if (*s) == nil { - (*s) = make(map[string]string) - } - (*s)["upper"] = strings.ToUpper(string(b)) - (*s)["lower"] = strings.ToLower(string(b)) - } - } - return nil - }, - want: &map[string]string{ - "upper": "HELLO, TESTING", - "lower": "hello, testing", - }, - }, - "custom content type error": { - contentType: "unit/testing", - in: []byte("Hello, Testing"), - decoder: func(ctx context.Context, in, out interface{}) error { - return fmt.Errorf("expecting unit test error") - }, - wantErr: "expecting unit test error", - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - if tc.decoder != nil { - datacodec.AddDecoder(tc.contentType, tc.decoder) - } - - got, _ := types.Allocate(tc.want) - - err := datacodec.Decode(context.TODO(), tc.contentType, tc.in, got) - - if tc.wantErr != "" || err != nil { - if diff := cmp.Diff(tc.wantErr, err.Error()); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if tc.want != nil { - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected data (-want, +got) = %v", diff) - } - } - }) - } -} - -func TestCodecEncode(t *testing.T) { - testCases := map[string]struct { - contentType string - encoder datacodec.Encoder - in interface{} - want []byte - wantErr string - }{ - "empty": {}, - "invalid content type": { - contentType: "unit/testing-invalid", - wantErr: `[encode] unsupported content type: "unit/testing-invalid"`, - }, - "blank": { - contentType: "", - in: map[string]string{ - "a": "apple", - "b": "banana", - }, - want: []byte(`{"a":"apple","b":"banana"}`), - }, - "application/json": { - contentType: "application/json", - in: map[string]string{ - "a": "apple", - "b": "banana", - }, - want: []byte(`{"a":"apple","b":"banana"}`), - }, - "application/xml": { - contentType: "application/xml", - in: &Example{Sequence: 7, Message: "Hello, Structured Encoding v0.2!"}, - want: []byte(`7Hello, Structured Encoding v0.2!`), - }, - - "custom content type": { - contentType: "unit/testing", - in: []string{ - "Hello,", - "Testing", - }, - encoder: func(ctx context.Context, in interface{}) ([]byte, error) { - if s, ok := in.([]string); ok { - sb := strings.Builder{} - for _, v := range s { - if sb.Len() > 0 { - sb.WriteString(" ") - } - sb.WriteString(v) - } - return []byte(sb.String()), nil - } - return nil, fmt.Errorf("don't get here") - }, - want: []byte("Hello, Testing"), - }, - "custom content type error": { - contentType: "unit/testing", - in: []byte("Hello, Testing"), - encoder: func(ctx context.Context, in interface{}) ([]byte, error) { - return nil, fmt.Errorf("expecting unit test error") - }, - wantErr: "expecting unit test error", - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - if tc.encoder != nil { - datacodec.AddEncoder(tc.contentType, tc.encoder) - } - - got, err := datacodec.Encode(context.TODO(), tc.contentType, tc.in) - - if tc.wantErr != "" || err != nil { - if diff := cmp.Diff(tc.wantErr, err.Error()); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if tc.want != nil { - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected data (-want, +got) = %v", diff) - } - } - }) - } -} - -// -//func TestCodecRoundTrip(t *testing.T) { -// testCases := map[string]struct { -// contentType string -// decoder datacodec.Decoder -// encoder datacodec.Encoder -// in interface{} -// want interface{} -// wantErr string -// }{ -// "empty": {}, -// } -// for n, tc := range testCases { -// t.Run(n, func(t *testing.T) { -// -// if tc.decoder != nil { -// datacodec.AddDecoder(tc.contentType, tc.decoder) -// } -// -// -// // TODO -// got, _ := types.Allocate(tc.want) -// -// err := datacodec.Decode(tc.contentType, tc.in, got) -// -// if tc.wantErr != "" || err != nil { -// if diff := cmp.Diff(tc.wantErr, err.Error()); diff != "" { -// t.Errorf("unexpected error (-want, +got) = %v", diff) -// } -// return -// } -// -// if tc.want != nil { -// if diff := cmp.Diff(tc.want, got); diff != "" { -// t.Errorf("unexpected data (-want, +got) = %v", diff) -// } -// } -// }) -// } -//} diff --git a/v1/cloudevents/datacodec/doc.go b/v1/cloudevents/datacodec/doc.go deleted file mode 100644 index 9e401534e..000000000 --- a/v1/cloudevents/datacodec/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -/* -Package datacodec holds the data codec registry and adds known encoders and decoders supporting media types such as -`application/json` and `application/xml`. -*/ -package datacodec diff --git a/v1/cloudevents/datacodec/json/data.go b/v1/cloudevents/datacodec/json/data.go deleted file mode 100644 index 31ef508b1..000000000 --- a/v1/cloudevents/datacodec/json/data.go +++ /dev/null @@ -1,97 +0,0 @@ -package json - -import ( - "context" - "encoding/json" - "fmt" - "reflect" - "strconv" - - "github.com/cloudevents/sdk-go/v1/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) -} diff --git a/v1/cloudevents/datacodec/json/data_test.go b/v1/cloudevents/datacodec/json/data_test.go deleted file mode 100644 index 956814973..000000000 --- a/v1/cloudevents/datacodec/json/data_test.go +++ /dev/null @@ -1,252 +0,0 @@ -package json_test - -import ( - "context" - "encoding/json" - "fmt" - "testing" - "time" - - cej "github.com/cloudevents/sdk-go/v1/cloudevents/datacodec/json" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/google/go-cmp/cmp" -) - -type DataExample struct { - AnInt int `json:"a,omitempty"` - AString string `json:"b,omitempty"` - AnArray []string `json:"c,omitempty"` - AMap map[string]map[string]int `json:"d,omitempty"` - ATime *time.Time `json:"e,omitempty"` -} - -type BadMarshal struct{} - -func (b BadMarshal) MarshalJSON() ([]byte, error) { - return nil, fmt.Errorf("BadMashal Error") -} - -func TestCodecDecode(t *testing.T) { - now := time.Now() - - testCases := map[string]struct { - in interface{} - want interface{} - wantErr string - }{ - "empty": {}, - "out nil": { - in: "not nil", - wantErr: "out is nil", - }, - "not a []byte": { - in: "something that is not a map", - want: &map[string]string{ - "an": "error", - }, - wantErr: `[json] found bytes ""something that is not a map"", but failed to unmarshal: json: cannot unmarshal string into Go value of type map[string]string`, - }, - "BadMarshal": { - in: BadMarshal{}, - want: &BadMarshal{}, - wantErr: "[json] failed to marshal in: json: error calling MarshalJSON for type json_test.BadMarshal: BadMashal Error", - }, - "Bad Quotes": { - in: []byte{'\\', '"'}, - want: &map[string]string{ - "an": "error", - }, - wantErr: "[json] failed to unquote in: invalid syntax", - }, - "simple": { - in: []byte(`{"a":"apple","b":"banana"}`), - want: &map[string]string{ - "a": "apple", - "b": "banana", - }, - }, - "complex empty": { - in: []byte(`{}`), - want: &DataExample{}, - }, - "simple array": { - in: []byte(`["apple","banana"]`), - want: &[]string{ - "apple", - "banana", - }, - }, - "simple quoted array": { - in: []byte(`"[\"apple\",\"banana\"]"`), - want: &[]string{ - "apple", - "banana", - }, - }, - "complex filled": { - in: func() []byte { - data := &DataExample{ - AnInt: 42, - AMap: map[string]map[string]int{ - "a": {"1": 1, "2": 2, "3": 3}, - "z": {"3": 3, "2": 2, "1": 1}, - }, - AString: "Hello, World!", - ATime: &now, - AnArray: []string{"Anne", "Bob", "Chad"}, - } - - j, err := json.Marshal(data) - if err != nil { - t.Errorf("failed to marshal test data: %s", err.Error()) - } - return j - }(), - want: &DataExample{ - AnInt: 42, - AMap: map[string]map[string]int{ - "a": {"1": 1, "2": 2, "3": 3}, - "z": {"3": 3, "2": 2, "1": 1}, - }, - AString: "Hello, World!", - ATime: &now, - AnArray: []string{"Anne", "Bob", "Chad"}, - }, - }, - "object in": { - in: &DataExample{ - AnInt: 42, - }, - want: &DataExample{ - AnInt: 42, - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - got, _ := types.Allocate(tc.want) - - err := cej.Decode(context.TODO(), tc.in, got) - if tc.wantErr != "" || err != nil { - var gotErr string - if err != nil { - gotErr = err.Error() - } - if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if tc.want != nil { - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected data (-want, +got) = %v", diff) - } - } - }) - } -} - -// TODO: test for bad []byte input? -func TestCodecEncode(t *testing.T) { - now := time.Now() - - testCases := map[string]struct { - in interface{} - want []byte - wantErr string - }{ - "empty": {}, - "BadMarshal": { - in: BadMarshal{}, - wantErr: "json: error calling MarshalJSON for type json_test.BadMarshal: BadMashal Error", - }, - "already encoded object": { - in: []byte(`{"a":"apple","b":"banana"}`), - want: []byte(`{"a":"apple","b":"banana"}`), - }, - "already encoded quote": { - in: []byte(`"{"a":"apple","b":"banana"}"`), - want: []byte(`"{"a":"apple","b":"banana"}"`), - }, - "already encoded slice": { - in: []byte(`["apple","banana"]`), - want: []byte(`["apple","banana"]`), - }, - "simple": { - in: map[string]string{ - "a": "apple", - "b": "banana", - }, - want: []byte(`{"a":"apple","b":"banana"}`), - }, - "complex empty": { - in: DataExample{}, - want: []byte(`{}`), - }, - "simple array": { - in: &[]string{ - "apple", - "banana", - }, - want: []byte(`["apple","banana"]`), - }, - "complex filled": { - in: &DataExample{ - AnInt: 42, - AMap: map[string]map[string]int{ - "a": {"1": 1, "2": 2, "3": 3}, - "z": {"3": 3, "2": 2, "1": 1}, - }, - AString: "Hello, World!", - ATime: &now, - AnArray: []string{"Anne", "Bob", "Chad"}, - }, - want: func() []byte { - data := &DataExample{ - AnInt: 42, - AMap: map[string]map[string]int{ - "a": {"1": 1, "2": 2, "3": 3}, - "z": {"3": 3, "2": 2, "1": 1}, - }, - AString: "Hello, World!", - ATime: &now, - AnArray: []string{"Anne", "Bob", "Chad"}, - } - - j, err := json.Marshal(data) - if err != nil { - t.Errorf("failed to marshal test data: %s", err.Error()) - } - return j - }(), - }, - "object in": { - in: &DataExample{ - AnInt: 42, - }, - want: []byte(`{"a":42}`), - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - got, err := cej.Encode(context.TODO(), tc.in) - if tc.wantErr != "" || err != nil { - var gotErr string - if err != nil { - gotErr = err.Error() - } - if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if tc.want != nil { - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected data (-want, +got) = %v", diff) - } - } - }) - } -} diff --git a/v1/cloudevents/datacodec/json/doc.go b/v1/cloudevents/datacodec/json/doc.go deleted file mode 100644 index 86772c2e3..000000000 --- a/v1/cloudevents/datacodec/json/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -/* -Package json holds the encoder/decoder implementation for `application/json`. -*/ -package json diff --git a/v1/cloudevents/datacodec/json/observability.go b/v1/cloudevents/datacodec/json/observability.go deleted file mode 100644 index 3e88f4907..000000000 --- a/v1/cloudevents/datacodec/json/observability.go +++ /dev/null @@ -1,51 +0,0 @@ -package json - -import ( - "github.com/cloudevents/sdk-go/v1/cloudevents/observability" - "go.opencensus.io/stats" - "go.opencensus.io/stats/view" -) - -var ( - // LatencyMs measures the latency in milliseconds for the CloudEvents json - // data codec methods. - LatencyMs = stats.Float64("cloudevents.io/sdk-go/datacodec/json/latency", "The latency in milliseconds for the CloudEvents json data codec methods.", "ms") -) - -var ( - // LatencyView is an OpenCensus view that shows data codec json method latency. - LatencyView = &view.View{ - Name: "datacodec/json/latency", - Measure: LatencyMs, - Description: "The distribution of latency inside of the json 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 -} diff --git a/v1/cloudevents/datacodec/observability.go b/v1/cloudevents/datacodec/observability.go deleted file mode 100644 index a0ad7deb3..000000000 --- a/v1/cloudevents/datacodec/observability.go +++ /dev/null @@ -1,51 +0,0 @@ -package datacodec - -import ( - "github.com/cloudevents/sdk-go/v1/cloudevents/observability" - "go.opencensus.io/stats" - "go.opencensus.io/stats/view" -) - -var ( - // LatencyMs measures the latency in milliseconds for the CloudEvents generic - // codec data methods. - LatencyMs = stats.Float64("cloudevents.io/sdk-go/datacodec/latency", "The latency in milliseconds for the CloudEvents generic data codec methods.", "ms") -) - -var ( - // LatencyView is an OpenCensus view that shows data codec method latency. - LatencyView = &view.View{ - Name: "datacodec/latency", - Measure: LatencyMs, - Description: "The distribution of latency inside of the generic 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 -} diff --git a/v1/cloudevents/datacodec/text/text.go b/v1/cloudevents/datacodec/text/text.go deleted file mode 100644 index 3c37c5b13..000000000 --- a/v1/cloudevents/datacodec/text/text.go +++ /dev/null @@ -1,33 +0,0 @@ -// Text codec converts []byte or string to string and vice-versa. -package text - -import ( - "context" - "fmt" -) - -func Decode(_ context.Context, in, out interface{}) error { - p, _ := out.(*string) - if p == nil { - return fmt.Errorf("text.Decode out: want *string, got %T", out) - } - switch s := in.(type) { - 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 -} - -func Encode(_ context.Context, in interface{}) ([]byte, error) { - s, ok := in.(string) - if !ok { - return nil, fmt.Errorf("text.Encode in: want string, got %T", in) - } - return []byte(s), nil -} diff --git a/v1/cloudevents/datacodec/text/text_test.go b/v1/cloudevents/datacodec/text/text_test.go deleted file mode 100644 index ba51dc30f..000000000 --- a/v1/cloudevents/datacodec/text/text_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package text_test - -import ( - "context" - "testing" - - "github.com/cloudevents/sdk-go/v1/cloudevents/datacodec/text" - "github.com/stretchr/testify/assert" -) - -var ctx = context.Background() - -func TestEncode(t *testing.T) { - assert := assert.New(t) - - b, err := text.Encode(ctx, "") - assert.NoError(err) - assert.Empty(b) - - b, err = text.Encode(ctx, "hello😀") - assert.NoError(err) - assert.Equal("hello😀", string(b)) - - _, err = text.Encode(ctx, []byte("x")) - assert.EqualError(err, "text.Encode in: want string, got []uint8") - _, err = text.Encode(ctx, nil) - assert.EqualError(err, "text.Encode in: want string, got ") -} - -func TestDecode(t *testing.T) { - assert := assert.New(t) - var s string - assert.NoError(text.Decode(ctx, "hello", &s)) - assert.Equal("hello", s) - assert.NoError(text.Decode(ctx, []byte("bye"), &s)) - assert.Equal("bye", s) - assert.NoError(text.Decode(ctx, []byte{}, &s)) - assert.Equal("", s) - s = "xxx" - assert.NoError(text.Decode(ctx, nil, &s)) - assert.Equal("", s) - - assert.EqualError(text.Decode(ctx, 123, &s), "text.Decode in: want []byte or string, got int") - assert.EqualError(text.Decode(ctx, "", nil), "text.Decode out: want *string, got ") - assert.EqualError(text.Decode(ctx, "", 1), "text.Decode out: want *string, got int") -} diff --git a/v1/cloudevents/datacodec/xml/data.go b/v1/cloudevents/datacodec/xml/data.go deleted file mode 100644 index 9f42b7fa6..000000000 --- a/v1/cloudevents/datacodec/xml/data.go +++ /dev/null @@ -1,90 +0,0 @@ -package xml - -import ( - "context" - "encoding/base64" - "encoding/xml" - "fmt" - "strconv" - - "github.com/cloudevents/sdk-go/v1/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) -} diff --git a/v1/cloudevents/datacodec/xml/data_test.go b/v1/cloudevents/datacodec/xml/data_test.go deleted file mode 100644 index 751cf2b09..000000000 --- a/v1/cloudevents/datacodec/xml/data_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package xml_test - -import ( - "context" - "encoding/xml" - "fmt" - "strings" - "testing" - "time" - - cex "github.com/cloudevents/sdk-go/v1/cloudevents/datacodec/xml" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/google/go-cmp/cmp" -) - -type DataExample struct { - AnInt int `xml:"a,omitempty"` - AString string `xml:"b,omitempty"` - AnArray []string `xml:"c,omitempty"` - ATime *time.Time `xml:"e,omitempty"` -} - -type BadDataExample struct { - AnInt int `xml:"a,omitempty"` -} - -func (b BadDataExample) MarshalXML(e *xml.Encoder, start xml.StartElement) error { - return fmt.Errorf("unit test") -} - -// Basic data struct. -type Example struct { - Sequence int `json:"id"` - Message string `json:"message"` -} - -func TestCodecDecode(t *testing.T) { - now := time.Now() - - testCases := map[string]struct { - in interface{} - want interface{} - wantErr string - }{ - "empty": {}, - "not bytes": { - in: &BadDataExample{}, - wantErr: "[xml] failed to marshal in", - }, - "structured type encoding, escaped": { - in: []byte(`"7Hello, Structured Encoding v0.2!"`), - want: &Example{Sequence: 7, Message: "Hello, Structured Encoding v0.2!"}, - }, - "structured type encoding, escaped error": { - in: []byte(`"7Hello, Structured Encoding v0.2!"`), - wantErr: "[xml] found bytes, but failed to unmarshal", - }, - "structured type encoding, base64": { - in: []byte(`"PEV4YW1wbGU+PFNlcXVlbmNlPjc8L1NlcXVlbmNlPjxNZXNzYWdlPkhlbGxvLCBTdHJ1Y3R1cmVkIEVuY29kaW5nIHYwLjIhPC9NZXNzYWdlPjwvRXhhbXBsZT4="`), - want: &Example{Sequence: 7, Message: "Hello, Structured Encoding v0.2!"}, - }, - "structured type encoding, bad quote base64": { - in: []byte(`"PEV4YW1wbGU+PFNlcXVlbmNlPjc8L1NlcXVlbmNlPjxNZXNzYWdlPkhlbGxvLCBTdHJ1Y3R1cmVkIEVuY29kaW5nIHYwLjIhPC9NZXNzYWdlPjwvRXhhbXBsZT4=`), - wantErr: "[xml] failed to unquote quoted data", - }, - "structured type encoding, bad base64": { - in: []byte(`"?EV4YW1wbGU+PFNlcXVlbmNlPjc8L1NlcXVlbmNlPjxNZXNzYWdlPkhlbGxvLCBTdHJ1Y3R1cmVkIEVuY29kaW5nIHYwLjIhPC9NZXNzYWdlPjwvRXhhbXBsZT4="`), - wantErr: "[xml] failed to decode base64 encoded string", - }, - "complex filled": { - in: func() []byte { - data := &DataExample{ - AnInt: 42, - AString: "Hello, World!", - ATime: &now, - AnArray: []string{"Anne", "Bob", "Chad"}, - } - - j, err := xml.Marshal(data) - - if err != nil { - t.Errorf("failed to marshal test data: %s", err.Error()) - } - return j - }(), - want: &DataExample{ - AnInt: 42, - AString: "Hello, World!", - ATime: &now, - AnArray: []string{"Anne", "Bob", "Chad"}, - }, - }, - "object in": { - in: &DataExample{ - AnInt: 42, - }, - want: &DataExample{ - AnInt: 42, - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, _ := types.Allocate(tc.want) - - err := cex.Decode(context.TODO(), tc.in, got) - - if tc.wantErr != "" { - if err != nil { - gotErr := err.Error() - if !strings.Contains(gotErr, tc.wantErr) { - t.Errorf("unexpected error, expected to contain %q, got: %q", tc.wantErr, gotErr) - } - } else { - t.Errorf("expected error to contain %q, got: nil", tc.wantErr) - } - return - } - - if tc.want != nil { - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected data (-want, +got) = %v", diff) - } - } - }) - } -} diff --git a/v1/cloudevents/datacodec/xml/doc.go b/v1/cloudevents/datacodec/xml/doc.go deleted file mode 100644 index d90b7c444..000000000 --- a/v1/cloudevents/datacodec/xml/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -/* -Package xml holds the encoder/decoder implementation for `application/xml`. -*/ -package xml diff --git a/v1/cloudevents/datacodec/xml/observability.go b/v1/cloudevents/datacodec/xml/observability.go deleted file mode 100644 index 92102555c..000000000 --- a/v1/cloudevents/datacodec/xml/observability.go +++ /dev/null @@ -1,51 +0,0 @@ -package xml - -import ( - "github.com/cloudevents/sdk-go/v1/cloudevents/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/xml/latency", "The latency in milliseconds for the CloudEvents xml data codec methods.", "ms") -) - -var ( - // LatencyView is an OpenCensus view that shows data codec xml method latency. - LatencyView = &view.View{ - Name: "datacodec/xml/latency", - Measure: LatencyMs, - Description: "The distribution of latency inside of the xml 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 -} diff --git a/v1/cloudevents/doc.go b/v1/cloudevents/doc.go deleted file mode 100644 index cc2201da9..000000000 --- a/v1/cloudevents/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -/* -Package cloudevents provides primitives to work with CloudEvents specification: https://github.com/cloudevents/spec. -*/ -package cloudevents diff --git a/v1/cloudevents/event.go b/v1/cloudevents/event.go deleted file mode 100644 index 2f605fd3c..000000000 --- a/v1/cloudevents/event.go +++ /dev/null @@ -1,132 +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 - 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 // 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 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 - } - - // 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() -} diff --git a/v1/cloudevents/event_data.go b/v1/cloudevents/event_data.go deleted file mode 100644 index 63cc2d567..000000000 --- a/v1/cloudevents/event_data.go +++ /dev/null @@ -1,135 +0,0 @@ -package cloudevents - -import ( - "context" - "encoding/base64" - "errors" - "fmt" - "strconv" - - "github.com/cloudevents/sdk-go/v1/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) -} diff --git a/v1/cloudevents/event_data_test.go b/v1/cloudevents/event_data_test.go deleted file mode 100644 index 613c10855..000000000 --- a/v1/cloudevents/event_data_test.go +++ /dev/null @@ -1,199 +0,0 @@ -package cloudevents_test - -import ( - "strings" - "testing" - - ce "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/google/go-cmp/cmp" -) - -type DataTest struct { - event func(string) ce.Event - set interface{} - want interface{} - wantErr string -} - -func TestEventSetData_Json(t *testing.T) { - // All version should be the same, so run through them all. - - versions := []string{ce.CloudEventsVersionV01, ce.CloudEventsVersionV02, ce.CloudEventsVersionV03} - - testCases := map[string]DataTest{ - "empty": { - event: func(version string) ce.Event { - return ce.New(version) - }, - want: nil, - }, - "defaults": { - event: func(version string) ce.Event { - return ce.New(version) - }, - set: map[string]interface{}{ - "hello": "unittest", - }, - want: []byte(`{"hello":"unittest"}`), - }, - "text/json": { - event: func(version string) ce.Event { - e := ce.New(version) - e.SetDataContentType("text/json") - return e - }, - set: map[string]interface{}{ - "hello": "unittest", - }, - want: []byte(`{"hello":"unittest"}`), - }, - "application/json": { - event: func(version string) ce.Event { - e := ce.New(version) - e.SetDataContentType("application/json") - return e - }, - set: map[string]interface{}{ - "hello": "unittest", - }, - want: []byte(`{"hello":"unittest"}`), - }, - "application/json+base64": { - event: func(version string) ce.Event { - e := ce.New(version) - e.SetDataContentType("application/json") - e.SetDataContentEncoding(ce.Base64) - return e - }, - set: map[string]interface{}{ - "hello": "unittest", - }, - want: `eyJoZWxsbyI6InVuaXR0ZXN0In0=`, - }, - } - for n, tc := range testCases { - for _, version := range versions { - t.Run(n+":"+version, func(t *testing.T) { - // Make a versioned event. - event := tc.event(version) - - if tc.set != nil { - if err := event.SetData(tc.set); err != nil { - t.Errorf("unexpected error, %v", err) - } - } - got := event.Data - - as, _ := types.Allocate(tc.set) - - err := event.DataAs(&as) - validateData(t, tc, got, as, err) - }) - } - } -} - -type XmlExample struct { - AnInt int `xml:"a,omitempty"` - AString string `xml:"b,omitempty"` - AnArray []string `xml:"c,omitempty"` -} - -func TestEventSetData_xml(t *testing.T) { - // All version should be the same, so run through them all. - - versions := []string{ce.CloudEventsVersionV01, ce.CloudEventsVersionV02, ce.CloudEventsVersionV03} - - testCases := map[string]DataTest{ - "empty": { - event: func(version string) ce.Event { - e := ce.New(version) - e.SetDataContentType("application/xml") - return e - }, - want: nil, - }, - "text/xml": { - event: func(version string) ce.Event { - e := ce.New(version) - e.SetDataContentType("text/xml") - return e - }, - set: &XmlExample{ - AnInt: 42, - AString: "true fact", - AnArray: versions, - }, - want: []byte(`42true fact0.10.20.3`), - }, - "application/xml": { - event: func(version string) ce.Event { - e := ce.New(version) - e.SetDataContentType("application/xml") - return e - }, - set: &XmlExample{ - AnInt: 42, - AString: "true fact", - AnArray: versions, - }, - want: []byte(`42true fact0.10.20.3`), - }, - "application/xml+base64": { - event: func(version string) ce.Event { - e := ce.New(version) - e.SetDataContentType("application/xml") - e.SetDataContentEncoding(ce.Base64) - return e - }, - set: &XmlExample{ - AnInt: 42, - AString: "true fact", - AnArray: versions, - }, - want: `PFhtbEV4YW1wbGU+PGE+NDI8L2E+PGI+dHJ1ZSBmYWN0PC9iPjxjPjAuMTwvYz48Yz4wLjI8L2M+PGM+MC4zPC9jPjwvWG1sRXhhbXBsZT4=`, - }, - } - for n, tc := range testCases { - for _, version := range versions { - t.Run(n+":"+version, func(t *testing.T) { - // Make a versioned event. - event := tc.event(version) - - if tc.set != nil { - if err := event.SetData(tc.set); err != nil { - t.Errorf("unexpected error, %v", err) - } - } - got := event.Data - - as, _ := types.Allocate(tc.set) - - err := event.DataAs(&as) - validateData(t, tc, got, as, err) - }) - } - } -} - -func validateData(t *testing.T, tc DataTest, got, as interface{}, err error) { - var gotErr string - if err != nil { - gotErr = err.Error() - if tc.wantErr == "" { - t.Errorf("unexpected no error, got %q", gotErr) - } - } - if tc.wantErr != "" { - if !strings.Contains(gotErr, tc.wantErr) { - t.Errorf("unexpected error, expected to contain %q, got: %q ", tc.wantErr, gotErr) - } - } - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected data (-want, +got) = %v", diff) - } - if diff := cmp.Diff(tc.set, as); diff != "" { - t.Errorf("unexpected as (-want, +got) = %v", diff) - } -} diff --git a/v1/cloudevents/event_interface.go b/v1/cloudevents/event_interface.go deleted file mode 100644 index 37bb82ab5..000000000 --- a/v1/cloudevents/event_interface.go +++ /dev/null @@ -1,79 +0,0 @@ -package cloudevents - -import ( - "time" -) - -// EventWriter is the interface for reading through an event from attributes. -type EventReader interface { - // SpecVersion returns event.Context.GetSpecVersion(). - SpecVersion() string - // Type returns event.Context.GetType(). - Type() string - // Source returns event.Context.GetSource(). - Source() string - // Subject returns event.Context.GetSubject(). - Subject() string - // ID returns event.Context.GetID(). - ID() string - // Time returns event.Context.GetTime(). - Time() time.Time - // DataSchema returns event.Context.GetDataSchema(). - DataSchema() string - // DataContentType returns event.Context.GetDataContentType(). - DataContentType() string - // DataMediaType returns event.Context.GetDataMediaType(). - DataMediaType() string - // DeprecatedDataContentEncoding returns event.Context.DeprecatedGetDataContentEncoding(). - DeprecatedDataContentEncoding() string - - // Extension Attributes - - // Extensions returns the event.Context.GetExtensions(). - // Extensions use the CloudEvents type system, details in package cloudevents/types. - Extensions() map[string]interface{} - - // DEPRECATED: see event.Context.ExtensionAs - // ExtensionAs returns event.Context.ExtensionAs(name, obj). - ExtensionAs(string, interface{}) error - - // Data Attribute - - // DataAs attempts to populate the provided data object with the event payload. - // data should be a pointer type. - DataAs(interface{}) error -} - -// EventWriter is the interface for writing through an event onto attributes. -// 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 { - // Context Attributes - - // SetSpecVersion performs event.Context.SetSpecVersion. - SetSpecVersion(string) - // SetType performs event.Context.SetType. - SetType(string) - // SetSource performs event.Context.SetSource. - SetSource(string) - // SetSubject( performs event.Context.SetSubject. - SetSubject(string) - // SetID performs event.Context.SetID. - SetID(string) - // SetTime performs event.Context.SetTime. - SetTime(time.Time) - // SetDataSchema performs event.Context.SetDataSchema. - SetDataSchema(string) - // SetDataContentType performs event.Context.SetDataContentType. - SetDataContentType(string) - // DeprecatedSetDataContentEncoding performs event.Context.DeprecatedSetDataContentEncoding. - SetDataContentEncoding(string) - - // Extension Attributes - - // SetExtension performs event.Context.SetExtension. - SetExtension(string, interface{}) - - // SetData encodes the given payload with the current encoding settings. - SetData(interface{}) error -} diff --git a/v1/cloudevents/event_marshal.go b/v1/cloudevents/event_marshal.go deleted file mode 100644 index a1f10e5c4..000000000 --- a/v1/cloudevents/event_marshal.go +++ /dev/null @@ -1,394 +0,0 @@ -package cloudevents - -import ( - "context" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "strconv" - "strings" - - errors2 "github.com/pkg/errors" - - "github.com/cloudevents/sdk-go/v1/cloudevents/observability" -) - -// MarshalJSON implements a custom json marshal method used when this type is -// marshaled using json.Marshal. -func (e Event) MarshalJSON() ([]byte, error) { - _, r := observability.NewReporter(context.Background(), eventJSONObserved{o: reportMarshal, v: e.SpecVersion()}) - - if err := e.Validate(); err != nil { - r.Error() - return nil, err - } - - var b []byte - var err error - - switch e.SpecVersion() { - case CloudEventsVersionV01, CloudEventsVersionV02, CloudEventsVersionV03: - b, err = JsonEncodeLegacy(e) - case CloudEventsVersionV1: - b, err = JsonEncode(e) - default: - return nil, fmt.Errorf("unnknown spec version: %q", e.SpecVersion()) - } - - // Report the observable - if err != nil { - r.Error() - return nil, err - } else { - r.OK() - } - - return b, nil -} - -// UnmarshalJSON implements the json unmarshal method used when this type is -// unmarshaled using json.Unmarshal. -func (e *Event) UnmarshalJSON(b []byte) error { - raw := make(map[string]json.RawMessage) - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - - version := versionFromRawMessage(raw) - - _, r := observability.NewReporter(context.Background(), eventJSONObserved{o: reportUnmarshal, v: version}) - - var err error - switch version { - case CloudEventsVersionV01: - err = e.JsonDecodeV01(b, raw) - case CloudEventsVersionV02: - err = e.JsonDecodeV02(b, raw) - case CloudEventsVersionV03: - err = e.JsonDecodeV03(b, raw) - case CloudEventsVersionV1: - err = e.JsonDecodeV1(b, raw) - default: - return fmt.Errorf("unnknown spec version: %q", version) - } - - // Report the observable - if err != nil { - r.Error() - return err - } else { - r.OK() - } - return nil -} - -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 - if v, ok := raw["specversion"]; ok { - var version string - if err := json.Unmarshal(v, &version); err != nil { - return "" - } - return version - } - return "" -} - -// JsonEncode -func JsonEncode(e Event) ([]byte, error) { - data, err := e.DataBytes() - if err != nil { - return nil, err - } - return jsonEncode(e.Context, data, e.DataBinary) -} - -// JsonEncodeLegacy -func JsonEncodeLegacy(e Event) ([]byte, error) { - var data []byte - isBase64 := e.Context.DeprecatedGetDataContentEncoding() == Base64 - var err error - 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) { - var b map[string]json.RawMessage - var err error - - if ctx.GetSpecVersion() == CloudEventsVersionV01 { - b, err = marshalEventLegacy(ctx) - } else { - b, err = marshalEvent(ctx, ctx.GetExtensions()) - } - if err != nil { - return nil, err - } - - if data != nil { - // data is passed in as an encoded []byte. That slice might be any - // number of things but for json encoding of the envelope all we care - // 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() - if err != nil { - return nil, err - } - isJson := mediaType == "" || mediaType == ApplicationJSON || mediaType == TextJSON - // TODO(#60): we do not support json values at the moment, only objects and lists. - if isJson && !isBase64 { - b["data"] = data - } else { - var dataKey string - if ctx.GetSpecVersion() == CloudEventsVersionV1 { - 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('"') { - b[dataKey] = []byte(strconv.QuoteToASCII(string(data))) - } else { - // already quoted - b[dataKey] = data - } - } - } - - body, err := json.Marshal(b) - if err != nil { - return nil, err - } - - 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)) - 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 - 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 -// cloudevent.Event or an error if there are parsing errors. -func (e *Event) JsonDecodeV03(body []byte, raw map[string]json.RawMessage) error { - ec := EventContextV03{} - 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, "subject") - delete(raw, "id") - delete(raw, "time") - delete(raw, "schemaurl") - delete(raw, "datacontenttype") - delete(raw, "datacontentencoding") - - 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)) - 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 - e.Data = data - e.DataEncoded = data != nil - - return nil -} - -// JsonDecodeV1 takes in the byte representation of a version 1.0 structured json CloudEvent and returns a -// cloudevent.Event or an error if there are parsing errors. -func (e *Event) JsonDecodeV1(body []byte, raw map[string]json.RawMessage) error { - ec := EventContextV1{} - if err := json.Unmarshal(body, &ec); err != nil { - return err - } - - delete(raw, "specversion") - delete(raw, "type") - delete(raw, "source") - delete(raw, "subject") - delete(raw, "id") - delete(raw, "time") - delete(raw, "dataschema") - delete(raw, "datacontenttype") - - var data interface{} - if d, ok := raw["data"]; ok { - data = []byte(d) - } - delete(raw, "data") - - var dataBase64 []byte - if d, ok := raw["data_base64"]; ok { - var tmp []byte - if err := json.Unmarshal(d, &tmp); err != nil { - return err - } - dataBase64 = tmp - } - delete(raw, "data_base64") - - 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 - if data != nil && dataBase64 != nil { - return errors.New("parsing error: JSON decoder found both 'data', and 'data_base64' in JSON payload") - } - if data != nil { - e.Data = data - } else if dataBase64 != nil { - e.Data = dataBase64 - } - e.DataEncoded = data != nil - - return nil -} - -func marshalEventLegacy(event interface{}) (map[string]json.RawMessage, error) { - b, err := json.Marshal(event) - if err != nil { - return nil, err - } - - brm := map[string]json.RawMessage{} - if err := json.Unmarshal(b, &brm); err != nil { - return nil, err - } - - return brm, nil -} - -func marshalEvent(event interface{}, extensions map[string]interface{}) (map[string]json.RawMessage, error) { - b, err := json.Marshal(event) - if err != nil { - return nil, err - } - - brm := map[string]json.RawMessage{} - if err := json.Unmarshal(b, &brm); err != nil { - return nil, err - } - - for k, v := range extensions { - k = strings.ToLower(k) - vb, err := json.Marshal(v) - if err != nil { - return nil, err - } - // Don't overwrite spec keys. - if _, ok := brm[k]; !ok { - brm[k] = vb - } - } - - return brm, nil -} diff --git a/v1/cloudevents/event_marshal_test.go b/v1/cloudevents/event_marshal_test.go deleted file mode 100644 index 26288408c..000000000 --- a/v1/cloudevents/event_marshal_test.go +++ /dev/null @@ -1,812 +0,0 @@ -package cloudevents_test - -import ( - "encoding/json" - "fmt" - "net/url" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -//type DataExample struct { -// AnInt int `json:"a,omitempty"` -// AString string `json:"b,omitempty"` -// AnArray []string `json:"c,omitempty"` -// ATime *time.Time `json:"e,omitempty"` -//} - -func TestMarshal(t *testing.T) { - now := types.Timestamp{Time: time.Now().UTC()} - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - sourceV1 := &types.URIRef{URL: *sourceUrl} - - schemaUrl, _ := url.Parse("http://example.com/schema") - schema := &types.URLRef{URL: *schemaUrl} - schemaV1 := &types.URI{URL: *schemaUrl} - - testCases := map[string]struct { - event cloudevents.Event - eventExtensions map[string]interface{} - want []byte - wantErr *string - }{ - "empty struct": { - event: cloudevents.Event{}, - wantErr: strptr("json: error calling MarshalJSON for type cloudevents.Event: every event conforming to the CloudEvents specification MUST include a context"), - }, - "struct data v0.1": { - event: cloudevents.Event{ - Context: cloudevents.EventContextV01{ - EventType: "com.example.test", - Source: *source, - SchemaURL: schema, - EventTypeVersion: strptr("version1"), - EventID: "ABC-123", - EventTime: &now, - ContentType: cloudevents.StringOfApplicationJSON(), - }.AsV01(), - Data: DataExample{ - AnInt: 42, - AString: "testing", - }, - }, - eventExtensions: map[string]interface{}{ - "exbool": true, - "exint": int32(42), - "exstring": "exstring", - "exbinary": []byte{0, 1, 2, 3}, - "exurl": source, - "extime": &now, - }, - want: toBytes(map[string]interface{}{ - "cloudEventsVersion": "0.1", - "contentType": "application/json", - "data": map[string]interface{}{ - "a": 42, - "b": "testing", - }, - "eventID": "ABC-123", - "eventTime": now.Format(time.RFC3339Nano), - "eventType": "com.example.test", - "eventTypeVersion": "version1", - "extensions": map[string]interface{}{ - "exbool": true, - "exint": 42, - "exstring": "exstring", - "exbinary": "AAECAw==", - "exurl": "http://example.com/source", - "extime": now.Format(time.RFC3339Nano), - }, - "schemaURL": "http://example.com/schema", - "source": "http://example.com/source", - }), - }, - "struct data v0.2": { - event: cloudevents.Event{ - Context: cloudevents.EventContextV02{ - Type: "com.example.test", - Source: *source, - SchemaURL: schema, - ID: "ABC-123", - Time: &now, - ContentType: cloudevents.StringOfApplicationJSON(), - }.AsV02(), - Data: DataExample{ - AnInt: 42, - AString: "testing", - }, - }, - eventExtensions: map[string]interface{}{ - "exbool": true, - "exint": int32(42), - "exstring": "exstring", - "exbinary": []byte{0, 1, 2, 3}, - "exurl": source, - "extime": &now, - }, - want: toBytes(map[string]interface{}{ - "specversion": "0.2", - "contenttype": "application/json", - "data": map[string]interface{}{ - "a": 42, - "b": "testing", - }, - "id": "ABC-123", - "time": now.Format(time.RFC3339Nano), - "type": "com.example.test", - "exbool": true, - "exint": 42, - "exstring": "exstring", - "exbinary": "AAECAw==", - "exurl": "http://example.com/source", - "extime": now.Format(time.RFC3339Nano), - "schemaurl": "http://example.com/schema", - "source": "http://example.com/source", - }), - }, - "v0.2 cased extensions": { - event: cloudevents.Event{ - Context: cloudevents.EventContextV02{ - Type: "com.example.test", - Source: *source, - SchemaURL: schema, - ID: "ABC-123", - Time: &now, - ContentType: cloudevents.StringOfApplicationJSON(), - }.AsV02(), - Data: DataExample{ - AnInt: 42, - AString: "testing", - }, - }, - eventExtensions: map[string]interface{}{ - "exBool": true, - "Exint": int32(42), - "EXSTRING": "exstring", - "exbinary": []byte{0, 1, 2, 3}, - "exurl": source, - "extime": &now, - }, - want: toBytes(map[string]interface{}{ - "specversion": "0.2", - "contenttype": "application/json", - "data": map[string]interface{}{ - "a": 42, - "b": "testing", - }, - "id": "ABC-123", - "time": now.Format(time.RFC3339Nano), - "type": "com.example.test", - "exbool": true, - "exint": 42, - "exstring": "exstring", - "exbinary": "AAECAw==", - "exurl": "http://example.com/source", - "extime": now.Format(time.RFC3339Nano), - "schemaurl": "http://example.com/schema", - "source": "http://example.com/source", - }), - }, - "struct data v0.3": { - event: cloudevents.Event{ - Context: cloudevents.EventContextV03{ - Type: "com.example.test", - Source: *source, - SchemaURL: schema, - ID: "ABC-123", - Time: &now, - DataContentType: cloudevents.StringOfApplicationJSON(), - }.AsV03(), - Data: DataExample{ - AnInt: 42, - AString: "testing", - }, - }, - eventExtensions: map[string]interface{}{ - "exbool": true, - "exint": int32(42), - "exstring": "exstring", - "exbinary": []byte{0, 1, 2, 3}, - "exurl": source, - "extime": &now, - }, - want: toBytes(map[string]interface{}{ - "specversion": "0.3", - "datacontenttype": "application/json", - "data": map[string]interface{}{ - "a": 42, - "b": "testing", - }, - "id": "ABC-123", - "time": now.Format(time.RFC3339Nano), - "type": "com.example.test", - "exbool": true, - "exint": 42, - "exstring": "exstring", - "exbinary": "AAECAw==", - "exurl": "http://example.com/source", - "extime": now.Format(time.RFC3339Nano), - "schemaurl": "http://example.com/schema", - "source": "http://example.com/source", - }), - }, - "nil data v0.3": { - event: cloudevents.Event{ - Context: cloudevents.EventContextV03{ - Type: "com.example.test", - Source: *source, - SchemaURL: schema, - ID: "ABC-123", - Time: &now, - DataContentType: cloudevents.StringOfApplicationJSON(), - }.AsV03(), - }, - eventExtensions: map[string]interface{}{ - "exbool": true, - "exint": int32(42), - "exstring": "exstring", - "exbinary": []byte{0, 1, 2, 3}, - "exurl": source, - "extime": &now, - }, - want: toBytes(map[string]interface{}{ - "specversion": "0.3", - "datacontenttype": "application/json", - "id": "ABC-123", - "time": now.Format(time.RFC3339Nano), - "type": "com.example.test", - "exbool": true, - "exint": 42, - "exstring": "exstring", - "exbinary": "AAECAw==", - "exurl": "http://example.com/source", - "extime": now.Format(time.RFC3339Nano), - "schemaurl": "http://example.com/schema", - "source": "http://example.com/source", - }), - }, - "string data v0.3": { - event: cloudevents.Event{ - Context: cloudevents.EventContextV03{ - Type: "com.example.test", - Source: *source, - SchemaURL: schema, - ID: "ABC-123", - Time: &now, - DataContentType: cloudevents.StringOfApplicationJSON(), - }.AsV03(), - Data: "This is a string.", - }, - eventExtensions: map[string]interface{}{ - "exbool": true, - "exint": int32(42), - "exstring": "exstring", - "exbinary": []byte{0, 1, 2, 3}, - "exurl": source, - "extime": &now, - }, - want: toBytes(map[string]interface{}{ - "specversion": "0.3", - "datacontenttype": "application/json", - "data": "This is a string.", - "id": "ABC-123", - "time": now.Format(time.RFC3339Nano), - "type": "com.example.test", - "exbool": true, - "exint": 42, - "exstring": "exstring", - "exbinary": "AAECAw==", - "exurl": "http://example.com/source", - "extime": now.Format(time.RFC3339Nano), - "schemaurl": "http://example.com/schema", - "source": "http://example.com/source", - }), - }, - "struct data v1.0": { - event: cloudevents.Event{ - Context: cloudevents.EventContextV1{ - Type: "com.example.test", - Source: *sourceV1, - DataSchema: schemaV1, - ID: "ABC-123", - Time: &now, - DataContentType: cloudevents.StringOfApplicationJSON(), - }.AsV1(), - Data: DataExample{ - AnInt: 42, - AString: "testing", - }, - }, - eventExtensions: map[string]interface{}{ - "exbool": true, - "exint": int32(42), - "exstring": "exstring", - "exbinary": []byte{0, 1, 2, 3}, - "exurl": sourceV1, - "extime": &now, - }, - want: toBytes(map[string]interface{}{ - "specversion": "1.0", - "datacontenttype": "application/json", - "data": map[string]interface{}{ - "a": 42, - "b": "testing", - }, - "id": "ABC-123", - "time": now.Format(time.RFC3339Nano), - "type": "com.example.test", - "exbool": true, - "exint": 42, - "exstring": "exstring", - "exbinary": "AAECAw==", - "exurl": "http://example.com/source", - "extime": now.Format(time.RFC3339Nano), - "dataschema": "http://example.com/schema", - "source": "http://example.com/source", - }), - }, - "nil data v1.0": { - event: cloudevents.Event{ - Context: cloudevents.EventContextV1{ - Type: "com.example.test", - Source: *sourceV1, - DataSchema: schemaV1, - ID: "ABC-123", - Time: &now, - DataContentType: cloudevents.StringOfApplicationJSON(), - }.AsV1(), - }, - eventExtensions: map[string]interface{}{ - "exbool": true, - "exint": int32(42), - "exstring": "exstring", - "exbinary": []byte{0, 1, 2, 3}, - "exurl": sourceV1, - "extime": &now, - }, - want: toBytes(map[string]interface{}{ - "specversion": "1.0", - "datacontenttype": "application/json", - "id": "ABC-123", - "time": now.Format(time.RFC3339Nano), - "type": "com.example.test", - "exbool": true, - "exint": 42, - "exstring": "exstring", - "exbinary": "AAECAw==", - "exurl": "http://example.com/source", - "extime": now.Format(time.RFC3339Nano), - "dataschema": "http://example.com/schema", - "source": "http://example.com/source", - }), - }, - "string data v1.0": { - event: cloudevents.Event{ - Context: cloudevents.EventContextV1{ - Type: "com.example.test", - Source: *sourceV1, - DataSchema: schemaV1, - ID: "ABC-123", - Time: &now, - DataContentType: cloudevents.StringOfApplicationJSON(), - }.AsV1(), - Data: "This is a string.", - }, - eventExtensions: map[string]interface{}{ - "exbool": true, - "exint": int32(42), - "exstring": "exstring", - "exbinary": []byte{0, 1, 2, 3}, - "exurl": sourceV1, - "extime": &now, - }, - want: toBytes(map[string]interface{}{ - "specversion": "1.0", - "datacontenttype": "application/json", - "data": "This is a string.", - "id": "ABC-123", - "time": now.Format(time.RFC3339Nano), - "type": "com.example.test", - "exbool": true, - "exint": 42, - "exstring": "exstring", - "exbinary": "AAECAw==", - "exurl": "http://example.com/source", - "extime": now.Format(time.RFC3339Nano), - "dataschema": "http://example.com/schema", - "source": "http://example.com/source", - }), - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - event := tc.event - - for k, v := range tc.eventExtensions { - event.SetExtension(k, v) - } - - gotBytes, err := json.Marshal(event) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(*tc.wantErr, err.Error()); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - // so we can understand the diff, turn bytes to strings - want := string(tc.want) - got := string(gotBytes) - - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("unexpected event (-want, +got) = %v", diff) - } - }) - } -} - -func TestUnmarshal(t *testing.T) { - now := types.Timestamp{Time: time.Now().UTC()} - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - sourceV1 := &types.URIRef{URL: *sourceUrl} - - schemaUrl, _ := url.Parse("http://example.com/schema") - schema := &types.URLRef{URL: *schemaUrl} - schemaV1 := &types.URI{URL: *schemaUrl} - - testCases := map[string]struct { - body []byte - want *cloudevents.Event - wantErr error - }{ - "struct data v0.1": { - body: toBytes(map[string]interface{}{ - "cloudEventsVersion": "0.1", - "contentType": "application/json", - "data": map[string]interface{}{ - "a": 42, - "b": "testing", - }, - "eventID": "ABC-123", - "eventTime": now.Format(time.RFC3339Nano), - "eventType": "com.example.test", - "eventTypeVersion": "version1", - "extensions": map[string]interface{}{ - "exbool": true, - "exint": 42, - "exstring": "exstring", - "exbinary": "AAECAw==", - "exurl": "http://example.com/source", - "extime": now.Format(time.RFC3339Nano), - }, - "schemaURL": "http://example.com/schema", - "source": "http://example.com/source", - }), - want: &cloudevents.Event{ - Context: cloudevents.EventContextV01{ - EventType: "com.example.test", - Source: *source, - SchemaURL: schema, - EventTypeVersion: strptr("version1"), - EventID: "ABC-123", - EventTime: &now, - ContentType: cloudevents.StringOfApplicationJSON(), - Extensions: map[string]interface{}{ - "exbool": true, // Boolean should be preserved - "exint": float64(42), - "exstring": "exstring", - // Since byte, url and time are encoded as string, the unmarshal should just convert them to string - "exbinary": "AAECAw==", - "exurl": "http://example.com/source", - "extime": now.Format(time.RFC3339Nano), - }, - }.AsV01(), - Data: toBytes(DataExample{ - AnInt: 42, - AString: "testing", - }), - DataEncoded: true, - }, - }, - "struct data v0.2": { - body: toBytes(map[string]interface{}{ - "specversion": "0.2", - "contenttype": "application/json", - "data": map[string]interface{}{ - "a": 42, - "b": "testing", - }, - "id": "ABC-123", - "time": now.Format(time.RFC3339Nano), - "type": "com.example.test", - "exbool": true, - "exint": 42, - "exstring": "exstring", - "exbinary": "AAECAw==", - "exurl": "http://example.com/source", - "extime": now.Format(time.RFC3339Nano), - "schemaurl": "http://example.com/schema", - "source": "http://example.com/source", - }), - want: &cloudevents.Event{ - Context: cloudevents.EventContextV02{ - Type: "com.example.test", - Source: *source, - SchemaURL: schema, - ID: "ABC-123", - Time: &now, - ContentType: cloudevents.StringOfApplicationJSON(), - Extensions: map[string]interface{}{ - "exbool": true, // Boolean should be preserved - "exint": float64(42), - "exstring": "exstring", - // Since byte, url and time are encoded as string, the unmarshal should just convert them to string - "exbinary": "AAECAw==", - "exurl": "http://example.com/source", - "extime": now.Format(time.RFC3339Nano), - }, - }.AsV02(), - Data: toBytes(DataExample{ - AnInt: 42, - AString: "testing", - }), - DataEncoded: true, - }, - }, - "struct data v0.3": { - body: toBytes(map[string]interface{}{ - "specversion": "0.3", - "datacontenttype": "application/json", - "data": map[string]interface{}{ - "a": 42, - "b": "testing", - }, - "id": "ABC-123", - "time": now.Format(time.RFC3339Nano), - "type": "com.example.test", - "exbool": true, - "exint": 42, - "exstring": "exstring", - "exbinary": "AAECAw==", - "exurl": "http://example.com/source", - "extime": now.Format(time.RFC3339Nano), - "schemaurl": "http://example.com/schema", - "source": "http://example.com/source", - }), - want: &cloudevents.Event{ - Context: cloudevents.EventContextV03{ - Type: "com.example.test", - Source: *source, - SchemaURL: schema, - ID: "ABC-123", - Time: &now, - DataContentType: cloudevents.StringOfApplicationJSON(), - Extensions: map[string]interface{}{ - "exbool": true, // Boolean should be preserved - "exint": int32(42), - "exstring": "exstring", - // Since byte, url and time are encoded as string, the unmarshal should just convert them to string - "exbinary": "AAECAw==", - "exurl": "http://example.com/source", - "extime": now.Format(time.RFC3339Nano), - }, - }.AsV03(), - Data: toBytes(DataExample{ - AnInt: 42, - AString: "testing", - }), - DataEncoded: true, - }, - }, - "string data v0.3": { - body: toBytes(map[string]interface{}{ - "specversion": "0.3", - "datacontenttype": "application/json", - "data": "This is a string.", - "id": "ABC-123", - "time": now.Format(time.RFC3339Nano), - "type": "com.example.test", - "exbool": true, - "exint": 42, - "exstring": "exstring", - "exbinary": "AAECAw==", - "exurl": "http://example.com/source", - "extime": now.Format(time.RFC3339Nano), - "schemaurl": "http://example.com/schema", - "source": "http://example.com/source", - }), - want: &cloudevents.Event{ - Context: cloudevents.EventContextV03{ - Type: "com.example.test", - Source: *source, - SchemaURL: schema, - ID: "ABC-123", - Time: &now, - DataContentType: cloudevents.StringOfApplicationJSON(), - Extensions: map[string]interface{}{ - "exbool": true, // Boolean should be preserved - "exint": int32(42), - "exstring": "exstring", - // Since byte, url and time are encoded as string, the unmarshal should just convert them to string - "exbinary": "AAECAw==", - "exurl": "http://example.com/source", - "extime": now.Format(time.RFC3339Nano), - }, - }.AsV03(), - Data: toBytes("This is a string."), - DataEncoded: true, - }, - }, - "nil data v0.3": { - body: toBytes(map[string]interface{}{ - "specversion": "0.3", - "datacontenttype": "application/json", - "id": "ABC-123", - "time": now.Format(time.RFC3339Nano), - "type": "com.example.test", - "exbool": true, - "exint": 42, - "exstring": "exstring", - "exbinary": "AAECAw==", - "exurl": "http://example.com/source", - "extime": now.Format(time.RFC3339Nano), - "schemaurl": "http://example.com/schema", - "source": "http://example.com/source", - }), - want: &cloudevents.Event{ - Context: cloudevents.EventContextV03{ - Type: "com.example.test", - Source: *source, - SchemaURL: schema, - ID: "ABC-123", - Time: &now, - DataContentType: cloudevents.StringOfApplicationJSON(), - Extensions: map[string]interface{}{ - "exbool": true, // Boolean should be preserved - "exint": int32(42), - "exstring": "exstring", - // Since byte, url and time are encoded as string, the unmarshal should just convert them to string - "exbinary": "AAECAw==", - "exurl": "http://example.com/source", - "extime": now.Format(time.RFC3339Nano), - }, - }.AsV03(), - }, - }, - "struct data v1.0": { - body: toBytes(map[string]interface{}{ - "specversion": "1.0", - "datacontenttype": "application/json", - "data": map[string]interface{}{ - "a": 42, - "b": "testing", - }, - "id": "ABC-123", - "time": now.Format(time.RFC3339Nano), - "type": "com.example.test", - "exbool": true, - "exint": 42, - "exstring": "exstring", - "exbinary": "AAECAw==", - "exurl": "http://example.com/source", - "extime": now.Format(time.RFC3339Nano), - "dataschema": "http://example.com/schema", - "source": "http://example.com/source", - }), - want: &cloudevents.Event{ - Context: cloudevents.EventContextV1{ - Type: "com.example.test", - Source: *sourceV1, - DataSchema: schemaV1, - ID: "ABC-123", - Time: &now, - DataContentType: cloudevents.StringOfApplicationJSON(), - Extensions: map[string]interface{}{ - "exbool": true, // Boolean should be preserved - "exint": int32(42), - "exstring": "exstring", - // Since byte, url and time are encoded as string, the unmarshal should just convert them to string - "exbinary": "AAECAw==", - "exurl": "http://example.com/source", - "extime": now.Format(time.RFC3339Nano), - }, - }.AsV1(), - Data: toBytes(DataExample{ - AnInt: 42, - AString: "testing", - }), - DataEncoded: true, - }, - }, - "string data v1.0": { - body: toBytes(map[string]interface{}{ - "specversion": "1.0", - "datacontenttype": "application/json", - "data": "This is a string.", - "id": "ABC-123", - "time": now.Format(time.RFC3339Nano), - "type": "com.example.test", - "exbool": true, - "exint": 42, - "exstring": "exstring", - "exbinary": "AAECAw==", - "exurl": "http://example.com/source", - "extime": now.Format(time.RFC3339Nano), - "dataschema": "http://example.com/schema", - "source": "http://example.com/source", - }), - want: &cloudevents.Event{ - Context: cloudevents.EventContextV1{ - Type: "com.example.test", - Source: *sourceV1, - DataSchema: schemaV1, - ID: "ABC-123", - Time: &now, - DataContentType: cloudevents.StringOfApplicationJSON(), - Extensions: map[string]interface{}{ - "exbool": true, // Boolean should be preserved - "exint": int32(42), - "exstring": "exstring", - // Since byte, url and time are encoded as string, the unmarshal should just convert them to string - "exbinary": "AAECAw==", - "exurl": "http://example.com/source", - "extime": now.Format(time.RFC3339Nano), - }, - }.AsV1(), - Data: toBytes("This is a string."), - DataEncoded: true, - }, - }, - "nil data v1.0": { - body: toBytes(map[string]interface{}{ - "specversion": "1.0", - "datacontenttype": "application/json", - "id": "ABC-123", - "time": now.Format(time.RFC3339Nano), - "type": "com.example.test", - "exbool": true, - "exint": 42, - "exstring": "exstring", - "exbinary": "AAECAw==", - "exurl": "http://example.com/source", - "extime": now.Format(time.RFC3339Nano), - "dataschema": "http://example.com/schema", - "source": "http://example.com/source", - }), - want: &cloudevents.Event{ - Context: cloudevents.EventContextV1{ - Type: "com.example.test", - Source: *sourceV1, - DataSchema: schemaV1, - ID: "ABC-123", - Time: &now, - DataContentType: cloudevents.StringOfApplicationJSON(), - Extensions: map[string]interface{}{ - "exbool": true, // Boolean should be preserved - "exint": int32(42), - "exstring": "exstring", - // Since byte, url and time are encoded as string, the unmarshal should just convert them to string - "exbinary": "AAECAw==", - "exurl": "http://example.com/source", - "extime": now.Format(time.RFC3339Nano), - }, - }.AsV1(), - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got := &cloudevents.Event{} - err := json.Unmarshal(tc.body, got) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected event (-want, +got) = %v", diff) - } - }) - } -} - -func toBytes(body interface{}) []byte { - b, err := json.Marshal(body) - if err != nil { - return []byte(fmt.Sprintf(`{"error":%q}`, err.Error())) - } - return b -} diff --git a/v1/cloudevents/event_observability.go b/v1/cloudevents/event_observability.go deleted file mode 100644 index ef03abfe5..000000000 --- a/v1/cloudevents/event_observability.go +++ /dev/null @@ -1,77 +0,0 @@ -package cloudevents - -import ( - "fmt" - - "github.com/cloudevents/sdk-go/v1/cloudevents/observability" - "go.opencensus.io/stats" - "go.opencensus.io/stats/view" -) - -var ( - // EventMarshalLatencyMs measures the latency in milliseconds for the - // CloudEvents.Event marshal/unmarshalJSON methods. - EventMarshalLatencyMs = stats.Float64( - "cloudevents.io/sdk-go/event/json/latency", - "The latency in milliseconds of (un)marshalJSON methods for CloudEvents.Event.", - "ms") -) - -var ( - // LatencyView is an OpenCensus view that shows CloudEvents.Event (un)marshalJSON method latency. - EventMarshalLatencyView = &view.View{ - Name: "event/json/latency", - Measure: EventMarshalLatencyMs, - Description: "The distribution of latency inside of (un)marshalJSON methods for CloudEvents.Event.", - 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 ( - reportMarshal observed = iota - reportUnmarshal -) - -// MethodName implements Observable.MethodName -func (o observed) MethodName() string { - switch o { - case reportMarshal: - return "marshaljson" - case reportUnmarshal: - return "unmarshaljson" - default: - return "unknown" - } -} - -// LatencyMs implements Observable.LatencyMs -func (o observed) LatencyMs() *stats.Float64Measure { - return EventMarshalLatencyMs -} - -// eventJSONObserved is a wrapper to append version to observed. -type eventJSONObserved struct { - // Method - o observed - // Version - v string -} - -// Adheres to Observable -var _ observability.Observable = (*eventJSONObserved)(nil) - -// MethodName implements Observable.MethodName -func (c eventJSONObserved) MethodName() string { - return fmt.Sprintf("%s/%s", c.o.MethodName(), c.v) -} - -// LatencyMs implements Observable.LatencyMs -func (c eventJSONObserved) LatencyMs() *stats.Float64Measure { - return c.o.LatencyMs() -} diff --git a/v1/cloudevents/event_reader.go b/v1/cloudevents/event_reader.go deleted file mode 100644 index fe49e8424..000000000 --- a/v1/cloudevents/event_reader.go +++ /dev/null @@ -1,98 +0,0 @@ -package cloudevents - -import ( - "time" -) - -var _ EventReader = (*Event)(nil) - -// SpecVersion implements EventReader.SpecVersion -func (e Event) SpecVersion() string { - if e.Context != nil { - return e.Context.GetSpecVersion() - } - return "" -} - -// Type implements EventReader.Type -func (e Event) Type() string { - if e.Context != nil { - return e.Context.GetType() - } - return "" -} - -// Source implements EventReader.Source -func (e Event) Source() string { - if e.Context != nil { - return e.Context.GetSource() - } - return "" -} - -// Subject implements EventReader.Subject -func (e Event) Subject() string { - if e.Context != nil { - return e.Context.GetSubject() - } - return "" -} - -// ID implements EventReader.ID -func (e Event) ID() string { - if e.Context != nil { - return e.Context.GetID() - } - return "" -} - -// Time implements EventReader.Time -func (e Event) Time() time.Time { - if e.Context != nil { - return e.Context.GetTime() - } - return time.Time{} -} - -// DataSchema implements EventReader.DataSchema -func (e Event) DataSchema() string { - if e.Context != nil { - return e.Context.GetDataSchema() - } - return "" -} - -// DataContentType implements EventReader.DataContentType -func (e Event) DataContentType() string { - if e.Context != nil { - return e.Context.GetDataContentType() - } - return "" -} - -// DataMediaType returns the parsed DataMediaType of the event. If parsing -// fails, the empty string is returned. To retrieve the parsing error, use -// `Context.GetDataMediaType` instead. -func (e Event) DataMediaType() string { - if e.Context != nil { - mediaType, _ := e.Context.GetDataMediaType() - return mediaType - } - return "" -} - -// DeprecatedDataContentEncoding implements EventReader.DeprecatedDataContentEncoding -func (e Event) DeprecatedDataContentEncoding() string { - if e.Context != nil { - return e.Context.DeprecatedGetDataContentEncoding() - } - return "" -} - -// Extensions implements EventReader.Extensions -func (e Event) Extensions() map[string]interface{} { - if e.Context != nil { - return e.Context.GetExtensions() - } - return map[string]interface{}(nil) -} diff --git a/v1/cloudevents/event_reader_writer_test.go b/v1/cloudevents/event_reader_writer_test.go deleted file mode 100644 index 1fa421b8a..000000000 --- a/v1/cloudevents/event_reader_writer_test.go +++ /dev/null @@ -1,681 +0,0 @@ -package cloudevents_test - -import ( - "strings" - "testing" - "time" - - ce "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/google/go-cmp/cmp" -) - -type ReadWriteTest struct { - event ce.Event - set string - want interface{} - corrected interface{} // used in corrected tests. - wantErr string -} - -func TestEventRW_SpecVersion(t *testing.T) { - testCases := map[string]ReadWriteTest{ - "empty v01": { - event: ce.New(), - want: "1.0", - set: "0.1", - wantErr: "invalid version", - }, - "empty v02": { - event: ce.New(), - want: "1.0", - set: "0.2", - wantErr: "invalid version", - }, - "empty v03": { - event: ce.New(), - want: "1.0", - set: "0.3", - wantErr: "invalid version", - }, - "empty v1": { - event: ce.New(), - set: "1.0", - want: "1.0", - }, - "v01": { - event: ce.New("0.1"), - set: "0.1", - want: "0.1", - }, - "v02": { - event: ce.New("0.2"), - set: "0.2", - want: "0.2", - }, - "v03": { - event: ce.New("0.3"), - set: "0.3", - want: "0.3", - }, - "v1": { - event: ce.New("1.0"), - set: "1.0", - want: "1.0", - }, - "invalid v01": { - event: ce.New("0.1"), - want: "0.1", - set: "1.1", - wantErr: "invalid version", - }, - "invalid v02": { - event: ce.New("0.2"), - want: "0.2", - set: "1.2", - wantErr: "invalid version", - }, - "invalid v03": { - event: ce.New("0.3"), - want: "0.3", - set: "1.3", - wantErr: "invalid version", - }, - "invalid v1": { - event: ce.New("1.0"), - want: "1.0", - set: "1.3", - wantErr: "invalid version", - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - var got interface{} - - tc.event.SetSpecVersion(tc.set) - got = tc.event.SpecVersion() - - err := tc.event.Validate() - validateReaderWriter(t, tc, got, err) - }) - } -} - -func TestEventRW_Type(t *testing.T) { - testCases := map[string]ReadWriteTest{ - "v01": { - event: ce.New("0.1"), - set: "type.0.1", - want: "type.0.1", - }, - "v02": { - event: ce.New("0.2"), - set: "type.0.2", - want: "type.0.2", - }, - "v03": { - event: ce.New("0.3"), - set: "type.0.3", - want: "type.0.3", - }, - "spaced v01": { - event: ce.New("0.1"), - set: " type.0.1 ", - want: "type.0.1", - }, - "spaced v02": { - event: ce.New("0.2"), - set: " type.0.2 ", - want: "type.0.2", - }, - "spaced v03": { - event: ce.New("0.3"), - set: " type.0.3 ", - want: "type.0.3", - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - var got interface{} - - tc.event.SetType(tc.set) - got = tc.event.Type() - - err := tc.event.Validate() - validateReaderWriter(t, tc, got, err) - }) - } -} - -func TestEventRW_ID(t *testing.T) { - testCases := map[string]ReadWriteTest{ - "v01": { - event: ce.New("0.1"), - set: "id.0.1", - want: "id.0.1", - }, - "v02": { - event: ce.New("0.2"), - set: "id.0.2", - want: "id.0.2", - }, - "v03": { - event: ce.New("0.3"), - set: "id.0.3", - want: "id.0.3", - }, - "spaced v01": { - event: ce.New("0.1"), - set: " id.0.1 ", - want: "id.0.1", - }, - "spaced v02": { - event: ce.New("0.2"), - set: " id.0.2 ", - want: "id.0.2", - }, - "spaced v03": { - event: ce.New("0.3"), - set: " id.0.3 ", - want: "id.0.3", - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - var got interface{} - - tc.event.SetID(tc.set) - got = tc.event.ID() - - err := tc.event.Validate() - validateReaderWriter(t, tc, got, err) - }) - } -} - -func TestEventRW_Source(t *testing.T) { - testCases := map[string]ReadWriteTest{ - "v01": { - event: ce.New("0.1"), - set: "http://example/", - want: "http://example/", - }, - "v02": { - event: ce.New("0.2"), - set: "http://example/", - want: "http://example/", - }, - "v03": { - event: ce.New("0.3"), - set: "http://example/", - want: "http://example/", - }, - "invalid v01": { - event: ce.New("0.1"), - set: "%", - want: "", - wantErr: "invalid URL escape", - }, - "invalid v02": { - event: ce.New("0.2"), - set: "%", - want: "", - wantErr: "invalid URL escape", - }, - "invalid v03": { - event: ce.New("0.3"), - set: "%", - want: "", - wantErr: "invalid URL escape", - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - var got interface{} - - tc.event.SetSource(tc.set) - got = tc.event.Source() - - err := tc.event.Validate() - validateReaderWriter(t, tc, got, err) - }) - } -} - -// Set will be split on pipe, set1|set2 -func TestEventRW_Corrected_Source(t *testing.T) { - testCases := map[string]ReadWriteTest{ - "corrected v01": { - event: ce.New("0.1"), - set: "%|http://good", - want: "", - corrected: "http://good", - wantErr: "invalid URL escape", - }, - "corrected v02": { - event: ce.New("0.2"), - set: "%|http://good", - want: "", - corrected: "http://good", - wantErr: "invalid URL escape", - }, - "corrected v03": { - event: ce.New("0.3"), - set: "%|http://good", - want: "", - corrected: "http://good", - wantErr: "invalid URL escape", - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - var got interface{} - var err error - - // Split set on pipe. - set := strings.Split(tc.set, "|") - - // Set - - tc.event.SetSource(set[0]) - got = tc.event.Source() - err = tc.event.Validate() - validateReaderWriter(t, tc, got, err) - - // Correct - - tc.event.SetSource(set[1]) - got = tc.event.Source() - err = tc.event.Validate() - validateReaderWriterCorrected(t, tc, got, err) - }) - } -} - -func TestEventRW_Subject(t *testing.T) { - testCases := map[string]ReadWriteTest{ - "v01": { - event: ce.New("0.1"), - set: "subject.0.1", - want: "subject.0.1", - }, - "v02": { - event: ce.New("0.2"), - set: "subject.0.2", - want: "subject.0.2", - }, - "v03": { - event: ce.New("0.3"), - set: "subject.0.3", - want: "subject.0.3", - }, - "spaced v01": { - event: ce.New("0.1"), - set: " subject.0.1 ", - want: "subject.0.1", - }, - "spaced v02": { - event: ce.New("0.2"), - set: " subject.0.2 ", - want: "subject.0.2", - }, - "spaced v03": { - event: ce.New("0.3"), - set: " subject.0.3 ", - want: "subject.0.3", - }, - "nilled v01": { - event: func() ce.Event { - e := ce.New("0.1") - e.SetSource("should nil") - return e - }(), - want: "", - }, - "nilled v02": { - event: func() ce.Event { - e := ce.New("0.2") - e.SetSource("should nil") - return e - }(), - want: "", - }, - "nilled v03": { - event: func() ce.Event { - e := ce.New("0.3") - e.SetSource("should nil") - return e - }(), - want: "", - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - var got interface{} - - tc.event.SetSubject(tc.set) - got = tc.event.Subject() - - err := tc.event.Validate() - validateReaderWriter(t, tc, got, err) - }) - } -} - -func TestEventRW_Time(t *testing.T) { - now := time.Now() - - testCases := map[string]ReadWriteTest{ - "v01": { - event: ce.New("0.1"), - set: "now", // hack - want: now, - }, - "v02": { - event: ce.New("0.2"), - set: "now", // hack - want: now, - }, - "v03": { - event: ce.New("0.3"), - set: "now", // hack - want: now, - }, - "nilled v01": { - event: func() ce.Event { - e := ce.New("0.1") - e.SetTime(now) - return e - }(), - want: time.Time{}, - }, - "nilled v02": { - event: func() ce.Event { - e := ce.New("0.2") - e.SetTime(now) - return e - }(), - want: time.Time{}, - }, - "nilled v03": { - event: func() ce.Event { - e := ce.New("0.3") - e.SetTime(now) - return e - }(), - want: time.Time{}, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - var got interface{} - - if tc.set == "now" { - tc.event.SetTime(now) // pull now from outer test. - } else { - tc.event.SetTime(time.Time{}) // pull now from outer test. - } - got = tc.event.Time() - - err := tc.event.Validate() - validateReaderWriter(t, tc, got, err) - }) - } -} - -func TestEventRW_SchemaURL(t *testing.T) { - testCases := map[string]ReadWriteTest{ - "v01": { - event: ce.New("0.1"), - set: "http://example/", - want: "http://example/", - }, - "v02": { - event: ce.New("0.2"), - set: "http://example/", - want: "http://example/", - }, - "v03": { - event: ce.New("0.3"), - set: "http://example/", - want: "http://example/", - }, - "invalid v01": { - event: ce.New("0.1"), - set: "%", - want: "", - wantErr: "invalid URL escape", - }, - "invalid v02": { - event: ce.New("0.2"), - set: "%", - want: "", - wantErr: "invalid URL escape", - }, - "invalid v03": { - event: ce.New("0.3"), - set: "%", - want: "", - wantErr: "invalid URL escape", - }, - "nilled v01": { - event: func() ce.Event { - e := ce.New("0.1") - e.SetDataSchema("should nil") - return e - }(), - want: "", - }, - "nilled v02": { - event: func() ce.Event { - e := ce.New("0.2") - e.SetDataSchema("should nil") - return e - }(), - want: "", - }, - "nilled v03": { - event: func() ce.Event { - e := ce.New("0.3") - e.SetDataSchema("should nil") - return e - }(), - want: "", - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - var got interface{} - - tc.event.SetDataSchema(tc.set) - got = tc.event.DataSchema() - - err := tc.event.Validate() - validateReaderWriter(t, tc, got, err) - }) - } -} - -func TestEventRW_DataContentType(t *testing.T) { - testCases := map[string]ReadWriteTest{ - "v01": { - event: ce.New("0.1"), - set: "application/json", - want: "application/json", - }, - "v02": { - event: ce.New("0.2"), - set: "application/json", - want: "application/json", - }, - "v03": { - event: ce.New("0.3"), - set: "application/json", - want: "application/json", - }, - "spaced v01": { - event: ce.New("0.1"), - set: " application/json ", - want: "application/json", - }, - "spaced v02": { - event: ce.New("0.2"), - set: " application/json ", - want: "application/json", - }, - "spaced v03": { - event: ce.New("0.3"), - set: " application/json ", - want: "application/json", - }, - "nilled v01": { - event: func() ce.Event { - e := ce.New("0.1") - e.SetDataContentType("application/json") - return e - }(), - want: "", - }, - "nilled v02": { - event: func() ce.Event { - e := ce.New("0.2") - e.SetDataContentType("application/json") - return e - }(), - want: "", - }, - "nilled v03": { - event: func() ce.Event { - e := ce.New("0.3") - e.SetDataContentType("application/json") - return e - }(), - want: "", - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - var got interface{} - - tc.event.SetDataContentType(tc.set) - got = tc.event.DataContentType() - - err := tc.event.Validate() - validateReaderWriter(t, tc, got, err) - }) - } -} - -func TestEventRW_DataContentEncoding(t *testing.T) { - testCases := map[string]ReadWriteTest{ - "v01": { - event: ce.New("0.1"), - set: "base64", - want: "base64", - }, - "v02": { - event: ce.New("0.2"), - set: "base64", - want: "base64", - }, - "v03": { - event: ce.New("0.3"), - set: "base64", - want: "base64", - }, - "spaced v01": { - event: ce.New("0.1"), - set: " base64 ", - want: "base64", - }, - "spaced v02": { - event: ce.New("0.2"), - set: " base64 ", - want: "base64", - }, - "spaced v03": { - event: ce.New("0.3"), - set: " base64 ", - want: "base64", - }, - "cased v01": { - event: ce.New("0.1"), - set: " BaSe64 ", - want: "base64", - }, - "cased v02": { - event: ce.New("0.2"), - set: " BaSe64 ", - want: "base64", - }, - "cased v03": { - event: ce.New("0.3"), - set: " BaSe64 ", - want: "base64", - }, - "nilled v01": { - event: func() ce.Event { - e := ce.New("0.1") - e.SetDataContentEncoding("base64") - return e - }(), - want: "", - }, - "nilled v02": { - event: func() ce.Event { - e := ce.New("0.2") - e.SetDataContentEncoding("base64") - return e - }(), - want: "", - }, - "nilled v03": { - event: func() ce.Event { - e := ce.New("0.3") - e.SetDataContentEncoding("base64") - return e - }(), - want: "", - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - var got interface{} - - tc.event.SetDataContentEncoding(tc.set) - got = tc.event.DeprecatedDataContentEncoding() - - err := tc.event.Validate() - validateReaderWriter(t, tc, got, err) - }) - } -} - -func validateReaderWriter(t *testing.T, tc ReadWriteTest, got interface{}, err error) { - var gotErr string - if err != nil { - gotErr = err.Error() - } - if tc.wantErr != "" { - if !strings.Contains(gotErr, tc.wantErr) { - t.Errorf("unexpected error, expected to contain %q, got: %q ", tc.wantErr, gotErr) - } - } - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } -} - -func validateReaderWriterCorrected(t *testing.T, tc ReadWriteTest, got interface{}, err error) { - var gotErr string - if err != nil { - gotErr = err.Error() - } - if tc.wantErr != "" { - if strings.Contains(gotErr, tc.wantErr) { - t.Errorf("unexpected error, expected to NOT contain %q, got: %q ", tc.wantErr, gotErr) - } - } - if diff := cmp.Diff(tc.corrected, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } -} diff --git a/v1/cloudevents/event_response.go b/v1/cloudevents/event_response.go deleted file mode 100644 index 0e5f7ce75..000000000 --- a/v1/cloudevents/event_response.go +++ /dev/null @@ -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 -} diff --git a/v1/cloudevents/event_response_test.go b/v1/cloudevents/event_response_test.go deleted file mode 100644 index 4ea040db4..000000000 --- a/v1/cloudevents/event_response_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package cloudevents - -import ( - "testing" - - "github.com/google/go-cmp/cmp" -) - -func TestEventResponse_RespondWith(t *testing.T) { - testCases := map[string]struct { - t *EventResponse - e *Event - status int - want *EventResponse - }{ - "nil": {}, - "valid": { - t: &EventResponse{}, - e: &Event{Data: "unit test"}, - status: 200, - want: &EventResponse{ - Status: 200, - Event: &Event{Data: "unit test"}, - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - tc.t.RespondWith(tc.status, tc.e) - - got := tc.t - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} - -func TestEventResponse_Error(t *testing.T) { - testCases := map[string]struct { - t *EventResponse - msg string - status int - want *EventResponse - }{ - "nil": {}, - "valid": { - t: &EventResponse{}, - msg: "unit test", - status: 400, - want: &EventResponse{ - Status: 400, - Reason: "unit test", - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - tc.t.Error(tc.status, tc.msg) - - got := tc.t - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} diff --git a/v1/cloudevents/event_test.go b/v1/cloudevents/event_test.go deleted file mode 100644 index 10b38a3ca..000000000 --- a/v1/cloudevents/event_test.go +++ /dev/null @@ -1,996 +0,0 @@ -package cloudevents_test - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "net/url" - "strings" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - - ce "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -func TestGetDataContentType(t *testing.T) { - now := types.Timestamp{Time: time.Now()} - - testCases := map[string]struct { - event ce.Event - want string - }{ - "min v01, blank": { - event: ce.Event{ - Context: MinEventContextV01(), - }, - want: "", - }, - "full v01, json": { - event: ce.Event{ - Context: FullEventContextV01(now), - }, - want: "application/json", - }, - "min v02, blank": { - event: ce.Event{ - Context: MinEventContextV02(), - }, - want: "", - }, - "full v02, json": { - event: ce.Event{ - Context: FullEventContextV02(now), - }, - want: "application/json", - }, - "min v03, blank": { - event: ce.Event{ - Context: MinEventContextV03(), - }, - want: "", - }, - "full v03, json": { - event: ce.Event{ - Context: FullEventContextV03(now), - }, - want: "application/json", - }, - "min v1, blank": { - event: ce.Event{ - Context: MinEventContextV1(), - }, - want: "", - }, - "full v1, json": { - event: ce.Event{ - Context: FullEventContextV1(now), - }, - want: "application/json", - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, _ := tc.event.Context.GetDataMediaType() - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} - -func TestSource(t *testing.T) { - now := types.Timestamp{Time: time.Now()} - - source := "http://example.com/source" - - testCases := map[string]struct { - event ce.Event - want string - }{ - "min v01": { - event: ce.Event{ - Context: MinEventContextV01(), - }, - want: source, - }, - "full v01": { - event: ce.Event{ - Context: FullEventContextV01(now), - }, - want: source, - }, - "min v02": { - event: ce.Event{ - Context: MinEventContextV02(), - }, - want: source, - }, - "full v02": { - event: ce.Event{ - Context: FullEventContextV02(now), - }, - want: source, - }, - "min v03": { - event: ce.Event{ - Context: MinEventContextV03(), - }, - want: source, - }, - "full v03": { - event: ce.Event{ - Context: FullEventContextV03(now), - }, - want: source, - }, - "min v1": { - event: ce.Event{ - Context: MinEventContextV1(), - }, - want: source, - }, - "full v1": { - event: ce.Event{ - Context: FullEventContextV1(now), - }, - want: source, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got := tc.event.Source() - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} - -func TestSchemaURL(t *testing.T) { - now := types.Timestamp{Time: time.Now()} - - schema := "http://example.com/schema" - - testCases := map[string]struct { - event ce.Event - want string - }{ - "min v01, empty schema": { - event: ce.Event{ - Context: MinEventContextV01(), - }, - want: "", - }, - "full v01, schema": { - event: ce.Event{ - Context: FullEventContextV01(now), - }, - want: schema, - }, - "min v02, empty schema": { - event: ce.Event{ - Context: MinEventContextV02(), - }, - want: "", - }, - "full v02, schema": { - event: ce.Event{ - Context: FullEventContextV02(now), - }, - want: schema, - }, - "min v03, empty schema": { - event: ce.Event{ - Context: MinEventContextV03(), - }, - want: "", - }, - "full v03, schema": { - event: ce.Event{ - Context: FullEventContextV03(now), - }, - want: schema, - }, - "min v1, empty schema": { - event: ce.Event{ - Context: MinEventContextV1(), - }, - want: "", - }, - "full v1, schema": { - event: ce.Event{ - Context: FullEventContextV1(now), - }, - want: schema, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got := tc.event.DataSchema() - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} - -type DataExample struct { - AnInt int `json:"a,omitempty"` - AString string `json:"b,omitempty"` - AnArray []string `json:"c,omitempty"` - AMap map[string]map[string]int `json:"d,omitempty"` - ATime *time.Time `json:"e,omitempty"` -} - -func TestDataAs(t *testing.T) { - now := types.Timestamp{Time: time.Now()} - - testCases := map[string]struct { - event ce.Event - want interface{} - wantErr error - }{ - "empty": { - event: ce.Event{ - Context: MinEventContextV01(), - }, - want: nil, - }, - "json simple": { - event: ce.Event{ - Context: FullEventContextV01(now), - Data: []byte(`eyJhIjoiYXBwbGUiLCJiIjoiYmFuYW5hIn0K`), - DataEncoded: true, - }, - want: &map[string]string{ - "a": "apple", - "b": "banana", - }, - }, - "json complex empty": { - event: ce.Event{ - Context: FullEventContextV01(now), - Data: []byte(`e30K`), - DataEncoded: true, - }, - want: &DataExample{}, - }, - "json complex filled": { - event: ce.Event{ - Context: FullEventContextV01(now), - Data: func() []byte { - data := &DataExample{ - AnInt: 42, - AMap: map[string]map[string]int{ - "a": {"1": 1, "2": 2, "3": 3}, - "z": {"3": 3, "2": 2, "1": 1}, - }, - AString: "Hello, World!", - ATime: &now.Time, - AnArray: []string{"Anne", "Bob", "Chad"}, - } - j, err := json.Marshal(data) - if err != nil { - t.Errorf("failed to marshal test data: %s", err.Error()) - } - buf := make([]byte, base64.StdEncoding.EncodedLen(len(j))) - base64.StdEncoding.Encode(buf, j) - return buf - }(), - DataEncoded: true, - }, - want: &DataExample{ - AnInt: 42, - AMap: map[string]map[string]int{ - "a": {"1": 1, "2": 2, "3": 3}, - "z": {"3": 3, "2": 2, "1": 1}, - }, - AString: "Hello, World!", - ATime: &now.Time, - AnArray: []string{"Anne", "Bob", "Chad"}, - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, _ := types.Allocate(tc.want) - err := tc.event.DataAs(got) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if tc.want != nil { - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected data (-want, +got) = %v", diff) - } - } - }) - } -} - -func TestValidate(t *testing.T) { - now := types.Timestamp{Time: time.Now()} - - testCases := map[string]struct { - event ce.Event - want []string - }{ - "min valid v0.1": { - event: ce.Event{ - Context: MinEventContextV01(), - }, - }, - "min valid v0.2": { - event: ce.Event{ - Context: MinEventContextV02(), - }, - }, - "min valid v0.3": { - event: ce.Event{ - Context: MinEventContextV03(), - }, - }, - "min valid v1.0": { - event: ce.Event{ - Context: MinEventContextV1(), - }, - }, - "json valid, v0.1": { - event: ce.Event{ - Context: FullEventContextV01(now), - Data: []byte(`{"a":"apple","b":"banana"}`), - }, - }, - "json valid, v0.2": { - event: ce.Event{ - Context: FullEventContextV02(now), - Data: []byte(`{"a":"apple","b":"banana"}`), - }, - }, - "json valid, v0.3": { - event: ce.Event{ - Context: FullEventContextV03(now), - Data: []byte(`{"a":"apple","b":"banana"}`), - }, - }, - "json valid, v1.0": { - event: ce.Event{ - Context: FullEventContextV1(now), - Data: []byte(`{"a":"apple","b":"banana"}`), - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got := tc.event.Validate() - var gotErr string - if got != nil { - gotErr = got.Error() - - if len(tc.want) == 0 { - t.Errorf("unexpected no error, got %q", gotErr) - } - } - - for _, want := range tc.want { - if !strings.Contains(gotErr, want) { - t.Errorf("unexpected error, expected to contain %q, got: %q ", want, gotErr) - } - } - }) - } -} - -func TestString(t *testing.T) { - now := types.Timestamp{Time: time.Now()} - - testCases := map[string]struct { - event ce.Event - want string - }{ - "empty v0.1": { - event: ce.Event{ - Context: &ce.EventContextV01{}, - }, - want: `Validation: invalid -Validation Error: -eventType: MUST be a non-empty string -cloudEventsVersion: MUST be a non-empty string -source: REQUIRED -eventID: MUST be a non-empty string -Context Attributes, - cloudEventsVersion: - eventType: - source: - eventID: -`, - }, - "empty v0.2": { - event: ce.Event{ - Context: &ce.EventContextV02{}, - }, - want: `Validation: invalid -Validation Error: -type: MUST be a non-empty string -specversion: MUST be a non-empty string -source: REQUIRED -id: MUST be a non-empty string -Context Attributes, - specversion: - type: - source: - id: -`, - }, - "empty v0.3": { - event: ce.Event{ - Context: &ce.EventContextV03{}, - }, - want: `Validation: invalid -Validation Error: -type: MUST be a non-empty string -specversion: MUST be a non-empty string -source: REQUIRED -id: MUST be a non-empty string -Context Attributes, - specversion: - type: - source: - id: -`, - }, - "empty v1.0": { - event: ce.Event{ - Context: &ce.EventContextV1{}, - }, - want: `Validation: invalid -Validation Error: -id: MUST be a non-empty string -source: REQUIRED -specversion: MUST be a non-empty string -type: MUST be a non-empty string -Context Attributes, - specversion: - type: - source: - id: -`, - }, - "min v0.1": { - event: ce.Event{ - Context: MinEventContextV01(), - }, - want: `Validation: valid -Context Attributes, - cloudEventsVersion: 0.1 - eventType: com.example.simple - source: http://example.com/source - eventID: ABC-123 -`, - }, - "min v0.2": { - event: ce.Event{ - Context: MinEventContextV02(), - }, - want: `Validation: valid -Context Attributes, - specversion: 0.2 - type: com.example.simple - source: http://example.com/source - id: ABC-123 -`, - }, - "min v0.3": { - event: ce.Event{ - Context: MinEventContextV03(), - }, - want: `Validation: valid -Context Attributes, - specversion: 0.3 - type: com.example.simple - source: http://example.com/source - id: ABC-123 -`, - }, - "min v1.0": { - event: ce.Event{ - Context: MinEventContextV1(), - }, - want: `Validation: valid -Context Attributes, - specversion: 1.0 - type: com.example.simple - source: http://example.com/source - id: ABC-123 -`, - }, - "json simple, v0.1": { - event: ce.Event{ - Context: FullEventContextV01(now), - Data: []byte(`{"a":"apple","b":"banana"}`), - }, - want: fmt.Sprintf(`Validation: valid -Context Attributes, - cloudEventsVersion: 0.1 - eventType: com.example.simple - eventTypeVersion: v1alpha1 - source: http://example.com/source - eventID: ABC-123 - eventTime: %s - schemaURL: http://example.com/schema - contentType: application/json -Extensions, - anothertest: 1 - datacontentencoding: base64 - subject: topic - test: extended -Data, - { - "a": "apple", - "b": "banana" - } -`, now.String()), - }, - "json simple, v0.2": { - event: ce.Event{ - Context: FullEventContextV02(now), - Data: []byte(`{"a":"apple","b":"banana"}`), - }, - want: fmt.Sprintf(`Validation: valid -Context Attributes, - specversion: 0.2 - type: com.example.simple - source: http://example.com/source - id: ABC-123 - time: %s - schemaurl: http://example.com/schema - contenttype: application/json -Extensions, - anothertest: 1 - datacontentencoding: base64 - eventtypeversion: v1alpha1 - subject: topic - test: extended -Data, - { - "a": "apple", - "b": "banana" - } -`, now.String()), - }, - "json simple, v0.3": { - event: ce.Event{ - Context: FullEventContextV03(now), - Data: []byte(`{"a":"apple","b":"banana"}`), - }, - want: fmt.Sprintf(`Validation: valid -Context Attributes, - specversion: 0.3 - type: com.example.simple - source: http://example.com/source - subject: topic - id: ABC-123 - time: %s - schemaurl: http://example.com/schema - datacontenttype: application/json - datacontentencoding: base64 -Extensions, - anothertest: 1 - eventtypeversion: v1alpha1 - test: extended -Data, - { - "a": "apple", - "b": "banana" - } -`, now.String()), - }, - "json simple, v1.0": { - event: ce.Event{ - Context: FullEventContextV1(now), - Data: []byte(`{"a":"apple","b":"banana"}`), - }, - want: fmt.Sprintf(`Validation: valid -Context Attributes, - specversion: 1.0 - type: com.example.simple - source: http://example.com/source - subject: topic - id: ABC-123 - time: %s - dataschema: http://example.com/schema - datacontenttype: application/json -Extensions, - anothertest: 1 - datacontentencoding: base64 - eventtypeversion: v1alpha1 - test: extended -Data, - { - "a": "apple", - "b": "banana" - } -`, now.String()), - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got := tc.event.String() - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Log(got) - t.Errorf("unexpected string (-want, +got) = %v", diff) - } - }) - } -} - -// ExtensionAs is deprecated, replacement is TestExtensions below -func TestExtensionAs(t *testing.T) { - now := types.Timestamp{Time: time.Now()} - - testCases := map[string]struct { - event ce.Event - extension string - want string - wantError bool - wantErrorMsg string - }{ - "min v01, no extension": { - event: ce.Event{ - Context: MinEventContextV01(), - }, - extension: "test", - wantError: true, - wantErrorMsg: `extension "test" does not exist`, - }, - "full v01, test extension": { - event: ce.Event{ - Context: FullEventContextV01(now), - }, - extension: "test", - want: "extended", - }, - "full v01, anothertest extension invalid type": { - event: ce.Event{ - Context: FullEventContextV01(now), - }, - extension: "anothertest", - wantError: true, - wantErrorMsg: `invalid type for extension "anothertest"`, - }, - "min v02, no extension": { - event: ce.Event{ - Context: MinEventContextV02(), - }, - extension: "test", - wantError: true, - wantErrorMsg: `extension "test" does not exist`, - }, - "full v02, test extension": { - event: ce.Event{ - Context: FullEventContextV02(now), - }, - extension: "test", - want: "extended", - }, - "full v02, anothertest extension invalid type": { - event: ce.Event{ - Context: FullEventContextV02(now), - }, - extension: "anothertest", - wantError: true, - wantErrorMsg: `invalid type for extension "anothertest"`, - }, - "min v03, no extension": { - event: ce.Event{ - Context: MinEventContextV03(), - }, - extension: "test", - wantError: true, - wantErrorMsg: `extension "test" does not exist`, - }, - "full v03, test extension": { - event: ce.Event{ - Context: FullEventContextV03(now), - }, - extension: "test", - want: "extended", - }, - "full v03, anothertest extension invalid type": { - event: ce.Event{ - Context: FullEventContextV03(now), - }, - extension: "anothertest", - wantError: true, - wantErrorMsg: `invalid type for extension "anothertest"`, - }, - "full v1, test extension": { - event: ce.Event{ - Context: FullEventContextV1(now), - }, - extension: "test", - want: "extended", - }, - "full v1, anothertest extension invalid type": { - event: ce.Event{ - Context: FullEventContextV1(now), - }, - extension: "anothertest", - wantError: true, - wantErrorMsg: `unknown extension type *string`, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - var got string - err := tc.event.Context.ExtensionAs(tc.extension, &got) - - if tc.wantError { - if err == nil { - t.Errorf("expected error %q, got nil", tc.wantErrorMsg) - } else { - if diff := cmp.Diff(tc.wantErrorMsg, err.Error()); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - } - } else { - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - } - }) - } -} - -func TestExtensions(t *testing.T) { - now := types.Timestamp{Time: time.Now()} - - testCases := map[string]struct { - event ce.Event - extension string - want string - wantError bool - wantErrorMsg string - }{ - "full v01, test extension": { - event: ce.Event{ - Context: FullEventContextV01(now), - }, - extension: "test", - want: "extended", - }, - "full v01, anothertest extension invalid type": { - event: ce.Event{ - Context: FullEventContextV01(now), - }, - extension: "anothertest", - wantError: true, - wantErrorMsg: "cannot convert 1 to string", - }, - "full v02, test extension": { - event: ce.Event{ - Context: FullEventContextV02(now), - }, - extension: "test", - want: "extended", - }, - "full v02, anothertest extension invalid type": { - event: ce.Event{ - Context: FullEventContextV02(now), - }, - extension: "anothertest", - wantError: true, - wantErrorMsg: "cannot convert 1 to string", - }, - "full v03, test extension": { - event: ce.Event{ - Context: FullEventContextV03(now), - }, - extension: "test", - want: "extended", - }, - "full v03, anothertest extension invalid type": { - event: ce.Event{ - Context: FullEventContextV03(now), - }, - extension: "anothertest", - wantError: true, - wantErrorMsg: "cannot convert 1 to string", - }, - "full v1, test extension": { - event: ce.Event{ - Context: FullEventContextV1(now), - }, - extension: "test", - want: "extended", - }, - "full v1, anothertest extension invalid type": { - event: ce.Event{ - Context: FullEventContextV1(now), - }, - extension: "anothertest", - wantError: true, - wantErrorMsg: "cannot convert 1 to string", - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - var got string - got, err := types.ToString(tc.event.Context.GetExtensions()[tc.extension]) - if tc.wantError { - if err == nil { - t.Errorf("expected error %q, got nil", tc.wantErrorMsg) - } else { - if diff := cmp.Diff(tc.wantErrorMsg, err.Error()); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - } - } else { - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - } - }) - } -} - -func strptr(s string) *string { - return &s -} - -func MinEventContextV01() *ce.EventContextV01 { - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - return ce.EventContextV01{ - EventType: "com.example.simple", - Source: *source, - EventID: "ABC-123", - }.AsV01() -} - -func MinEventContextV02() *ce.EventContextV02 { - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - return ce.EventContextV02{ - Type: "com.example.simple", - Source: *source, - ID: "ABC-123", - }.AsV02() -} - -func MinEventContextV03() *ce.EventContextV03 { - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - return ce.EventContextV03{ - Type: "com.example.simple", - Source: *source, - ID: "ABC-123", - }.AsV03() -} - -func MinEventContextV1() *ce.EventContextV1 { - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URIRef{URL: *sourceUrl} - - return ce.EventContextV1{ - Type: "com.example.simple", - Source: *source, - ID: "ABC-123", - }.AsV1() -} - -func FullEventContextV01(now types.Timestamp) *ce.EventContextV01 { - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - schemaUrl, _ := url.Parse("http://example.com/schema") - schema := &types.URLRef{URL: *schemaUrl} - - eventContextV01 := ce.EventContextV01{ - EventID: "ABC-123", - EventTime: &now, - EventType: "com.example.simple", - EventTypeVersion: strptr("v1alpha1"), - SchemaURL: schema, - ContentType: ce.StringOfApplicationJSON(), - Source: *source, - } - _ = eventContextV01.SetExtension(ce.SubjectKey, "topic") - _ = eventContextV01.SetExtension(ce.DataContentEncodingKey, ce.Base64) - _ = eventContextV01.SetExtension("test", "extended") - _ = eventContextV01.SetExtension("anothertest", int32(1)) - return eventContextV01.AsV01() -} - -func FullEventContextV02(now types.Timestamp) *ce.EventContextV02 { - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - schemaUrl, _ := url.Parse("http://example.com/schema") - schema := &types.URLRef{URL: *schemaUrl} - - extensions := make(map[string]interface{}) - extensions["test"] = "extended" - extensions["anothertest"] = int32(1) - - eventContextV02 := ce.EventContextV02{ - ID: "ABC-123", - Time: &now, - Type: "com.example.simple", - SchemaURL: schema, - ContentType: ce.StringOfApplicationJSON(), - Source: *source, - Extensions: extensions, - } - _ = eventContextV02.SetExtension(ce.SubjectKey, "topic") - _ = eventContextV02.SetExtension(ce.DataContentEncodingKey, ce.Base64) - _ = eventContextV02.SetExtension(ce.EventTypeVersionKey, "v1alpha1") - return eventContextV02.AsV02() -} - -func FullEventContextV03(now types.Timestamp) *ce.EventContextV03 { - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - schemaUrl, _ := url.Parse("http://example.com/schema") - schema := &types.URLRef{URL: *schemaUrl} - - eventContextV03 := ce.EventContextV03{ - ID: "ABC-123", - Time: &now, - Type: "com.example.simple", - SchemaURL: schema, - DataContentType: ce.StringOfApplicationJSON(), - DataContentEncoding: ce.StringOfBase64(), - Source: *source, - Subject: strptr("topic"), - } - _ = eventContextV03.SetExtension("test", "extended") - _ = eventContextV03.SetExtension("anothertest", int32(1)) - _ = eventContextV03.SetExtension(ce.EventTypeVersionKey, "v1alpha1") - return eventContextV03.AsV03() -} - -func FullEventContextV1(now types.Timestamp) *ce.EventContextV1 { - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URIRef{URL: *sourceUrl} - - schemaUrl, _ := url.Parse("http://example.com/schema") - schema := &types.URI{URL: *schemaUrl} - - eventContextV1 := ce.EventContextV1{ - ID: "ABC-123", - Time: &now, - Type: "com.example.simple", - DataSchema: schema, - DataContentType: ce.StringOfApplicationJSON(), - Source: *source, - Subject: strptr("topic"), - } - _ = eventContextV1.SetExtension("test", "extended") - _ = eventContextV1.SetExtension("anothertest", 1) - _ = eventContextV1.SetExtension(ce.EventTypeVersionKey, "v1alpha1") - _ = eventContextV1.SetExtension(ce.DataContentEncodingKey, ce.Base64) - return eventContextV1.AsV1() -} diff --git a/v1/cloudevents/event_writer.go b/v1/cloudevents/event_writer.go deleted file mode 100644 index 19982ca5b..000000000 --- a/v1/cloudevents/event_writer.go +++ /dev/null @@ -1,116 +0,0 @@ -package cloudevents - -import ( - "fmt" - "time" -) - -var _ EventWriter = (*Event)(nil) - -// SetSpecVersion implements EventWriter.SetSpecVersion -func (e *Event) SetSpecVersion(v string) { - if e.Context == nil { - switch v { - case CloudEventsVersionV01: - e.Context = EventContextV01{}.AsV01() - case CloudEventsVersionV02: - e.Context = EventContextV02{}.AsV02() - case CloudEventsVersionV03: - e.Context = EventContextV03{}.AsV03() - case CloudEventsVersionV1: - e.Context = EventContextV1{}.AsV1() - default: - e.fieldError("specversion", fmt.Errorf("a valid spec version is required: [%s, %s, %s, %s]", - CloudEventsVersionV01, CloudEventsVersionV02, CloudEventsVersionV03, CloudEventsVersionV1)) - return - } - e.fieldOK("specversion") - return - } - if err := e.Context.SetSpecVersion(v); err != nil { - e.fieldError("specversion", err) - } else { - e.fieldOK("specversion") - } -} - -// SetType implements EventWriter.SetType -func (e *Event) SetType(t string) { - if err := e.Context.SetType(t); err != nil { - e.fieldError("type", err) - } else { - e.fieldOK("type") - } -} - -// SetSource implements EventWriter.SetSource -func (e *Event) SetSource(s string) { - if err := e.Context.SetSource(s); err != nil { - e.fieldError("source", err) - } else { - e.fieldOK("source") - } -} - -// SetSubject implements EventWriter.SetSubject -func (e *Event) SetSubject(s string) { - if err := e.Context.SetSubject(s); err != nil { - e.fieldError("subject", err) - } else { - e.fieldOK("subject") - } -} - -// SetID implements EventWriter.SetID -func (e *Event) SetID(id string) { - if err := e.Context.SetID(id); err != nil { - e.fieldError("id", err) - } else { - e.fieldOK("id") - } -} - -// SetTime implements EventWriter.SetTime -func (e *Event) SetTime(t time.Time) { - if err := e.Context.SetTime(t); err != nil { - e.fieldError("time", err) - } else { - e.fieldOK("time") - } -} - -// SetDataSchema implements EventWriter.SetDataSchema -func (e *Event) SetDataSchema(s string) { - if err := e.Context.SetDataSchema(s); err != nil { - e.fieldError("dataschema", err) - } else { - e.fieldOK("dataschema") - } -} - -// SetDataContentType implements EventWriter.SetDataContentType -func (e *Event) SetDataContentType(ct string) { - if err := e.Context.SetDataContentType(ct); err != nil { - e.fieldError("datacontenttype", err) - } else { - e.fieldOK("datacontenttype") - } -} - -// DeprecatedSetDataContentEncoding implements EventWriter.DeprecatedSetDataContentEncoding -func (e *Event) SetDataContentEncoding(enc string) { - if err := e.Context.DeprecatedSetDataContentEncoding(enc); err != nil { - e.fieldError("datacontentencoding", err) - } else { - e.fieldOK("datacontentencoding") - } -} - -// SetExtension implements EventWriter.SetExtension -func (e *Event) SetExtension(name string, obj interface{}) { - if err := e.Context.SetExtension(name, obj); err != nil { - e.fieldError("extension:"+name, err) - } else { - e.fieldOK("extension:" + name) - } -} diff --git a/v1/cloudevents/eventcontext.go b/v1/cloudevents/eventcontext.go deleted file mode 100644 index 5683a8292..000000000 --- a/v1/cloudevents/eventcontext.go +++ /dev/null @@ -1,132 +0,0 @@ -package cloudevents - -import "time" - -// EventContextReader are the methods required to be a reader of context -// attributes. -type EventContextReader interface { - // GetSpecVersion returns the native CloudEvents Spec version of the event - // context. - GetSpecVersion() string - // GetType returns the CloudEvents type from the context. - GetType() string - // GetSource returns the CloudEvents source from the context. - GetSource() string - // GetSubject returns the CloudEvents subject from the context. - GetSubject() string - // GetID returns the CloudEvents ID from the context. - GetID() string - // GetTime returns the CloudEvents creation time from the context. - GetTime() time.Time - // GetDataSchema returns the CloudEvents schema URL (if any) from the - // context. - GetDataSchema() string - // GetDataContentType returns content type on the context. - GetDataContentType() string - // DeprecatedGetDataContentEncoding returns content encoding on the context. - DeprecatedGetDataContentEncoding() string - - // GetDataMediaType returns the MIME media type for encoded data, which is - // needed by both encoding and decoding. This is a processed form of - // GetDataContentType and it may return an error. - GetDataMediaType() (string, error) - - // DEPRECATED: Access extensions directly via the GetExtensions() - // For example replace this: - // - // var i int - // err := ec.ExtensionAs("foo", &i) - // - // With this: - // - // i, err := types.ToInteger(ec.GetExtensions["foo"]) - // - ExtensionAs(string, interface{}) error - - // GetExtensions returns the full extensions map. - // - // Extensions use the CloudEvents type system, details in package cloudevents/types. - GetExtensions() map[string]interface{} - - // GetExtension returns the extension associated with with the given key. - // The given key is case insensitive. If the extension can not be found, - // an error will be returned. - GetExtension(string) (interface{}, error) -} - -// EventContextWriter are the methods required to be a writer of context -// attributes. -type EventContextWriter interface { - // SetSpecVersion sets the spec version of the context. - SetSpecVersion(string) error - // SetType sets the type of the context. - SetType(string) error - // SetSource sets the source of the context. - SetSource(string) error - // SetSubject sets the subject of the context. - SetSubject(string) error - // SetID sets the ID of the context. - SetID(string) error - // SetTime sets the time of the context. - SetTime(time time.Time) error - // SetDataSchema sets the schema url of the context. - SetDataSchema(string) error - // SetDataContentType sets the data content type of the context. - SetDataContentType(string) error - // DeprecatedSetDataContentEncoding sets the data context encoding of the context. - DeprecatedSetDataContentEncoding(string) error - - // SetExtension sets the given interface onto the extension attributes - // 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. - SetExtension(string, interface{}) error -} - -// EventContextConverter are the methods that allow for event version -// conversion. -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 - // CloudEvent was to the equivalent in v0.3 field names, moving fields to or - // from extensions as necessary. - AsV03() *EventContextV03 - - // AsV1 provides a translation from whatever the "native" encoding of the - // CloudEvent was to the equivalent in v1.0 field names, moving fields to or - // from extensions as necessary. - AsV1() *EventContextV1 -} - -// EventContext is conical interface for a CloudEvents Context. -type EventContext interface { - // EventContextConverter allows for conversion between versions. - EventContextConverter - - // EventContextReader adds methods for reading context. - EventContextReader - - // EventContextWriter adds methods for writing to context. - EventContextWriter - - // Validate the event based on the specifics of the CloudEvents spec version - // represented by this event context. - Validate() error - - // Clone clones the event context. - Clone() EventContext - - // String returns a pretty-printed representation of the EventContext. - String() string -} diff --git a/v1/cloudevents/eventcontext_test.go b/v1/cloudevents/eventcontext_test.go deleted file mode 100644 index 0b20a4730..000000000 --- a/v1/cloudevents/eventcontext_test.go +++ /dev/null @@ -1,353 +0,0 @@ -package cloudevents_test - -import ( - "testing" - "time" - - ce "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/google/go-cmp/cmp" - "github.com/stretchr/testify/require" -) - -func TestContextAsV01(t *testing.T) { - now := types.Timestamp{Time: time.Now()} - - testCases := map[string]struct { - event ce.Event - want *ce.EventContextV01 - }{ - "empty, no conversion": { - event: ce.Event{ - Context: &ce.EventContextV01{}, - }, - want: &ce.EventContextV01{ - CloudEventsVersion: "0.1", - }, - }, - "min v01, no conversion": { - event: ce.Event{ - Context: MinEventContextV01(), - }, - want: MinEventContextV01(), - }, - "full v01, no conversion": { - event: ce.Event{ - Context: FullEventContextV01(now), - }, - want: FullEventContextV01(now), - }, - "min v02 -> v01": { - event: ce.Event{ - Context: MinEventContextV02(), - }, - want: MinEventContextV01(), - }, - "full v02 -> v01": { - event: ce.Event{ - Context: FullEventContextV02(now), - }, - want: FullEventContextV01(now), - }, - "min v03 -> v01": { - event: ce.Event{ - Context: MinEventContextV03(), - }, - want: MinEventContextV01(), - }, - "full v03 -> v01": { - event: ce.Event{ - Context: FullEventContextV03(now), - }, - want: FullEventContextV01(now), - }, - "min v1 -> v01": { - event: ce.Event{ - Context: MinEventContextV1(), - }, - want: MinEventContextV01(), - }, - "full v1 -> v01": { - event: ce.Event{ - Context: FullEventContextV1(now), - }, - want: FullEventContextV01(now), - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got := tc.event.Context.AsV01() - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} - -func TestContextAsV02(t *testing.T) { - now := types.Timestamp{Time: time.Now()} - - testCases := map[string]struct { - event ce.Event - want *ce.EventContextV02 - }{ - "empty, no conversion": { - event: ce.Event{ - Context: &ce.EventContextV02{}, - }, - want: &ce.EventContextV02{ - SpecVersion: "0.2", - }, - }, - "min v01 -> v02": { - event: ce.Event{ - Context: MinEventContextV01(), - }, - want: MinEventContextV02(), - }, - "full v01 -> v02": { - event: ce.Event{ - Context: FullEventContextV01(now), - }, - want: FullEventContextV02(now), - }, - "min v02, no conversion": { - event: ce.Event{ - Context: MinEventContextV02(), - }, - want: MinEventContextV02(), - }, - "full v02, no conversion": { - event: ce.Event{ - Context: FullEventContextV02(now), - }, - want: FullEventContextV02(now), - }, - "min v03 -> v02": { - event: ce.Event{ - Context: MinEventContextV03(), - }, - want: MinEventContextV02(), - }, - "full v03 -> v02": { - event: ce.Event{ - Context: FullEventContextV03(now), - }, - want: FullEventContextV02(now), - }, - - "min v1 -> v02": { - event: ce.Event{ - Context: MinEventContextV1(), - }, - want: MinEventContextV02(), - }, - "full v1 -> v02": { - event: ce.Event{ - Context: FullEventContextV1(now), - }, - want: FullEventContextV02(now), - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got := tc.event.Context.AsV02() - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} - -func TestContextAsV03(t *testing.T) { - now := types.Timestamp{Time: time.Now()} - - testCases := map[string]struct { - event ce.Event - want *ce.EventContextV03 - }{ - "empty, no conversion": { - event: ce.Event{ - Context: &ce.EventContextV03{}, - }, - want: &ce.EventContextV03{ - SpecVersion: "0.3", - }, - }, - "min v01 -> v03": { - event: ce.Event{ - Context: MinEventContextV01(), - }, - want: MinEventContextV03(), - }, - "full v01 -> v03": { - event: ce.Event{ - Context: FullEventContextV01(now), - }, - want: FullEventContextV03(now), - }, - "min v02 -> v03": { - event: ce.Event{ - Context: MinEventContextV02(), - }, - want: MinEventContextV03(), - }, - "full v02 -> v03": { - event: ce.Event{ - Context: FullEventContextV02(now), - }, - want: FullEventContextV03(now), - }, - "min v03, no conversion": { - event: ce.Event{ - Context: MinEventContextV03(), - }, - want: MinEventContextV03(), - }, - "full v03, no conversion": { - event: ce.Event{ - Context: FullEventContextV03(now), - }, - want: FullEventContextV03(now), - }, - "min v1 -> v03": { - event: ce.Event{ - Context: MinEventContextV1(), - }, - want: MinEventContextV03(), - }, - "full v1 -> v03": { - event: ce.Event{ - Context: FullEventContextV1(now), - }, - want: FullEventContextV03(now), - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got := tc.event.Context.AsV03() - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} - -func TestContextAsV1(t *testing.T) { - now := types.Timestamp{Time: time.Now()} - - testCases := map[string]struct { - event ce.Event - want *ce.EventContextV1 - }{ - "empty, no conversion": { - event: ce.Event{ - Context: &ce.EventContextV1{}, - }, - want: &ce.EventContextV1{ - SpecVersion: "1.0", - }, - }, - "min v01 -> v1": { - event: ce.Event{ - Context: MinEventContextV01(), - }, - want: MinEventContextV1(), - }, - "full v01 -> v1": { - event: ce.Event{ - Context: FullEventContextV01(now), - }, - want: FullEventContextV1(now), - }, - "min v02 -> v1": { - event: ce.Event{ - Context: MinEventContextV02(), - }, - want: MinEventContextV1(), - }, - "full v02 -> v1": { - event: ce.Event{ - Context: FullEventContextV02(now), - }, - want: FullEventContextV1(now), - }, - "min v03 -> v1": { - event: ce.Event{ - Context: MinEventContextV03(), - }, - want: MinEventContextV1(), - }, - "full v03 -> v1": { - event: ce.Event{ - Context: FullEventContextV03(now), - }, - want: FullEventContextV1(now), - }, - "min v1, no conversion": { - event: ce.Event{ - Context: MinEventContextV1(), - }, - want: MinEventContextV1(), - }, - "full v1, no conversion": { - event: ce.Event{ - Context: FullEventContextV1(now), - }, - want: FullEventContextV1(now), - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got := tc.event.Context.AsV1() - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} - -func TestEventContextClone(t *testing.T) { - tests := []struct { - name string - context ce.EventContext - }{ - { - name: "v0.1", - context: FullEventContextV01(types.Timestamp{Time: time.Now()}), - }, - { - name: "v0.2", - context: FullEventContextV02(types.Timestamp{Time: time.Now()}), - }, - { - name: "v0.3", - context: FullEventContextV03(types.Timestamp{Time: time.Now()}), - }, - { - name: "v1.0", - context: FullEventContextV1(types.Timestamp{Time: time.Now()}), - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - initial := test.context - require.NoError(t, initial.SetExtension("aaa", "bbb")) - - clone := initial.Clone() - require.NoError(t, clone.SetExtension("aaa", "ccc")) - - val, err := initial.GetExtension("aaa") - require.NoError(t, err) - require.Equal(t, "bbb", val) - }) - } -} diff --git a/v1/cloudevents/eventcontext_v01.go b/v1/cloudevents/eventcontext_v01.go deleted file mode 100644 index 0ecacf68f..000000000 --- a/v1/cloudevents/eventcontext_v01.go +++ /dev/null @@ -1,286 +0,0 @@ -package cloudevents - -import ( - "fmt" - "sort" - "strings" - - "github.com/cloudevents/sdk-go/v1/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 { - ev01 := ec.AsV01() - ev01.Extensions = ev01.cloneExtensions() - return ev01 -} - -func (ec *EventContextV01) cloneExtensions() map[string]interface{} { - old := ec.Extensions - if old == nil { - return nil - } - new := make(map[string]interface{}, len(ec.Extensions)) - for k, v := range old { - new[k] = v - } - return new -} - -// 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() -} diff --git a/v1/cloudevents/eventcontext_v01_reader.go b/v1/cloudevents/eventcontext_v01_reader.go deleted file mode 100644 index 8d75ea70c..000000000 --- a/v1/cloudevents/eventcontext_v01_reader.go +++ /dev/null @@ -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 -} diff --git a/v1/cloudevents/eventcontext_v01_test.go b/v1/cloudevents/eventcontext_v01_test.go deleted file mode 100644 index e0127b309..000000000 --- a/v1/cloudevents/eventcontext_v01_test.go +++ /dev/null @@ -1,205 +0,0 @@ -package cloudevents_test - -import ( - "net/url" - "strings" - "testing" - "time" - - ce "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/google/go-cmp/cmp" -) - -func TestValidateV01(t *testing.T) { - now := types.Timestamp{Time: time.Now()} - - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - schemaUrl, _ := url.Parse("http://example.com/schema") - schema := &types.URLRef{URL: *schemaUrl} - - extensions := make(map[string]interface{}) - extensions["test"] = "extended" - - testCases := map[string]struct { - ctx ce.EventContextV01 - want []string - }{ - "min valid": { - ctx: ce.EventContextV01{ - CloudEventsVersion: ce.CloudEventsVersionV01, - EventID: "ABC-123", - EventType: "com.example.simple", - Source: *source, - }, - }, - "full valid": { - ctx: ce.EventContextV01{ - CloudEventsVersion: ce.CloudEventsVersionV01, - EventID: "ABC-123", - EventTime: &now, - EventType: "com.example.simple", - EventTypeVersion: strptr("v1alpha1"), - SchemaURL: schema, - ContentType: ce.StringOfApplicationJSON(), - Source: *source, - Extensions: extensions, - }, - }, - "no eventType": { - ctx: ce.EventContextV01{ - CloudEventsVersion: ce.CloudEventsVersionV01, - EventID: "ABC-123", - Source: *source, - }, - want: []string{"eventType:"}, - }, - "non-empty cloudEventsVersion": { - ctx: ce.EventContextV01{ - CloudEventsVersion: "", - EventID: "ABC-123", - EventType: "com.example.simple", - EventTypeVersion: strptr("v1alpha1"), - Source: *source, - }, - want: []string{"cloudEventsVersion:"}, - }, - "non-empty eventTypeVersion": { - ctx: ce.EventContextV01{ - CloudEventsVersion: ce.CloudEventsVersionV01, - EventID: "ABC-123", - EventType: "com.example.simple", - EventTypeVersion: strptr(""), - Source: *source, - }, - want: []string{"eventTypeVersion:"}, - }, - "missing source": { - ctx: ce.EventContextV01{ - CloudEventsVersion: ce.CloudEventsVersionV01, - EventID: "ABC-123", - EventType: "com.example.simple", - }, - want: []string{"source:"}, - }, - "non-empty eventID": { - ctx: ce.EventContextV01{ - CloudEventsVersion: ce.CloudEventsVersionV01, - EventID: "", - EventType: "com.example.simple", - Source: *source, - }, - want: []string{"eventID:"}, - }, - "empty schemaURL": { - ctx: ce.EventContextV01{ - CloudEventsVersion: ce.CloudEventsVersionV01, - EventID: "ABC-123", - EventType: "com.example.simple", - SchemaURL: &types.URLRef{}, - Source: *source, - }, - want: []string{"schemaURL:"}, - }, - "non-empty contentType": { - ctx: ce.EventContextV01{ - CloudEventsVersion: ce.CloudEventsVersionV01, - EventID: "ABC-123", - EventType: "com.example.simple", - Source: *source, - ContentType: strptr(""), - }, - want: []string{"contentType:"}, - }, - "empty extensions": { - ctx: ce.EventContextV01{ - CloudEventsVersion: ce.CloudEventsVersionV01, - EventID: "ABC-123", - EventType: "com.example.simple", - Source: *source, - Extensions: make(map[string]interface{}), - }, - want: []string{"extensions:"}, - }, - "all errors": { - ctx: ce.EventContextV01{ - CloudEventsVersion: "", - EventID: "", - SchemaURL: &types.URLRef{}, - ContentType: strptr(""), - Extensions: make(map[string]interface{}), - }, - want: []string{ - "eventType:", - "eventID:", - "extensions:", - "cloudEventsVersion:", - "source:", - "contentType:", - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got := tc.ctx.Validate() - var gotErr string - if got != nil { - gotErr = got.Error() - - if len(tc.want) == 0 { - t.Errorf("unexpected no error, got %q", gotErr) - } - } - - for _, want := range tc.want { - if !strings.Contains(gotErr, want) { - t.Errorf("unexpected error, expected to contain %q, got: %q ", want, gotErr) - } - } - }) - } -} - -func TestGetMediaTypeV01(t *testing.T) { - testCases := map[string]struct { - t string - want string - }{ - "nil": { - want: "", - }, - "just encoding": { - t: "charset=utf-8", - want: "", - }, - "text/html with encoding": { - t: "text/html; charset=utf-8", - want: "text/html", - }, - "application/json with encoding": { - t: "application/json; charset=utf-8", - want: "application/json", - }, - "application/json": { - t: "application/json", - want: "application/json", - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - ec := ce.EventContextV01{} - if tc.t != "" { - ec.ContentType = &tc.t - } - got, _ := ec.GetDataMediaType() - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} diff --git a/v1/cloudevents/eventcontext_v01_writer.go b/v1/cloudevents/eventcontext_v01_writer.go deleted file mode 100644 index f8e5888ab..000000000 --- a/v1/cloudevents/eventcontext_v01_writer.go +++ /dev/null @@ -1,104 +0,0 @@ -package cloudevents - -import ( - "errors" - "fmt" - "net/url" - "strings" - "time" - - "github.com/cloudevents/sdk-go/v1/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) -} diff --git a/v1/cloudevents/eventcontext_v02.go b/v1/cloudevents/eventcontext_v02.go deleted file mode 100644 index f4401047f..000000000 --- a/v1/cloudevents/eventcontext_v02.go +++ /dev/null @@ -1,305 +0,0 @@ -package cloudevents - -import ( - "encoding/json" - "fmt" - "sort" - "strings" - - "github.com/cloudevents/sdk-go/v1/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 { - ec02 := ec.AsV02() - ec02.Extensions = ec02.cloneExtensions() - return ec02 -} - -func (ec *EventContextV02) cloneExtensions() map[string]interface{} { - old := ec.Extensions - if old == nil { - return nil - } - new := make(map[string]interface{}, len(ec.Extensions)) - for k, v := range old { - new[k] = v - } - return new -} - -// 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() -} diff --git a/v1/cloudevents/eventcontext_v02_reader.go b/v1/cloudevents/eventcontext_v02_reader.go deleted file mode 100644 index 120cdb87e..000000000 --- a/v1/cloudevents/eventcontext_v02_reader.go +++ /dev/null @@ -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 -} diff --git a/v1/cloudevents/eventcontext_v02_test.go b/v1/cloudevents/eventcontext_v02_test.go deleted file mode 100644 index 78aff7be6..000000000 --- a/v1/cloudevents/eventcontext_v02_test.go +++ /dev/null @@ -1,192 +0,0 @@ -package cloudevents_test - -import ( - "net/url" - "strings" - "testing" - "time" - - ce "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/google/go-cmp/cmp" -) - -func TestValidateV02(t *testing.T) { - now := types.Timestamp{Time: time.Now()} - - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - schemaUrl, _ := url.Parse("http://example.com/schema") - schema := &types.URLRef{URL: *schemaUrl} - - extensions := make(map[string]interface{}) - extensions["test"] = "extended" - - testCases := map[string]struct { - ctx ce.EventContextV02 - want []string - }{ - "min valid": { - ctx: ce.EventContextV02{ - SpecVersion: ce.CloudEventsVersionV02, - ID: "ABC-123", - Type: "com.example.simple", - Source: *source, - }, - }, - "full valid": { - ctx: ce.EventContextV02{ - SpecVersion: ce.CloudEventsVersionV02, - ID: "ABC-123", - Time: &now, - Type: "com.example.simple", - SchemaURL: schema, - ContentType: ce.StringOfApplicationJSON(), - Source: *source, - Extensions: extensions, - }, - }, - "no Type": { - ctx: ce.EventContextV02{ - SpecVersion: ce.CloudEventsVersionV02, - ID: "ABC-123", - Source: *source, - }, - want: []string{"type:"}, - }, - "non-empty SpecVersion": { - ctx: ce.EventContextV02{ - SpecVersion: "", - ID: "ABC-123", - Type: "com.example.simple", - Source: *source, - }, - want: []string{"specversion:"}, - }, - "missing source": { - ctx: ce.EventContextV02{ - SpecVersion: ce.CloudEventsVersionV02, - ID: "ABC-123", - Type: "com.example.simple", - }, - want: []string{"source:"}, - }, - "non-empty ID": { - ctx: ce.EventContextV02{ - SpecVersion: ce.CloudEventsVersionV02, - ID: "", - Type: "com.example.simple", - Source: *source, - }, - want: []string{"id:"}, - }, - "empty schemaURL": { - ctx: ce.EventContextV02{ - SpecVersion: ce.CloudEventsVersionV02, - ID: "ABC-123", - Type: "com.example.simple", - SchemaURL: &types.URLRef{}, - Source: *source, - }, - want: []string{"schemaurl:"}, - }, - "non-empty contentType": { - ctx: ce.EventContextV02{ - SpecVersion: ce.CloudEventsVersionV02, - ID: "ABC-123", - Type: "com.example.simple", - Source: *source, - ContentType: strptr(""), - }, - want: []string{"contenttype:"}, - }, - //"empty extensions": { - // ctx: ce.EventContextV02{ - // SpecVersion: ce.CloudEventsVersionV02, - // ID: "ABC-123", - // Type: "com.example.simple", - // Source: *source, - // Extensions: make(map[string]interface{}), - // }, - // want: []string{"extensions:"}, - //}, - "all errors": { - ctx: ce.EventContextV02{ - SpecVersion: "", - ID: "", - SchemaURL: &types.URLRef{}, - ContentType: strptr(""), - Extensions: make(map[string]interface{}), - }, - want: []string{ - "type:", - "id:", - "specversion:", - "source:", - "contenttype:", - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got := tc.ctx.Validate() - var gotErr string - if got != nil { - gotErr = got.Error() - - if len(tc.want) == 0 { - t.Errorf("unexpected no error, got %q", gotErr) - } - } - - for _, want := range tc.want { - if !strings.Contains(gotErr, want) { - t.Errorf("unexpected error, expected to contain %q, got: %q ", want, gotErr) - } - } - }) - } -} - -func TestGetMediaTypeV02(t *testing.T) { - testCases := map[string]struct { - t string - want string - }{ - "nil": { - want: "", - }, - "just encoding": { - t: "charset=utf-8", - want: "", - }, - "text/html with encoding": { - t: "text/html; charset=utf-8", - want: "text/html", - }, - "application/json with encoding": { - t: "application/json; charset=utf-8", - want: "application/json", - }, - "application/json": { - t: "application/json", - want: "application/json", - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - ec := ce.EventContextV02{} - if tc.t != "" { - ec.ContentType = &tc.t - } - got, _ := ec.GetDataMediaType() - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} diff --git a/v1/cloudevents/eventcontext_v02_writer.go b/v1/cloudevents/eventcontext_v02_writer.go deleted file mode 100644 index 733680aa2..000000000 --- a/v1/cloudevents/eventcontext_v02_writer.go +++ /dev/null @@ -1,104 +0,0 @@ -package cloudevents - -import ( - "errors" - "fmt" - "net/url" - "strings" - "time" - - "github.com/cloudevents/sdk-go/v1/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) -} diff --git a/v1/cloudevents/eventcontext_v03.go b/v1/cloudevents/eventcontext_v03.go deleted file mode 100644 index 0b855ff71..000000000 --- a/v1/cloudevents/eventcontext_v03.go +++ /dev/null @@ -1,345 +0,0 @@ -package cloudevents - -import ( - "encoding/json" - "fmt" - "sort" - "strings" - - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -const ( - // CloudEventsVersionV03 represents the version 0.3 of the CloudEvents spec. - CloudEventsVersionV03 = "0.3" -) - -// EventContextV03 represents the non-data attributes of a CloudEvents v0.3 -// event. -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 string `json:"type"` - // Source - A URI describing the event producer. - Source types.URLRef `json:"source"` - // Subject - The subject of the event in the context of the event producer - // (identified by `source`). - Subject *string `json:"subject,omitempty"` - // ID of the event; must be non-empty and unique within the scope of the producer. - ID string `json:"id"` - // Time - A Timestamp when the event happened. - Time *types.Timestamp `json:"time,omitempty"` - // DataSchema - A link to the schema that the `data` attribute adheres to. - SchemaURL *types.URLRef `json:"schemaurl,omitempty"` - // 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"` - // DeprecatedDataContentEncoding describes the content encoding for the `data` attribute. Valid: nil, `Base64`. - DataContentEncoding *string `json:"datacontentencoding,omitempty"` - // Extensions - Additional extension metadata beyond the base spec. - Extensions map[string]interface{} `json:"-"` -} - -// Adhere to EventContext -var _ EventContext = (*EventContextV03)(nil) - -// ExtensionAs implements EventContext.ExtensionAs -func (ec EventContextV03) 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 *EventContextV03) SetExtension(name string, value interface{}) error { - if ec.Extensions == nil { - ec.Extensions = make(map[string]interface{}) - } - if value == nil { - delete(ec.Extensions, name) - } else { - v, err := types.Validate(value) - if err == nil { - ec.Extensions[name] = v - } - return err - } - return nil -} - -// Clone implements EventContextConverter.Clone -func (ec EventContextV03) Clone() EventContext { - ec03 := ec.AsV03() - ec03.Extensions = ec03.cloneExtensions() - return ec03 -} - -func (ec *EventContextV03) cloneExtensions() map[string]interface{} { - old := ec.Extensions - if old == nil { - return nil - } - new := make(map[string]interface{}, len(ec.Extensions)) - for k, v := range old { - new[k] = v - } - return new -} - -// AsV01 implements EventContextConverter.AsV01 -func (ec EventContextV03) AsV01() *EventContextV01 { - ecv2 := ec.AsV02() - return ecv2.AsV01() -} - -// 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. - if ec.Subject != nil { - _ = ret.SetExtension(SubjectKey, *ec.Subject) - } - // DeprecatedDataContentEncoding was introduced in 0.3, so put it in an extension for 0.2. - 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 -func (ec EventContextV03) AsV03() *EventContextV03 { - ec.SpecVersion = CloudEventsVersionV03 - return &ec -} - -// AsV04 implements EventContextConverter.AsV04 -func (ec EventContextV03) AsV1() *EventContextV1 { - ret := EventContextV1{ - SpecVersion: CloudEventsVersionV1, - ID: ec.ID, - Time: ec.Time, - Type: ec.Type, - DataContentType: ec.DataContentType, - Source: types.URIRef{URL: ec.Source.URL}, - Subject: ec.Subject, - Extensions: make(map[string]interface{}), - } - if ec.SchemaURL != nil { - ret.DataSchema = &types.URI{URL: ec.SchemaURL.URL} - } - - // DataContentEncoding was removed in 1.0, so put it in an extension for 1.0. - if ec.DataContentEncoding != nil { - _ = ret.SetExtension(DataContentEncodingKey, *ec.DataContentEncoding) - } - - if ec.Extensions != nil { - for k, v := range ec.Extensions { - k = strings.ToLower(k) - ret.Extensions[k] = v - } - } - if len(ret.Extensions) == 0 { - ret.Extensions = nil - } - return &ret -} - -// Validate returns errors based on requirements from the CloudEvents spec. -// For more details, see https://github.com/cloudevents/spec/blob/master/spec.md -// As of Feb 26, 2019, commit 17c32ea26baf7714ad027d9917d03d2fff79fc7e -// + https://github.com/cloudevents/spec/pull/387 -> datacontentencoding -// + https://github.com/cloudevents/spec/pull/406 -> subject -func (ec EventContextV03) 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") - } - - // subject - // Type: String - // Constraints: - // OPTIONAL - // MUST be a non-empty string - if ec.Subject != nil { - subject := strings.TrimSpace(*ec.Subject) - if subject == "" { - errors = append(errors, "subject: if present, MUST be a non-empty string") - } - } - - // 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") - } - } - - // datacontenttype - // Type: String per RFC 2046 - // Constraints: - // OPTIONAL - // If present, MUST adhere to the format specified in RFC 2046 - if ec.DataContentType != nil { - dataContentType := strings.TrimSpace(*ec.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") - } - } - - // datacontentencoding - // Type: String per RFC 2045 Section 6.1 - // Constraints: - // The attribute MUST be set if the data attribute contains string-encoded binary data. - // Otherwise the attribute MUST NOT be set. - // If present, MUST adhere to RFC 2045 Section 6.1 - if ec.DataContentEncoding != nil { - dataContentEncoding := strings.ToLower(strings.TrimSpace(*ec.DataContentEncoding)) - if dataContentEncoding != Base64 { - // TODO: need to test for RFC 2046 - errors = append(errors, "datacontentencoding: if present, MUST adhere to RFC 2045 Section 6.1") - } - } - - if len(errors) > 0 { - return fmt.Errorf(strings.Join(errors, "\n")) - } - return nil -} - -// String returns a pretty-printed representation of the EventContext. -func (ec EventContextV03) 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") - if ec.Subject != nil { - b.WriteString(" subject: " + *ec.Subject + "\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.DataContentType != nil { - b.WriteString(" datacontenttype: " + *ec.DataContentType + "\n") - } - if ec.DataContentEncoding != nil { - b.WriteString(" datacontentencoding: " + *ec.DataContentEncoding + "\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() -} diff --git a/v1/cloudevents/eventcontext_v03_reader.go b/v1/cloudevents/eventcontext_v03_reader.go deleted file mode 100644 index 2b3cc207f..000000000 --- a/v1/cloudevents/eventcontext_v03_reader.go +++ /dev/null @@ -1,96 +0,0 @@ -package cloudevents - -import ( - "fmt" - "mime" - "time" -) - -// GetSpecVersion implements EventContextReader.GetSpecVersion -func (ec EventContextV03) GetSpecVersion() string { - if ec.SpecVersion != "" { - return ec.SpecVersion - } - return CloudEventsVersionV03 -} - -// GetDataContentType implements EventContextReader.GetDataContentType -func (ec EventContextV03) GetDataContentType() string { - if ec.DataContentType != nil { - return *ec.DataContentType - } - return "" -} - -// GetDataMediaType implements EventContextReader.GetDataMediaType -func (ec EventContextV03) GetDataMediaType() (string, error) { - if ec.DataContentType != nil { - mediaType, _, err := mime.ParseMediaType(*ec.DataContentType) - if err != nil { - return "", err - } - return mediaType, nil - } - return "", nil -} - -// GetType implements EventContextReader.GetType -func (ec EventContextV03) GetType() string { - return ec.Type -} - -// GetSource implements EventContextReader.GetSource -func (ec EventContextV03) GetSource() string { - return ec.Source.String() -} - -// GetSubject implements EventContextReader.GetSubject -func (ec EventContextV03) GetSubject() string { - if ec.Subject != nil { - return *ec.Subject - } - return "" -} - -// GetTime implements EventContextReader.GetTime -func (ec EventContextV03) GetTime() time.Time { - if ec.Time != nil { - return ec.Time.Time - } - return time.Time{} -} - -// GetID implements EventContextReader.GetID -func (ec EventContextV03) GetID() string { - return ec.ID -} - -// GetDataSchema implements EventContextReader.GetDataSchema -func (ec EventContextV03) GetDataSchema() string { - if ec.SchemaURL != nil { - return ec.SchemaURL.String() - } - return "" -} - -// DeprecatedGetDataContentEncoding implements EventContextReader.DeprecatedGetDataContentEncoding -func (ec EventContextV03) DeprecatedGetDataContentEncoding() string { - if ec.DataContentEncoding != nil { - return *ec.DataContentEncoding - } - return "" -} - -// GetExtensions implements EventContextReader.GetExtensions -func (ec EventContextV03) GetExtensions() map[string]interface{} { - return ec.Extensions -} - -// GetExtension implements EventContextReader.GetExtension -func (ec EventContextV03) GetExtension(key string) (interface{}, error) { - v, ok := caseInsensitiveSearch(key, ec.Extensions) - if !ok { - return "", fmt.Errorf("%q not found", key) - } - return v, nil -} diff --git a/v1/cloudevents/eventcontext_v03_test.go b/v1/cloudevents/eventcontext_v03_test.go deleted file mode 100644 index 344b82f5f..000000000 --- a/v1/cloudevents/eventcontext_v03_test.go +++ /dev/null @@ -1,227 +0,0 @@ -package cloudevents_test - -import ( - "net/url" - "strings" - "testing" - "time" - - ce "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/google/go-cmp/cmp" -) - -func TestValidateV03(t *testing.T) { - now := types.Timestamp{Time: time.Now()} - - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - subject := "a subject" - - schemaUrl, _ := url.Parse("http://example.com/schema") - schema := &types.URLRef{URL: *schemaUrl} - - extensions := make(map[string]interface{}) - extensions["test"] = "extended" - - testCases := map[string]struct { - ctx ce.EventContextV03 - want []string - }{ - "min valid": { - ctx: ce.EventContextV03{ - SpecVersion: ce.CloudEventsVersionV03, - ID: "ABC-123", - Type: "com.example.simple", - Source: *source, - }, - }, - "full valid": { - ctx: ce.EventContextV03{ - SpecVersion: ce.CloudEventsVersionV03, - ID: "ABC-123", - Time: &now, - Type: "com.example.simple", - SchemaURL: schema, - DataContentType: ce.StringOfApplicationJSON(), - DataContentEncoding: ce.StringOfBase64(), - Source: *source, - Subject: &subject, - Extensions: extensions, - }, - }, - "no Type": { - ctx: ce.EventContextV03{ - SpecVersion: ce.CloudEventsVersionV03, - ID: "ABC-123", - Source: *source, - }, - want: []string{"type:"}, - }, - "non-empty SpecVersion": { - ctx: ce.EventContextV03{ - SpecVersion: "", - ID: "ABC-123", - Type: "com.example.simple", - Source: *source, - }, - want: []string{"specversion:"}, - }, - "missing source": { - ctx: ce.EventContextV03{ - SpecVersion: ce.CloudEventsVersionV03, - ID: "ABC-123", - Type: "com.example.simple", - }, - want: []string{"source:"}, - }, - "non-empty subject": { - ctx: ce.EventContextV03{ - SpecVersion: ce.CloudEventsVersionV03, - ID: "", - Type: "com.example.simple", - Source: *source, - Subject: strptr(" "), - }, - want: []string{"subject:"}, - }, - "non-empty ID": { - ctx: ce.EventContextV03{ - SpecVersion: ce.CloudEventsVersionV03, - ID: "", - Type: "com.example.simple", - Source: *source, - }, - want: []string{"id:"}, - }, - "empty schemaURL": { - ctx: ce.EventContextV03{ - SpecVersion: ce.CloudEventsVersionV03, - ID: "ABC-123", - Type: "com.example.simple", - SchemaURL: &types.URLRef{}, - Source: *source, - }, - want: []string{"schemaurl:"}, - }, - "non-empty contentType": { - ctx: ce.EventContextV03{ - SpecVersion: ce.CloudEventsVersionV03, - ID: "ABC-123", - Type: "com.example.simple", - Source: *source, - DataContentType: strptr(""), - }, - want: []string{"datacontenttype:"}, - }, - "non-empty dataContentEncoding": { - ctx: ce.EventContextV03{ - SpecVersion: ce.CloudEventsVersionV03, - ID: "ABC-123", - Type: "com.example.simple", - Source: *source, - DataContentEncoding: strptr(""), - }, - want: []string{"datacontentencoding:"}, - }, - "invalid dataContentEncoding": { - ctx: ce.EventContextV03{ - SpecVersion: ce.CloudEventsVersionV03, - ID: "ABC-123", - Type: "com.example.simple", - Source: *source, - DataContentEncoding: strptr("binary"), - }, - want: []string{"datacontentencoding:"}, - }, - - //"empty extensions": { - // ctx: ce.EventContextV03{ - // SpecVersion: ce.CloudEventsVersionV03, - // ID: "ABC-123", - // Type: "com.example.simple", - // Source: *source, - // Extensions: make(map[string]interface{}), - // }, - // want: []string{"extensions:"}, - //}, - "all errors": { - ctx: ce.EventContextV03{ - SpecVersion: "", - ID: "", - SchemaURL: &types.URLRef{}, - DataContentType: strptr(""), - Extensions: make(map[string]interface{}), - }, - want: []string{ - "type:", - "id:", - "specversion:", - "source:", - "contenttype:", - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got := tc.ctx.Validate() - var gotErr string - if got != nil { - gotErr = got.Error() - - if len(tc.want) == 0 { - t.Errorf("unexpected no error, got %q", gotErr) - } - } - - for _, want := range tc.want { - if !strings.Contains(gotErr, want) { - t.Errorf("unexpected error, expected to contain %q, got: %q ", want, gotErr) - } - } - }) - } -} - -func TestGetMediaTypeV03(t *testing.T) { - testCases := map[string]struct { - t string - want string - }{ - "nil": { - want: "", - }, - "just encoding": { - t: "charset=utf-8", - want: "", - }, - "text/html with encoding": { - t: "text/html; charset=utf-8", - want: "text/html", - }, - "application/json with encoding": { - t: "application/json; charset=utf-8", - want: "application/json", - }, - "application/json": { - t: "application/json", - want: "application/json", - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - ec := ce.EventContextV03{} - if tc.t != "" { - ec.DataContentType = &tc.t - } - got, _ := ec.GetDataMediaType() - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} diff --git a/v1/cloudevents/eventcontext_v03_writer.go b/v1/cloudevents/eventcontext_v03_writer.go deleted file mode 100644 index 479bc0729..000000000 --- a/v1/cloudevents/eventcontext_v03_writer.go +++ /dev/null @@ -1,108 +0,0 @@ -package cloudevents - -import ( - "errors" - "fmt" - "net/url" - "strings" - "time" - - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -// Adhere to EventContextWriter -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 -func (ec *EventContextV03) SetDataContentType(ct string) error { - ct = strings.TrimSpace(ct) - if ct == "" { - ec.DataContentType = nil - } else { - ec.DataContentType = &ct - } - return nil -} - -// SetType implements EventContextWriter.SetType -func (ec *EventContextV03) SetType(t string) error { - t = strings.TrimSpace(t) - ec.Type = t - return nil -} - -// SetSource implements EventContextWriter.SetSource -func (ec *EventContextV03) 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 *EventContextV03) SetSubject(s string) error { - s = strings.TrimSpace(s) - if s == "" { - ec.Subject = nil - } else { - ec.Subject = &s - } - return nil -} - -// SetID implements EventContextWriter.SetID -func (ec *EventContextV03) 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 *EventContextV03) 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 *EventContextV03) 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 *EventContextV03) DeprecatedSetDataContentEncoding(e string) error { - e = strings.ToLower(strings.TrimSpace(e)) - if e == "" { - ec.DataContentEncoding = nil - } else { - ec.DataContentEncoding = &e - } - return nil -} diff --git a/v1/cloudevents/eventcontext_v1.go b/v1/cloudevents/eventcontext_v1.go deleted file mode 100644 index c3d055776..000000000 --- a/v1/cloudevents/eventcontext_v1.go +++ /dev/null @@ -1,315 +0,0 @@ -package cloudevents - -import ( - "errors" - "fmt" - "mime" - "sort" - "strings" - - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -// WIP: AS OF SEP 20, 2019 - -const ( - // CloudEventsVersionV1 represents the version 1.0 of the CloudEvents spec. - CloudEventsVersionV1 = "1.0" -) - -// EventContextV1 represents the non-data attributes of a CloudEvents v1.0 -// event. -type EventContextV1 struct { - // ID of the event; must be non-empty and unique within the scope of the producer. - // +required - ID string `json:"id"` - // Source - A URI describing the event producer. - // +required - 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. - // +required - Type string `json:"type"` - - // DataContentType - A MIME (RFC2046) string describing the media type of `data`. - // +optional - DataContentType *string `json:"datacontenttype,omitempty"` - // Subject - The subject of the event in the context of the event producer - // (identified by `source`). - // +optional - Subject *string `json:"subject,omitempty"` - // Time - A Timestamp when the event happened. - // +optional - Time *types.Timestamp `json:"time,omitempty"` - // DataSchema - A link to the schema that the `data` attribute adheres to. - // +optional - DataSchema *types.URI `json:"dataschema,omitempty"` - - // Extensions - Additional extension metadata beyond the base spec. - // +optional - Extensions map[string]interface{} `json:"-"` -} - -// Adhere to EventContext -var _ EventContext = (*EventContextV1)(nil) - -// ExtensionAs implements EventContext.ExtensionAs -func (ec EventContextV1) ExtensionAs(name string, obj interface{}) error { - name = strings.ToLower(name) - value, ok := ec.Extensions[name] - if !ok { - return fmt.Errorf("extension %q does not exist", name) - } - - // Only support *string for now. - if v, ok := obj.(*string); ok { - if *v, ok = value.(string); ok { - return nil - } - } - return fmt.Errorf("unknown extension type %T", obj) -} - -// 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 { - 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") - } - - name = strings.ToLower(name) - if ec.Extensions == nil { - ec.Extensions = make(map[string]interface{}) - } - if value == nil { - delete(ec.Extensions, name) - return nil - } else { - v, err := types.Validate(value) // Ensure it's a legal CE attribute value - if err == nil { - ec.Extensions[name] = v - } - return err - } -} - -// Clone implements EventContextConverter.Clone -func (ec EventContextV1) Clone() EventContext { - ec1 := ec.AsV1() - ec1.Extensions = ec1.cloneExtensions() - return ec1 -} - -func (ec *EventContextV1) cloneExtensions() map[string]interface{} { - old := ec.Extensions - if old == nil { - return nil - } - new := make(map[string]interface{}, len(ec.Extensions)) - for k, v := range old { - new[k] = v - } - return new -} - -// AsV01 implements EventContextConverter.AsV01 -func (ec EventContextV1) AsV01() *EventContextV01 { - ecv2 := ec.AsV02() - return ecv2.AsV01() -} - -// AsV02 implements EventContextConverter.AsV02 -func (ec EventContextV1) AsV02() *EventContextV02 { - ecv3 := ec.AsV03() - return ecv3.AsV02() -} - -// AsV03 implements EventContextConverter.AsV03 -func (ec EventContextV1) AsV03() *EventContextV03 { - ret := EventContextV03{ - SpecVersion: CloudEventsVersionV03, - ID: ec.ID, - Time: ec.Time, - Type: ec.Type, - DataContentType: ec.DataContentType, - Source: types.URLRef{URL: ec.Source.URL}, - Subject: ec.Subject, - Extensions: make(map[string]interface{}), - } - - if ec.DataSchema != nil { - ret.SchemaURL = &types.URLRef{URL: ec.DataSchema.URL} - } - - // TODO: DeprecatedDataContentEncoding needs to be moved to extensions. - if ec.Extensions != nil { - for k, v := range ec.Extensions { - k = strings.ToLower(k) - // DeprecatedDataContentEncoding was introduced in 0.3, removed in 1.0 - 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 -} - -// AsV04 implements EventContextConverter.AsV04 -func (ec EventContextV1) AsV1() *EventContextV1 { - ec.SpecVersion = CloudEventsVersionV1 - return &ec -} - -// Validate returns errors based on requirements from the CloudEvents spec. -// For more details, see https://github.com/cloudevents/spec/blob/v1.0-rc1/spec.md. -func (ec EventContextV1) Validate() error { - errors := []string(nil) - - // 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" - } - - // source - // Type: URI-reference - // Constraints: - // REQUIRED - // MUST be a non-empty URI-reference - // An absolute URI is RECOMMENDED - source := strings.TrimSpace(ec.Source.String()) - if source == "" { - 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: 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") - } - - // The following attributes are optional but still have validation. - - // datacontenttype - // Type: String per RFC 2046 - // Constraints: - // OPTIONAL - // If present, MUST adhere to the format specified in RFC 2046 - if ec.DataContentType != nil { - dataContentType := strings.TrimSpace(*ec.DataContentType) - if dataContentType == "" { - 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 media type, %s", err.Error())) - } - } - } - - // dataschema - // Type: URI - // Constraints: - // OPTIONAL - // If present, MUST adhere to the format specified in RFC 3986 - if ec.DataSchema != nil { - dataSchema := strings.TrimSpace(ec.DataSchema.String()) - // empty string is not RFC 3986 compatible. - if dataSchema == "" { - errors = append(errors, "dataschema: if present, MUST adhere to the format specified in RFC 3986") - } - } - - // subject - // Type: String - // Constraints: - // OPTIONAL - // MUST be a non-empty string - if ec.Subject != nil { - subject := strings.TrimSpace(*ec.Subject) - if subject == "" { - errors = append(errors, "subject: if present, MUST be a non-empty string") - } - } - - // 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. - - if len(errors) > 0 { - return fmt.Errorf(strings.Join(errors, "\n")) - } - return nil -} - -// String returns a pretty-printed representation of the EventContext. -func (ec EventContextV1) 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") - if ec.Subject != nil { - b.WriteString(" subject: " + *ec.Subject + "\n") - } - b.WriteString(" id: " + ec.ID + "\n") - if ec.Time != nil { - b.WriteString(" time: " + ec.Time.String() + "\n") - } - if ec.DataSchema != nil { - b.WriteString(" dataschema: " + ec.DataSchema.String() + "\n") - } - if ec.DataContentType != nil { - b.WriteString(" datacontenttype: " + *ec.DataContentType + "\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() -} diff --git a/v1/cloudevents/eventcontext_v1_reader.go b/v1/cloudevents/eventcontext_v1_reader.go deleted file mode 100644 index e3f329d31..000000000 --- a/v1/cloudevents/eventcontext_v1_reader.go +++ /dev/null @@ -1,98 +0,0 @@ -package cloudevents - -import ( - "fmt" - "mime" - "time" -) - -// GetSpecVersion implements EventContextReader.GetSpecVersion -func (ec EventContextV1) GetSpecVersion() string { - if ec.SpecVersion != "" { - return ec.SpecVersion - } - return CloudEventsVersionV03 -} - -// GetDataContentType implements EventContextReader.GetDataContentType -func (ec EventContextV1) GetDataContentType() string { - if ec.DataContentType != nil { - return *ec.DataContentType - } - return "" -} - -// GetDataMediaType implements EventContextReader.GetDataMediaType -func (ec EventContextV1) GetDataMediaType() (string, error) { - if ec.DataContentType != nil { - mediaType, _, err := mime.ParseMediaType(*ec.DataContentType) - if err != nil { - return "", err - } - return mediaType, nil - } - return "", nil -} - -// GetType implements EventContextReader.GetType -func (ec EventContextV1) GetType() string { - return ec.Type -} - -// GetSource implements EventContextReader.GetSource -func (ec EventContextV1) GetSource() string { - return ec.Source.String() -} - -// GetSubject implements EventContextReader.GetSubject -func (ec EventContextV1) GetSubject() string { - if ec.Subject != nil { - return *ec.Subject - } - return "" -} - -// GetTime implements EventContextReader.GetTime -func (ec EventContextV1) GetTime() time.Time { - if ec.Time != nil { - return ec.Time.Time - } - return time.Time{} -} - -// GetID implements EventContextReader.GetID -func (ec EventContextV1) GetID() string { - return ec.ID -} - -// GetDataSchema implements EventContextReader.GetDataSchema -func (ec EventContextV1) GetDataSchema() string { - if ec.DataSchema != nil { - return ec.DataSchema.String() - } - return "" -} - -// DeprecatedGetDataContentEncoding implements EventContextReader.DeprecatedGetDataContentEncoding -func (ec EventContextV1) DeprecatedGetDataContentEncoding() string { - return "" -} - -// GetExtensions implements EventContextReader.GetExtensions -func (ec EventContextV1) GetExtensions() map[string]interface{} { - // For now, convert the extensions of v1.0 to the pre-v1.0 style. - ext := make(map[string]interface{}, len(ec.Extensions)) - for k, v := range ec.Extensions { - ext[k] = v - } - return ext -} - -// GetExtension implements EventContextReader.GetExtension -func (ec EventContextV1) GetExtension(key string) (interface{}, error) { - v, ok := caseInsensitiveSearch(key, ec.Extensions) - if !ok { - return "", fmt.Errorf("%q not found", key) - } - return v, nil -} diff --git a/v1/cloudevents/eventcontext_v1_test.go b/v1/cloudevents/eventcontext_v1_test.go deleted file mode 100644 index 97501633a..000000000 --- a/v1/cloudevents/eventcontext_v1_test.go +++ /dev/null @@ -1,206 +0,0 @@ -package cloudevents_test - -import ( - "net/url" - "strings" - "testing" - "time" - - ce "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/google/go-cmp/cmp" -) - -func TestValidateV1(t *testing.T) { - now := types.Timestamp{Time: time.Now()} - - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URIRef{URL: *sourceUrl} - - subject := "a subject" - - DataSchema, _ := url.Parse("http://example.com/schema") - schema := &types.URI{URL: *DataSchema} - - extensions := make(map[string]interface{}) - extensions["test"] = "extended" - - testCases := map[string]struct { - ctx ce.EventContextV1 - want []string - }{ - "min valid": { - ctx: ce.EventContextV1{ - SpecVersion: ce.CloudEventsVersionV03, - ID: "ABC-123", - Type: "com.example.simple", - Source: *source, - }, - }, - "full valid": { - ctx: ce.EventContextV1{ - SpecVersion: ce.CloudEventsVersionV03, - ID: "ABC-123", - Time: &now, - Type: "com.example.simple", - DataSchema: schema, - DataContentType: ce.StringOfApplicationJSON(), - Source: *source, - Subject: &subject, - Extensions: extensions, - }, - }, - "no Type": { - ctx: ce.EventContextV1{ - SpecVersion: ce.CloudEventsVersionV03, - ID: "ABC-123", - Source: *source, - }, - want: []string{"type:"}, - }, - "non-empty SpecVersion": { - ctx: ce.EventContextV1{ - SpecVersion: "", - ID: "ABC-123", - Type: "com.example.simple", - Source: *source, - }, - want: []string{"specversion:"}, - }, - "missing source": { - ctx: ce.EventContextV1{ - SpecVersion: ce.CloudEventsVersionV03, - ID: "ABC-123", - Type: "com.example.simple", - }, - want: []string{"source:"}, - }, - "non-empty subject": { - ctx: ce.EventContextV1{ - SpecVersion: ce.CloudEventsVersionV03, - ID: "", - Type: "com.example.simple", - Source: *source, - Subject: strptr(" "), - }, - want: []string{"subject:"}, - }, - "non-empty ID": { - ctx: ce.EventContextV1{ - SpecVersion: ce.CloudEventsVersionV03, - ID: "", - Type: "com.example.simple", - Source: *source, - }, - want: []string{"id:"}, - }, - "empty DataSchema": { - ctx: ce.EventContextV1{ - SpecVersion: ce.CloudEventsVersionV03, - ID: "ABC-123", - Type: "com.example.simple", - DataSchema: &types.URI{}, - Source: *source, - }, - want: []string{"dataschema:"}, - }, - "non-empty contentType": { - ctx: ce.EventContextV1{ - SpecVersion: ce.CloudEventsVersionV03, - ID: "ABC-123", - Type: "com.example.simple", - Source: *source, - DataContentType: strptr(""), - }, - want: []string{"datacontenttype:"}, - }, - "invalid contentType": { - ctx: ce.EventContextV1{ - SpecVersion: ce.CloudEventsVersionV03, - ID: "ABC-123", - Type: "com.example.simple", - Source: *source, - DataContentType: strptr("bogus ;========="), - }, - want: []string{"datacontenttype:"}, - }, - - "all errors": { - ctx: ce.EventContextV1{ - SpecVersion: "", - ID: "", - DataSchema: &types.URI{}, - DataContentType: strptr(""), - Extensions: make(map[string]interface{}), - }, - want: []string{ - "type:", - "id:", - "specversion:", - "source:", - "contenttype:", - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got := tc.ctx.Validate() - var gotErr string - if got != nil { - gotErr = got.Error() - - if len(tc.want) == 0 { - t.Errorf("unexpected no error, got %q", gotErr) - } - } - - for _, want := range tc.want { - if !strings.Contains(gotErr, want) { - t.Errorf("unexpected error, expected to contain %q, got: %q ", want, gotErr) - } - } - }) - } -} - -func TestGetMediaTypeV1(t *testing.T) { - testCases := map[string]struct { - t string - want string - }{ - "nil": { - want: "", - }, - "just encoding": { - t: "charset=utf-8", - want: "", - }, - "text/html with encoding": { - t: "text/html; charset=utf-8", - want: "text/html", - }, - "application/json with encoding": { - t: "application/json; charset=utf-8", - want: "application/json", - }, - "application/json": { - t: "application/json", - want: "application/json", - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - ec := ce.EventContextV1{} - if tc.t != "" { - ec.DataContentType = &tc.t - } - got, _ := ec.GetDataMediaType() - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} diff --git a/v1/cloudevents/eventcontext_v1_writer.go b/v1/cloudevents/eventcontext_v1_writer.go deleted file mode 100644 index 5765fdd25..000000000 --- a/v1/cloudevents/eventcontext_v1_writer.go +++ /dev/null @@ -1,102 +0,0 @@ -package cloudevents - -import ( - "errors" - "fmt" - "net/url" - "strings" - "time" - - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -// Adhere to EventContextWriter -var _ EventContextWriter = (*EventContextV1)(nil) - -// SetSpecVersion implements EventContextWriter.SetSpecVersion -func (ec *EventContextV1) SetSpecVersion(v string) error { - if v != CloudEventsVersionV1 { - return fmt.Errorf("invalid version %q, expecting %q", v, CloudEventsVersionV1) - } - ec.SpecVersion = CloudEventsVersionV1 - return nil -} - -// SetDataContentType implements EventContextWriter.SetDataContentType -func (ec *EventContextV1) SetDataContentType(ct string) error { - ct = strings.TrimSpace(ct) - if ct == "" { - ec.DataContentType = nil - } else { - ec.DataContentType = &ct - } - return nil -} - -// SetType implements EventContextWriter.SetType -func (ec *EventContextV1) SetType(t string) error { - t = strings.TrimSpace(t) - ec.Type = t - return nil -} - -// SetSource implements EventContextWriter.SetSource -func (ec *EventContextV1) SetSource(u string) error { - pu, err := url.Parse(u) - if err != nil { - return err - } - ec.Source = types.URIRef{URL: *pu} - return nil -} - -// SetSubject implements EventContextWriter.SetSubject -func (ec *EventContextV1) SetSubject(s string) error { - s = strings.TrimSpace(s) - if s == "" { - ec.Subject = nil - } else { - ec.Subject = &s - } - return nil -} - -// SetID implements EventContextWriter.SetID -func (ec *EventContextV1) 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 *EventContextV1) 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 *EventContextV1) SetDataSchema(u string) error { - u = strings.TrimSpace(u) - if u == "" { - ec.DataSchema = nil - return nil - } - pu, err := url.Parse(u) - if err != nil { - return err - } - ec.DataSchema = &types.URI{URL: *pu} - return nil -} - -// DeprecatedSetDataContentEncoding implements EventContextWriter.DeprecatedSetDataContentEncoding -func (ec *EventContextV1) DeprecatedSetDataContentEncoding(e string) error { - return errors.New("deprecated: SetDataContentEncoding is not supported in v1.0 of CloudEvents") -} diff --git a/v1/cloudevents/extensions.go b/v1/cloudevents/extensions.go deleted file mode 100644 index e6a7d5325..000000000 --- a/v1/cloudevents/extensions.go +++ /dev/null @@ -1,30 +0,0 @@ -package cloudevents - -import ( - "regexp" - "strings" -) - -const ( - // DataContentEncodingKey is the key to DeprecatedDataContentEncoding for versions that do not support data content encoding - // directly. - DataContentEncodingKey = "datacontentencoding" - - // EventTypeVersionKey is the key to EventTypeVersion for versions that do not support event type version directly. - EventTypeVersionKey = "eventtypeversion" - - // SubjectKey is the key to Subject for versions that do not support subject directly. - SubjectKey = "subject" -) - -func caseInsensitiveSearch(key string, space map[string]interface{}) (interface{}, bool) { - lkey := strings.ToLower(key) - for k, v := range space { - if strings.EqualFold(lkey, strings.ToLower(k)) { - return v, true - } - } - return nil, false -} - -var IsAlphaNumeric = regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString diff --git a/v1/cloudevents/extensions/distributed_tracing_extension.go b/v1/cloudevents/extensions/distributed_tracing_extension.go deleted file mode 100644 index c6aa241e2..000000000 --- a/v1/cloudevents/extensions/distributed_tracing_extension.go +++ /dev/null @@ -1,135 +0,0 @@ -package extensions - -import ( - "context" - "reflect" - "strings" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/lightstep/tracecontext.go/traceparent" - "github.com/lightstep/tracecontext.go/tracestate" - "go.opencensus.io/trace" - octs "go.opencensus.io/trace/tracestate" -) - -const ( - TraceParentExtension = "traceparent" - TraceStateExtension = "tracestate" -) - -// EventTracer interface allows setting extension for cloudevents context. -type EventTracer interface { - SetExtension(k string, v interface{}) error -} - -// DistributedTracingExtension represents the extension for cloudevents context -type DistributedTracingExtension struct { - TraceParent string `json:"traceparent"` - TraceState string `json:"tracestate"` -} - -// AddTracingAttributes adds the tracing attributes traceparent and tracestate to the cloudevents context -func (d DistributedTracingExtension) AddTracingAttributes(ec EventTracer) error { - if d.TraceParent != "" { - value := reflect.ValueOf(d) - typeOf := value.Type() - - for i := 0; i < value.NumField(); i++ { - k := strings.ToLower(typeOf.Field(i).Name) - v := value.Field(i).Interface() - if k == TraceStateExtension && v == "" { - continue - } - if err := ec.SetExtension(k, v); err != nil { - return err - } - } - } - return nil -} - -func GetDistributedTracingExtension(event cloudevents.Event) (DistributedTracingExtension, bool) { - if tp, ok := event.Extensions()[TraceParentExtension]; ok { - if tpStr, err := types.ToString(tp); err == nil { - var tsStr string - if ts, ok := event.Extensions()[TraceStateExtension]; ok { - tsStr, _ = types.ToString(ts) - } - return DistributedTracingExtension{TraceParent: tpStr, TraceState: tsStr}, true - } - } - return DistributedTracingExtension{}, false -} - -// FromSpanContext populates DistributedTracingExtension from a SpanContext. -func FromSpanContext(sc trace.SpanContext) DistributedTracingExtension { - tp := traceparent.TraceParent{ - TraceID: sc.TraceID, - SpanID: sc.SpanID, - Flags: traceparent.Flags{ - Recorded: sc.IsSampled(), - }, - } - - entries := make([]string, 0, len(sc.Tracestate.Entries())) - for _, entry := range sc.Tracestate.Entries() { - entries = append(entries, strings.Join([]string{entry.Key, entry.Value}, "=")) - } - - return DistributedTracingExtension{ - TraceParent: tp.String(), - TraceState: strings.Join(entries, ","), - } -} - -// ToSpanContext creates a SpanContext from a DistributedTracingExtension instance. -func (d DistributedTracingExtension) ToSpanContext() (trace.SpanContext, error) { - tp, err := traceparent.ParseString(d.TraceParent) - if err != nil { - return trace.SpanContext{}, err - } - sc := trace.SpanContext{ - TraceID: tp.TraceID, - SpanID: tp.SpanID, - } - if tp.Flags.Recorded { - sc.TraceOptions |= 1 - } - - if ts, err := tracestate.ParseString(d.TraceState); err == nil { - entries := make([]octs.Entry, 0, len(ts)) - for _, member := range ts { - var key string - if member.Tenant != "" { - // Due to github.com/lightstep/tracecontext.go/issues/6, - // the meaning of Vendor and Tenant are swapped here. - key = member.Vendor + "@" + member.Tenant - } else { - key = member.Vendor - } - entries = append(entries, octs.Entry{Key: key, Value: member.Value}) - } - sc.Tracestate, _ = octs.New(nil, entries...) - } - - return sc, nil -} - -func (d DistributedTracingExtension) StartChildSpan(ctx context.Context, name string, opts ...trace.StartOption) (context.Context, *trace.Span) { - if sc, err := d.ToSpanContext(); err == nil { - tSpan := trace.FromContext(ctx) - ctx, span := trace.StartSpanWithRemoteParent(ctx, name, sc, opts...) - if tSpan != nil { - // Add link to the previous in-process trace. - tsc := tSpan.SpanContext() - span.AddLink(trace.Link{ - TraceID: tsc.TraceID, - SpanID: tsc.SpanID, - Type: trace.LinkTypeParent, - }) - } - return ctx, span - } - return ctx, nil -} diff --git a/v1/cloudevents/extensions/distributed_tracing_extension_test.go b/v1/cloudevents/extensions/distributed_tracing_extension_test.go deleted file mode 100644 index 7054cf74e..000000000 --- a/v1/cloudevents/extensions/distributed_tracing_extension_test.go +++ /dev/null @@ -1,340 +0,0 @@ -package extensions_test - -import ( - "encoding/hex" - "net/url" - "testing" - "time" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/extensions" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/google/go-cmp/cmp" - "go.opencensus.io/trace" - "go.opencensus.io/trace/tracestate" -) - -type Data struct { - Message string -} - -var now = types.Timestamp{Time: time.Now().UTC()} - -var sourceUrl, _ = url.Parse("http://example.com/source") -var source = &types.URLRef{URL: *sourceUrl} - -var schemaUrl, _ = url.Parse("http://example.com/schema") -var schema = &types.URLRef{URL: *schemaUrl} - -type values struct { - context interface{} - want map[string]interface{} -} - -func TestAddTracingAttributes_Scenario1(t *testing.T) { - var st = extensions.DistributedTracingExtension{ - TraceParent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", - TraceState: "rojo=00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01,congo=lZWRzIHRoNhcm5hbCBwbGVhc3VyZS4=", - } - - var eventContextVersions = map[string]values{ - "EventContextV01": { - context: cloudevents.EventContextV01{ - EventID: "ABC-123", - EventTime: &now, - EventType: "com.example.test", - SchemaURL: schema, - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - }, - want: map[string]interface{}{"traceparent": st.TraceParent, "tracestate": st.TraceState}, - }, - "EventContextV02": { - context: cloudevents.EventContextV02{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - }, - want: map[string]interface{}{"traceparent": st.TraceParent, "tracestate": st.TraceState}, - }, - "EventContextV03": { - context: cloudevents.EventContextV03{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - }, - want: map[string]interface{}{"traceparent": st.TraceParent, "tracestate": st.TraceState}, - }, - } - - for k, ecv := range eventContextVersions { - testAddTracingAttributesFunc(t, st, ecv, k) - } -} - -func TestAddTracingAttributes_Scenario2(t *testing.T) { - var st = extensions.DistributedTracingExtension{ - TraceParent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", - } - - var eventContextVersions = map[string]values{ - "EventContextV01": { - context: cloudevents.EventContextV01{ - EventID: "ABC-123", - EventTime: &now, - EventType: "com.example.test", - SchemaURL: schema, - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - }, - want: map[string]interface{}{"traceparent": st.TraceParent}, - }, - "EventContextV02": { - context: cloudevents.EventContextV02{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - }, - want: map[string]interface{}{"traceparent": st.TraceParent}, - }, - "EventContextV03": { - context: cloudevents.EventContextV03{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - }, - want: map[string]interface{}{"traceparent": st.TraceParent}, - }, - } - - for k, ecv := range eventContextVersions { - testAddTracingAttributesFunc(t, st, ecv, k) - } -} - -func TestAddTracingAttributes_Scenario3(t *testing.T) { - var st = extensions.DistributedTracingExtension{} - - var eventContextVersions = map[string]values{ - "EventContextV01": { - context: cloudevents.EventContextV01{ - EventID: "ABC-123", - EventTime: &now, - EventType: "com.example.test", - SchemaURL: schema, - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - }, - want: map[string]interface{}(nil), - }, - "EventContextV02": { - context: cloudevents.EventContextV02{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - }, - want: map[string]interface{}(nil), - }, - "EventContextV03": { - context: cloudevents.EventContextV03{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - }, - want: map[string]interface{}(nil), - }, - } - - for k, ecv := range eventContextVersions { - testAddTracingAttributesFunc(t, st, ecv, k) - } -} - -func TestAddTracingAttributes_Scenario4(t *testing.T) { - var st = extensions.DistributedTracingExtension{ - TraceState: "rojo=00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01,congo=lZWRzIHRoNhcm5hbCBwbGVhc3VyZS4=", - } - - var eventContextVersions = map[string]values{ - "EventContextV01": { - context: cloudevents.EventContextV01{ - EventID: "ABC-123", - EventTime: &now, - EventType: "com.example.test", - SchemaURL: schema, - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - }, - want: map[string]interface{}(nil), - }, - "EventContextV02": { - context: cloudevents.EventContextV02{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - }, - want: map[string]interface{}(nil), - }, - "EventContextV03": { - context: cloudevents.EventContextV03{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - }, - want: map[string]interface{}(nil), - }, - } - - for k, ecv := range eventContextVersions { - testAddTracingAttributesFunc(t, st, ecv, k) - } -} - -func testAddTracingAttributesFunc(t *testing.T, st extensions.DistributedTracingExtension, ecv values, ces string) { - var event cloudevents.Event - switch ces { - case "EventContextV01": - ectx := ecv.context.(cloudevents.EventContextV01).AsV01() - st.AddTracingAttributes(ectx) - event = cloudevents.Event{Context: ectx, Data: &Data{Message: "Hello world"}} - case "EventContextV02": - ectx := ecv.context.(cloudevents.EventContextV02).AsV02() - st.AddTracingAttributes(ectx) - event = cloudevents.Event{Context: ectx, Data: &Data{Message: "Hello world"}} - case "EventContextV03": - ectx := ecv.context.(cloudevents.EventContextV03).AsV03() - st.AddTracingAttributes(ectx) - event = cloudevents.Event{Context: ectx, Data: &Data{Message: "Hello world"}} - } - got := event.Extensions() - - if diff := cmp.Diff(ecv.want, got); diff != "" { - t.Errorf("\nunexpected (-want, +got) = %v", diff) - } -} - -func decodeTID(s string) (tid [16]byte, err error) { - buf, err := hex.DecodeString(s) - copy(tid[:], buf) - return -} - -func decodeSID(s string) (sid [8]byte, err error) { - buf, err := hex.DecodeString(s) - copy(sid[:], buf) - return -} - -func TestConvertSpanContext(t *testing.T) { - tid, err := decodeTID("4bf92f3577b34da6a3ce929d0e0e4736") - if err != nil { - t.Fatalf("failed to decode traceID: %v", err) - } - sid, err := decodeSID("00f067aa0ba902b7") - if err != nil { - t.Fatalf("failed to decode spanID: %v", err) - } - ts, err := tracestate.New(nil, - tracestate.Entry{ - Key: "rojo", - Value: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", - }, - tracestate.Entry{ - Key: "tenant@congo", - Value: "lZWRzIHRoNhcm5hbCBwbGVhc3VyZS4", - }, - ) - if err != nil { - t.Fatalf("failed to make tracestate: %v", err) - } - tests := []struct { - name string - sc trace.SpanContext - ext extensions.DistributedTracingExtension - }{{ - name: "with tracestate", - sc: trace.SpanContext{ - TraceID: trace.TraceID(tid), - SpanID: sid, - TraceOptions: 1, - Tracestate: ts, - }, - ext: extensions.DistributedTracingExtension{ - TraceParent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", - TraceState: "rojo=00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01,tenant@congo=lZWRzIHRoNhcm5hbCBwbGVhc3VyZS4", - }, - }, { - name: "without tracestate", - sc: trace.SpanContext{ - TraceID: trace.TraceID(tid), - SpanID: sid, - TraceOptions: 1, - }, - ext: extensions.DistributedTracingExtension{ - TraceParent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", - }, - }, { - name: "unsampled", - sc: trace.SpanContext{ - TraceID: trace.TraceID(tid), - SpanID: sid, - TraceOptions: 0, - }, - ext: extensions.DistributedTracingExtension{ - TraceParent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00", - }, - }} - - for _, tt := range tests { - tt := tt - t.Run("FromSpanContext: "+tt.name, func(t *testing.T) { - t.Parallel() - got := extensions.FromSpanContext(tt.sc) - if diff := cmp.Diff(tt.ext, got); diff != "" { - t.Errorf("\nunexpected (-want, +got) = %v", diff) - } - }) - t.Run("ToSpanContext: "+tt.name, func(t *testing.T) { - t.Parallel() - got, err := tt.ext.ToSpanContext() - if err != nil { - t.Error(err) - } - if diff := cmp.Diff( - tt.sc, got, - cmp.Transformer( - "entries", - func(ts tracestate.Tracestate) []tracestate.Entry { - return ts.Entries() - }, - ), - ); diff != "" { - t.Errorf("\nunexpected (-want, +got) = %v", diff) - } - }) - } -} diff --git a/v1/cloudevents/observability/doc.go b/v1/cloudevents/observability/doc.go deleted file mode 100644 index 3067ebe7e..000000000 --- a/v1/cloudevents/observability/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -/* -Package observability holds metrics and tracing recording implementations. -*/ -package observability diff --git a/v1/cloudevents/observability/keys.go b/v1/cloudevents/observability/keys.go deleted file mode 100644 index f032b10ec..000000000 --- a/v1/cloudevents/observability/keys.go +++ /dev/null @@ -1,19 +0,0 @@ -package observability - -import ( - "go.opencensus.io/tag" -) - -var ( - // KeyMethod is the tag used for marking method on a metric. - KeyMethod, _ = tag.NewKey("method") - // KeyResult is the tag used for marking result on a metric. - KeyResult, _ = tag.NewKey("result") -) - -const ( - // ResultError is a shared result tag value for error. - ResultError = "error" - // ResultOK is a shared result tag value for success. - ResultOK = "success" -) diff --git a/v1/cloudevents/observability/observer.go b/v1/cloudevents/observability/observer.go deleted file mode 100644 index b27ffa973..000000000 --- a/v1/cloudevents/observability/observer.go +++ /dev/null @@ -1,87 +0,0 @@ -package observability - -import ( - "context" - "sync" - "time" - - "go.opencensus.io/stats" - "go.opencensus.io/tag" -) - -// Observable represents the the customization used by the Reporter for a given -// measurement and trace for a single method. -type Observable interface { - MethodName() string - LatencyMs() *stats.Float64Measure -} - -// Reporter represents a running latency counter. When Error or OK are -// called, the latency is calculated. Error or OK are only allowed to -// be called once. -type Reporter interface { - Error() - OK() -} - -type reporter struct { - ctx context.Context - on Observable - start time.Time - once sync.Once -} - -// All tags used for Latency measurements. -func LatencyTags() []tag.Key { - return []tag.Key{KeyMethod, KeyResult} -} - -// Deprecated. Tracing is always enabled. -func EnableTracing(enabled bool) {} - -// NewReporter creates and returns a reporter wrapping the provided Observable. -func NewReporter(ctx context.Context, on Observable) (context.Context, Reporter) { - r := &reporter{ - ctx: ctx, - on: on, - start: time.Now(), - } - r.tagMethod() - return ctx, r -} - -func (r *reporter) tagMethod() { - var err error - r.ctx, err = tag.New(r.ctx, tag.Insert(KeyMethod, r.on.MethodName())) - if err != nil { - panic(err) // or ignore? - } -} - -func (r *reporter) record() { - ms := float64(time.Since(r.start) / time.Millisecond) - stats.Record(r.ctx, r.on.LatencyMs().M(ms)) -} - -// Error records the result as an error. -func (r *reporter) Error() { - r.once.Do(func() { - r.result(ResultError) - }) -} - -// OK records the result as a success. -func (r *reporter) OK() { - r.once.Do(func() { - r.result(ResultOK) - }) -} - -func (r *reporter) result(v string) { - var err error - r.ctx, err = tag.New(r.ctx, tag.Insert(KeyResult, v)) - if err != nil { - panic(err) // or ignore? - } - r.record() -} diff --git a/v1/cloudevents/transport/amqp/doc.go b/v1/cloudevents/transport/amqp/doc.go deleted file mode 100644 index 42465d779..000000000 --- a/v1/cloudevents/transport/amqp/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -/* -Package amqp implements the CloudEvent transport implementation using amqp. -*/ -package amqp - -// TODO(alanconway) deprecated, use bindings/amqp directly diff --git a/v1/cloudevents/transport/amqp/encoding.go b/v1/cloudevents/transport/amqp/encoding.go deleted file mode 100644 index 8b13d2fdd..000000000 --- a/v1/cloudevents/transport/amqp/encoding.go +++ /dev/null @@ -1,66 +0,0 @@ -package amqp - -// Encoding to use for amqp transport. -type Encoding int32 - -const ( - // Default allows amqp transport implementation to pick. - Default Encoding = iota - // 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 - // BinaryV1 is Binary CloudEvents spec v1.0. - BinaryV1 - // StructuredV1 is Structured CloudEvents spec v1.0. - StructuredV1 - // Unknown is unknown. - Unknown -) - -// String pretty-prints the encoding as a string. -func (e Encoding) String() string { - switch e { - case Default: - return "Default Encoding " + e.Version() - - // Binary - case BinaryV02, BinaryV03, BinaryV1: - return "Binary Encoding " + e.Version() - - // Structured - case StructuredV02, StructuredV03, StructuredV1: - return "Structured Encoding " + e.Version() - - default: - return "Unknown Encoding" - } -} - -// Version pretty-prints the encoding version as a string. -func (e Encoding) Version() string { - switch e { - - // Version 0.2 - case Default: // <-- Move when a new default is wanted. - fallthrough - case StructuredV02: - return "v0.2" - - // Version 0.3 - case StructuredV03: - return "v0.3" - - // Version 1.0 - case StructuredV1: - return "v1.0" - - // Unknown - default: - return "Unknown" - } -} diff --git a/v1/cloudevents/transport/amqp/message.go b/v1/cloudevents/transport/amqp/message.go deleted file mode 100644 index 510eabacd..000000000 --- a/v1/cloudevents/transport/amqp/message.go +++ /dev/null @@ -1,46 +0,0 @@ -package amqp - -import ( - "encoding/json" - - "github.com/cloudevents/sdk-go/v1/cloudevents/transport" -) - -// type check that this transport message impl matches the contract -var _ transport.Message = (*Message)(nil) - -type Message struct { - ContentType string - ApplicationProperties map[string]interface{} - Body []byte -} - -// TODO: update this to work with AMQP -func (m Message) CloudEventsVersion() string { - // Check as Binary encoding first. - if m.ApplicationProperties != nil { - // Binary v0.2, v0.3: - if v := m.ApplicationProperties["cloudEvents:specversion"]; v != nil { - if s, ok := v.(string); ok { - return s - } - } - } - - // Now check as Structured encoding. - raw := make(map[string]json.RawMessage) - if err := json.Unmarshal(m.Body, &raw); err != nil { - return "" - } - - // structured v0.2, v0.3, v1.0 - if v, ok := raw["specversion"]; ok { - var version string - if err := json.Unmarshal(v, &version); err != nil { - return "" - } - return version - } - - return "" -} diff --git a/v1/cloudevents/transport/amqp/options.go b/v1/cloudevents/transport/amqp/options.go deleted file mode 100644 index 21b9b741f..000000000 --- a/v1/cloudevents/transport/amqp/options.go +++ /dev/null @@ -1,51 +0,0 @@ -package amqp - -import "pack.ag/amqp" - -// Option is the function signature required to be considered an amqp.Option. -type Option func(*Transport) error - -// WithEncoding sets the encoding for amqp transport. -func WithEncoding(encoding Encoding) Option { - return func(t *Transport) error { - t.Encoding = encoding - return nil - } -} - -// WithConnOpt sets a connection option for amqp -func WithConnOpt(opt amqp.ConnOption) Option { - return func(t *Transport) error { - t.connOpts = append(t.connOpts, opt) - return nil - } -} - -// WithConnSASLPlain sets SASLPlain connection option for amqp -func WithConnSASLPlain(username, password string) Option { - return WithConnOpt(amqp.ConnSASLPlain(username, password)) -} - -// WithSessionOpt sets a session option for amqp -func WithSessionOpt(opt amqp.SessionOption) Option { - return func(t *Transport) error { - t.sessionOpts = append(t.sessionOpts, opt) - return nil - } -} - -// WithSenderLinkOption sets a link option for amqp -func WithSenderLinkOption(opt amqp.LinkOption) Option { - return func(t *Transport) error { - t.senderLinkOpts = append(t.senderLinkOpts, opt) - return nil - } -} - -// WithReceiverLinkOption sets a link option for amqp -func WithReceiverLinkOption(opt amqp.LinkOption) Option { - return func(t *Transport) error { - t.receiverLinkOpts = append(t.receiverLinkOpts, opt) - return nil - } -} diff --git a/v1/cloudevents/transport/amqp/transport.go b/v1/cloudevents/transport/amqp/transport.go deleted file mode 100644 index cd263a31b..000000000 --- a/v1/cloudevents/transport/amqp/transport.go +++ /dev/null @@ -1,165 +0,0 @@ -package amqp - -import ( - "context" - - "pack.ag/amqp" - - "github.com/cloudevents/sdk-go/v1/binding" - "github.com/cloudevents/sdk-go/v1/binding/spec" - "github.com/cloudevents/sdk-go/v1/binding/transcoder" - bindings_amqp "github.com/cloudevents/sdk-go/v1/bindings/amqp" - cecontext "github.com/cloudevents/sdk-go/v1/cloudevents/context" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport" -) - -// Transport adheres to transport.Transport. -var _ transport.Transport = (*Transport)(nil) - -const ( - // TransportName is the name of this transport. - TransportName = "AMQP" -) - -type Transport struct { - binding.BindingTransport - connOpts []amqp.ConnOption - sessionOpts []amqp.SessionOption - senderLinkOpts []amqp.LinkOption - receiverLinkOpts []amqp.LinkOption - - // Encoding - Encoding Encoding - - // AMQP - Client *amqp.Client - Session *amqp.Session - Sender *amqp.Sender - Node string - - // Receiver - Receiver transport.Receiver - // Converter is invoked if the incoming transport receives an undecodable - // message. - Converter transport.Converter -} - -// New creates a new amqp transport. -func New(server, queue string, opts ...Option) (*Transport, error) { - t := &Transport{ - Node: queue, - connOpts: []amqp.ConnOption(nil), - sessionOpts: []amqp.SessionOption(nil), - senderLinkOpts: []amqp.LinkOption(nil), - receiverLinkOpts: []amqp.LinkOption(nil), - } - if err := t.applyOptions(opts...); err != nil { - return nil, err - } - - client, err := amqp.Dial(server, t.connOpts...) - if err != nil { - return nil, err - } - t.Client = client - - // Open a session - session, err := client.NewSession(t.sessionOpts...) - if err != nil { - _ = client.Close() - return nil, err - } - t.Session = session - - t.senderLinkOpts = append(t.senderLinkOpts, amqp.LinkTargetAddress(queue)) - - // Create a sender - sender, err := session.NewSender(t.senderLinkOpts...) - if err != nil { - _ = client.Close() - _ = session.Close(context.Background()) - return nil, err - } - // TODO: in the future we might have more than one sender. - t.BindingTransport.Sender, t.BindingTransport.SenderContextDecorators = t.applyEncoding(sender) - return t, nil -} - -func (t *Transport) applyEncoding(amqpSender *amqp.Sender) (binding.Sender, []func(context.Context) context.Context) { - switch t.Encoding { - case BinaryV02: - return bindings_amqp.NewSender( - amqpSender, - bindings_amqp.WithTranscoder(transcoder.Version(spec.V02)), - ), []func(context.Context) context.Context{binding.WithForceBinary} - case BinaryV03: - return bindings_amqp.NewSender( - amqpSender, - bindings_amqp.WithTranscoder(transcoder.Version(spec.V03)), - ), []func(context.Context) context.Context{binding.WithForceBinary} - case BinaryV1: - return bindings_amqp.NewSender( - amqpSender, - bindings_amqp.WithTranscoder(transcoder.Version(spec.V1)), - ), []func(context.Context) context.Context{binding.WithForceBinary} - case StructuredV02: - return bindings_amqp.NewSender( - amqpSender, - bindings_amqp.WithTranscoder(transcoder.Version(spec.V02)), - ), []func(context.Context) context.Context{binding.WithForceStructured} - case StructuredV03: - return bindings_amqp.NewSender( - amqpSender, - bindings_amqp.WithTranscoder(transcoder.Version(spec.V03)), - ), []func(context.Context) context.Context{binding.WithForceStructured} - case StructuredV1: - return bindings_amqp.NewSender( - amqpSender, - bindings_amqp.WithTranscoder(transcoder.Version(spec.V1)), - ), []func(context.Context) context.Context{binding.WithForceStructured} - } - return bindings_amqp.NewSender(amqpSender), []func(context.Context) context.Context{} -} - -func (t *Transport) applyOptions(opts ...Option) error { - for _, fn := range opts { - if err := fn(t); err != nil { - return err - } - } - return nil -} - -// 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 { - logger := cecontext.LoggerFrom(ctx) - logger.Info("StartReceiver on ", t.Node) - - t.receiverLinkOpts = append(t.receiverLinkOpts, amqp.LinkSourceAddress(t.Node)) - receiver, err := t.Session.NewReceiver(t.receiverLinkOpts...) - if err != nil { - return err - } - t.BindingTransport.Receiver = &bindings_amqp.Receiver{AMQP: receiver} - return t.BindingTransport.StartReceiver(ctx) -} - -// HasTracePropagation implements Transport.HasTracePropagation -func (t *Transport) HasTracePropagation() bool { - return false -} - -func (t *Transport) Close() error { - return t.Client.Close() -} diff --git a/v1/cloudevents/transport/amqp/transport_test.go b/v1/cloudevents/transport/amqp/transport_test.go deleted file mode 100644 index 62d21ddcc..000000000 --- a/v1/cloudevents/transport/amqp/transport_test.go +++ /dev/null @@ -1,109 +0,0 @@ -// +build amqp - -package amqp_test - -import ( - "context" - "net/url" - "os" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/cloudevents/sdk-go/v1/binding/spec" - "github.com/cloudevents/sdk-go/v1/binding/test" - ce "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport/amqp" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -// Requires an external AMQP broker or router, skip if not available. -// The env variable TEST_AMQP_URL provides the URL, default is "/test" -// -// One option is http://qpid.apache.org/components/dispatch-router/indext.html. -// It can be installed from source or from RPMs, see https://qpid.apache.org/packages.html -// Run `qdrouterd` and the tests will work with no further config. -func testTransport(t *testing.T, opts ...amqp.Option) *amqp.Transport { - t.Helper() - addr := "test" - s := os.Getenv("TEST_AMQP_URL") - if u, err := url.Parse(s); err == nil && u.Path != "" { - addr = u.Path - } - transport, err := amqp.New(s, addr, opts...) - if err != nil { - t.Skipf("ampq.New(%#v): %v", s, err) - } - return transport -} - -type tester struct { - s, r transport.Transport - got chan interface{} // ce.Event or error -} - -func (t *tester) Receive(_ context.Context, e ce.Event, _ *ce.EventResponse) error { - t.got <- e - return nil -} - -func (t *tester) Close() { - _ = t.s.(*amqp.Transport).Close() - _ = t.r.(*amqp.Transport).Close() -} - -func newTester(t *testing.T, sendOpts, recvOpts []amqp.Option) *tester { - t.Helper() - tester := &tester{ - s: testTransport(t, sendOpts...), - r: testTransport(t, recvOpts...), - got: make(chan interface{}), - } - got := make(chan interface{}, 100) - go func() { - defer func() { close(got) }() - tester.r.SetReceiver(tester) - err := tester.r.StartReceiver(context.Background()) - if err != nil { - got <- err - } - }() - return tester -} - -func exurl(e ce.Event) ce.Event { - // Flatten exurl to string, AMQP doesn't preserve the URL type. - // It should preserve other attribute types. - if s, _ := types.Format(e.Extensions()["exurl"]); s != "" { - e.SetExtension("exurl", s) - } - return e -} - -func TestSendReceive(t *testing.T) { - ctx := context.Background() - tester := newTester(t, nil, nil) - defer tester.Close() - test.EachEvent(t, test.Events(), func(t *testing.T, e ce.Event) { - _, _, err := tester.s.Send(ctx, e) - require.NoError(t, err) - got := <-tester.got - test.AssertEventEquals(t, exurl(e), got.(ce.Event)) - }) -} - -func TestWithEncoding(t *testing.T) { - ctx := context.Background() - tester := newTester(t, []amqp.Option{amqp.WithEncoding(amqp.StructuredV03)}, nil) - defer tester.Close() - // FIXME(alanconway) problem with JSON round-tripping extensions - events := test.NoExtensions(test.Events()) - test.EachEvent(t, events, func(t *testing.T, e ce.Event) { - _, _, err := tester.s.Send(ctx, e) - require.NoError(t, err) - got := <-tester.got - e.Context = spec.V03.Convert(e.Context) - test.AssertEventEquals(t, exurl(e), got.(ce.Event)) - }) -} diff --git a/v1/cloudevents/transport/codec.go b/v1/cloudevents/transport/codec.go deleted file mode 100644 index 97d83701b..000000000 --- a/v1/cloudevents/transport/codec.go +++ /dev/null @@ -1,35 +0,0 @@ -package transport - -import ( - "context" - "fmt" - - "github.com/cloudevents/sdk-go/v1/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) -} diff --git a/v1/cloudevents/transport/codec_test.go b/v1/cloudevents/transport/codec_test.go deleted file mode 100644 index fe81d83d3..000000000 --- a/v1/cloudevents/transport/codec_test.go +++ /dev/null @@ -1,9 +0,0 @@ -package transport_test - -import ( - "testing" -) - -func TestCodec(t *testing.T) { - // TODO: add a test. This makes coverage count this dir. -} diff --git a/v1/cloudevents/transport/doc.go b/v1/cloudevents/transport/doc.go deleted file mode 100644 index c2cbadde0..000000000 --- a/v1/cloudevents/transport/doc.go +++ /dev/null @@ -1,12 +0,0 @@ -/* - -Package transport defines interfaces to decouple the client package -from transport implementations. - -Most event sender and receiver applications should not use this -package, they should use the client package. This package is for -infrastructure developers implementing new transports, or intermediary -components like importers, channels or brokers. - -*/ -package transport diff --git a/v1/cloudevents/transport/error.go b/v1/cloudevents/transport/error.go deleted file mode 100644 index bb4e8ec9f..000000000 --- a/v1/cloudevents/transport/error.go +++ /dev/null @@ -1,37 +0,0 @@ -package transport - -import "fmt" - -// ErrTransportMessageConversion is an error produced when the transport -// message can not be converted. -type ErrTransportMessageConversion struct { - fatal bool - handled bool - transport string - message string -} - -// NewErrMessageEncodingUnknown makes a new ErrMessageEncodingUnknown. -func NewErrTransportMessageConversion(transport, message string, handled, fatal bool) *ErrTransportMessageConversion { - return &ErrTransportMessageConversion{ - transport: transport, - message: message, - handled: handled, - fatal: fatal, - } -} - -// IsFatal reports if this error should be considered fatal. -func (e *ErrTransportMessageConversion) IsFatal() bool { - return e.fatal -} - -// Handled reports if this error should be considered accepted and no further action. -func (e *ErrTransportMessageConversion) Handled() bool { - return e.handled -} - -// Error implements error.Error -func (e *ErrTransportMessageConversion) Error() string { - return fmt.Sprintf("transport %s failed to convert message: %s", e.transport, e.message) -} diff --git a/v1/cloudevents/transport/http/codec.go b/v1/cloudevents/transport/http/codec.go deleted file mode 100644 index f7eca8cce..000000000 --- a/v1/cloudevents/transport/http/codec.go +++ /dev/null @@ -1,152 +0,0 @@ -package http - -import ( - "context" - "errors" - "fmt" - "sync" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - cecontext "github.com/cloudevents/sdk-go/v1/cloudevents/context" - "github.com/cloudevents/sdk-go/v1/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 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, Default: - 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.AsV1() - 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 -} diff --git a/v1/cloudevents/transport/http/codec_structured.go b/v1/cloudevents/transport/http/codec_structured.go deleted file mode 100644 index 097e9361e..000000000 --- a/v1/cloudevents/transport/http/codec_structured.go +++ /dev/null @@ -1,44 +0,0 @@ -package http - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/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 -} diff --git a/v1/cloudevents/transport/http/codec_test.go b/v1/cloudevents/transport/http/codec_test.go deleted file mode 100644 index 86dca3c54..000000000 --- a/v1/cloudevents/transport/http/codec_test.go +++ /dev/null @@ -1,976 +0,0 @@ -package http_test - -import ( - "context" - "encoding/json" - "fmt" - nethttp "net/http" - "net/url" - "testing" - "time" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport/http" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/google/go-cmp/cmp" -) - -func strptr(s string) *string { - return &s -} - -func TestDefaultBinaryEncodingSelectionStrategy(t *testing.T) { - testCases := map[string]struct { - event cloudevents.Event - want http.Encoding - }{ - "default, unknown version": { - event: cloudevents.Event{ - Context: &cloudevents.EventContextV01{ - CloudEventsVersion: "unknown", - }, - }, - want: http.Default, - }, - "v0.1": { - event: cloudevents.Event{ - Context: cloudevents.EventContextV01{}.AsV01(), - }, - want: http.BinaryV01, - }, - "v0.2": { - event: cloudevents.Event{ - Context: cloudevents.EventContextV02{}.AsV02(), - }, - want: http.BinaryV02, - }, - "v0.3": { - event: cloudevents.Event{ - Context: cloudevents.EventContextV03{}.AsV03(), - }, - want: http.BinaryV03, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got := http.DefaultBinaryEncodingSelectionStrategy(context.TODO(), tc.event) - - if got != tc.want { - t.Errorf("unexpected selection want: %s, got: %s", tc.want, got) - } - }) - } -} - -func TestDefaultStructuredEncodingSelectionStrategy(t *testing.T) { - testCases := map[string]struct { - event cloudevents.Event - want http.Encoding - }{ - "default, unknown version": { - event: cloudevents.Event{ - Context: &cloudevents.EventContextV01{ - CloudEventsVersion: "unknown", - }, - }, - want: http.Default, - }, - "v0.1": { - event: cloudevents.Event{ - Context: cloudevents.EventContextV01{}.AsV01(), - }, - want: http.StructuredV01, - }, - "v0.2": { - event: cloudevents.Event{ - Context: cloudevents.EventContextV02{}.AsV02(), - }, - want: http.StructuredV02, - }, - "v0.3": { - event: cloudevents.Event{ - Context: cloudevents.EventContextV03{}.AsV03(), - }, - want: http.StructuredV03, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got := http.DefaultStructuredEncodingSelectionStrategy(context.TODO(), tc.event) - - if got != tc.want { - t.Errorf("unexpected selection want: %s, got: %s", tc.want, got) - } - }) - } -} - -func TestCodecEncode(t *testing.T) { - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - testCases := map[string]struct { - codec *http.Codec - event cloudevents.Event - want *http.Message - wantErr error - }{ - "default v0.1 binary": { - codec: &http.Codec{ - DefaultEncodingSelectionFn: http.DefaultBinaryEncodingSelectionStrategy, - }, - event: cloudevents.Event{ - Context: cloudevents.EventContextV01{ - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - }.AsV01(), - }, - want: &http.Message{ - Header: map[string][]string{ - "CE-CloudEventsVersion": {"0.1"}, - "CE-EventID": {"ABC-123"}, - "CE-EventType": {"com.example.test"}, - "CE-Source": {"http://example.com/source"}, - }, - }, - }, - "default v0.1 structured": { - codec: &http.Codec{ - DefaultEncodingSelectionFn: http.DefaultStructuredEncodingSelectionStrategy, - }, - event: cloudevents.Event{ - Context: cloudevents.EventContextV01{ - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - }.AsV01(), - }, - want: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: func() []byte { - body := map[string]interface{}{ - "cloudEventsVersion": "0.1", - "eventID": "ABC-123", - "eventType": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - "default v0.2 binary": { - codec: &http.Codec{ - DefaultEncodingSelectionFn: http.DefaultBinaryEncodingSelectionStrategy, - }, - event: cloudevents.Event{ - Context: cloudevents.EventContextV02{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV02(), - }, - want: &http.Message{ - Header: map[string][]string{ - "Ce-Specversion": {"0.2"}, - "Ce-Id": {"ABC-123"}, - "Ce-Type": {"com.example.test"}, - "Ce-Source": {"http://example.com/source"}, - }, - }, - }, - "default v0.2 structured": { - codec: &http.Codec{ - DefaultEncodingSelectionFn: http.DefaultStructuredEncodingSelectionStrategy, - }, - event: cloudevents.Event{ - Context: cloudevents.EventContextV02{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV02(), - }, - want: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "0.2", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - "default v0.3 binary": { - codec: &http.Codec{ - DefaultEncodingSelectionFn: http.DefaultBinaryEncodingSelectionStrategy, - }, - event: cloudevents.Event{ - Context: cloudevents.EventContextV03{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV03(), - }, - want: &http.Message{ - Header: map[string][]string{ - "Ce-Specversion": {"0.3"}, - "Ce-Id": {"ABC-123"}, - "Ce-Type": {"com.example.test"}, - "Ce-Source": {"http://example.com/source"}, - }, - }, - }, - "default v0.3 structured": { - codec: &http.Codec{ - DefaultEncodingSelectionFn: http.DefaultStructuredEncodingSelectionStrategy, - }, - event: cloudevents.Event{ - Context: cloudevents.EventContextV03{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV03(), - }, - want: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "0.3", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - "simple v0.1 binary": { - codec: &http.Codec{Encoding: http.BinaryV01}, - event: cloudevents.Event{ - Context: &cloudevents.EventContextV01{ - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - }, - }, - want: &http.Message{ - Header: map[string][]string{ - "CE-CloudEventsVersion": {"0.1"}, - "CE-EventID": {"ABC-123"}, - "CE-EventType": {"com.example.test"}, - "CE-Source": {"http://example.com/source"}, - }, - }, - }, - "simple v0.1 structured": { - codec: &http.Codec{Encoding: http.StructuredV01}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV01{ - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - }.AsV01(), - }, - want: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: func() []byte { - body := map[string]interface{}{ - "cloudEventsVersion": "0.1", - "eventID": "ABC-123", - "eventType": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - "simple v0.2 binary": { - codec: &http.Codec{Encoding: http.BinaryV02}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV02{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV02(), - }, - want: &http.Message{ - Header: map[string][]string{ - "Ce-Specversion": {"0.2"}, - "Ce-Id": {"ABC-123"}, - "Ce-Type": {"com.example.test"}, - "Ce-Source": {"http://example.com/source"}, - }, - }, - }, - "simple v0.2 structured": { - codec: &http.Codec{Encoding: http.StructuredV02}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV02{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV02(), - }, - want: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "0.2", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - "simple v0.3 binary": { - codec: &http.Codec{Encoding: http.BinaryV03}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV03{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV03(), - }, - want: &http.Message{ - Header: map[string][]string{ - "Ce-Specversion": {"0.3"}, - "Ce-Id": {"ABC-123"}, - "Ce-Type": {"com.example.test"}, - "Ce-Source": {"http://example.com/source"}, - }, - }, - }, - "simple v0.3 structured": { - codec: &http.Codec{Encoding: http.StructuredV03}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV03{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV03(), - }, - want: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "0.3", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, err := tc.codec.Encode(context.TODO(), tc.event) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - - if msg, ok := got.(*http.Message); ok { - // It is hard to read the byte dump - want := string(tc.want.Body) - got := string(msg.Body) - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - return - } - } - t.Errorf("unexpected message (-want, +got) = %v", diff) - } - }) - } -} - -// A cmp.Transformer to normalize case of http.Header map keys. -var normalizeHeaders = cmp.Transformer("NormalizeHeaders", - func(in nethttp.Header) nethttp.Header { - out := nethttp.Header{} - for k, v := range in { - out[nethttp.CanonicalHeaderKey(k)] = v - } - return out - }) - -func TestCodecDecode(t *testing.T) { - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - testCases := map[string]struct { - codec *http.Codec - msg *http.Message - want *cloudevents.Event - wantErr error - }{ - "simple v0.1 binary": { - codec: &http.Codec{Encoding: http.BinaryV01}, - msg: &http.Message{ - Header: map[string][]string{ - "CE-CloudEventsVersion": {"0.1"}, - "CE-EventID": {"ABC-123"}, - "CE-EventType": {"com.example.test"}, - "CE-Source": {"http://example.com/source"}, - "Content-Type": {"application/json"}, - }, - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV01{ - CloudEventsVersion: cloudevents.CloudEventsVersionV01, - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - ContentType: cloudevents.StringOfApplicationJSON(), - }, - }, - }, - "simple v0.1 structured": { - codec: &http.Codec{Encoding: http.StructuredV01}, - msg: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: func() []byte { - body := map[string]interface{}{ - "cloudEventsVersion": "0.1", - "eventID": "ABC-123", - "eventType": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV01{ - CloudEventsVersion: cloudevents.CloudEventsVersionV01, - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - }, - }, - }, - "simple v0.2 binary": { - codec: &http.Codec{Encoding: http.BinaryV02}, - msg: &http.Message{ - Header: map[string][]string{ - "Ce-Specversion": {"0.2"}, - "Ce-Id": {"ABC-123"}, - "Ce-Type": {"com.example.test"}, - "Ce-Source": {"http://example.com/source"}, - "Content-Type": {"application/json"}, - }, - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV02{ - SpecVersion: cloudevents.CloudEventsVersionV02, - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - ContentType: cloudevents.StringOfApplicationJSON(), - }, - }, - }, - "simple v0.2 structured": { - codec: &http.Codec{Encoding: http.StructuredV02}, - msg: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "0.2", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV02{ - SpecVersion: cloudevents.CloudEventsVersionV02, - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }, - }, - }, - - "simple v0.3 binary": { - codec: &http.Codec{Encoding: http.BinaryV03}, - msg: &http.Message{ - Header: map[string][]string{ - "Ce-Specversion": {"0.3"}, - "Ce-Id": {"ABC-123"}, - "Ce-Type": {"com.example.test"}, - "Ce-Source": {"http://example.com/source"}, - "Content-Type": {"application/json"}, - }, - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV03{ - SpecVersion: cloudevents.CloudEventsVersionV03, - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - DataContentType: cloudevents.StringOfApplicationJSON(), - }, - }, - }, - "simple v0.3 structured": { - codec: &http.Codec{Encoding: http.StructuredV03}, - msg: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "0.3", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV03{ - SpecVersion: cloudevents.CloudEventsVersionV03, - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }, - }, - }, - - // Conversion tests. - - "simple v0.1 binary -> v0.2 binary": { - codec: &http.Codec{Encoding: http.BinaryV02}, - msg: &http.Message{ - Header: map[string][]string{ - "CE-CloudEventsVersion": {"0.1"}, - "CE-EventID": {"ABC-123"}, - "CE-EventType": {"com.example.test"}, - "CE-Source": {"http://example.com/source"}, - "Content-Type": {"application/json"}, - }, - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV02{ - SpecVersion: cloudevents.CloudEventsVersionV02, - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - ContentType: cloudevents.StringOfApplicationJSON(), - }, - }, - }, - "simple v0.1 structured -> v0.2 structured": { - codec: &http.Codec{Encoding: http.StructuredV02}, - msg: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: func() []byte { - body := map[string]interface{}{ - "cloudEventsVersion": "0.1", - "eventID": "ABC-123", - "eventType": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV02{ - SpecVersion: cloudevents.CloudEventsVersionV02, - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }, - }, - }, - "simple v0.2 binary -> v0.1 binary": { - codec: &http.Codec{Encoding: http.BinaryV01}, - msg: &http.Message{ - Header: map[string][]string{ - "Ce-Specversion": {"0.2"}, - "Ce-Id": {"ABC-123"}, - "Ce-Type": {"com.example.test"}, - "Ce-Source": {"http://example.com/source"}, - "Content-Type": {"application/json"}, - }, - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV01{ - CloudEventsVersion: cloudevents.CloudEventsVersionV01, - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - ContentType: cloudevents.StringOfApplicationJSON(), - }, - }, - }, - "simple v0.2 structured -> v0.1 structured": { - codec: &http.Codec{Encoding: http.StructuredV01}, - msg: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "0.2", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV01{ - CloudEventsVersion: cloudevents.CloudEventsVersionV01, - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - }, - }, - }, - // TODO:: add the v0.3 conversion tests. Might want to think of a new way to do this. - // The tests are getting very long... - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, err := tc.codec.Decode(context.TODO(), tc.msg) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected event (-want, +got) = %v", diff) - } - // Round trip thru a http.Request - var req nethttp.Request - tc.msg.ToRequest(&req) - gotm, err := http.NewMessage(req.Header, req.Body) - if err != nil { - t.Error(err) - } - if diff := cmp.Diff(tc.msg, gotm, normalizeHeaders); diff != "" { - t.Errorf("unexpected message (-want, +got) = %v", diff) - } - }) - } -} - -type DataExample struct { - AnInt int `json:"a,omitempty"` - AString string `json:"b,omitempty"` - AnArray []string `json:"c,omitempty"` - ATime *time.Time `json:"e,omitempty"` -} - -func TestCodecRoundTrip(t *testing.T) { - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - for _, encoding := range []http.Encoding{http.BinaryV01, http.BinaryV02, http.StructuredV01, http.StructuredV02} { - - testCases := map[string]struct { - codec *http.Codec - event cloudevents.Event - want cloudevents.Event - wantErr error - }{ - "simple data v0.1": { - codec: &http.Codec{Encoding: encoding}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV01{ - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - }.AsV01(), - Data: map[string]string{ - "a": "apple", - "b": "banana", - }, - }, - want: cloudevents.Event{ - Context: cloudevents.EventContextV01{ - CloudEventsVersion: cloudevents.CloudEventsVersionV01, - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - }.AsV01(), - Data: map[string]interface{}{ - "a": "apple", - "b": "banana", - }, - DataEncoded: true, - }, - }, - "struct data v0.1": { - codec: &http.Codec{Encoding: encoding}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV01{ - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - }.AsV01(), - Data: DataExample{ - AnInt: 42, - AString: "testing", - }, - }, - want: cloudevents.Event{ - Context: cloudevents.EventContextV01{ - CloudEventsVersion: cloudevents.CloudEventsVersionV01, - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - }.AsV01(), - Data: &DataExample{ - AnInt: 42, - AString: "testing", - }, - DataEncoded: true, - }, - }, - // TODO: add tests for other versions. (note not really needed because these is tested internally too) - } - for n, tc := range testCases { - n = fmt.Sprintf("%s, %s", encoding, n) - t.Run(n, func(t *testing.T) { - - msg, err := tc.codec.Encode(context.TODO(), tc.event) - if err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - got, err := tc.codec.Decode(context.TODO(), msg) - if err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if tc.event.Data != nil { - // We have to be pretty tricky in the test to get the correct type. - data, _ := types.Allocate(tc.want.Data) - err := got.DataAs(&data) - if err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - got.Data = data - } - - if tc.wantErr != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - // fix the context back to v1 to test. - ctxv1 := got.Context.AsV01() - got.Context = ctxv1 - - if diff := cmp.Diff(tc.want, *got); diff != "" { - t.Errorf("unexpected event (-want, +got) = %v", diff) - } - }) - } - - } -} - -// Tests v0.1 -> X -> v0.1 -func TestCodecAsMiddleware(t *testing.T) { - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - for _, contentType := range []string{"application/json", "application/xml"} { - for _, encoding := range []http.Encoding{http.BinaryV01, http.BinaryV02, http.BinaryV03, http.StructuredV01, http.StructuredV02, http.StructuredV03} { - - testCases := map[string]struct { - codec *http.Codec - event cloudevents.Event - want cloudevents.Event - wantErr error - }{ - // TODO: this is commented out because xml does not support maps. - //"simple data": { - // codec: http.Codec{Encoding: encoding}, - // event: cloudevents.Event{ - // Context: &cloudevents.EventContextV01{ - // EventType: "com.example.test", - // Source: *source, - // EventID: "ABC-123", - // ContentType: contentType, - // }, - // Data: map[string]string{ - // "a": "apple", - // "b": "banana", - // }, - // }, - // want: cloudevents.Event{ - // Context: &cloudevents.EventContextV01{ - // CloudEventsVersion: cloudevents.CloudEventsVersionV01, - // EventType: "com.example.test", - // Source: *source, - // EventID: "ABC-123", - // ContentType: contentType, - // }, - // Data: map[string]interface{}{ - // "a": "apple", - // "b": "banana", - // }, - // }, - //}, - "struct data": { - codec: &http.Codec{Encoding: encoding}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV01{ - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - ContentType: strptr(contentType), - }.AsV01(), - Data: DataExample{ - AnInt: 42, - AString: "testing", - }, - }, - want: cloudevents.Event{ - Context: &cloudevents.EventContextV01{ - CloudEventsVersion: cloudevents.CloudEventsVersionV01, - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - ContentType: strptr(contentType), - }, - Data: &DataExample{ - AnInt: 42, - AString: "testing", - }, - DataEncoded: true, - }, - }, - } - for n, tc := range testCases { - n = fmt.Sprintf("%s[%s],%s", encoding, contentType, n) - t.Run(n, func(t *testing.T) { - - msg1, err := tc.codec.Encode(context.TODO(), tc.event) - if err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - midEvent, err := tc.codec.Decode(context.TODO(), msg1) - if err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - msg2, err := tc.codec.Encode(context.TODO(), *midEvent) - if err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - got, err := tc.codec.Decode(context.TODO(), msg2) - if err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if tc.event.Data != nil { - // We have to be pretty tricky in the test to get the correct type. - data, _ := types.Allocate(tc.want.Data) - err := got.DataAs(&data) - if err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - got.Data = data - } - - if tc.wantErr != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - // fix the context back to v1 to test. - ctxv1 := got.Context.AsV01() - got.Context = ctxv1 - - if diff := cmp.Diff(tc.want, *got); diff != "" { - t.Errorf("unexpected event (-want, +got) = %v", diff) - } - }) - } - } - } -} - -func toBytes(body map[string]interface{}) []byte { - b, err := json.Marshal(body) - if err != nil { - return []byte(fmt.Sprintf(`{"error":%q}`, err.Error())) - } - return b -} diff --git a/v1/cloudevents/transport/http/codec_v01.go b/v1/cloudevents/transport/http/codec_v01.go deleted file mode 100644 index 78da7b2fb..000000000 --- a/v1/cloudevents/transport/http/codec_v01.go +++ /dev/null @@ -1,232 +0,0 @@ -package http - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/textproto" - "strings" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - cecontext "github.com/cloudevents/sdk-go/v1/cloudevents/context" - "github.com/cloudevents/sdk-go/v1/cloudevents/observability" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport" - "github.com/cloudevents/sdk-go/v1/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 -} diff --git a/v1/cloudevents/transport/http/codec_v01_test.go b/v1/cloudevents/transport/http/codec_v01_test.go deleted file mode 100644 index aac2d5283..000000000 --- a/v1/cloudevents/transport/http/codec_v01_test.go +++ /dev/null @@ -1,410 +0,0 @@ -package http_test - -import ( - "context" - "net/url" - "testing" - "time" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport/http" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/google/go-cmp/cmp" -) - -func TestCodecV01_Encode(t *testing.T) { - now := types.Timestamp{Time: time.Now().UTC()} - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - schemaUrl, _ := url.Parse("http://example.com/schema") - schema := &types.URLRef{URL: *schemaUrl} - - testCases := map[string]struct { - codec http.CodecV01 - event cloudevents.Event - want *http.Message - wantErr error - }{ - "simple v0.1 default": { - codec: http.CodecV01{}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV01{ - CloudEventsVersion: "TestIfDefaulted", - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - }.AsV01(), - }, - want: &http.Message{ - Header: map[string][]string{ - "CE-CloudEventsVersion": {"0.1"}, - "CE-EventID": {"ABC-123"}, - "CE-EventType": {"com.example.test"}, - "CE-Source": {"http://example.com/source"}, - }, - }, - }, - "full v0.1 default": { - codec: http.CodecV01{}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV01{ - EventID: "ABC-123", - EventTime: &now, - EventType: "com.example.full", - EventTypeVersion: strptr("v1alpha1"), - SchemaURL: schema, - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }.AsV01(), - Data: map[string]interface{}{ - "hello": "world", - }, - }, - want: &http.Message{ - Header: map[string][]string{ - "CE-CloudEventsVersion": {"0.1"}, - "CE-EventID": {"ABC-123"}, - "CE-EventTime": {now.UTC().Format(time.RFC3339Nano)}, - "CE-EventType": {"com.example.full"}, - "CE-EventTypeVersion": {"v1alpha1"}, - "CE-Source": {"http://example.com/source"}, - "CE-DataSchema": {"http://example.com/schema"}, - "Content-Type": {"application/json"}, - "CE-X-Test": {`"extended"`}, - }, - Body: []byte(`{"hello":"world"}`), - }, - }, - "simple v0.1 binary": { - codec: http.CodecV01{DefaultEncoding: http.BinaryV01}, - event: cloudevents.Event{ - Context: &cloudevents.EventContextV01{ - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - }, - }, - want: &http.Message{ - Header: map[string][]string{ - "CE-CloudEventsVersion": {"0.1"}, - "CE-EventID": {"ABC-123"}, - "CE-EventType": {"com.example.test"}, - "CE-Source": {"http://example.com/source"}, - }, - }, - }, - "full v0.1 binary": { - codec: http.CodecV01{DefaultEncoding: http.BinaryV01}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV01{ - EventID: "ABC-123", - EventTime: &now, - EventType: "com.example.full", - EventTypeVersion: strptr("v1alpha1"), - SchemaURL: schema, - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }.AsV01(), - Data: map[string]interface{}{ - "hello": "world", - }, - }, - want: &http.Message{ - Header: map[string][]string{ - "CE-CloudEventsVersion": {"0.1"}, - "CE-EventID": {"ABC-123"}, - "CE-EventTime": {now.UTC().Format(time.RFC3339Nano)}, - "CE-EventType": {"com.example.full"}, - "CE-EventTypeVersion": {"v1alpha1"}, - "CE-Source": {"http://example.com/source"}, - "CE-DataSchema": {"http://example.com/schema"}, - "Content-Type": {"application/json"}, - "CE-X-Test": {`"extended"`}, - }, - Body: []byte(`{"hello":"world"}`), - }, - }, - "simple v0.1 structured": { - codec: http.CodecV01{DefaultEncoding: http.StructuredV01}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV01{ - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - }.AsV01(), - }, - want: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: func() []byte { - body := map[string]interface{}{ - "cloudEventsVersion": "0.1", - "eventID": "ABC-123", - "eventType": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - "full v0.1 structured": { - codec: http.CodecV01{DefaultEncoding: http.StructuredV01}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV01{ - EventID: "ABC-123", - EventTime: &now, - EventType: "com.example.full", - EventTypeVersion: strptr("v1alpha1"), - SchemaURL: schema, - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }.AsV01(), - Data: map[string]interface{}{ - "hello": "world", - }, - }, - want: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: func() []byte { - body := map[string]interface{}{ - "cloudEventsVersion": "0.1", - "contentType": "application/json", - "data": map[string]interface{}{ - "hello": "world", - }, - "eventID": "ABC-123", - "eventTime": now, - "eventType": "com.example.full", - "eventTypeVersion": "v1alpha1", - "extensions": map[string]interface{}{ - "test": "extended", - }, - "schemaURL": "http://example.com/schema", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, err := tc.codec.Encode(context.TODO(), tc.event) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - - if msg, ok := got.(*http.Message); ok { - // It is hard to read the byte dump - want := string(tc.want.Body) - got := string(msg.Body) - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("unexpected message body (-want, +got) = %v", diff) - return - } - } - t.Errorf("unexpected message (-want, +got) = %v", diff) - } - }) - } -} - -func TestCodecV01_Decode(t *testing.T) { - now := types.Timestamp{Time: time.Now()} - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - schemaUrl, _ := url.Parse("http://example.com/schema") - schema := &types.URLRef{URL: *schemaUrl} - - testCases := map[string]struct { - codec http.CodecV01 - msg *http.Message - want *cloudevents.Event - wantErr error - }{ - "simple v0.1 binary": { - codec: http.CodecV01{}, - msg: &http.Message{ - Header: map[string][]string{ - "CE-CloudEventsVersion": {"0.1"}, - "CE-EventID": {"ABC-123"}, - "CE-EventType": {"com.example.test"}, - "CE-Source": {"http://example.com/source"}, - "Content-Type": {"application/json"}, - }, - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV01{ - CloudEventsVersion: cloudevents.CloudEventsVersionV01, - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - ContentType: cloudevents.StringOfApplicationJSON(), - }, - }, - }, - "full v0.1 binary": { - codec: http.CodecV01{}, - msg: &http.Message{ - Header: map[string][]string{ - "CE-CloudEventsVersion": {"0.1"}, - "CE-EventID": {"ABC-123"}, - "CE-EventTime": {now.UTC().Format(time.RFC3339Nano)}, - "CE-EventType": {"com.example.full"}, - "CE-EventTypeVersion": {"v1alpha1"}, - "CE-Source": {"http://example.com/source"}, - "CE-DataSchema": {"http://example.com/schema"}, - "Content-Type": {"application/json"}, - "CE-X-Test": {`"extended"`}, - }, - Body: toBytes(map[string]interface{}{ - "hello": "world", - }), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV01{ - CloudEventsVersion: cloudevents.CloudEventsVersionV01, - EventID: "ABC-123", - EventTime: &now, - EventType: "com.example.full", - EventTypeVersion: strptr("v1alpha1"), - SchemaURL: schema, - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Extensions: map[string]interface{}{ - "Test": "extended", - }, - }, - Data: toBytes(map[string]interface{}{ - "hello": "world", - }), - DataEncoded: true, - }, - }, - "simple v0.1 structured": { - codec: http.CodecV01{}, - msg: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: toBytes(map[string]interface{}{ - "cloudEventsVersion": "0.1", - "eventID": "ABC-123", - "eventType": "com.example.test", - "source": "http://example.com/source", - }), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV01{ - CloudEventsVersion: cloudevents.CloudEventsVersionV01, - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - }, - }, - }, - "full v0.1 structured": { - codec: http.CodecV01{}, - msg: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: toBytes(map[string]interface{}{ - "cloudEventsVersion": "0.1", - "contentType": "application/json", - "data": map[string]interface{}{ - "hello": "world", - }, - "eventID": "ABC-123", - "eventTime": now, - "eventType": "com.example.full", - "eventTypeVersion": "v1alpha1", - "extensions": map[string]interface{}{ - "test": "extended", - }, - "schemaURL": "http://example.com/schema", - "source": "http://example.com/source", - }), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV01{ - CloudEventsVersion: cloudevents.CloudEventsVersionV01, - EventID: "ABC-123", - EventTime: &now, - EventType: "com.example.full", - EventTypeVersion: strptr("v1alpha1"), - SchemaURL: schema, - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }, - Data: toBytes(map[string]interface{}{ - "hello": "world", - }), - DataEncoded: true, - }, - }, - "simple v0.1 binary with short header": { - codec: http.CodecV01{}, - msg: &http.Message{ - Header: map[string][]string{ - "CE-CloudEventsVersion": {"0.1"}, - "CE-EventID": {"ABC-123"}, - "CE-EventType": {"com.example.test"}, - "CE-Source": {"http://example.com/source"}, - "Content-Type": {"application/json"}, - "X": {"Notice how short the header's name is"}, - }, - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV01{ - CloudEventsVersion: cloudevents.CloudEventsVersionV01, - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - ContentType: cloudevents.StringOfApplicationJSON(), - }, - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, err := tc.codec.Decode(context.TODO(), tc.msg) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected event (-want, +got) = %v", diff) - } - }) - } -} diff --git a/v1/cloudevents/transport/http/codec_v02.go b/v1/cloudevents/transport/http/codec_v02.go deleted file mode 100644 index 8f4328a76..000000000 --- a/v1/cloudevents/transport/http/codec_v02.go +++ /dev/null @@ -1,261 +0,0 @@ -package http - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/textproto" - "strings" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - cecontext "github.com/cloudevents/sdk-go/v1/cloudevents/context" - "github.com/cloudevents/sdk-go/v1/cloudevents/observability" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport" - "github.com/cloudevents/sdk-go/v1/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 -} diff --git a/v1/cloudevents/transport/http/codec_v02_test.go b/v1/cloudevents/transport/http/codec_v02_test.go deleted file mode 100644 index 56f1eff3e..000000000 --- a/v1/cloudevents/transport/http/codec_v02_test.go +++ /dev/null @@ -1,417 +0,0 @@ -package http_test - -import ( - "context" - "net/url" - "testing" - "time" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport/http" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/google/go-cmp/cmp" -) - -func TestCodecV02_Encode(t *testing.T) { - now := types.Timestamp{Time: time.Now().UTC()} - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - schemaUrl, _ := url.Parse("http://example.com/schema") - schema := &types.URLRef{URL: *schemaUrl} - - testCases := map[string]struct { - codec http.CodecV02 - event cloudevents.Event - want *http.Message - wantErr error - }{ - "simple v0.2 default": { - codec: http.CodecV02{}, - event: cloudevents.Event{ - Context: &cloudevents.EventContextV02{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }, - }, - want: &http.Message{ - Header: map[string][]string{ - "Ce-Specversion": {"0.2"}, - "Ce-Id": {"ABC-123"}, - "Ce-Type": {"com.example.test"}, - "Ce-Source": {"http://example.com/source"}, - }, - }, - }, - "full v0.2 default": { - codec: http.CodecV02{}, - event: cloudevents.Event{ - Context: &cloudevents.EventContextV02{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }, - Data: map[string]interface{}{ - "hello": "world", - }, - }, - want: &http.Message{ - Header: map[string][]string{ - "Ce-Specversion": {"0.2"}, - "Ce-Id": {"ABC-123"}, - "Ce-Time": {now.Format(time.RFC3339Nano)}, - "Ce-Type": {"com.example.test"}, - "Ce-Source": {"http://example.com/source"}, - "Ce-Schemaurl": {"http://example.com/schema"}, - "Ce-Test": {`"extended"`}, - "Content-Type": {"application/json"}, - }, - Body: []byte(`{"hello":"world"}`), - }, - }, - "simple v0.2 binary": { - codec: http.CodecV02{DefaultEncoding: http.BinaryV02}, - event: cloudevents.Event{ - Context: &cloudevents.EventContextV02{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }, - }, - want: &http.Message{ - Header: map[string][]string{ - "Ce-Specversion": {"0.2"}, - "Ce-Id": {"ABC-123"}, - "Ce-Type": {"com.example.test"}, - "Ce-Source": {"http://example.com/source"}, - }, - }, - }, - "full v0.2 binary": { - codec: http.CodecV02{DefaultEncoding: http.BinaryV02}, - event: cloudevents.Event{ - Context: &cloudevents.EventContextV02{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - "asmap": map[string]interface{}{ - "a": "apple", - "b": "banana", - "c": map[string]interface{}{ - "d": "dog", - "e": "eel", - }, - }, - }, - }, - Data: map[string]interface{}{ - "hello": "world", - }, - }, - want: &http.Message{ - Header: map[string][]string{ - "Ce-Specversion": {"0.2"}, - "Ce-Id": {"ABC-123"}, - "Ce-Time": {now.Format(time.RFC3339Nano)}, - "Ce-Type": {"com.example.test"}, - "Ce-Source": {"http://example.com/source"}, - "Ce-Schemaurl": {"http://example.com/schema"}, - "Ce-Test": {`"extended"`}, - "Ce-Asmap-A": {`"apple"`}, - "Ce-Asmap-B": {`"banana"`}, - "Ce-Asmap-C": {`{"d":"dog","e":"eel"}`}, - "Content-Type": {"application/json"}, - }, - Body: []byte(`{"hello":"world"}`), - }, - }, - "simple v0.2 structured": { - codec: http.CodecV02{DefaultEncoding: http.StructuredV02}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV02{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV02(), - }, - want: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "0.2", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - "full v0.2 structured": { - codec: http.CodecV02{DefaultEncoding: http.StructuredV02}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV02{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }.AsV02(), - Data: map[string]interface{}{ - "hello": "world", - }, - }, - want: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "0.2", - "contenttype": "application/json", - "data": map[string]interface{}{ - "hello": "world", - }, - "id": "ABC-123", - "time": now, - "type": "com.example.test", - "test": "extended", - "schemaurl": "http://example.com/schema", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, err := tc.codec.Encode(context.TODO(), tc.event) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - - if msg, ok := got.(*http.Message); ok { - // It is hard to read the byte dump - want := string(tc.want.Body) - got := string(msg.Body) - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("unexpected message body (-want, +got) = %v", diff) - return - } - } - - t.Errorf("unexpected message (-want, +got) = %v", diff) - } - }) - } -} - -// TODO: figure out extensions for v0.2 - -func TestCodecV02_Decode(t *testing.T) { - now := types.Timestamp{Time: time.Now()} - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - schemaUrl, _ := url.Parse("http://example.com/schema") - schema := &types.URLRef{URL: *schemaUrl} - - testCases := map[string]struct { - codec http.CodecV02 - msg *http.Message - want *cloudevents.Event - wantErr error - }{ - "simple v0.2 binary": { - codec: http.CodecV02{}, - msg: &http.Message{ - Header: map[string][]string{ - "ce-specversion": {"0.2"}, - "ce-id": {"ABC-123"}, - "ce-type": {"com.example.test"}, - "ce-source": {"http://example.com/source"}, - "Content-Type": {"application/json"}, - }, - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV02{ - SpecVersion: cloudevents.CloudEventsVersionV02, - ContentType: cloudevents.StringOfApplicationJSON(), - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }, - }, - }, - "full v0.2 binary": { - codec: http.CodecV02{}, - msg: &http.Message{ - Header: map[string][]string{ - "ce-specversion": {"0.2"}, - "ce-id": {"ABC-123"}, - "ce-time": {now.Format(time.RFC3339Nano)}, - "ce-type": {"com.example.test"}, - "ce-source": {"http://example.com/source"}, - "ce-schemaurl": {"http://example.com/schema"}, - "ce-test": {`"extended"`}, - "ce-asmap-a": {`"apple"`}, - "ce-asmap-b": {`"banana"`}, - "ce-asmap-c": {`{"d":"dog","e":"eel"}`}, - "Content-Type": {"application/json"}, - }, - Body: toBytes(map[string]interface{}{ - "hello": "world", - }), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV02{ - SpecVersion: cloudevents.CloudEventsVersionV02, - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - "asmap": map[string]interface{}{ - "a": []string{`"apple"`}, - "b": []string{`"banana"`}, - "c": []string{`{"d":"dog","e":"eel"}`}, - }, - }, - }, - Data: toBytes(map[string]interface{}{ - "hello": "world", - }), - DataEncoded: true, - }, - }, - "simple v0.2 structured": { - codec: http.CodecV02{}, - msg: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: toBytes(map[string]interface{}{ - "specversion": "0.2", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - }), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV02{ - SpecVersion: cloudevents.CloudEventsVersionV02, - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }, - }, - }, - "full v0.2 structured": { - codec: http.CodecV02{}, - msg: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: toBytes(map[string]interface{}{ - "specversion": "0.2", - "contenttype": "application/json", - "data": map[string]interface{}{ - "hello": "world", - }, - "id": "ABC-123", - "time": now, - "type": "com.example.test", - "test": "extended", - "schemaurl": "http://example.com/schema", - "source": "http://example.com/source", - }), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV02{ - SpecVersion: cloudevents.CloudEventsVersionV02, - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }, - Data: toBytes(map[string]interface{}{ - "hello": "world", - }), - DataEncoded: true, - }, - }, - "simple v0.2 binary with short header": { - codec: http.CodecV02{}, - msg: &http.Message{ - Header: map[string][]string{ - "ce-specversion": {"0.2"}, - "ce-id": {"ABC-123"}, - "ce-type": {"com.example.test"}, - "ce-source": {"http://example.com/source"}, - "Content-Type": {"application/json"}, - "X": {"Notice how short the header's name is"}, - }, - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV02{ - SpecVersion: cloudevents.CloudEventsVersionV02, - ContentType: cloudevents.StringOfApplicationJSON(), - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }, - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, err := tc.codec.Decode(context.TODO(), tc.msg) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected event (-want, +got) = %v", diff) - } - }) - } -} diff --git a/v1/cloudevents/transport/http/codec_v03.go b/v1/cloudevents/transport/http/codec_v03.go deleted file mode 100644 index 02ba1fb2a..000000000 --- a/v1/cloudevents/transport/http/codec_v03.go +++ /dev/null @@ -1,302 +0,0 @@ -package http - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/textproto" - "strings" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - cecontext "github.com/cloudevents/sdk-go/v1/cloudevents/context" - "github.com/cloudevents/sdk-go/v1/cloudevents/observability" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport" - "github.com/cloudevents/sdk-go/v1/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 -} diff --git a/v1/cloudevents/transport/http/codec_v03_test.go b/v1/cloudevents/transport/http/codec_v03_test.go deleted file mode 100644 index 87f054c78..000000000 --- a/v1/cloudevents/transport/http/codec_v03_test.go +++ /dev/null @@ -1,603 +0,0 @@ -package http_test - -import ( - "context" - "net/url" - "testing" - "time" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport/http" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/google/go-cmp/cmp" -) - -func TestCodecV03_Encode(t *testing.T) { - now := types.Timestamp{Time: time.Now().UTC()} - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - subject := "resource" - - schemaUrl, _ := url.Parse("http://example.com/schema") - schema := &types.URLRef{URL: *schemaUrl} - - testCases := map[string]struct { - codec http.CodecV03 - event cloudevents.Event - want *http.Message - wantErr error - }{ - "simple v0.3 default": { - codec: http.CodecV03{}, - event: cloudevents.Event{ - Context: &cloudevents.EventContextV03{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }, - }, - want: &http.Message{ - Header: map[string][]string{ - "Ce-Specversion": {"0.3"}, - "Ce-Id": {"ABC-123"}, - "Ce-Type": {"com.example.test"}, - "Ce-Source": {"http://example.com/source"}, - }, - }, - }, - "full v0.3 default": { - codec: http.CodecV03{}, - event: cloudevents.Event{ - Context: &cloudevents.EventContextV03{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Subject: &subject, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }, - Data: map[string]interface{}{ - "hello": "world", - }, - }, - want: &http.Message{ - Header: map[string][]string{ - "Ce-Specversion": {"0.3"}, - "Ce-Id": {"ABC-123"}, - "Ce-Time": {now.Format(time.RFC3339Nano)}, - "Ce-Type": {"com.example.test"}, - "Ce-Source": {"http://example.com/source"}, - "Ce-Subject": {"resource"}, - "Ce-Schemaurl": {"http://example.com/schema"}, - "Ce-Test": {"extended"}, - "Content-Type": {"application/json"}, - }, - Body: []byte(`{"hello":"world"}`), - }, - }, - "simple v0.3 binary": { - codec: http.CodecV03{DefaultEncoding: http.BinaryV03}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV03{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV03(), - }, - want: &http.Message{ - Header: map[string][]string{ - "Ce-Specversion": {"0.3"}, - "Ce-Id": {"ABC-123"}, - "Ce-Type": {"com.example.test"}, - "Ce-Source": {"http://example.com/source"}, - }, - }, - }, - "full v0.3 binary": { - codec: http.CodecV03{DefaultEncoding: http.BinaryV03}, - event: cloudevents.Event{ - Context: &cloudevents.EventContextV03{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Subject: &subject, - Extensions: map[string]interface{}{ - "test": "extended", - "asmap": map[string]interface{}{ - "a": "apple", - "b": "banana", - "c": map[string]interface{}{ - "d": "dog", - "e": "eel", - }, - }, - }, - }, - Data: map[string]interface{}{ - "hello": "world", - }, - }, - want: &http.Message{ - Header: map[string][]string{ - "Ce-Specversion": {"0.3"}, - "Ce-Id": {"ABC-123"}, - "Ce-Time": {now.Format(time.RFC3339Nano)}, - "Ce-Type": {"com.example.test"}, - "Ce-Source": {"http://example.com/source"}, - "Ce-Subject": {"resource"}, - "Ce-Schemaurl": {"http://example.com/schema"}, - "Ce-Test": {"extended"}, - "Ce-Asmap-A": {`"apple"`}, - "Ce-Asmap-B": {`"banana"`}, - "Ce-Asmap-C": {`{"d":"dog","e":"eel"}`}, - "Content-Type": {"application/json"}, - }, - Body: []byte(`{"hello":"world"}`), - }, - }, - "full v0.3 binary base64": { - codec: http.CodecV03{DefaultEncoding: http.BinaryV03}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV03{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - DataContentEncoding: cloudevents.StringOfBase64(), - Source: *source, - Subject: &subject, - Extensions: map[string]interface{}{ - "test": "extended", - "asmap": map[string]interface{}{ - "a": "apple", - "b": "banana", - "c": map[string]interface{}{ - "d": "dog", - "e": "eel", - }, - }, - }, - }.AsV03(), - Data: map[string]interface{}{ - "hello": "world", - }, - }, - want: &http.Message{ - Header: map[string][]string{ - "Ce-Specversion": {"0.3"}, - "Ce-Id": {"ABC-123"}, - "Ce-Time": {now.Format(time.RFC3339Nano)}, - "Ce-Type": {"com.example.test"}, - "Ce-Source": {"http://example.com/source"}, - "Ce-Subject": {"resource"}, - "Ce-Schemaurl": {"http://example.com/schema"}, - "Ce-Test": {"extended"}, - "Ce-Asmap-A": {`"apple"`}, - "Ce-Asmap-B": {`"banana"`}, - "Ce-Asmap-C": {`{"d":"dog","e":"eel"}`}, - "Content-Type": {"application/json"}, - "Ce-Datacontentencoding": {"base64"}, - }, - Body: []byte("eyJoZWxsbyI6IndvcmxkIn0="), - }, - }, - "simple v0.3 structured": { - codec: http.CodecV03{DefaultEncoding: http.StructuredV03}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV03{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV03(), - }, - want: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "0.3", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - "full v0.3 structured": { - codec: http.CodecV03{DefaultEncoding: http.StructuredV03}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV03{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Subject: &subject, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }.AsV03(), - Data: map[string]interface{}{ - "hello": "world", - }, - }, - want: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "0.3", - "datacontenttype": "application/json", - "data": map[string]interface{}{ - "hello": "world", - }, - "id": "ABC-123", - "time": now, - "type": "com.example.test", - "test": "extended", - "schemaurl": "http://example.com/schema", - "source": "http://example.com/source", - "subject": "resource", - } - return toBytes(body) - }(), - }, - }, - "full v0.3 structured base64": { - codec: http.CodecV03{DefaultEncoding: http.StructuredV03}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV03{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - DataContentEncoding: cloudevents.StringOfBase64(), - Source: *source, - Subject: &subject, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }.AsV03(), - Data: map[string]interface{}{ - "hello": "world", - }, - }, - want: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "0.3", - "datacontentencoding": "base64", - "datacontenttype": "application/json", - "data": "eyJoZWxsbyI6IndvcmxkIn0=", - "id": "ABC-123", - "time": now, - "type": "com.example.test", - "test": "extended", - "schemaurl": "http://example.com/schema", - "source": "http://example.com/source", - "subject": "resource", - } - return toBytes(body) - }(), - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, err := tc.codec.Encode(context.TODO(), tc.event) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - - if msg, ok := got.(*http.Message); ok { - // It is hard to read the byte dump - want := string(tc.want.Body) - got := string(msg.Body) - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("unexpected message body (-want, +got) = %v", diff) - return - } - } - - t.Errorf("unexpected message (-want, +got) = %v", diff) - } - }) - } -} - -// TODO: figure out extensions for v0.3 - -func TestCodecV03_Decode(t *testing.T) { - now := types.Timestamp{Time: time.Now()} - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - subject := "resource" - - schemaUrl, _ := url.Parse("http://example.com/schema") - schema := &types.URLRef{URL: *schemaUrl} - - testCases := map[string]struct { - codec http.CodecV03 - msg *http.Message - want *cloudevents.Event - wantErr error - }{ - "simple v0.3 binary": { - codec: http.CodecV03{}, - msg: &http.Message{ - Header: map[string][]string{ - "ce-specversion": {"0.3"}, - "ce-id": {"ABC-123"}, - "ce-type": {"com.example.test"}, - "ce-source": {"http://example.com/source"}, - "Content-Type": {"application/json"}, - }, - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV03{ - SpecVersion: cloudevents.CloudEventsVersionV03, - DataContentType: cloudevents.StringOfApplicationJSON(), - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }, - }, - }, - "full v0.3 binary": { - codec: http.CodecV03{}, - msg: &http.Message{ - Header: map[string][]string{ - "ce-specversion": {"0.3"}, - "ce-id": {"ABC-123"}, - "ce-time": {now.Format(time.RFC3339Nano)}, - "ce-type": {"com.example.test"}, - "ce-source": {"http://example.com/source"}, - "ce-subject": {"resource"}, - "ce-schemaurl": {"http://example.com/schema"}, - "ce-test": {`"extended"`}, - "ce-asmap-a": {`"apple"`}, - "ce-asmap-b": {`"banana"`}, - "ce-asmap-c": {`{"d":"dog","e":"eel"}`}, - "Content-Type": {"application/json"}, - }, - Body: toBytes(map[string]interface{}{ - "hello": "world", - }), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV03{ - SpecVersion: cloudevents.CloudEventsVersionV03, - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Subject: &subject, - Extensions: map[string]interface{}{ - "test": "extended", - "asmap": map[string]interface{}{ - "a": []string{`"apple"`}, - "b": []string{`"banana"`}, - "c": []string{`{"d":"dog","e":"eel"}`}, - }, - }, - }, - Data: toBytes(map[string]interface{}{ - "hello": "world", - }), - DataEncoded: true, - }, - }, - "full v0.3 binary base64": { - codec: http.CodecV03{}, - msg: &http.Message{ - Header: map[string][]string{ - "ce-specversion": {"0.3"}, - "ce-id": {"ABC-123"}, - "ce-time": {now.Format(time.RFC3339Nano)}, - "ce-type": {"com.example.test"}, - "ce-source": {"http://example.com/source"}, - "ce-subject": {"resource"}, - "ce-schemaurl": {"http://example.com/schema"}, - "ce-test": {`"extended"`}, - "ce-asmap-a": {`"apple"`}, - "ce-asmap-b": {`"banana"`}, - "ce-asmap-c": {`{"d":"dog","e":"eel"}`}, - "Content-Type": {"application/json"}, - "ce-datacontentencoding": {"base64"}, - }, - Body: []byte("eyJoZWxsbyI6IndvcmxkIn0="), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV03{ - SpecVersion: cloudevents.CloudEventsVersionV03, - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Subject: &subject, - DataContentEncoding: cloudevents.StringOfBase64(), - Extensions: map[string]interface{}{ - "test": "extended", - "asmap": map[string]interface{}{ - "a": []string{`"apple"`}, - "b": []string{`"banana"`}, - "c": []string{`{"d":"dog","e":"eel"}`}, - }, - }, - }, - Data: []byte(`eyJoZWxsbyI6IndvcmxkIn0=`), - DataEncoded: true, - }, - }, - "simple v0.3 structured": { - codec: http.CodecV03{}, - msg: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: toBytes(map[string]interface{}{ - "specversion": "0.3", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - }), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV03{ - SpecVersion: cloudevents.CloudEventsVersionV03, - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }, - }, - }, - "full v0.3 structured": { - codec: http.CodecV03{}, - msg: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: toBytes(map[string]interface{}{ - "specversion": "0.3", - "datacontenttype": "application/json", - "data": map[string]interface{}{ - "hello": "world", - }, - "id": "ABC-123", - "time": now, - "type": "com.example.test", - "test": "extended", - "schemaurl": "http://example.com/schema", - "source": "http://example.com/source", - "subject": "resource", - }), - }, - want: &cloudevents.Event{ - Context: cloudevents.EventContextV03{ - SpecVersion: cloudevents.CloudEventsVersionV03, - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Subject: &subject, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }.AsV03(), - Data: toBytes(map[string]interface{}{ - "hello": "world", - }), - DataEncoded: true, - }, - }, - "full v0.3 structured base64": { - codec: http.CodecV03{}, - msg: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: toBytes(map[string]interface{}{ - "specversion": "0.3", - "datacontentencoding": "base64", - "datacontenttype": "application/json", - "data": "eyJoZWxsbyI6IndvcmxkIn0=", - "id": "ABC-123", - "time": now, - "type": "com.example.test", - "test": "extended", - "schemaurl": "http://example.com/schema", - "source": "http://example.com/source", - "subject": "resource", - }), - }, - want: &cloudevents.Event{ - Context: cloudevents.EventContextV03{ - SpecVersion: cloudevents.CloudEventsVersionV03, - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - DataContentEncoding: cloudevents.StringOfBase64(), - Source: *source, - Subject: &subject, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }.AsV03(), - Data: []byte(`"eyJoZWxsbyI6IndvcmxkIn0="`), // TODO: structured comes in quoted. Unquote? - DataEncoded: true, - }, - }, - "simple v0.3 binary with short header": { - codec: http.CodecV03{}, - msg: &http.Message{ - Header: map[string][]string{ - "ce-specversion": {"0.3"}, - "ce-id": {"ABC-123"}, - "ce-type": {"com.example.test"}, - "ce-source": {"http://example.com/source"}, - "Content-Type": {"application/json"}, - "X": {"Notice how short the header's name is"}, - }, - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV03{ - SpecVersion: cloudevents.CloudEventsVersionV03, - DataContentType: cloudevents.StringOfApplicationJSON(), - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }, - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, err := tc.codec.Decode(context.TODO(), tc.msg) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected event (-want, +got) = %v", diff) - } - }) - } -} diff --git a/v1/cloudevents/transport/http/codec_v1.go b/v1/cloudevents/transport/http/codec_v1.go deleted file mode 100644 index 4a9c0b8e5..000000000 --- a/v1/cloudevents/transport/http/codec_v1.go +++ /dev/null @@ -1,245 +0,0 @@ -package http - -import ( - "context" - "fmt" - "net/http" - "net/textproto" - "strings" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - cecontext "github.com/cloudevents/sdk-go/v1/cloudevents/context" - "github.com/cloudevents/sdk-go/v1/cloudevents/observability" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport" - "github.com/cloudevents/sdk-go/v1/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 -} diff --git a/v1/cloudevents/transport/http/codec_v1_test.go b/v1/cloudevents/transport/http/codec_v1_test.go deleted file mode 100644 index dd719a31d..000000000 --- a/v1/cloudevents/transport/http/codec_v1_test.go +++ /dev/null @@ -1,487 +0,0 @@ -package http_test - -import ( - "context" - "net/url" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport/http" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -func TestCodecV1_Encode(t *testing.T) { - now := types.Timestamp{Time: time.Now().UTC()} - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URIRef{URL: *sourceUrl} - - subject := "resource" - - DataSchema, _ := url.Parse("http://example.com/schema") - schema := &types.URI{URL: *DataSchema} - - testCases := map[string]struct { - codec http.CodecV1 - event cloudevents.Event - want *http.Message - wantErr error - }{ - "simple v1.0 default": { - codec: http.CodecV1{}, - event: cloudevents.Event{ - Context: &cloudevents.EventContextV1{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }, - }, - want: &http.Message{ - Header: map[string][]string{ - "Ce-Specversion": {"1.0"}, - "Ce-Id": {"ABC-123"}, - "Ce-Type": {"com.example.test"}, - "Ce-Source": {"http://example.com/source"}, - }, - }, - }, - "full v1.0 default": { - codec: http.CodecV1{}, - event: cloudevents.Event{ - Context: &cloudevents.EventContextV1{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - DataSchema: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Subject: &subject, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }, - Data: map[string]interface{}{ - "hello": "world", - }, - }, - want: &http.Message{ - Header: map[string][]string{ - "Ce-Specversion": {"1.0"}, - "Ce-Id": {"ABC-123"}, - "Ce-Time": {now.Format(time.RFC3339Nano)}, - "Ce-Type": {"com.example.test"}, - "Ce-Source": {"http://example.com/source"}, - "Ce-Subject": {"resource"}, - "Ce-Dataschema": {"http://example.com/schema"}, - "Ce-Test": {"extended"}, - "Content-Type": {"application/json"}, - }, - Body: []byte(`{"hello":"world"}`), - }, - }, - "simple v1.0 binary": { - codec: http.CodecV1{DefaultEncoding: http.BinaryV1}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV1{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV1(), - }, - want: &http.Message{ - Header: map[string][]string{ - "Ce-Specversion": {"1.0"}, - "Ce-Id": {"ABC-123"}, - "Ce-Type": {"com.example.test"}, - "Ce-Source": {"http://example.com/source"}, - }, - }, - }, - "full v1.0 binary": { - codec: http.CodecV1{DefaultEncoding: http.BinaryV1}, - event: cloudevents.Event{ - Context: &cloudevents.EventContextV1{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - DataSchema: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Subject: &subject, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }, - Data: map[string]interface{}{ - "hello": "world", - }, - }, - want: &http.Message{ - Header: map[string][]string{ - "Ce-Specversion": {"1.0"}, - "Ce-Id": {"ABC-123"}, - "Ce-Time": {now.Format(time.RFC3339Nano)}, - "Ce-Type": {"com.example.test"}, - "Ce-Source": {"http://example.com/source"}, - "Ce-Subject": {"resource"}, - "Ce-Dataschema": {"http://example.com/schema"}, - "Ce-Test": {"extended"}, - "Content-Type": {"application/json"}, - }, - Body: []byte(`{"hello":"world"}`), - }, - }, - "simple v1.0 structured": { - codec: http.CodecV1{DefaultEncoding: http.StructuredV1}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV1{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV1(), - }, - want: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "1.0", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - "full v1.0 structured": { - codec: http.CodecV1{DefaultEncoding: http.StructuredV1}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV1{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - DataSchema: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Subject: &subject, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }.AsV1(), - Data: map[string]interface{}{ - "hello": "world", - }, - }, - want: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "1.0", - "datacontenttype": "application/json", - "data": map[string]interface{}{ - "hello": "world", - }, - "id": "ABC-123", - "time": now, - "type": "com.example.test", - "test": "extended", - "dataschema": "http://example.com/schema", - "source": "http://example.com/source", - "subject": "resource", - } - return toBytes(body) - }(), - }, - }, - "full v1.0 structured base64": { - codec: http.CodecV1{DefaultEncoding: http.StructuredV1}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV1{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - DataSchema: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Subject: &subject, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }.AsV1(), - Data: []byte(`{"hello":"world"}`), - DataBinary: true, - DataEncoded: true, - }, - want: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "1.0", - "datacontenttype": "application/json", - "data_base64": "eyJoZWxsbyI6IndvcmxkIn0=", - "id": "ABC-123", - "time": now, - "type": "com.example.test", - "test": "extended", - "dataschema": "http://example.com/schema", - "source": "http://example.com/source", - "subject": "resource", - } - return toBytes(body) - }(), - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, err := tc.codec.Encode(context.TODO(), tc.event) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - - if msg, ok := got.(*http.Message); ok { - // It is hard to read the byte dump - want := string(tc.want.Body) - got := string(msg.Body) - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("unexpected message body (-want, +got) = %v", diff) - return - } - } - - t.Errorf("unexpected message (-want, +got) = %v", diff) - } - }) - } -} - -func TestCodecV1_Decode(t *testing.T) { - now := types.Timestamp{Time: time.Now()} - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URIRef{URL: *sourceUrl} - - subject := "resource" - - DataSchema, _ := url.Parse("http://example.com/schema") - schema := &types.URI{URL: *DataSchema} - - testCases := map[string]struct { - codec http.CodecV1 - msg *http.Message - want *cloudevents.Event - wantErr error - }{ - "simple v1.0 binary": { - codec: http.CodecV1{}, - msg: &http.Message{ - Header: map[string][]string{ - "ce-specversion": {"1.0"}, - "ce-id": {"ABC-123"}, - "ce-type": {"com.example.test"}, - "ce-source": {"http://example.com/source"}, - "Content-Type": {"application/json"}, - }, - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV1{ - SpecVersion: cloudevents.CloudEventsVersionV1, - DataContentType: cloudevents.StringOfApplicationJSON(), - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }, - }, - }, - "full v1.0 binary": { - codec: http.CodecV1{}, - msg: &http.Message{ - Header: map[string][]string{ - "ce-specversion": {"1.0"}, - "ce-id": {"ABC-123"}, - "ce-time": {now.Format(time.RFC3339Nano)}, - "ce-type": {"com.example.test"}, - "ce-source": {"http://example.com/source"}, - "ce-subject": {"resource"}, - "ce-dataschema": {"http://example.com/schema"}, - "ce-test": {"extended binary"}, - "Content-Type": {"application/json"}, - }, - Body: toBytes(map[string]interface{}{ - "hello": "world", - }), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV1{ - SpecVersion: cloudevents.CloudEventsVersionV1, - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - DataSchema: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Subject: &subject, - Extensions: map[string]interface{}{ - "test": "extended binary", - }, - }, - Data: toBytes(map[string]interface{}{ - "hello": "world", - }), - DataEncoded: true, - }, - }, - "simple v1.0 structured": { - codec: http.CodecV1{}, - msg: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: toBytes(map[string]interface{}{ - "specversion": "1.0", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - }), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV1{ - SpecVersion: cloudevents.CloudEventsVersionV1, - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }, - }, - }, - "full v1.0 structured": { - codec: http.CodecV1{}, - msg: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: toBytes(map[string]interface{}{ - "specversion": "1.0", - "datacontenttype": "application/json", - "data": map[string]interface{}{ - "hello": "world", - }, - "id": "ABC-123", - "time": now, - "type": "com.example.test", - "test": "extended", - "dataschema": "http://example.com/schema", - "source": "http://example.com/source", - "subject": "resource", - }), - }, - want: &cloudevents.Event{ - Context: cloudevents.EventContextV1{ - SpecVersion: cloudevents.CloudEventsVersionV1, - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - DataSchema: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Subject: &subject, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }.AsV1(), - Data: toBytes(map[string]interface{}{ - "hello": "world", - }), - DataEncoded: true, - }, - }, - "full v1.0 structured base64": { - codec: http.CodecV1{}, - msg: &http.Message{ - Header: map[string][]string{ - "Content-Type": {"application/cloudevents+json"}, - }, - Body: toBytes(map[string]interface{}{ - "specversion": "1.0", - "datacontenttype": "application/json", - "data_base64": "eyJoZWxsbyI6IndvcmxkIn0=", - "id": "ABC-123", - "time": now, - "type": "com.example.test", - "test": "extended", - "dataschema": "http://example.com/schema", - "source": "http://example.com/source", - "subject": "resource", - }), - }, - want: &cloudevents.Event{ - Context: cloudevents.EventContextV1{ - SpecVersion: cloudevents.CloudEventsVersionV1, - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - DataSchema: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Subject: &subject, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }.AsV1(), - Data: []byte(`{"hello":"world"}`), - }, - }, - "simple v1.0 binary with short header": { - codec: http.CodecV1{}, - msg: &http.Message{ - Header: map[string][]string{ - "ce-specversion": {"1.0"}, - "ce-id": {"ABC-123"}, - "ce-type": {"com.example.test"}, - "ce-source": {"http://example.com/source"}, - "Content-Type": {"application/json"}, - "X": {"Notice how short the header's name is"}, - }, - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV1{ - SpecVersion: cloudevents.CloudEventsVersionV1, - DataContentType: cloudevents.StringOfApplicationJSON(), - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }, - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, err := tc.codec.Decode(context.TODO(), tc.msg) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected event (-want, +got) = %v", diff) - } - }) - } -} diff --git a/v1/cloudevents/transport/http/context.go b/v1/cloudevents/transport/http/context.go deleted file mode 100644 index 8e3c7b44e..000000000 --- a/v1/cloudevents/transport/http/context.go +++ /dev/null @@ -1,212 +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 -} - -// SetContextHeader sets the context's headers replacing any headers currently in context. -func SetContextHeaders(ctx context.Context, headers http.Header) context.Context { - return context.WithValue(ctx, headerKey, headers) -} - -// 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 -} diff --git a/v1/cloudevents/transport/http/context_test.go b/v1/cloudevents/transport/http/context_test.go deleted file mode 100644 index c7efe7144..000000000 --- a/v1/cloudevents/transport/http/context_test.go +++ /dev/null @@ -1,315 +0,0 @@ -package http_test - -import ( - "context" - nethttp "net/http" - "sort" - "testing" - - "github.com/cloudevents/sdk-go/v1/cloudevents/transport/http" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" -) - -func TestTransportContext(t *testing.T) { - testCases := map[string]struct { - t http.TransportContext - ctx context.Context - want http.TransportContext - }{ - "nil context": {}, - "nil context, set transport context": { - t: http.TransportContext{ - Host: "unit test", - Method: "unit test", - }, - want: http.TransportContext{ - Host: "unit test", - Method: "unit test", - }, - }, - "todo context, set transport context": { - ctx: context.TODO(), - t: http.TransportContext{ - Host: "unit test", - Method: "unit test", - }, - want: http.TransportContext{ - Host: "unit test", - Method: "unit test", - }, - }, - "bad transport context": { - ctx: context.TODO(), - }, - "already set transport context": { - ctx: http.WithTransportContext(context.TODO(), - http.TransportContext{ - Host: "existing test", - Method: "exiting test", - }), - t: http.TransportContext{ - Host: "unit test", - Method: "unit test", - }, - want: http.TransportContext{ - Host: "unit test", - Method: "unit test", - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - ctx := http.WithTransportContext(tc.ctx, tc.t) - - got := http.TransportContextFrom(ctx) - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} - -func TestNewTransportContext(t *testing.T) { - testCases := map[string]struct { - r *nethttp.Request - want http.TransportContext - wantStr string - }{ - "nil request": { - want: http.TransportContext{}, - wantStr: `Transport Context, - nil -`, - }, - "full request": { - r: &nethttp.Request{ - Host: "unit test host", - Method: "unit test method", - RequestURI: "unit test uri", - Header: func() nethttp.Header { - h := nethttp.Header{} - h.Set("unit", "test header") - return h - }(), - }, - want: http.TransportContext{ - Host: "unit test host", - Method: "unit test method", - URI: "unit test uri", - Header: func() nethttp.Header { - h := nethttp.Header{} - h.Set("unit", "test header") - return h - }(), - }, - wantStr: `Transport Context, - URI: unit test uri - Host: unit test host - Method: unit test method - Header: - Unit: test header -`, - }, - "no headers request": { - r: &nethttp.Request{ - Host: "unit test host", - Method: "unit test method", - RequestURI: "unit test uri", - }, - want: http.TransportContext{ - Host: "unit test host", - Method: "unit test method", - URI: "unit test uri", - }, - wantStr: `Transport Context, - URI: unit test uri - Host: unit test host - Method: unit test method -`, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got := http.NewTransportContext(tc.r) - - if diff := cmp.Diff(tc.want, got, cmpopts.IgnoreFields(http.TransportContext{}, "IgnoreHeaderPrefixes")); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - - if tc.wantStr != "" { - gotStr := got.String() - - if diff := cmp.Diff(tc.wantStr, gotStr); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - } - }) - } -} - -func TestNewTransportContextFromResponse(t *testing.T) { - testCases := map[string]struct { - r *nethttp.Response - want http.TransportContext - wantStr string - }{ - "nil response": { - want: http.TransportContext{ - StatusCode: nethttp.StatusInternalServerError, - }, - wantStr: `Transport Context, - StatusCode: 500 -`, - }, - "full response": { - r: &nethttp.Response{ - Header: func() nethttp.Header { - h := nethttp.Header{} - h.Set("unit", "test header") - return h - }(), - StatusCode: nethttp.StatusOK, - }, - want: http.TransportContext{ - Header: func() nethttp.Header { - h := nethttp.Header{} - h.Set("unit", "test header") - return h - }(), - StatusCode: nethttp.StatusOK, - }, - wantStr: `Transport Context, - StatusCode: 200 - Header: - Unit: test header -`, - }, - "no headers response": { - r: &nethttp.Response{ - StatusCode: nethttp.StatusOK, - }, - want: http.TransportContext{ - StatusCode: nethttp.StatusOK, - }, - wantStr: `Transport Context, - StatusCode: 200 -`, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got := http.NewTransportContextFromResponse(tc.r) - - if diff := cmp.Diff(tc.want, got, cmpopts.IgnoreFields(http.TransportContext{}, "IgnoreHeaderPrefixes")); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - - if tc.wantStr != "" { - gotStr := got.String() - - if diff := cmp.Diff(tc.wantStr, gotStr); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - } - }) - } -} - -func TestAttendToHeader(t *testing.T) { - testCases := map[string]struct { - header nethttp.Header - ignore []string - want []string - }{ - "nil": {}, - "no new ignore": { - header: func() nethttp.Header { - h := nethttp.Header{} - h.Set("unit", "test header") - h.Set("testing", "header unit") - return h - }(), - want: []string{"Unit", "Testing"}, - }, - "with ignore": { - header: func() nethttp.Header { - h := nethttp.Header{} - h.Set("unit", "test header") - h.Set("testing", "header unit") - return h - }(), - ignore: []string{"test"}, - want: []string{"Unit"}, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - tx := http.NewTransportContext(&nethttp.Request{ - Header: tc.header, - }) - - tx.AddIgnoreHeaderPrefix(tc.ignore...) - - got := tx.AttendToHeaders() - - // Sort to make the test work. - sort.Strings(got) - sort.Strings(tc.want) - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} - -func TestSetHeaders(t *testing.T) { - testCases := map[string]struct { - initial nethttp.Header - want nethttp.Header - }{ - "nil": { - want: nethttp.Header{}, - }, - "no initial": { - want: func() nethttp.Header { - h := nethttp.Header{} - h.Set("unit", "test header") - h.Set("testing", "header unit") - return h - }(), - }, - "with initial": { - initial: func() nethttp.Header { - h := nethttp.Header{} - h.Set("unit", "test header") - h.Set("testing", "header unit") - return h - }(), - want: func() nethttp.Header { - h := nethttp.Header{} - h.Set("new", "test header") - h.Set("testing", "header unit") - return h - }(), - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - ctx := context.TODO() - ctx = http.SetContextHeaders(ctx, tc.initial) - ctx = http.SetContextHeaders(ctx, tc.want) - got := http.HeaderFrom(ctx) - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} diff --git a/v1/cloudevents/transport/http/doc.go b/v1/cloudevents/transport/http/doc.go deleted file mode 100644 index 1a171e46e..000000000 --- a/v1/cloudevents/transport/http/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -/* -Package http implements the CloudEvent transport implementation using HTTP. -*/ -package http diff --git a/v1/cloudevents/transport/http/encoding.go b/v1/cloudevents/transport/http/encoding.go deleted file mode 100644 index 70ea6989f..000000000 --- a/v1/cloudevents/transport/http/encoding.go +++ /dev/null @@ -1,205 +0,0 @@ -package http - -import ( - "context" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - cecontext "github.com/cloudevents/sdk-go/v1/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 - } -} diff --git a/v1/cloudevents/transport/http/message.go b/v1/cloudevents/transport/http/message.go deleted file mode 100644 index 5693c6eab..000000000 --- a/v1/cloudevents/transport/http/message.go +++ /dev/null @@ -1,148 +0,0 @@ -package http - -import ( - "bytes" - "encoding/json" - - "io" - "io/ioutil" - "net/http" - - "github.com/cloudevents/sdk-go/v1/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 -} diff --git a/v1/cloudevents/transport/http/message_test.go b/v1/cloudevents/transport/http/message_test.go deleted file mode 100644 index 093add598..000000000 --- a/v1/cloudevents/transport/http/message_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package http_test - -import ( - "bytes" - "io/ioutil" - "net/http" - "testing" - - cehttp "github.com/cloudevents/sdk-go/v1/cloudevents/transport/http" - "github.com/google/go-cmp/cmp" -) - -func TestNewMessage(t *testing.T) { - h := http.Header{"A": []string{"b"}, "X": []string{"y"}} - b := ioutil.NopCloser(bytes.NewBuffer([]byte("hello"))) - m, err := cehttp.NewMessage(h, b) - if err != nil { - t.Error(err) - } - if s := cmp.Diff(h, m.Header); s != "" { - t.Error(s) - } - if s := cmp.Diff("hello", string(m.Body)); s != "" { - t.Error(s) - } - // Make sure the Message map is an independent copy - h.Set("a", "A") - if s := cmp.Diff(m.Header.Get("a"), "b"); s != "" { - t.Error(s) - } -} - -func TestNewResponse(t *testing.T) { - h := http.Header{"A": []string{"b"}, "X": []string{"y"}} - b := ioutil.NopCloser(bytes.NewBuffer([]byte("hello"))) - m, err := cehttp.NewResponse(h, b, 42) - if err != nil { - t.Error(err) - } - if s := cmp.Diff(42, m.StatusCode); s != "" { - if s := cmp.Diff(h, m.Header); s != "" { - t.Error(s) - } - if s := cmp.Diff("hello", string(m.Body)); s != "" { - t.Error(s) - } - } -} - -func TestToRequest(t *testing.T) { - h := http.Header{"a": []string{"b"}, "x": []string{"y"}} - b := ioutil.NopCloser(bytes.NewBuffer([]byte("hello"))) - m, err := cehttp.NewMessage(h, b) - if err != nil { - t.Error(err) - } - var req http.Request - m.ToRequest(&req) - if s := cmp.Diff(m.Header, req.Header); s != "" { - t.Error(s) - } - data, err := ioutil.ReadAll(req.Body) - if err != nil { - t.Error(err) - } - if s := cmp.Diff("hello", string(data)); s != "" { - t.Error(s) - } - if s := cmp.Diff(len(data), int(req.ContentLength)); s != "" { - t.Error(s) - } - if s := cmp.Diff("POST", req.Method); s != "" { - t.Error(s) - } -} - -func TestToResponse(t *testing.T) { - h := http.Header{"a": []string{"b"}, "x": []string{"y"}} - b := ioutil.NopCloser(bytes.NewBuffer([]byte("hello"))) - m, err := cehttp.NewResponse(h, b, 42) - if err != nil { - t.Error(err) - } - var resp http.Response - m.ToResponse(&resp) - if s := cmp.Diff(m.Header, resp.Header); s != "" { - t.Error(s) - } - data, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Error(err) - } - if s := cmp.Diff("hello", string(data)); s != "" { - t.Error(s) - } - if s := cmp.Diff(len(data), int(resp.ContentLength)); s != "" { - t.Error(s) - } -} diff --git a/v1/cloudevents/transport/http/observability.go b/v1/cloudevents/transport/http/observability.go deleted file mode 100644 index d60a2ab61..000000000 --- a/v1/cloudevents/transport/http/observability.go +++ /dev/null @@ -1,86 +0,0 @@ -package http - -import ( - "fmt" - - "github.com/cloudevents/sdk-go/v1/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 -) - -// 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) - -// 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() -} diff --git a/v1/cloudevents/transport/http/options.go b/v1/cloudevents/transport/http/options.go deleted file mode 100644 index fde7598b0..000000000 --- a/v1/cloudevents/transport/http/options.go +++ /dev/null @@ -1,274 +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 HTTP verb (GET, POST, PUT, etc.) to use -// 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") - } -} - -// WithHTTPTransport sets the HTTP client transport. -func WithHTTPTransport(httpTransport nethttp.RoundTripper) Option { - return func(t *Transport) error { - t.transport = httpTransport - return nil - } -} diff --git a/v1/cloudevents/transport/http/options_test.go b/v1/cloudevents/transport/http/options_test.go deleted file mode 100644 index d917e52b3..000000000 --- a/v1/cloudevents/transport/http/options_test.go +++ /dev/null @@ -1,803 +0,0 @@ -package http - -import ( - "context" - "fmt" - "net" - "net/http" - "net/url" - "testing" - "time" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" -) - -func TestWithTarget(t *testing.T) { - t.Skip("Fails on Golang 1.14") - testCases := map[string]struct { - t *Transport - target string - want *Transport - wantErr string - }{ - "valid url": { - t: &Transport{ - Req: &http.Request{}, - }, - target: "http://localhost:8080/", - want: &Transport{ - Req: &http.Request{ - URL: func() *url.URL { - u, _ := url.Parse("http://localhost:8080/") - return u - }(), - }, - }, - }, - "valid url, unset req": { - t: &Transport{}, - target: "http://localhost:8080/", - want: &Transport{ - Req: &http.Request{ - Method: http.MethodPost, - URL: func() *url.URL { - u, _ := url.Parse("http://localhost:8080/") - return u - }(), - }, - }, - }, - "invalid url": { - t: &Transport{ - Req: &http.Request{}, - }, - target: "%", - wantErr: `http target option failed to parse target url: parse %: invalid URL escape "%"`, - }, - "empty target": { - t: &Transport{ - Req: &http.Request{}, - }, - target: "", - wantErr: `http target option was empty string`, - }, - "whitespace target": { - t: &Transport{ - Req: &http.Request{}, - }, - target: " \t\n", - wantErr: `http target option was empty string`, - }, - "nil transport": { - wantErr: `http target option can not set nil transport`, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - err := tc.t.applyOptions(WithTarget(tc.target)) - - if tc.wantErr != "" || err != nil { - var gotErr string - if err != nil { - gotErr = err.Error() - } - if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - got := tc.t - - if diff := cmp.Diff(tc.want, got, - cmpopts.IgnoreUnexported(Transport{}), cmpopts.IgnoreUnexported(http.Request{})); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} - -func TestWithMethod(t *testing.T) { - testCases := map[string]struct { - t *Transport - method string - want *Transport - wantErr string - }{ - "valid method": { - t: &Transport{ - Req: &http.Request{}, - }, - method: "GET", - want: &Transport{ - Req: &http.Request{ - Method: http.MethodGet, - }, - }, - }, - "valid method, unset req": { - t: &Transport{}, - method: "PUT", - want: &Transport{ - Req: &http.Request{ - Method: http.MethodPut, - }, - }, - }, - "empty method": { - t: &Transport{ - Req: &http.Request{}, - }, - method: "", - wantErr: `http method option was empty string`, - }, - "whitespace method": { - t: &Transport{ - Req: &http.Request{}, - }, - method: " \t\n", - wantErr: `http method option was empty string`, - }, - "nil transport": { - wantErr: `http method option can not set nil transport`, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - err := tc.t.applyOptions(WithMethod(tc.method)) - - if tc.wantErr != "" || err != nil { - var gotErr string - if err != nil { - gotErr = err.Error() - } - if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - got := tc.t - - if diff := cmp.Diff(tc.want, got, - cmpopts.IgnoreUnexported(Transport{}), cmpopts.IgnoreUnexported(http.Request{})); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} - -func TestWithHeader(t *testing.T) { - testCases := map[string]struct { - t *Transport - key string - value string - want *Transport - wantErr string - }{ - "valid header": { - t: &Transport{ - Req: &http.Request{}, - }, - key: "unit", - value: "test", - want: &Transport{ - Req: &http.Request{ - Header: http.Header{ - "Unit": { - "test", - }, - }, - }, - }, - }, - "valid header, unset req": { - t: &Transport{}, - key: "unit", - value: "test", - want: &Transport{ - Req: &http.Request{ - Header: http.Header{ - "Unit": { - "test", - }, - }, - }, - }, - }, - "empty header key": { - t: &Transport{ - Req: &http.Request{}, - }, - value: "test", - wantErr: `http header option was empty string`, - }, - "whitespace key": { - t: &Transport{ - Req: &http.Request{}, - }, - key: " \t\n", - value: "test", - wantErr: `http header option was empty string`, - }, - "nil transport": { - wantErr: `http header option can not set nil transport`, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - err := tc.t.applyOptions(WithHeader(tc.key, tc.value)) - - if tc.wantErr != "" || err != nil { - var gotErr string - if err != nil { - gotErr = err.Error() - } - if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - got := tc.t - - if diff := cmp.Diff(tc.want, got, - cmpopts.IgnoreUnexported(Transport{}), cmpopts.IgnoreUnexported(http.Request{})); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} - -func TestWithShutdownTimeout(t *testing.T) { - testCases := map[string]struct { - t *Transport - timeout time.Duration - want *Transport - wantErr string - }{ - "valid timeout": { - t: &Transport{}, - timeout: time.Minute * 4, - want: &Transport{ - ShutdownTimeout: durationptr(time.Minute * 4), - }, - }, - "nil transport": { - wantErr: `http shutdown timeout option can not set nil transport`, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - err := tc.t.applyOptions(WithShutdownTimeout(tc.timeout)) - - if tc.wantErr != "" || err != nil { - var gotErr string - if err != nil { - gotErr = err.Error() - } - if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - got := tc.t - - if diff := cmp.Diff(tc.want, got, - cmpopts.IgnoreUnexported(Transport{})); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} - -func durationptr(duration time.Duration) *time.Duration { - return &duration -} - -func intptr(i int) *int { - return &i -} - -func TestWithPort(t *testing.T) { - testCases := map[string]struct { - t *Transport - port int - want *Transport - wantErr string - }{ - "valid port": { - t: &Transport{}, - port: 8181, - want: &Transport{ - Port: intptr(8181), - }, - }, - "invalid port, low": { - t: &Transport{}, - port: -1, - wantErr: `http port option was given an invalid port: -1`, - }, - "invalid port, high": { - t: &Transport{}, - port: 65536, - wantErr: `http port option was given an invalid port: 65536`, - }, - "nil transport": { - wantErr: `http port option can not set nil transport`, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - err := tc.t.applyOptions(WithPort(tc.port)) - - if tc.wantErr != "" || err != nil { - var gotErr string - if err != nil { - gotErr = err.Error() - } - if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - got := tc.t - - if diff := cmp.Diff(tc.want, got, - cmpopts.IgnoreUnexported(Transport{}), cmpopts.IgnoreUnexported(http.Request{})); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} - -// Force a transport to close its server/listener by cancelling StartReceiver -func forceClose(tr *Transport) { - ctx, cancel := context.WithCancel(context.Background()) - go func() { _ = tr.StartReceiver(ctx) }() - cancel() -} - -func TestWithPort0(t *testing.T) { - testCases := map[string]func() (*Transport, error){ - "WithPort0": func() (*Transport, error) { return New(WithPort(0)) }, - "SetPort0": func() (*Transport, error) { return &Transport{Port: new(int)}, nil }, - } - for name, f := range testCases { - t.Run(name, func(t *testing.T) { - tr, err := f() - if err != nil { - t.Fatal(err) - } - defer func() { forceClose(tr) }() - port := tr.GetPort() - if port <= 0 { - t.Error("no dynamic port") - } - if d := cmp.Diff(port, *tr.Port); d != "" { - t.Error(d) - } - }) - } -} - -func TestWithListener(t *testing.T) { - l, err := net.Listen("tcp", ":0") - if err != nil { - t.Fatal(err) - } - tr, err := New(WithListener(l)) - defer func() { forceClose(tr) }() - if err != nil { - t.Fatal(err) - } - port := tr.GetPort() - if port <= 0 { - t.Error("no dynamic port") - } - if d := cmp.Diff(port, l.Addr().(*net.TCPAddr).Port); d != "" { - t.Error(d) - } -} - -func TestWithPath(t *testing.T) { - testCases := map[string]struct { - t *Transport - path string - want *Transport - wantErr string - }{ - "valid path": { - t: &Transport{}, - path: "/test", - want: &Transport{ - Path: "/test", - }, - }, - "invalid path": { - t: &Transport{}, - path: "", - wantErr: `http path option was given an invalid path: ""`, - }, - "nil transport": { - wantErr: `http path option can not set nil transport`, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - err := tc.t.applyOptions(WithPath(tc.path)) - - if tc.wantErr != "" || err != nil { - var gotErr string - if err != nil { - gotErr = err.Error() - } - if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - got := tc.t - - if diff := cmp.Diff(tc.want, got, - cmpopts.IgnoreUnexported(Transport{}), cmpopts.IgnoreUnexported(http.Request{})); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} - -func TestWithEncoding(t *testing.T) { - testCases := map[string]struct { - t *Transport - encoding Encoding - want *Transport - wantErr string - }{ - "valid encoding": { - t: &Transport{}, - encoding: StructuredV03, - want: &Transport{ - Encoding: StructuredV03, - }, - }, - "nil transport": { - wantErr: `http encoding option can not set nil transport`, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - err := tc.t.applyOptions(WithEncoding(tc.encoding)) - - if tc.wantErr != "" || err != nil { - var gotErr string - if err != nil { - gotErr = err.Error() - } - if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - got := tc.t - - if diff := cmp.Diff(tc.want, got, - cmpopts.IgnoreUnexported(Transport{})); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} - -func TestWithDefaultEncodingSelector(t *testing.T) { - - fn := func(ctx context.Context, e cloudevents.Event) Encoding { - return Default - } - - testCases := map[string]struct { - t *Transport - fn EncodingSelector - want *Transport - wantErr string - }{ - "valid fn": { - t: &Transport{}, - fn: fn, - want: &Transport{ - DefaultEncodingSelectionFn: fn, - }, - }, - "invalid fn": { - t: &Transport{}, - fn: nil, - wantErr: "http fn for DefaultEncodingSelector was nil", - }, - "nil transport": { - wantErr: `http default encoding selector option can not set nil transport`, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - err := tc.t.applyOptions(WithDefaultEncodingSelector(tc.fn)) - - if tc.wantErr != "" || err != nil { - var gotErr string - if err != nil { - gotErr = err.Error() - } - if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - got := tc.t - - if diff := cmp.Diff(tc.want, got, - cmpopts.IgnoreUnexported(Transport{}), - cmpopts.IgnoreFields(Transport{}, "DefaultEncodingSelectionFn")); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - if tc.fn == nil { - if got.DefaultEncodingSelectionFn != nil { - t.Errorf("expected nil DefaultEncodingSelectionFn") - } - } else { - want := fmt.Sprintf("%v", tc.fn) - got := fmt.Sprintf("%v", got.DefaultEncodingSelectionFn) - if got != want { - t.Errorf("unexpected DefaultEncodingSelectionFn; want: %v; got: %v", want, got) - } - - } - }) - } -} - -func TestWithBinaryEncoding(t *testing.T) { - - fn := DefaultBinaryEncodingSelectionStrategy - - testCases := map[string]struct { - t *Transport - fn EncodingSelector - want *Transport - wantErr string - }{ - "valid": { - t: &Transport{}, - fn: fn, - want: &Transport{ - DefaultEncodingSelectionFn: fn, - }, - }, - "nil transport": { - wantErr: `http binary encoding option can not set nil transport`, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - err := tc.t.applyOptions(WithBinaryEncoding()) - - if tc.wantErr != "" || err != nil { - var gotErr string - if err != nil { - gotErr = err.Error() - } - if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - got := tc.t - - if diff := cmp.Diff(tc.want, got, - cmpopts.IgnoreUnexported(Transport{}), - cmpopts.IgnoreFields(Transport{}, "DefaultEncodingSelectionFn")); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - - if tc.fn == nil { - if got.DefaultEncodingSelectionFn != nil { - t.Errorf("expected nil DefaultEncodingSelectionFn") - } - } else { - want := fmt.Sprintf("%v", tc.fn) - got := fmt.Sprintf("%v", got.DefaultEncodingSelectionFn) - if got != want { - t.Errorf("unexpected DefaultEncodingSelectionFn; want: %v; got: %v", want, got) - } - } - }) - } -} - -func TestWithStructuredEncoding(t *testing.T) { - - fn := DefaultStructuredEncodingSelectionStrategy - - testCases := map[string]struct { - t *Transport - fn EncodingSelector - want *Transport - wantErr string - }{ - "valid": { - t: &Transport{}, - fn: fn, - want: &Transport{ - DefaultEncodingSelectionFn: fn, - }, - }, - "nil transport": { - wantErr: `http structured encoding option can not set nil transport`, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - err := tc.t.applyOptions(WithStructuredEncoding()) - - if tc.wantErr != "" || err != nil { - var gotErr string - if err != nil { - gotErr = err.Error() - } - if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - got := tc.t - - if diff := cmp.Diff(tc.want, got, - cmpopts.IgnoreUnexported(Transport{}), - cmpopts.IgnoreFields(Transport{}, "DefaultEncodingSelectionFn")); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - if tc.fn == nil { - if got.DefaultEncodingSelectionFn != nil { - t.Errorf("expected nil DefaultEncodingSelectionFn") - } - } else { - want := fmt.Sprintf("%v", tc.fn) - got := fmt.Sprintf("%v", got.DefaultEncodingSelectionFn) - if got != want { - t.Errorf("unexpected DefaultEncodingSelectionFn; want: %v; got: %v", want, got) - } - } - }) - } -} - -func TestWithMiddleware(t *testing.T) { - testCases := map[string]struct { - t *Transport - wantErr string - }{ - "nil transport": { - wantErr: "http middleware option can not set nil transport", - }, - "non-nil transport": { - t: &Transport{}, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - err := tc.t.applyOptions(WithMiddleware(func(next http.Handler) http.Handler { - return next - })) - if tc.wantErr != "" { - if err == nil || err.Error() != tc.wantErr { - t.Fatalf("Expected error '%s'. Actual '%v'", tc.wantErr, err) - } - } else if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - }) - } -} - -func TestWithLongPollTarget(t *testing.T) { - t.Skip("Fails on Golang 1.14") - testCases := map[string]struct { - t *Transport - target string - want *Transport - wantErr string - }{ - "valid url": { - t: &Transport{ - LongPollReq: &http.Request{}, - }, - target: "http://localhost:8080/", - want: &Transport{ - LongPollReq: &http.Request{ - URL: func() *url.URL { - u, _ := url.Parse("http://localhost:8080/") - return u - }(), - }, - }, - }, - "valid url, unset req": { - t: &Transport{}, - target: "http://localhost:8080/", - want: &Transport{ - LongPollReq: &http.Request{ - Method: http.MethodGet, - URL: func() *url.URL { - u, _ := url.Parse("http://localhost:8080/") - return u - }(), - }, - }, - }, - "invalid url": { - t: &Transport{ - LongPollReq: &http.Request{}, - }, - target: "%", - wantErr: `http long poll target option failed to parse target url: parse %: invalid URL escape "%"`, - }, - "empty target": { - t: &Transport{ - LongPollReq: &http.Request{}, - }, - target: "", - wantErr: `http long poll target option was empty string`, - }, - "whitespace target": { - t: &Transport{ - LongPollReq: &http.Request{}, - }, - target: " \t\n", - wantErr: `http long poll target option was empty string`, - }, - "nil transport": { - wantErr: `http long poll target option can not set nil transport`, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - err := tc.t.applyOptions(WithLongPollTarget(tc.target)) - - if tc.wantErr != "" || err != nil { - var gotErr string - if err != nil { - gotErr = err.Error() - } - if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - got := tc.t - - if diff := cmp.Diff(tc.want, got, - cmpopts.IgnoreUnexported(Transport{}), cmpopts.IgnoreUnexported(http.Request{})); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} diff --git a/v1/cloudevents/transport/http/transport.go b/v1/cloudevents/transport/http/transport.go deleted file mode 100644 index 87c0d25c5..000000000 --- a/v1/cloudevents/transport/http/transport.go +++ /dev/null @@ -1,711 +0,0 @@ -package http - -import ( - "context" - "errors" - "fmt" - "io/ioutil" - "net" - "net/http" - "net/url" - "strconv" - "strings" - "sync" - "time" - - "go.opencensus.io/plugin/ochttp" - "go.opencensus.io/plugin/ochttp/propagation/tracecontext" - "go.uber.org/zap" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - cecontext "github.com/cloudevents/sdk-go/v1/cloudevents/context" - "github.com/cloudevents/sdk-go/v1/cloudevents/observability" - "github.com/cloudevents/sdk-go/v1/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 - - // Deprecated - setting http client will override use of the - // HTTP transport set with WithHTTPTransport. - 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 - - // transport is the http client transport that will be used to send requests. - // If nil, the default transport will be used. - transport http.RoundTripper -} - -func New(opts ...Option) (*Transport, error) { - t := &Transport{ - Req: &http.Request{ - Method: http.MethodPost, - }, - } - if err := t.applyOptions(opts...); err != nil { - return nil, err - } - t.transport = &ochttp.Transport{ - Base: t.transport, - Propagation: &tracecontext.HTTPFormat{}, - NewClientTrace: ochttp.NewSpanAnnotatingClientTrace, - FormatSpanName: formatSpanName, - } - 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) { - 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) - client := t.Client - if client == nil { - client = &http.Client{Transport: t.transport} - } - return httpDo(ctx, 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: &ochttp.Handler{ - Propagation: &tracecontext.HTTPFormat{}, - Handler: attachMiddleware(t.Handler, t.middleware), - FormatSpanName: formatSpanName, - }, - } - - // 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 - } -} - -// HasTracePropagation implements Transport.HasTracePropagation -func (t *Transport) HasTracePropagation() bool { - return true -} - -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) - isClosed := false - - go func(ch chan<- Message) { - for { - if isClosed { - return - } - - 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(): - isClosed = true - 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 -} - -func formatSpanName(r *http.Request) string { - return "cloudevents.http." + r.URL.Path -} - -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 -} diff --git a/v1/cloudevents/transport/http/transport_test.go b/v1/cloudevents/transport/http/transport_test.go deleted file mode 100644 index 79839b071..000000000 --- a/v1/cloudevents/transport/http/transport_test.go +++ /dev/null @@ -1,268 +0,0 @@ -package http_test - -import ( - "bytes" - "context" - "fmt" - "io/ioutil" - "net" - "net/http" - "net/http/httptest" - "strings" - "sync/atomic" - "testing" - "time" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - cehttp "github.com/cloudevents/sdk-go/v1/cloudevents/transport/http" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "golang.org/x/sync/errgroup" -) - -type task func() error - -// We can't use net/http/httptest.Server here because it's connection -// tracking logic interferes with the connection lifecycle under test -func startTestServer(handler http.Handler) (*http.Server, error) { - listener, err := net.Listen("tcp", ":0") - if err != nil { - return nil, err - } - server := &http.Server{ - Addr: listener.Addr().String(), - Handler: handler, - } - go func() { _ = server.Serve(listener) }() - return server, nil -} - -func doConcurrently(concurrency int, duration time.Duration, fn task) error { - var group errgroup.Group - for i := 0; i < concurrency; i++ { - group.Go(func() error { - done := time.After(duration) - for { - select { - case <-done: - return nil - default: - if err := fn(); err != nil { - return err - } - } - } - }) - } - - if err := group.Wait(); err != nil { - return err - } - return nil -} - -// An example of how to make a stable client under sustained -// concurrency sending to a single host -func makeStableClient(addr string) (*cehttp.Transport, error) { - netHTTPTransport := &http.Transport{ - MaxIdleConnsPerHost: 1000, - MaxConnsPerHost: 5000, - } - ceClient, err := cehttp.New( - cehttp.WithTarget(addr), - cehttp.WithHTTPTransport(netHTTPTransport), - ) - if err != nil { - return nil, err - } - return ceClient, nil -} - -func TestStableConnectionsToSingleHost(t *testing.T) { - // Start a dummy HTTP server - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - time.Sleep(50 * time.Millisecond) - w.WriteHeader(http.StatusAccepted) - }) - sinkServer, err := startTestServer(handler) - if err != nil { - t.Fatalf("unexpected error starting test http server %v", err.Error()) - } - defer sinkServer.Close() - - // Keep track of all new connections to that dummy HTTP server - var newConnectionCount uint64 - sinkServer.ConnState = func(connection net.Conn, state http.ConnState) { - if state == http.StateNew { - atomic.AddUint64(&newConnectionCount, 1) - } - } - - ceClient, err := makeStableClient("http://" + sinkServer.Addr) - if err != nil { - t.Fatalf("unexpected error creating CloudEvents client %v", err.Error()) - } - event := cloudevents.Event{ - Context: &cloudevents.EventContextV02{ - SpecVersion: cloudevents.CloudEventsVersionV02, - Type: "test.event", - ID: "abc-123", - Source: *types.ParseURLRef("test"), - }, - } - - ctx := context.TODO() - concurrency := 64 - duration := 1 * time.Second - var sent uint64 - err = doConcurrently(concurrency, duration, func() error { - rctx, _, err := ceClient.Send(ctx, event) - if err != nil { - return fmt.Errorf("unexpected error sending CloudEvent: %v", err.Error()) - } - trctx := cehttp.TransportContextFrom(rctx) - if trctx.StatusCode != http.StatusAccepted { - return fmt.Errorf("unexpected status code: want %d, got %d", http.StatusAccepted, trctx.StatusCode) - } - atomic.AddUint64(&sent, 1) - return nil - }) - if err != nil { - t.Errorf("error sending concurrent CloudEvents: %v", err) - } - - // newConnectionCount usually equals concurrency, but give some - // leeway. When this fails, it fails by a lot - if newConnectionCount > uint64(concurrency*2) { - t.Errorf("too many new connections opened: expected %d, got %d", concurrency, newConnectionCount) - } -} - -func TestMiddleware(t *testing.T) { - testCases := map[string]struct { - middleware []string - }{ - "none": {}, - "one": { - middleware: []string{"Foo"}, - }, - "nested": { - middleware: []string{"Foo", "Bar", "Qux"}, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - m := make([]cehttp.Option, 0, len(tc.middleware)+2) - m = append(m, cehttp.WithPort(0), cehttp.WithShutdownTimeout(time.Nanosecond)) - for _, ms := range tc.middleware { - ms := ms - m = append(m, cehttp.WithMiddleware(func(next http.Handler) http.Handler { - return &namedHandler{ - name: ms, - next: next, - } - })) - } - tr, err := cehttp.New(m...) - if err != nil { - t.Fatalf("Unable to create transport, %v", err) - } - innermostResponse := "Original" - origResponse := makeRequestToServer(t, tr, innermostResponse) - - // Verify that the response is all the middlewares run in the correct order (as a stack). - response := string(origResponse) - for i := len(tc.middleware) - 1; i >= 0; i-- { - expected := tc.middleware[i] - if !strings.HasPrefix(response, expected) { - t.Fatalf("Incorrect prefix at offset %d. Expected %s. Actual %s", i, tc.middleware[i], string(origResponse)) - } - response = strings.TrimPrefix(response, expected) - } - if response != innermostResponse { - t.Fatalf("Incorrect prefix at last offset. Expected '%s'. Actual %s", innermostResponse, string(origResponse)) - } - }) - } -} - -// makeRequestToServer starts the transport and makes a request to it, pointing at a custom path that will return -// responseText. -func makeRequestToServer(t *testing.T, tr *cehttp.Transport, responseText string) string { - // Create a custom path that will be used to respond with responseText. - tr.Handler = http.NewServeMux() - path := "/123" - tr.Handler.HandleFunc(path, func(w http.ResponseWriter, _ *http.Request) { - _, _ = w.Write([]byte(responseText)) - }) - - // Start the server. - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - go func() { _ = tr.StartReceiver(ctx) }() - - // Give some time for the receiver to start. One second was chosen arbitrarily. - time.Sleep(time.Second) - - // Make the request. - port := tr.GetPort() - r, err := http.Post(fmt.Sprintf("http://localhost:%d%s", port, path), "text", &bytes.Buffer{}) - if err != nil { - t.Fatalf("Error posting: %v", err) - } - rb, err := ioutil.ReadAll(r.Body) - if err != nil { - t.Fatalf("Error reading: %v", err) - } - return string(rb) -} - -type namedHandler struct { - name string - next http.Handler -} - -func (h *namedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte(h.name)) - h.next.ServeHTTP(w, r) -} - -func TestServeHTTPSpecVersion(t *testing.T) { - tests := []struct { - specVersion string - expectedStatus int - }{ - { - // Missing specVersion -> HTTP 400 - specVersion: "", - expectedStatus: http.StatusBadRequest, - }, - { - // Valid request - specVersion: "0.3", - expectedStatus: http.StatusNoContent, - }, - { - // Future specVersion -> HTTP 400 - specVersion: "999999.99", - expectedStatus: http.StatusBadRequest, - }, - } - - for _, test := range tests { - t.Run(test.specVersion, func(t *testing.T) { - transport := &cehttp.Transport{} - req := httptest.NewRequest("GET", "/", nil) - req.Header.Set("ce-specversion", test.specVersion) - w := httptest.NewRecorder() - - transport.ServeHTTP(w, req) - - actualStatus := w.Result().StatusCode - if actualStatus != test.expectedStatus { - t.Errorf("actual status (%d) != expected status (%d)", actualStatus, test.expectedStatus) - t.Errorf("response body: %s", w.Body.String()) - } - }) - } - -} diff --git a/v1/cloudevents/transport/message.go b/v1/cloudevents/transport/message.go deleted file mode 100644 index e2ed55c97..000000000 --- a/v1/cloudevents/transport/message.go +++ /dev/null @@ -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 -} diff --git a/v1/cloudevents/transport/nats/codec.go b/v1/cloudevents/transport/nats/codec.go deleted file mode 100644 index e87188909..000000000 --- a/v1/cloudevents/transport/nats/codec.go +++ /dev/null @@ -1,100 +0,0 @@ -package nats - -import ( - "context" - "fmt" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport" -) - -type Codec struct { - Encoding Encoding - - v02 *CodecV02 - v03 *CodecV03 - v1 *CodecV1 -} - -var _ transport.Codec = (*Codec)(nil) - -func (c *Codec) Encode(ctx context.Context, e cloudevents.Event) (transport.Message, error) { - switch c.Encoding { - case Default: - fallthrough - case StructuredV02: - if c.v02 == nil { - c.v02 = &CodecV02{Encoding: c.Encoding} - } - return c.v02.Encode(ctx, e) - case StructuredV03: - if c.v03 == nil { - c.v03 = &CodecV03{Encoding: c.Encoding} - } - return c.v03.Encode(ctx, e) - case StructuredV1: - if c.v1 == nil { - c.v1 = &CodecV1{Encoding: c.Encoding} - } - return c.v1.Encode(ctx, e) - default: - return nil, fmt.Errorf("unknown encoding: %d", c.Encoding) - } -} - -func (c *Codec) Decode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) { - switch c.inspectEncoding(ctx, msg) { - case Default: - fallthrough - case StructuredV02: - if c.v02 == nil { - c.v02 = &CodecV02{Encoding: c.Encoding} - } - return c.v02.Decode(ctx, msg) - case StructuredV03: - if c.v03 == nil { - c.v03 = &CodecV03{Encoding: c.Encoding} - } - return c.v03.Decode(ctx, msg) - case StructuredV1: - if c.v1 == nil { - c.v1 = &CodecV1{Encoding: c.Encoding} - } - return c.v1.Decode(ctx, msg) - default: - return nil, transport.NewErrMessageEncodingUnknown("wrapper", TransportName) - } -} - -func (c *Codec) inspectEncoding(ctx context.Context, msg transport.Message) Encoding { - - if c.v1 == nil { - c.v1 = &CodecV1{Encoding: c.Encoding} - } - // Try v1.0. - encoding := c.v1.inspectEncoding(ctx, msg) - if encoding != Unknown { - return encoding - } - - if c.v03 == nil { - c.v03 = &CodecV03{Encoding: c.Encoding} - } - // Try v0.3. - encoding = c.v03.inspectEncoding(ctx, msg) - if encoding != Unknown { - return encoding - } - - if c.v02 == nil { - c.v02 = &CodecV02{Encoding: c.Encoding} - } - // Try v0.2. - encoding = c.v02.inspectEncoding(ctx, msg) - if encoding != Unknown { - return encoding - } - - // We do not understand the message encoding. - return Unknown -} diff --git a/v1/cloudevents/transport/nats/codec_structured.go b/v1/cloudevents/transport/nats/codec_structured.go deleted file mode 100644 index cc4f8a147..000000000 --- a/v1/cloudevents/transport/nats/codec_structured.go +++ /dev/null @@ -1,39 +0,0 @@ -package nats - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport" -) - -// CodecStructured represents an structured http transport codec for all versions. -// Intended to be used as a base class. -type CodecStructured struct { - Encoding Encoding -} - -func (v CodecStructured) encodeStructured(ctx context.Context, e cloudevents.Event) (transport.Message, error) { - body, err := json.Marshal(e) - if err != nil { - return nil, err - } - - msg := &Message{ - 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 nats.Message") - } - event := cloudevents.New(version) - err := json.Unmarshal(m.Body, &event) - return &event, err -} diff --git a/v1/cloudevents/transport/nats/codec_test.go b/v1/cloudevents/transport/nats/codec_test.go deleted file mode 100644 index f5f96d381..000000000 --- a/v1/cloudevents/transport/nats/codec_test.go +++ /dev/null @@ -1,399 +0,0 @@ -package nats_test - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "testing" - "time" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport/nats" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/google/go-cmp/cmp" -) - -func TestCodecEncode(t *testing.T) { - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - testCases := map[string]struct { - codec nats.Codec - event cloudevents.Event - want *nats.Message - wantErr error - }{ - "simple v02 structured binary": { - codec: nats.Codec{Encoding: nats.StructuredV02}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV02{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV02(), - }, - want: &nats.Message{ - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "0.2", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, err := tc.codec.Encode(context.TODO(), tc.event) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - if msg, ok := got.(*nats.Message); ok { - // It is hard to read the byte dump - want := string(tc.want.Body) - got := string(msg.Body) - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - return - } - } - t.Errorf("unexpected message (-want, +got) = %v", diff) - } - }) - } -} - -func TestCodecDecode(t *testing.T) { - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - testCases := map[string]struct { - codec nats.Codec - msg *nats.Message - want *cloudevents.Event - wantErr error - }{ - "simple v2 structured": { - codec: nats.Codec{Encoding: nats.StructuredV02}, - msg: &nats.Message{ - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "0.2", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV02{ - SpecVersion: cloudevents.CloudEventsVersionV02, - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }, - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, err := tc.codec.Decode(context.TODO(), tc.msg) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected event (-want, +got) = %v", diff) - } - }) - } -} - -type DataExample struct { - AnInt int `json:"a,omitempty" xml:"a,omitempty"` - AString string `json:"b,omitempty" xml:"b,omitempty"` - AnArray []string `json:"c,omitempty" xml:"c,omitempty"` - AMap map[string]map[string]int `json:"d,omitempty" xml:"d,omitempty"` - ATime *time.Time `json:"e,omitempty" xml:"e,omitempty"` -} - -func TestCodecRoundTrip(t *testing.T) { - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - for _, encoding := range []nats.Encoding{nats.StructuredV02} { - - testCases := map[string]struct { - codec nats.Codec - event cloudevents.Event - want cloudevents.Event - wantErr error - }{ - "simple data": { - codec: nats.Codec{Encoding: encoding}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV01{ - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - }.AsV02(), - Data: map[string]string{ - "a": "apple", - "b": "banana", - }, - }, - want: cloudevents.Event{ - Context: &cloudevents.EventContextV01{ - CloudEventsVersion: cloudevents.CloudEventsVersionV01, - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - }, - Data: map[string]interface{}{ - "a": "apple", - "b": "banana", - }, - DataEncoded: true, - }, - }, - "struct data": { - codec: nats.Codec{Encoding: encoding}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV01{ - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - }.AsV02(), - Data: DataExample{ - AnInt: 42, - AString: "testing", - }, - }, - want: cloudevents.Event{ - Context: &cloudevents.EventContextV01{ - CloudEventsVersion: cloudevents.CloudEventsVersionV01, - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - }, - Data: &DataExample{ - AnInt: 42, - AString: "testing", - }, - DataEncoded: true, - }, - }, - } - for n, tc := range testCases { - n = fmt.Sprintf("%s, %s", encoding, n) - t.Run(n, func(t *testing.T) { - - msg, err := tc.codec.Encode(context.TODO(), tc.event) - if err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - got, err := tc.codec.Decode(context.TODO(), msg) - if err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if tc.event.Data != nil { - // We have to be pretty tricky in the test to get the correct type. - data, _ := types.Allocate(tc.want.Data) - err := got.DataAs(&data) - if err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - got.Data = data - } - - if tc.wantErr != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - // fix the context back to v01 to test. - ctxv1 := got.Context.AsV01() - got.Context = ctxv1 - - if diff := cmp.Diff(tc.want, *got); diff != "" { - t.Errorf("unexpected event (-want, +got) = %v", diff) - } - }) - } - - } -} - -func TestCodecAsMiddleware(t *testing.T) { - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - for _, encoding := range []nats.Encoding{nats.StructuredV02} { - - testCases := map[string]struct { - codec nats.Codec - event cloudevents.Event - want cloudevents.Event - wantErr error - }{ - "simple data": { - codec: nats.Codec{Encoding: encoding}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV01{ - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - }.AsV02(), - Data: map[string]string{ - "a": "apple", - "b": "banana", - }, - }, - want: cloudevents.Event{ - Context: &cloudevents.EventContextV01{ - CloudEventsVersion: cloudevents.CloudEventsVersionV01, - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - }, - Data: map[string]interface{}{ - "a": "apple", - "b": "banana", - }, - DataEncoded: true, - }, - }, - "struct data": { - codec: nats.Codec{Encoding: encoding}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV01{ - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - }.AsV02(), - Data: DataExample{ - AnInt: 42, - AString: "testing", - }, - }, - want: cloudevents.Event{ - Context: &cloudevents.EventContextV01{ - CloudEventsVersion: cloudevents.CloudEventsVersionV01, - EventType: "com.example.test", - Source: *source, - EventID: "ABC-123", - }, - Data: &DataExample{ - AnInt: 42, - AString: "testing", - }, - DataEncoded: true, - }, - }, - } - for n, tc := range testCases { - n = fmt.Sprintf("%s, %s", encoding, n) - t.Run(n, func(t *testing.T) { - - msg1, err := tc.codec.Encode(context.TODO(), tc.event) - if err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - midEvent, err := tc.codec.Decode(context.TODO(), msg1) - if err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - msg2, err := tc.codec.Encode(context.TODO(), *midEvent) - if err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - got, err := tc.codec.Decode(context.TODO(), msg2) - if err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if tc.event.Data != nil { - // We have to be pretty tricky in the test to get the correct type. - data, _ := types.Allocate(tc.want.Data) - err := got.DataAs(&data) - if err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - got.Data = data - } - - if tc.wantErr != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - // fix the context back to v1 to test. - ctxv1 := got.Context.AsV01() - got.Context = ctxv1 - - if diff := cmp.Diff(tc.want, *got); diff != "" { - t.Errorf("unexpected event (-want, +got) = %v", diff) - } - }) - } - - } -} - -func toBytes(body map[string]interface{}) []byte { - b, err := json.Marshal(body) - if err != nil { - return []byte(fmt.Sprintf(`{"error":%q}`, err.Error())) - } - return b -} diff --git a/v1/cloudevents/transport/nats/codec_v02.go b/v1/cloudevents/transport/nats/codec_v02.go deleted file mode 100644 index 550375c39..000000000 --- a/v1/cloudevents/transport/nats/codec_v02.go +++ /dev/null @@ -1,50 +0,0 @@ -package nats - -import ( - "context" - "fmt" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport" -) - -type CodecV02 struct { - CodecStructured - - Encoding Encoding -} - -var _ transport.Codec = (*CodecV02)(nil) - -func (v CodecV02) Encode(ctx context.Context, e cloudevents.Event) (transport.Message, error) { - switch v.Encoding { - case Default: - fallthrough - case StructuredV02: - return v.encodeStructured(ctx, e) - default: - return nil, fmt.Errorf("unknown encoding: %d", v.Encoding) - } -} - -func (v CodecV02) Decode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) { - // only structured is supported as of v0.2 - switch v.inspectEncoding(ctx, msg) { - case StructuredV02: - return v.decodeStructured(ctx, cloudevents.CloudEventsVersionV02, msg) - default: - return nil, transport.NewErrMessageEncodingUnknown("v02", TransportName) - } -} - -func (v CodecV02) inspectEncoding(ctx context.Context, msg transport.Message) Encoding { - version := msg.CloudEventsVersion() - if version != cloudevents.CloudEventsVersionV02 { - return Unknown - } - _, ok := msg.(*Message) - if !ok { - return Unknown - } - return StructuredV02 -} diff --git a/v1/cloudevents/transport/nats/codec_v02_test.go b/v1/cloudevents/transport/nats/codec_v02_test.go deleted file mode 100644 index 5ec4bd6f9..000000000 --- a/v1/cloudevents/transport/nats/codec_v02_test.go +++ /dev/null @@ -1,265 +0,0 @@ -package nats_test - -import ( - "context" - "net/url" - "testing" - "time" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport/nats" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/google/go-cmp/cmp" -) - -func TestCodecV02_Encode(t *testing.T) { - now := types.Timestamp{Time: time.Now().UTC()} - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - schemaUrl, _ := url.Parse("http://example.com/schema") - schema := &types.URLRef{URL: *schemaUrl} - - testCases := map[string]struct { - codec nats.CodecV02 - event cloudevents.Event - want *nats.Message - wantErr error - }{ - "simple v2 default": { - codec: nats.CodecV02{}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV02{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV02(), - }, - want: &nats.Message{ - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "0.2", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - "full v2 default": { - codec: nats.CodecV02{}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV02{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }.AsV02(), - Data: map[string]interface{}{ - "hello": "world", - }, - }, - want: &nats.Message{ - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "0.2", - "contenttype": "application/json", - "data": map[string]interface{}{ - "hello": "world", - }, - "id": "ABC-123", - "time": now, - "type": "com.example.test", - "test": "extended", - "schemaurl": "http://example.com/schema", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - "simple v2 structured": { - codec: nats.CodecV02{Encoding: nats.StructuredV02}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV02{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV02(), - }, - want: &nats.Message{ - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "0.2", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - "full v2 structured": { - codec: nats.CodecV02{Encoding: nats.StructuredV02}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV02{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }.AsV02(), - Data: map[string]interface{}{ - "hello": "world", - }, - }, - want: &nats.Message{ - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "0.2", - "contenttype": "application/json", - "data": map[string]interface{}{ - "hello": "world", - }, - "id": "ABC-123", - "time": now, - "type": "com.example.test", - "test": "extended", - "schemaurl": "http://example.com/schema", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, err := tc.codec.Encode(context.TODO(), tc.event) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - - if msg, ok := got.(*nats.Message); ok { - // It is hard to read the byte dump - want := string(tc.want.Body) - got := string(msg.Body) - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("unexpected message body (-want, +got) = %v", diff) - return - } - } - - t.Errorf("unexpected message (-want, +got) = %v", diff) - } - }) - } -} - -// TODO: figure out extensions for v2 - -func TestCodecV02_Decode(t *testing.T) { - now := types.Timestamp{Time: time.Now()} - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - schemaUrl, _ := url.Parse("http://example.com/schema") - schema := &types.URLRef{URL: *schemaUrl} - - testCases := map[string]struct { - codec nats.CodecV02 - msg *nats.Message - want *cloudevents.Event - wantErr error - }{ - "simple v2 structured": { - codec: nats.CodecV02{}, - msg: &nats.Message{ - Body: toBytes(map[string]interface{}{ - "specversion": "0.2", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - }), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV02{ - SpecVersion: cloudevents.CloudEventsVersionV02, - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }, - }, - }, - "full v2 structured": { - codec: nats.CodecV02{}, - msg: &nats.Message{ - Body: toBytes(map[string]interface{}{ - "specversion": "0.2", - "contenttype": "application/json", - "data": map[string]interface{}{ - "hello": "world", - }, - "id": "ABC-123", - "time": now, - "type": "com.example.test", - "test": "extended", - "schemaurl": "http://example.com/schema", - "source": "http://example.com/source", - }), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV02{ - SpecVersion: cloudevents.CloudEventsVersionV02, - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - ContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }, - Data: toBytes(map[string]interface{}{ - "hello": "world", - }), - DataEncoded: true, - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, err := tc.codec.Decode(context.TODO(), tc.msg) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected event (-want, +got) = %v", diff) - } - }) - } -} diff --git a/v1/cloudevents/transport/nats/codec_v03.go b/v1/cloudevents/transport/nats/codec_v03.go deleted file mode 100644 index fa8bfe3f8..000000000 --- a/v1/cloudevents/transport/nats/codec_v03.go +++ /dev/null @@ -1,50 +0,0 @@ -package nats - -import ( - "context" - "fmt" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport" -) - -type CodecV03 struct { - CodecStructured - - Encoding Encoding -} - -var _ transport.Codec = (*CodecV03)(nil) - -func (v CodecV03) Encode(ctx context.Context, e cloudevents.Event) (transport.Message, error) { - switch v.Encoding { - case Default: - fallthrough - case StructuredV03: - return v.encodeStructured(ctx, e) - default: - return nil, fmt.Errorf("unknown encoding: %d", v.Encoding) - } -} - -func (v CodecV03) Decode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) { - // only structured is supported as of v0.3 - switch v.inspectEncoding(ctx, msg) { - case StructuredV03: - return v.decodeStructured(ctx, cloudevents.CloudEventsVersionV03, msg) - default: - return nil, transport.NewErrMessageEncodingUnknown("v03", TransportName) - } -} - -func (v CodecV03) inspectEncoding(ctx context.Context, msg transport.Message) Encoding { - version := msg.CloudEventsVersion() - if version != cloudevents.CloudEventsVersionV03 { - return Unknown - } - _, ok := msg.(*Message) - if !ok { - return Unknown - } - return StructuredV03 -} diff --git a/v1/cloudevents/transport/nats/codec_v03_test.go b/v1/cloudevents/transport/nats/codec_v03_test.go deleted file mode 100644 index 5fe64ebaf..000000000 --- a/v1/cloudevents/transport/nats/codec_v03_test.go +++ /dev/null @@ -1,266 +0,0 @@ -package nats_test - -import ( - "context" - "net/url" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport/nats" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -func TestCodecV03_Encode(t *testing.T) { - now := types.Timestamp{Time: time.Now().UTC()} - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - schemaUrl, _ := url.Parse("http://example.com/schema") - schema := &types.URLRef{URL: *schemaUrl} - - testCases := map[string]struct { - codec nats.CodecV03 - event cloudevents.Event - want *nats.Message - wantErr error - }{ - "simple v0.3 default": { - codec: nats.CodecV03{}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV03{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV03(), - }, - want: &nats.Message{ - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "0.3", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - "full v0.3 default": { - codec: nats.CodecV03{}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV03{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }.AsV03(), - Data: map[string]interface{}{ - "hello": "world", - }, - }, - want: &nats.Message{ - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "0.3", - "datacontenttype": "application/json", - "data": map[string]interface{}{ - "hello": "world", - }, - "id": "ABC-123", - "time": now, - "type": "com.example.test", - "test": "extended", - "schemaurl": "http://example.com/schema", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - "simple v0.3 structured": { - codec: nats.CodecV03{Encoding: nats.StructuredV03}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV03{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV03(), - }, - want: &nats.Message{ - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "0.3", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - "full v0.3 structured": { - codec: nats.CodecV03{Encoding: nats.StructuredV03}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV03{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }.AsV03(), - Data: map[string]interface{}{ - "hello": "world", - }, - }, - want: &nats.Message{ - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "0.3", - "datacontenttype": "application/json", - "data": map[string]interface{}{ - "hello": "world", - }, - "id": "ABC-123", - "time": now, - "type": "com.example.test", - "test": "extended", - "schemaurl": "http://example.com/schema", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, err := tc.codec.Encode(context.TODO(), tc.event) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - - if msg, ok := got.(*nats.Message); ok { - // It is hard to read the byte dump - want := string(tc.want.Body) - got := string(msg.Body) - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("unexpected message body (-want, +got) = %v", diff) - return - } - } - - t.Errorf("unexpected message (-want, +got) = %v", diff) - } - }) - } -} - -// TODO: figure out extensions for v0.3 - -func TestCodecV03_Decode(t *testing.T) { - now := types.Timestamp{Time: time.Now()} - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - schemaUrl, _ := url.Parse("http://example.com/schema") - schema := &types.URLRef{URL: *schemaUrl} - - testCases := map[string]struct { - codec nats.CodecV03 - msg *nats.Message - want *cloudevents.Event - wantErr error - }{ - "simple v0.3 structured": { - codec: nats.CodecV03{}, - msg: &nats.Message{ - Body: toBytes(map[string]interface{}{ - "specversion": "0.3", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - }), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV03{ - SpecVersion: cloudevents.CloudEventsVersionV03, - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }, - }, - }, - "full v0.3 structured": { - codec: nats.CodecV03{}, - msg: &nats.Message{ - Body: toBytes(map[string]interface{}{ - "specversion": "0.3", - "datacontenttype": "application/json", - "data": map[string]interface{}{ - "hello": "world", - }, - "id": "ABC-123", - "time": now, - "type": "com.example.test", - "test": "extended", - "schemaurl": "http://example.com/schema", - "source": "http://example.com/source", - }), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV03{ - SpecVersion: cloudevents.CloudEventsVersionV03, - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }, - Data: toBytes(map[string]interface{}{ - "hello": "world", - }), - DataEncoded: true, - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, err := tc.codec.Decode(context.TODO(), tc.msg) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected event (-want, +got) = %v", diff) - } - }) - } -} diff --git a/v1/cloudevents/transport/nats/codec_v1.go b/v1/cloudevents/transport/nats/codec_v1.go deleted file mode 100644 index 5f2a6a762..000000000 --- a/v1/cloudevents/transport/nats/codec_v1.go +++ /dev/null @@ -1,50 +0,0 @@ -package nats - -import ( - "context" - "fmt" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport" -) - -type CodecV1 struct { - CodecStructured - - Encoding Encoding -} - -var _ transport.Codec = (*CodecV1)(nil) - -func (v CodecV1) Encode(ctx context.Context, e cloudevents.Event) (transport.Message, error) { - switch v.Encoding { - case Default: - fallthrough - case StructuredV1: - return v.encodeStructured(ctx, e) - default: - return nil, fmt.Errorf("unknown encoding: %d", v.Encoding) - } -} - -func (v CodecV1) Decode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) { - // only structured is supported as of v0.3 - switch v.inspectEncoding(ctx, msg) { - case StructuredV1: - return v.decodeStructured(ctx, cloudevents.CloudEventsVersionV1, msg) - default: - return nil, transport.NewErrMessageEncodingUnknown("V1", TransportName) - } -} - -func (v CodecV1) inspectEncoding(ctx context.Context, msg transport.Message) Encoding { - version := msg.CloudEventsVersion() - if version != cloudevents.CloudEventsVersionV1 { - return Unknown - } - _, ok := msg.(*Message) - if !ok { - return Unknown - } - return StructuredV1 -} diff --git a/v1/cloudevents/transport/nats/codec_v1_test.go b/v1/cloudevents/transport/nats/codec_v1_test.go deleted file mode 100644 index ba05403b3..000000000 --- a/v1/cloudevents/transport/nats/codec_v1_test.go +++ /dev/null @@ -1,264 +0,0 @@ -package nats_test - -import ( - "context" - "net/url" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport/nats" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -func TestCodecV1_Encode(t *testing.T) { - now := types.Timestamp{Time: time.Now().UTC()} - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URIRef{URL: *sourceUrl} - - schemaUrl, _ := url.Parse("http://example.com/schema") - schema := &types.URI{URL: *schemaUrl} - - testCases := map[string]struct { - codec nats.CodecV1 - event cloudevents.Event - want *nats.Message - wantErr error - }{ - "simple v1.0 default": { - codec: nats.CodecV1{}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV1{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV1(), - }, - want: &nats.Message{ - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "1.0", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - "full v1.0 default": { - codec: nats.CodecV1{}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV1{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - DataSchema: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }.AsV1(), - Data: map[string]interface{}{ - "hello": "world", - }, - }, - want: &nats.Message{ - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "1.0", - "datacontenttype": "application/json", - "data": map[string]interface{}{ - "hello": "world", - }, - "id": "ABC-123", - "time": now, - "type": "com.example.test", - "test": "extended", - "dataschema": "http://example.com/schema", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - "simple v1.0 structured": { - codec: nats.CodecV1{Encoding: nats.StructuredV1}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV1{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV1(), - }, - want: &nats.Message{ - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "1.0", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - "full v1.0 structured": { - codec: nats.CodecV1{Encoding: nats.StructuredV1}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV1{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - DataSchema: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }.AsV1(), - Data: map[string]interface{}{ - "hello": "world", - }, - }, - want: &nats.Message{ - Body: func() []byte { - body := map[string]interface{}{ - "specversion": "1.0", - "datacontenttype": "application/json", - "data": map[string]interface{}{ - "hello": "world", - }, - "id": "ABC-123", - "time": now, - "type": "com.example.test", - "test": "extended", - "dataschema": "http://example.com/schema", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, err := tc.codec.Encode(context.TODO(), tc.event) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - - if msg, ok := got.(*nats.Message); ok { - // It is hard to read the byte dump - want := string(tc.want.Body) - got := string(msg.Body) - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("unexpected message body (-want, +got) = %v", diff) - return - } - } - - t.Errorf("unexpected message (-want, +got) = %v", diff) - } - }) - } -} - -func TestCodecV1_Decode(t *testing.T) { - now := types.Timestamp{Time: time.Now()} - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URIRef{URL: *sourceUrl} - - schemaUrl, _ := url.Parse("http://example.com/schema") - schema := &types.URI{URL: *schemaUrl} - - testCases := map[string]struct { - codec nats.CodecV1 - msg *nats.Message - want *cloudevents.Event - wantErr error - }{ - "simple v1.0 structured": { - codec: nats.CodecV1{}, - msg: &nats.Message{ - Body: toBytes(map[string]interface{}{ - "specversion": "1.0", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - }), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV1{ - SpecVersion: cloudevents.CloudEventsVersionV1, - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }, - }, - }, - "full v1.0 structured": { - codec: nats.CodecV1{}, - msg: &nats.Message{ - Body: toBytes(map[string]interface{}{ - "specversion": "1.0", - "datacontenttype": "application/json", - "data": map[string]interface{}{ - "hello": "world", - }, - "id": "ABC-123", - "time": now, - "type": "com.example.test", - "test": "extended", - "dataschema": "http://example.com/schema", - "source": "http://example.com/source", - }), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV1{ - SpecVersion: cloudevents.CloudEventsVersionV1, - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - DataSchema: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }, - Data: toBytes(map[string]interface{}{ - "hello": "world", - }), - DataEncoded: true, - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, err := tc.codec.Decode(context.TODO(), tc.msg) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected event (-want, +got) = %v", diff) - } - }) - } -} diff --git a/v1/cloudevents/transport/nats/doc.go b/v1/cloudevents/transport/nats/doc.go deleted file mode 100644 index a621ae0c1..000000000 --- a/v1/cloudevents/transport/nats/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -/* -Package nats implements the CloudEvent transport implementation using NATS. -*/ -package nats diff --git a/v1/cloudevents/transport/nats/encoding.go b/v1/cloudevents/transport/nats/encoding.go deleted file mode 100644 index 2bbdca7c1..000000000 --- a/v1/cloudevents/transport/nats/encoding.go +++ /dev/null @@ -1,56 +0,0 @@ -package nats - -// Encoding to use for NATS transport. -type Encoding int32 - -const ( - // Default allows NATS transport implementation to pick. - Default Encoding = iota - // StructuredV02 is Structured CloudEvents spec v0.2. - StructuredV02 - // StructuredV03 is Structured CloudEvents spec v0.3. - StructuredV03 - // StructuredV1 is Structured CloudEvents spec v1.0. - StructuredV1 - // Unknown is unknown. - Unknown -) - -// String pretty-prints the encoding as a string. -func (e Encoding) String() string { - switch e { - case Default: - return "Default Encoding " + e.Version() - - // Structured - case StructuredV02, StructuredV03, StructuredV1: - return "Structured Encoding " + e.Version() - - default: - return "Unknown Encoding" - } -} - -// Version pretty-prints the encoding version as a string. -func (e Encoding) Version() string { - switch e { - - // Version 0.2 - case Default: // <-- Move when a new default is wanted. - fallthrough - case StructuredV02: - return "v0.2" - - // Version 0.3 - case StructuredV03: - return "v0.3" - - // Version 1.0 - case StructuredV1: - return "v1.0" - - // Unknown - default: - return "Unknown" - } -} diff --git a/v1/cloudevents/transport/nats/message.go b/v1/cloudevents/transport/nats/message.go deleted file mode 100644 index 49c4ec84c..000000000 --- a/v1/cloudevents/transport/nats/message.go +++ /dev/null @@ -1,32 +0,0 @@ -package nats - -import ( - "encoding/json" - - "github.com/cloudevents/sdk-go/v1/cloudevents/transport" -) - -// type check that this transport message impl matches the contract -var _ transport.Message = (*Message)(nil) - -type Message struct { - Body []byte -} - -func (m Message) CloudEventsVersion() string { - raw := make(map[string]json.RawMessage) - if err := json.Unmarshal(m.Body, &raw); err != nil { - return "" - } - - // v0.2 - if v, ok := raw["specversion"]; ok { - var version string - if err := json.Unmarshal(v, &version); err != nil { - return "" - } - return version - } - - return "" -} diff --git a/v1/cloudevents/transport/nats/options.go b/v1/cloudevents/transport/nats/options.go deleted file mode 100644 index 0010768dd..000000000 --- a/v1/cloudevents/transport/nats/options.go +++ /dev/null @@ -1,28 +0,0 @@ -package nats - -import ( - "github.com/nats-io/nats.go" -) - -// Option is the function signature required to be considered an nats.Option. -type Option func(*Transport) error - -// WithEncoding sets the encoding for NATS transport. -func WithEncoding(encoding Encoding) Option { - return func(t *Transport) error { - t.Encoding = encoding - return nil - } -} - -// WithConnOptions supplies NATS connection options that will be used when setting -// up the internal NATS connection -func WithConnOptions(opts ...nats.Option) Option { - return func(t *Transport) error { - for _, o := range opts { - t.ConnOptions = append(t.ConnOptions, o) - } - - return nil - } -} diff --git a/v1/cloudevents/transport/nats/options_test.go b/v1/cloudevents/transport/nats/options_test.go deleted file mode 100644 index d102fc714..000000000 --- a/v1/cloudevents/transport/nats/options_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package nats - -import ( - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - natsd "github.com/nats-io/nats-server/v2/server" - "github.com/nats-io/nats.go" -) - -func TestWithEncoding(t *testing.T) { - testCases := map[string]struct { - t *Transport - encoding Encoding - want *Transport - wantErr string - }{ - "valid encoding": { - t: &Transport{}, - encoding: StructuredV03, - want: &Transport{ - Encoding: StructuredV03, - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - err := tc.t.applyOptions(WithEncoding(tc.encoding)) - - if tc.wantErr != "" || err != nil { - var gotErr string - if err != nil { - gotErr = err.Error() - } - if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - got := tc.t - - if diff := cmp.Diff(tc.want, got, - cmpopts.IgnoreUnexported(Transport{})); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} - -func TestWithConnOptions(t *testing.T) { - opts := []nats.Option{ - nats.DontRandomize(), - nats.NoEcho(), - } - - srv, err := natsd.NewServer(&natsd.Options{Port: -1}) - if err != nil { - t.Errorf("could not start nats server: %s", err) - } - go srv.Start() - defer srv.Shutdown() - - if !srv.ReadyForConnections(10 * time.Second) { - t.Errorf("nats server did not start") - } - - tr, err := New(srv.Addr().String(), "testing", WithConnOptions(opts...)) - if err != nil { - t.Errorf("connection failed: %s", err) - } - - if !tr.Conn.Opts.NoRandomize { - t.Errorf("NoRandomize option was not set") - } - - if !tr.Conn.Opts.NoEcho { - t.Errorf("NoEcho option was not set") - } -} diff --git a/v1/cloudevents/transport/nats/transport.go b/v1/cloudevents/transport/nats/transport.go deleted file mode 100644 index 218e2183d..000000000 --- a/v1/cloudevents/transport/nats/transport.go +++ /dev/null @@ -1,187 +0,0 @@ -package nats - -import ( - "context" - "fmt" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - context2 "github.com/cloudevents/sdk-go/v1/cloudevents/context" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport" - "github.com/nats-io/nats.go" - "go.uber.org/zap" -) - -// Transport adheres to transport.Transport. -var _ transport.Transport = (*Transport)(nil) - -const ( - // TransportName is the name of this transport. - TransportName = "NATS" -) - -// Transport acts as both a NATS client and a NATS handler. -type Transport struct { - Encoding Encoding - Conn *nats.Conn - ConnOptions []nats.Option - NatsURL string - Subject string - - sub *nats.Subscription - - Receiver transport.Receiver - // Converter is invoked if the incoming transport receives an undecodable - // message. - Converter transport.Converter - - codec transport.Codec -} - -// New creates a new NATS transport. -func New(natsURL, subject string, opts ...Option) (*Transport, error) { - t := &Transport{ - Subject: subject, - NatsURL: natsURL, - ConnOptions: []nats.Option{}, - } - - err := t.applyOptions(opts...) - if err != nil { - return nil, err - } - - err = t.connect() - if err != nil { - return nil, err - } - - return t, nil -} - -func (t *Transport) connect() error { - var err error - - t.Conn, err = nats.Connect(t.NatsURL, t.ConnOptions...) - - return err -} - -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() bool { - if t.codec == nil { - switch t.Encoding { - case Default: - t.codec = &Codec{} - case StructuredV02: - t.codec = &CodecV02{Encoding: t.Encoding} - case StructuredV03: - t.codec = &CodecV03{Encoding: t.Encoding} - case StructuredV1: - t.codec = &CodecV1{Encoding: t.Encoding} - default: - return false - } - } - return true -} - -// Send implements Transport.Send -func (t *Transport) Send(ctx context.Context, event cloudevents.Event) (context.Context, *cloudevents.Event, error) { - // TODO populate response context properly. - if ok := t.loadCodec(); !ok { - return ctx, nil, fmt.Errorf("unknown encoding set on transport: %d", t.Encoding) - } - - msg, err := t.codec.Encode(ctx, event) - if err != nil { - return ctx, nil, err - } - - if m, ok := msg.(*Message); ok { - return ctx, nil, t.Conn.Publish(t.Subject, m.Body) - } - - return ctx, nil, fmt.Errorf("failed to encode Event into a Message") -} - -// 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) (err error) { - if t.Conn == nil { - return fmt.Errorf("no active nats connection") - } - if t.sub != nil { - return fmt.Errorf("already subscribed") - } - if ok := t.loadCodec(); !ok { - return fmt.Errorf("unknown encoding set on transport: %d", t.Encoding) - } - - // TODO: there could be more than one subscription. Might have to do a map - // of subject to subscription. - - if t.Subject == "" { - return fmt.Errorf("subject required for nats listen") - } - - // Simple Async Subscriber - t.sub, err = t.Conn.Subscribe(t.Subject, func(m *nats.Msg) { - logger := context2.LoggerFrom(ctx) - msg := &Message{ - Body: m.Data, - } - event, err := t.codec.Decode(ctx, msg) - // If codec returns and error, try with the converter if it is set. - if err != nil && t.HasConverter() { - event, err = t.Converter.Convert(ctx, msg, err) - } - if err != nil { - logger.Errorw("failed to decode message", zap.Error(err)) // TODO: create an error channel to pass this up - return - } - // TODO: I do not know enough about NATS to implement reply. - // For now, NATS does not support reply. - if err := t.Receiver.Receive(context.TODO(), *event, nil); err != nil { - logger.Warnw("nats receiver return err", zap.Error(err)) - } - }) - defer func() { - if t.sub != nil { - err2 := t.sub.Unsubscribe() - if err != nil { - err = err2 // Set the returned error if not already set. - } - t.sub = nil - } - }() - <-ctx.Done() - return err -} - -// HasTracePropagation implements Transport.HasTracePropagation -func (t *Transport) HasTracePropagation() bool { - return false -} diff --git a/v1/cloudevents/transport/pubsub/codec.go b/v1/cloudevents/transport/pubsub/codec.go deleted file mode 100644 index b36864e92..000000000 --- a/v1/cloudevents/transport/pubsub/codec.go +++ /dev/null @@ -1,116 +0,0 @@ -package pubsub - -import ( - "context" - "errors" - "fmt" - "sync" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - cecontext "github.com/cloudevents/sdk-go/v1/cloudevents/context" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport" -) - -type Codec struct { - Encoding Encoding - - // DefaultEncodingSelectionFn allows for encoding selection strategies to be injected. - DefaultEncodingSelectionFn EncodingSelector - - v03 *CodecV03 - v1 *CodecV1 - - _v03 sync.Once - _v1 sync.Once -} - -const ( - prefix = "ce-" -) - -var _ transport.Codec = (*Codec)(nil) - -func (c *Codec) loadCodec(encoding Encoding) (transport.Codec, error) { - switch encoding { - case Default: - fallthrough - case BinaryV1, StructuredV1: - c._v1.Do(func() { - c.v1 = &CodecV1{DefaultEncoding: c.Encoding} - }) - return c.v1, nil - case BinaryV03, StructuredV03: - c._v03.Do(func() { - c.v03 = &CodecV03{DefaultEncoding: c.Encoding} - }) - return c.v03, nil - } - - return nil, fmt.Errorf("unknown encoding: %s", encoding) -} - -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) -} - -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 BinaryV03, StructuredV03: - ca := event.Context.AsV03() - event.Context = ca - return event, nil - case BinaryV1, StructuredV1: - ca := event.Context.AsV1() - 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 - } - - // We do not understand the message encoding. - return Unknown -} diff --git a/v1/cloudevents/transport/pubsub/codec_structured.go b/v1/cloudevents/transport/pubsub/codec_structured.go deleted file mode 100644 index 0e62e5d9a..000000000 --- a/v1/cloudevents/transport/pubsub/codec_structured.go +++ /dev/null @@ -1,40 +0,0 @@ -package pubsub - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport" -) - -// CodecStructured represents an structured http transport codec for all versions. -// Intended to be used as a base class. -type CodecStructured struct { - Encoding Encoding -} - -func (v CodecStructured) encodeStructured(ctx context.Context, e cloudevents.Event) (transport.Message, error) { - data, err := json.Marshal(e) - if err != nil { - return nil, err - } - - msg := &Message{ - Attributes: map[string]string{StructuredContentType: cloudevents.ApplicationCloudEventsJSON}, - Data: data, - } - - 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 pubsub.Message") - } - event := cloudevents.New(version) - err := json.Unmarshal(m.Data, &event) - return &event, err -} diff --git a/v1/cloudevents/transport/pubsub/codec_test.go b/v1/cloudevents/transport/pubsub/codec_test.go deleted file mode 100644 index fd8050dd0..000000000 --- a/v1/cloudevents/transport/pubsub/codec_test.go +++ /dev/null @@ -1,309 +0,0 @@ -package pubsub_test - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "testing" - "time" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport/pubsub" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/google/go-cmp/cmp" -) - -const ( - prefix = "ce-" -) - -func strptr(s string) *string { - return &s -} - -func TestCodecEncode(t *testing.T) { - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - testCases := map[string]struct { - codec *pubsub.Codec - event *cloudevents.Event - want *pubsub.Message - wantErr error - }{ - "simple v03 structured": { - codec: &pubsub.Codec{Encoding: pubsub.StructuredV03}, - event: &cloudevents.Event{ - Context: cloudevents.EventContextV03{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - Subject: strptr("a-subject"), - }.AsV03(), - }, - want: &pubsub.Message{ - Attributes: map[string]string{ - "Content-Type": cloudevents.ApplicationCloudEventsJSON, - }, - Data: func() []byte { - body := map[string]interface{}{ - "specversion": "0.3", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - "subject": "a-subject", - } - return toBytes(body) - }(), - }, - }, - "simple v03 binary": { - codec: &pubsub.Codec{Encoding: pubsub.BinaryV03}, - event: &cloudevents.Event{ - Context: &cloudevents.EventContextV03{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - Subject: strptr("a-subject"), - }, - }, - want: &pubsub.Message{ - Attributes: map[string]string{ - "ce-specversion": "0.3", - "ce-id": "ABC-123", - "ce-type": "com.example.test", - "ce-source": "http://example.com/source", - "ce-subject": "a-subject", - "ce-datacontenttype": "application/json", - }, - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, err := tc.codec.Encode(context.TODO(), *tc.event) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - if msg, ok := got.(*pubsub.Message); ok { - // It is hard to read the byte dump - want := string(tc.want.Data) - got := string(msg.Data) - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - return - } - } - t.Errorf("unexpected message (-want, +got) = %v", diff) - } - }) - } -} - -func TestCodecDecode(t *testing.T) { - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - testCases := map[string]struct { - codec *pubsub.Codec - msg *pubsub.Message - want *cloudevents.Event - wantErr error - }{ - "simple v3 structured": { - codec: &pubsub.Codec{Encoding: pubsub.StructuredV03}, - msg: &pubsub.Message{ - Attributes: map[string]string{ - "Content-Type": cloudevents.ApplicationCloudEventsJSON, - }, - Data: func() []byte { - body := map[string]interface{}{ - "specversion": "0.3", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - "subject": "a-subject", - } - return toBytes(body) - }(), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV03{ - SpecVersion: cloudevents.CloudEventsVersionV03, - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - Subject: strptr("a-subject"), - }, - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, err := tc.codec.Decode(context.TODO(), tc.msg) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected event (-want, +got) = %v", diff) - } - }) - } -} - -type DataExample struct { - AnInt int `json:"a,omitempty" xml:"a,omitempty"` - AString string `json:"b,omitempty" xml:"b,omitempty"` - AnArray []string `json:"c,omitempty" xml:"c,omitempty"` - AMap map[string]map[string]int `json:"d,omitempty" xml:"d,omitempty"` - ATime *time.Time `json:"e,omitempty" xml:"e,omitempty"` -} - -func TestCodecAsMiddleware(t *testing.T) { - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - for _, encoding := range []pubsub.Encoding{pubsub.StructuredV03} { - - testCases := map[string]struct { - codec *pubsub.Codec - event *cloudevents.Event - want *cloudevents.Event - wantErr error - }{ - "simple data": { - codec: &pubsub.Codec{Encoding: encoding}, - event: &cloudevents.Event{ - Context: cloudevents.EventContextV03{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV03(), - Data: map[string]string{ - "a": "apple", - "b": "banana", - }, - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV03{ - SpecVersion: cloudevents.CloudEventsVersionV03, - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }, - Data: map[string]interface{}{ - "a": "apple", - "b": "banana", - }, - DataEncoded: true, - }, - }, - "struct data": { - codec: &pubsub.Codec{Encoding: encoding}, - event: &cloudevents.Event{ - Context: cloudevents.EventContextV03{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV03(), - Data: DataExample{ - AnInt: 42, - AString: "testing", - }, - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV03{ - SpecVersion: cloudevents.CloudEventsVersionV03, - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }, - Data: &DataExample{ - AnInt: 42, - AString: "testing", - }, - DataEncoded: true, - }, - }, - } - for n, tc := range testCases { - n = fmt.Sprintf("%s, %s", encoding, n) - t.Run(n, func(t *testing.T) { - - msg1, err := tc.codec.Encode(context.TODO(), *tc.event) - if err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - midEvent, err := tc.codec.Decode(context.TODO(), msg1) - if err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - msg2, err := tc.codec.Encode(context.TODO(), *midEvent) - if err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - got, err := tc.codec.Decode(context.TODO(), msg2) - if err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if tc.event.Data != nil { - // We have to be pretty tricky in the test to get the correct type. - data, _ := types.Allocate(tc.want.Data) - err := got.DataAs(&data) - if err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - got.Data = data - } - - ctxv3 := got.Context.AsV03() - got.Context = ctxv3 - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected event (-want, +got) = %v", diff) - } - }) - } - } -} - -func toBytes(body map[string]interface{}) []byte { - b, err := json.Marshal(body) - if err != nil { - return []byte(fmt.Sprintf(`{"error":%q}`, err.Error())) - } - return b -} diff --git a/v1/cloudevents/transport/pubsub/codec_v03.go b/v1/cloudevents/transport/pubsub/codec_v03.go deleted file mode 100644 index 8d41fb12a..000000000 --- a/v1/cloudevents/transport/pubsub/codec_v03.go +++ /dev/null @@ -1,286 +0,0 @@ -package pubsub - -import ( - "context" - "encoding/json" - "fmt" - "strings" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - cecontext "github.com/cloudevents/sdk-go/v1/cloudevents/context" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -const ( - StructuredContentType = "Content-Type" -) - -type CodecV03 struct { - CodecStructured - - DefaultEncoding Encoding -} - -var _ transport.Codec = (*CodecV03)(nil) - -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 - } - } - switch encoding { - case Default: - fallthrough - case StructuredV03: - return v.encodeStructured(ctx, e) - case BinaryV03: - return v.encodeBinary(ctx, e) - default: - return nil, fmt.Errorf("unknown encoding: %d", encoding) - } -} - -func (v CodecV03) Decode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) { - // only structured is supported as of v0.3 - switch v.inspectEncoding(ctx, msg) { - case StructuredV03: - return v.decodeStructured(ctx, cloudevents.CloudEventsVersionV03, msg) - case BinaryV03: - event := cloudevents.New(cloudevents.CloudEventsVersionV03) - return v.decodeBinary(ctx, msg, &event) - default: - return nil, transport.NewErrMessageEncodingUnknown("v03", TransportName) - } -} - -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 - } - if m.Attributes[StructuredContentType] == cloudevents.ApplicationCloudEventsJSON { - return StructuredV03 - } - return BinaryV03 -} - -func (v CodecV03) encodeBinary(ctx context.Context, e cloudevents.Event) (transport.Message, error) { - attributes, err := v.toAttributes(e) - if err != nil { - return nil, err - } - data, err := e.DataBytes() - if err != nil { - return nil, err - } - - msg := &Message{ - Attributes: attributes, - Data: data, - } - - return msg, nil -} - -func (v CodecV03) toAttributes(e cloudevents.Event) (map[string]string, error) { - a := make(map[string]string) - a[prefix+"specversion"] = e.SpecVersion() - a[prefix+"type"] = e.Type() - a[prefix+"source"] = e.Source() - a[prefix+"id"] = e.ID() - if !e.Time().IsZero() { - t := types.Timestamp{Time: e.Time()} // TODO: change e.Time() to return string so I don't have to do this. - a[prefix+"time"] = t.String() - } - if e.DataSchema() != "" { - a[prefix+"schemaurl"] = e.DataSchema() - } - - if e.DataContentType() != "" { - a[prefix+"datacontenttype"] = e.DataContentType() - } else { - a[prefix+"datacontenttype"] = cloudevents.ApplicationJSON - } - - if e.Subject() != "" { - a[prefix+"subject"] = e.Subject() - } - - if e.DeprecatedDataContentEncoding() != "" { - a[prefix+"datacontentencoding"] = e.DeprecatedDataContentEncoding() - } - - for k, v := range e.Extensions() { - if mapVal, ok := v.(map[string]interface{}); ok { - for subkey, subval := range mapVal { - encoded, err := json.Marshal(subval) - if err != nil { - return nil, err - } - a[prefix+k+"-"+subkey] = string(encoded) - } - continue - } - if s, ok := v.(string); ok { - a[prefix+k] = s - continue - } - encoded, err := json.Marshal(v) - if err != nil { - return nil, err - } - a[prefix+k] = string(encoded) - } - - return a, nil -} - -func (v CodecV03) decodeBinary(ctx context.Context, msg transport.Message, event *cloudevents.Event) (*cloudevents.Event, error) { - m, ok := msg.(*Message) - if !ok { - return nil, fmt.Errorf("failed to convert transport.Message to pubsub.Message") - } - err := v.fromAttributes(m.Attributes, event) - if err != nil { - return nil, err - } - var data interface{} - if len(m.Data) > 0 { - data = m.Data - } - event.Data = data - event.DataEncoded = true - return event, nil -} - -func (v CodecV03) fromAttributes(a map[string]string, event *cloudevents.Event) error { - // Normalize attributes. - for k, v := range a { - ck := strings.ToLower(k) - if k != ck { - delete(a, k) - a[ck] = v - } - } - - ec := event.Context - - if sv := a[prefix+"specversion"]; sv != "" { - if err := ec.SetSpecVersion(sv); err != nil { - return err - } - } - delete(a, prefix+"specversion") - - if id := a[prefix+"id"]; id != "" { - if err := ec.SetID(id); err != nil { - return err - } - } - delete(a, prefix+"id") - - if t := a[prefix+"type"]; t != "" { - if err := ec.SetType(t); err != nil { - return err - } - } - delete(a, prefix+"type") - - if s := a[prefix+"source"]; s != "" { - if err := ec.SetSource(s); err != nil { - return err - } - } - delete(a, prefix+"source") - - if t := a[prefix+"time"]; t != "" { - if timestamp, err := types.ParseTimestamp(t); err != nil { - return err - } else if err := ec.SetTime(timestamp.Time); err != nil { - return err - } - } - delete(a, prefix+"time") - - if s := a[prefix+"schemaurl"]; s != "" { - if err := ec.SetDataSchema(s); err != nil { - return err - } - } - delete(a, prefix+"schemaurl") - - if s := a[prefix+"subject"]; s != "" { - if err := ec.SetSubject(s); err != nil { - return err - } - } - delete(a, prefix+"subject") - - if s := a[prefix+"datacontenttype"]; s != "" { - if err := ec.SetDataContentType(s); err != nil { - return err - } - } - delete(a, prefix+"datacontenttype") - - if s := a[prefix+"datacontentencoding"]; s != "" { - if err := ec.DeprecatedSetDataContentEncoding(s); err != nil { - return err - } - } - delete(a, prefix+"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 a { - if len(k) > len(prefix) && strings.EqualFold(k[:len(prefix)], prefix) { - ak := strings.ToLower(k[len(prefix):]) - 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 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), &tmp); err == nil { - extensions[ak] = tmp - } else { - // If we can't unmarshal the data, treat it as a string. - extensions[ak] = v - } - } - } - } - event.Context = ec - if len(extensions) > 0 { - for k, v := range extensions { - event.SetExtension(k, v) - } - } - return nil -} diff --git a/v1/cloudevents/transport/pubsub/codec_v03_test.go b/v1/cloudevents/transport/pubsub/codec_v03_test.go deleted file mode 100644 index 6a6ce026e..000000000 --- a/v1/cloudevents/transport/pubsub/codec_v03_test.go +++ /dev/null @@ -1,283 +0,0 @@ -package pubsub_test - -import ( - "context" - "net/url" - "testing" - "time" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport/pubsub" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/google/go-cmp/cmp" -) - -func TestCodecV03_Encode(t *testing.T) { - now := types.Timestamp{Time: time.Now().UTC()} - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - schemaUrl, _ := url.Parse("http://example.com/schema") - schema := &types.URLRef{URL: *schemaUrl} - - testCases := map[string]struct { - codec pubsub.CodecV03 - event cloudevents.Event - want *pubsub.Message - wantErr error - }{ - "simple v0.3 default": { - codec: pubsub.CodecV03{}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV03{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV03(), - }, - want: &pubsub.Message{ - Attributes: map[string]string{ - "Content-Type": cloudevents.ApplicationCloudEventsJSON, - }, - Data: func() []byte { - body := map[string]interface{}{ - "specversion": "0.3", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - "full v0.3 default": { - codec: pubsub.CodecV03{}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV03{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }.AsV03(), - Data: map[string]interface{}{ - "hello": "world", - }, - }, - want: &pubsub.Message{ - Attributes: map[string]string{ - "Content-Type": cloudevents.ApplicationCloudEventsJSON, - }, - Data: func() []byte { - body := map[string]interface{}{ - "specversion": "0.3", - "datacontenttype": "application/json", - "data": map[string]interface{}{ - "hello": "world", - }, - "id": "ABC-123", - "time": now, - "type": "com.example.test", - "test": "extended", - "schemaurl": "http://example.com/schema", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - "simple v0.3 structured": { - codec: pubsub.CodecV03{DefaultEncoding: pubsub.StructuredV03}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV03{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV03(), - }, - want: &pubsub.Message{ - Attributes: map[string]string{ - "Content-Type": cloudevents.ApplicationCloudEventsJSON, - }, - Data: func() []byte { - body := map[string]interface{}{ - "specversion": "0.3", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - "full v0.3 structured": { - codec: pubsub.CodecV03{DefaultEncoding: pubsub.StructuredV03}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV03{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }.AsV03(), - Data: map[string]interface{}{ - "hello": "world", - }, - }, - want: &pubsub.Message{ - Attributes: map[string]string{ - "Content-Type": cloudevents.ApplicationCloudEventsJSON, - }, - Data: func() []byte { - body := map[string]interface{}{ - "specversion": "0.3", - "datacontenttype": "application/json", - "data": map[string]interface{}{ - "hello": "world", - }, - "id": "ABC-123", - "time": now, - "type": "com.example.test", - "test": "extended", - "schemaurl": "http://example.com/schema", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, err := tc.codec.Encode(context.TODO(), tc.event) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - - if msg, ok := got.(*pubsub.Message); ok { - // It is hard to read the byte dump - want := string(tc.want.Data) - got := string(msg.Data) - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("unexpected message body (-want, +got) = %v", diff) - return - } - } - - t.Errorf("unexpected message (-want, +got) = %v", diff) - } - }) - } -} - -// TODO: figure out extensions for v0.3 - -func TestCodecV03_Decode(t *testing.T) { - now := types.Timestamp{Time: time.Now()} - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URLRef{URL: *sourceUrl} - - schemaUrl, _ := url.Parse("http://example.com/schema") - schema := &types.URLRef{URL: *schemaUrl} - - testCases := map[string]struct { - codec pubsub.CodecV03 - msg *pubsub.Message - want *cloudevents.Event - wantErr error - }{ - "simple v0.3 structured": { - codec: pubsub.CodecV03{}, - msg: &pubsub.Message{ - Attributes: map[string]string{ - "Content-Type": cloudevents.ApplicationCloudEventsJSON, - }, - Data: toBytes(map[string]interface{}{ - "specversion": "0.3", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - }), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV03{ - SpecVersion: cloudevents.CloudEventsVersionV03, - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }, - }, - }, - "full v0.3 structured": { - codec: pubsub.CodecV03{}, - msg: &pubsub.Message{ - Attributes: map[string]string{ - "Content-Type": cloudevents.ApplicationCloudEventsJSON, - }, - Data: toBytes(map[string]interface{}{ - "specversion": "0.3", - "datacontenttype": "application/json", - "data": map[string]interface{}{ - "hello": "world", - }, - "id": "ABC-123", - "time": now, - "type": "com.example.test", - "test": "extended", - "schemaurl": "http://example.com/schema", - "source": "http://example.com/source", - }), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV03{ - SpecVersion: cloudevents.CloudEventsVersionV03, - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - SchemaURL: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }, - Data: toBytes(map[string]interface{}{ - "hello": "world", - }), - DataEncoded: true, - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, err := tc.codec.Decode(context.TODO(), tc.msg) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected event (-want, +got) = %v", diff) - } - }) - } -} diff --git a/v1/cloudevents/transport/pubsub/codec_v1.go b/v1/cloudevents/transport/pubsub/codec_v1.go deleted file mode 100644 index 092bf6d6d..000000000 --- a/v1/cloudevents/transport/pubsub/codec_v1.go +++ /dev/null @@ -1,242 +0,0 @@ -package pubsub - -import ( - "context" - "fmt" - "strings" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - cecontext "github.com/cloudevents/sdk-go/v1/cloudevents/context" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -type CodecV1 struct { - CodecStructured - - DefaultEncoding Encoding -} - -var _ transport.Codec = (*CodecV1)(nil) - -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 - } - } - switch encoding { - case Default: - fallthrough - case StructuredV1: - return v.encodeStructured(ctx, e) - case BinaryV1: - return v.encodeBinary(ctx, e) - default: - return nil, fmt.Errorf("unknown encoding: %d", v.Encoding) - } -} - -func (v CodecV1) Decode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) { - // only structured is supported as of v0.3 - switch v.inspectEncoding(ctx, msg) { - case StructuredV1: - return v.decodeStructured(ctx, cloudevents.CloudEventsVersionV1, msg) - case BinaryV1: - event := cloudevents.New(cloudevents.CloudEventsVersionV1) - return v.decodeBinary(ctx, msg, &event) - default: - return nil, transport.NewErrMessageEncodingUnknown("V1", TransportName) - } -} - -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 - } - if m.Attributes[StructuredContentType] == cloudevents.ApplicationCloudEventsJSON { - return StructuredV1 - } - return BinaryV1 -} - -func (v CodecV1) encodeBinary(ctx context.Context, e cloudevents.Event) (transport.Message, error) { - attributes, err := v.toAttributes(e) - if err != nil { - return nil, err - } - data, err := e.DataBytes() - if err != nil { - return nil, err - } - - msg := &Message{ - Attributes: attributes, - Data: data, - } - - return msg, nil -} - -func (v CodecV1) toAttributes(e cloudevents.Event) (map[string]string, error) { - a := make(map[string]string) - a[prefix+"specversion"] = e.SpecVersion() - a[prefix+"type"] = e.Type() - a[prefix+"source"] = e.Source() - a[prefix+"id"] = e.ID() - if !e.Time().IsZero() { - t := types.Timestamp{Time: e.Time()} // TODO: change e.Time() to return string so I don't have to do this. - a[prefix+"time"] = t.String() - } - if e.DataSchema() != "" { - a[prefix+"dataschema"] = e.DataSchema() - } - - if e.DataContentType() != "" { - a[prefix+"datacontenttype"] = e.DataContentType() - } else { - a[prefix+"datacontenttype"] = cloudevents.ApplicationJSON - } - - if e.Subject() != "" { - a[prefix+"subject"] = e.Subject() - } - - if e.DeprecatedDataContentEncoding() != "" { - a[prefix+"datacontentencoding"] = e.DeprecatedDataContentEncoding() - } - - for k, v := range e.Extensions() { - k = strings.ToLower(k) - cstr, err := types.Format(v) - if err != nil { - return a, err - } - a[prefix+k] = cstr - } - return a, nil -} - -func (v CodecV1) decodeBinary(ctx context.Context, msg transport.Message, event *cloudevents.Event) (*cloudevents.Event, error) { - m, ok := msg.(*Message) - if !ok { - return nil, fmt.Errorf("failed to convert transport.Message to pubsub.Message") - } - err := v.fromAttributes(m.Attributes, event) - if err != nil { - return nil, err - } - var data interface{} - if len(m.Data) > 0 { - data = m.Data - } - event.Data = data - event.DataEncoded = true - return event, nil -} - -func (v CodecV1) fromAttributes(a map[string]string, event *cloudevents.Event) error { - // Normalize attributes. - for k, v := range a { - ck := strings.ToLower(k) - if k != ck { - delete(a, k) - a[ck] = v - } - } - - ec := event.Context - - if sv := a[prefix+"specversion"]; sv != "" { - if err := ec.SetSpecVersion(sv); err != nil { - return err - } - } - delete(a, prefix+"specversion") - - if id := a[prefix+"id"]; id != "" { - if err := ec.SetID(id); err != nil { - return err - } - } - delete(a, prefix+"id") - - if t := a[prefix+"type"]; t != "" { - if err := ec.SetType(t); err != nil { - return err - } - } - delete(a, prefix+"type") - - if s := a[prefix+"source"]; s != "" { - if err := ec.SetSource(s); err != nil { - return err - } - } - delete(a, prefix+"source") - - if t := a[prefix+"time"]; t != "" { - if timestamp, err := types.ParseTimestamp(t); err != nil { - return err - } else if err := ec.SetTime(timestamp.Time); err != nil { - return err - } - } - delete(a, prefix+"time") - - if s := a[prefix+"dataschema"]; s != "" { - if err := ec.SetDataSchema(s); err != nil { - return err - } - } - delete(a, prefix+"dataschema") - - if s := a[prefix+"subject"]; s != "" { - if err := ec.SetSubject(s); err != nil { - return err - } - } - delete(a, prefix+"subject") - - if s := a[prefix+"datacontenttype"]; s != "" { - if err := ec.SetDataContentType(s); err != nil { - return err - } - } - delete(a, prefix+"datacontenttype") - - if s := a[prefix+"datacontentencoding"]; s != "" { - if err := ec.DeprecatedSetDataContentEncoding(s); err != nil { - return err - } - } - delete(a, prefix+"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 a { - if len(k) > len(prefix) && strings.EqualFold(k[:len(prefix)], prefix) { - ak := strings.ToLower(k[len(prefix):]) - extensions[ak] = v - } - } - event.Context = ec - if len(extensions) > 0 { - for k, v := range extensions { - event.SetExtension(k, v) - } - } - return nil -} diff --git a/v1/cloudevents/transport/pubsub/codec_v1_test.go b/v1/cloudevents/transport/pubsub/codec_v1_test.go deleted file mode 100644 index b43b31496..000000000 --- a/v1/cloudevents/transport/pubsub/codec_v1_test.go +++ /dev/null @@ -1,362 +0,0 @@ -package pubsub_test - -import ( - "context" - "net/url" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport/pubsub" - "github.com/cloudevents/sdk-go/v1/cloudevents/types" -) - -func TestCodecV1_Encode(t *testing.T) { - now := types.Timestamp{Time: time.Now().UTC()} - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URIRef{URL: *sourceUrl} - - schemaUrl, _ := url.Parse("http://example.com/schema") - schema := &types.URI{URL: *schemaUrl} - - testCases := map[string]struct { - codec pubsub.CodecV1 - event cloudevents.Event - want *pubsub.Message - wantErr error - }{ - "simple v1.0 default": { - codec: pubsub.CodecV1{}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV1{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV1(), - }, - want: &pubsub.Message{ - Attributes: map[string]string{ - "Content-Type": cloudevents.ApplicationCloudEventsJSON, - }, - Data: func() []byte { - body := map[string]interface{}{ - "specversion": "1.0", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - "full v1.0 default": { - codec: pubsub.CodecV1{}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV1{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - DataSchema: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - "generation": "1579743478182200", - }, - }.AsV1(), - Data: map[string]interface{}{ - "hello": "world", - }, - }, - want: &pubsub.Message{ - Attributes: map[string]string{ - "Content-Type": cloudevents.ApplicationCloudEventsJSON, - }, - Data: func() []byte { - body := map[string]interface{}{ - "specversion": "1.0", - "datacontenttype": "application/json", - "data": map[string]interface{}{ - "hello": "world", - }, - "id": "ABC-123", - "time": now, - "type": "com.example.test", - "test": "extended", - "generation": "1579743478182200", - "dataschema": "http://example.com/schema", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - "full v1.0 binary": { - codec: pubsub.CodecV1{ - DefaultEncoding: pubsub.BinaryV1, - }, - event: cloudevents.Event{ - Context: cloudevents.EventContextV1{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - DataSchema: schema, - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - "generation": "1579743478182200", - }, - }.AsV1(), - Data: map[string]interface{}{ - "hello": "world", - }, - }, - want: &pubsub.Message{ - Attributes: map[string]string{ - prefix + "specversion": "1.0", - prefix + "id": "ABC-123", - prefix + "datacontenttype": cloudevents.ApplicationJSON, - prefix + "time": now.String(), - prefix + "type": "com.example.test", - prefix + "dataschema": "http://example.com/schema", - prefix + "source": "http://example.com/source", - prefix + "test": "extended", - prefix + "generation": "1579743478182200", - }, - Data: func() []byte { - data := map[string]interface{}{ - "hello": "world", - } - return toBytes(data) - }(), - }, - }, - "simple v1.0 structured": { - codec: pubsub.CodecV1{DefaultEncoding: pubsub.StructuredV1}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV1{ - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }.AsV1(), - }, - want: &pubsub.Message{ - Attributes: map[string]string{ - "Content-Type": cloudevents.ApplicationCloudEventsJSON, - }, - Data: func() []byte { - body := map[string]interface{}{ - "specversion": "1.0", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - "full v1.0 structured": { - codec: pubsub.CodecV1{DefaultEncoding: pubsub.StructuredV1}, - event: cloudevents.Event{ - Context: cloudevents.EventContextV1{ - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - DataSchema: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - }, - }.AsV1(), - Data: map[string]interface{}{ - "hello": "world", - }, - }, - want: &pubsub.Message{ - Attributes: map[string]string{ - "Content-Type": cloudevents.ApplicationCloudEventsJSON, - }, - Data: func() []byte { - body := map[string]interface{}{ - "specversion": "1.0", - "datacontenttype": "application/json", - "data": map[string]interface{}{ - "hello": "world", - }, - "id": "ABC-123", - "time": now, - "type": "com.example.test", - "test": "extended", - "dataschema": "http://example.com/schema", - "source": "http://example.com/source", - } - return toBytes(body) - }(), - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, err := tc.codec.Encode(context.TODO(), tc.event) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - - if msg, ok := got.(*pubsub.Message); ok { - // It is hard to read the byte dump - want := string(tc.want.Data) - got := string(msg.Data) - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("unexpected message body (-want, +got) = %v", diff) - return - } - } - - t.Errorf("unexpected message (-want, +got) = %v", diff) - } - }) - } -} - -func TestCodecV1_Decode(t *testing.T) { - now := types.Timestamp{Time: time.Now()} - sourceUrl, _ := url.Parse("http://example.com/source") - source := &types.URIRef{URL: *sourceUrl} - - schemaUrl, _ := url.Parse("http://example.com/schema") - schema := &types.URI{URL: *schemaUrl} - - testCases := map[string]struct { - codec pubsub.CodecV1 - msg *pubsub.Message - want *cloudevents.Event - wantErr error - }{ - "simple v1.0 structured": { - codec: pubsub.CodecV1{}, - msg: &pubsub.Message{ - Attributes: map[string]string{ - "Content-Type": cloudevents.ApplicationCloudEventsJSON, - }, - Data: toBytes(map[string]interface{}{ - "specversion": "1.0", - "id": "ABC-123", - "type": "com.example.test", - "source": "http://example.com/source", - }), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV1{ - SpecVersion: cloudevents.CloudEventsVersionV1, - Type: "com.example.test", - Source: *source, - ID: "ABC-123", - }, - }, - }, - "full v1.0 structured": { - codec: pubsub.CodecV1{}, - msg: &pubsub.Message{ - Attributes: map[string]string{ - "Content-Type": cloudevents.ApplicationCloudEventsJSON, - }, - Data: toBytes(map[string]interface{}{ - "specversion": "1.0", - "datacontenttype": "application/json", - "data": map[string]interface{}{ - "hello": "world", - }, - "id": "ABC-123", - "time": now, - "type": "com.example.test", - "test": "extended", - "generation": "1579743478182200", - "dataschema": "http://example.com/schema", - "source": "http://example.com/source", - }), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV1{ - SpecVersion: cloudevents.CloudEventsVersionV1, - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - DataSchema: schema, - DataContentType: cloudevents.StringOfApplicationJSON(), - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - "generation": "1579743478182200", - }, - }, - Data: toBytes(map[string]interface{}{ - "hello": "world", - }), - DataEncoded: true, - }, - }, - "full v1.0 binary": { - codec: pubsub.CodecV1{}, - msg: &pubsub.Message{ - Attributes: map[string]string{ - prefix + "specversion": "1.0", - prefix + "id": "ABC-123", - prefix + "time": now.String(), - prefix + "type": "com.example.test", - prefix + "dataschema": "http://example.com/schema", - prefix + "source": "http://example.com/source", - prefix + "test": "extended", - prefix + "generation": "1579743478182200", - }, - Data: toBytes(map[string]interface{}{ - "hello": "world", - }), - }, - want: &cloudevents.Event{ - Context: &cloudevents.EventContextV1{ - SpecVersion: cloudevents.CloudEventsVersionV1, - ID: "ABC-123", - Time: &now, - Type: "com.example.test", - DataSchema: schema, - Source: *source, - Extensions: map[string]interface{}{ - "test": "extended", - "generation": "1579743478182200", - }, - }, - Data: toBytes(map[string]interface{}{ - "hello": "world", - }), - DataEncoded: true, - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - got, err := tc.codec.Decode(context.TODO(), tc.msg) - - if tc.wantErr != nil || err != nil { - if diff := cmp.Diff(tc.wantErr, err); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected event (-want, +got) = %v", diff) - } - }) - } -} diff --git a/v1/cloudevents/transport/pubsub/context/context.go b/v1/cloudevents/transport/pubsub/context/context.go deleted file mode 100644 index e64a5ee71..000000000 --- a/v1/cloudevents/transport/pubsub/context/context.go +++ /dev/null @@ -1,94 +0,0 @@ -package context - -import ( - "context" - "strings" - "time" - - "cloud.google.com/go/pubsub" -) - -// TransportContext allows a Receiver to understand the context of a request. -type TransportContext struct { - ID string - PublishTime time.Time - Project string - Topic string - Subscription string - Method string // push or pull -} - -// NewTransportContext creates a new TransportContext from a pubsub.Message. -func NewTransportContext(project, topic, subscription, method string, msg *pubsub.Message) TransportContext { - var tx *TransportContext - if msg != nil { - tx = &TransportContext{ - ID: msg.ID, - PublishTime: msg.PublishTime, - Project: project, - Topic: topic, - Subscription: subscription, - Method: method, - } - } else { - tx = &TransportContext{} - } - return *tx -} - -// 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") - - if tx.ID != "" { - b.WriteString(" ID: " + tx.ID + "\n") - } - if !tx.PublishTime.IsZero() { - b.WriteString(" PublishTime: " + tx.PublishTime.String() + "\n") - } - - if tx.Project != "" { - b.WriteString(" Project: " + tx.Project + "\n") - } - - if tx.Topic != "" { - b.WriteString(" Topic: " + tx.Topic + "\n") - } - - if tx.Subscription != "" { - b.WriteString(" Subscription: " + tx.Subscription + "\n") - } - - if tx.Method != "" { - b.WriteString(" Method: " + tx.Method + "\n") - } - - return b.String() -} - -// 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{} -} diff --git a/v1/cloudevents/transport/pubsub/context/context_test.go b/v1/cloudevents/transport/pubsub/context/context_test.go deleted file mode 100644 index 960b2b7c5..000000000 --- a/v1/cloudevents/transport/pubsub/context/context_test.go +++ /dev/null @@ -1 +0,0 @@ -package context_test diff --git a/v1/cloudevents/transport/pubsub/doc.go b/v1/cloudevents/transport/pubsub/doc.go deleted file mode 100644 index b45504609..000000000 --- a/v1/cloudevents/transport/pubsub/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -/* -Package pubsub implements the CloudEvent transport implementation using pubsub. -*/ -package pubsub diff --git a/v1/cloudevents/transport/pubsub/encoding.go b/v1/cloudevents/transport/pubsub/encoding.go deleted file mode 100644 index 1edea5aed..000000000 --- a/v1/cloudevents/transport/pubsub/encoding.go +++ /dev/null @@ -1,114 +0,0 @@ -package pubsub - -import ( - "context" - - "github.com/cloudevents/sdk-go/v1/cloudevents" -) - -// Encoding to use for pubsub transport. -type Encoding int32 - -type EncodingSelector func(context.Context, cloudevents.Event) Encoding - -const ( - // Default allows pubsub transport implementation to pick. - Default Encoding = iota - // BinaryV03 is Binary CloudEvents spec v0.3. - BinaryV03 - // BinaryV1 is Binary CloudEvents spec v1.0. - BinaryV1 - // StructuredV03 is Structured CloudEvents spec v0.3. - StructuredV03 - // StructuredV1 is Structured CloudEvents spec v1.0. - StructuredV1 - - // 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" -) - -// 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, cloudevents.CloudEventsVersionV02, 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, cloudevents.CloudEventsVersionV02, 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 BinaryV03, BinaryV1: - return "Binary Encoding " + e.Version() - - // Structured - case StructuredV03, StructuredV1: - return "Structured Encoding " + e.Version() - - default: - return "Unknown Encoding" - } -} - -// Version pretty-prints the encoding version as a string. -func (e Encoding) Version() string { - switch e { - - // Version 0.2 - // Version 0.3 - case Default, BinaryV03, StructuredV03: - return "v0.3" - - // Version 1.0 - case BinaryV1, StructuredV1: - return "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 BinaryV03, BinaryV1: - return Binary - case StructuredV03, StructuredV1: - return Structured - default: - return Binary - } -} diff --git a/v1/cloudevents/transport/pubsub/internal/connection.go b/v1/cloudevents/transport/pubsub/internal/connection.go deleted file mode 100644 index af1237e12..000000000 --- a/v1/cloudevents/transport/pubsub/internal/connection.go +++ /dev/null @@ -1,310 +0,0 @@ -package internal - -import ( - "context" - "errors" - "fmt" - "sync" - "time" - - "cloud.google.com/go/pubsub" - "github.com/cloudevents/sdk-go/v1/cloudevents" - pscontext "github.com/cloudevents/sdk-go/v1/cloudevents/transport/pubsub/context" -) - -type topicInfo struct { - topic *pubsub.Topic - wasCreated bool - once sync.Once - err error -} - -type subInfo struct { - sub *pubsub.Subscription - wasCreated bool - once sync.Once - err error -} - -// Connection acts as either a pubsub topic or a pubsub subscription . -type Connection struct { - // AllowCreateTopic controls if the transport can create a topic if it does - // not exist. - AllowCreateTopic bool - - // AllowCreateSubscription controls if the transport can create a - // subscription if it does not exist. - AllowCreateSubscription bool - - ProjectID string - - Client *pubsub.Client - - TopicID string - topicInfo *topicInfo - - SubscriptionID string - subInfo *subInfo - - // Held when reading or writing topicInfo and subInfo. This is only - // held while reading the pointer, the structure internally manage - // their own internal concurrency. This also controls - // the update of AckDeadline and RetentionDuration if those are - // nil on start. - initLock sync.Mutex - - // ReceiveSettings is used to configure Pubsub pull subscription. - ReceiveSettings *pubsub.ReceiveSettings - - // AckDeadline is Pub/Sub AckDeadline. - // Default is 30 seconds. - // This can only be set prior to first call of any function. - AckDeadline *time.Duration - // RetentionDuration is Pub/Sub RetentionDuration. - // Default is 25 hours. - // This can only be set prior to first call of any function. - RetentionDuration *time.Duration -} - -const ( - DefaultAckDeadline = 30 * time.Second - DefaultRetentionDuration = 25 * time.Hour -) - -var DefaultReceiveSettings = pubsub.ReceiveSettings{ - // Pubsub default receive settings will fill in other values. - // https://godoc.org/cloud.google.com/go/pubsub#Client.Subscription - - // Override the default number of goroutines. - // This is a magical number now. This has shown throughput improvements empirically - // by at least 10x (compared to the default value). - NumGoroutines: 1000, - Synchronous: false, -} - -func (c *Connection) getOrCreateTopicInfo(ctx context.Context, getAlreadyOpenOnly bool) (*topicInfo, error) { - // See if a topic has already been created or is in the process of being created. - // If not, start creating one. - c.initLock.Lock() - ti := c.topicInfo - if ti == nil && !getAlreadyOpenOnly { - c.topicInfo = &topicInfo{} - ti = c.topicInfo - } - c.initLock.Unlock() - if ti == nil { - return nil, fmt.Errorf("no already open topic") - } - - // Make sure the topic structure is initialized at most once. - ti.once.Do(func() { - var ok bool - // Load the topic. - topic := c.Client.Topic(c.TopicID) - ok, ti.err = topic.Exists(ctx) - if ti.err != nil { - return - } - // If the topic does not exist, create a new topic with the given name. - if !ok { - if !c.AllowCreateTopic { - ti.err = fmt.Errorf("transport not allowed to create topic %q", c.TopicID) - return - } - topic, ti.err = c.Client.CreateTopic(ctx, c.TopicID) - if ti.err != nil { - return - } - ti.wasCreated = true - } - // Success. - ti.topic = topic - }) - if ti.topic == nil { - // Initialization failed, remove this attempt so that future callers - // will try to initialize again. - c.initLock.Lock() - if c.topicInfo == ti { - c.topicInfo = nil - } - c.initLock.Unlock() - - return nil, fmt.Errorf("unable to get or create topic %q, %v", c.TopicID, ti.err) - } - return ti, nil -} - -func (c *Connection) getOrCreateTopic(ctx context.Context, getAlreadyOpenOnly bool) (*pubsub.Topic, error) { - ti, err := c.getOrCreateTopicInfo(ctx, getAlreadyOpenOnly) - if ti != nil { - return ti.topic, nil - } else { - return nil, err - } -} - -// DeleteTopic -func (c *Connection) DeleteTopic(ctx context.Context) error { - ti, err := c.getOrCreateTopicInfo(ctx, true) - - if err != nil { - return errors.New("topic not open") - } - if !ti.wasCreated { - return errors.New("topic was not created by pubsub transport") - } - if err := ti.topic.Delete(ctx); err != nil { - return err - } - - ti.topic.Stop() - - c.initLock.Lock() - if ti == c.topicInfo { - c.topicInfo = nil - } - c.initLock.Unlock() - - return nil -} - -func (c *Connection) getOrCreateSubscriptionInfo(ctx context.Context, getAlreadyOpenOnly bool) (*subInfo, error) { - c.initLock.Lock() - // Default the ack deadline and retention duration config. - // We only do this once. - if c.AckDeadline == nil { - ackDeadline := DefaultAckDeadline - c.AckDeadline = &(ackDeadline) - } - if c.RetentionDuration == nil { - retentionDuration := DefaultRetentionDuration - c.RetentionDuration = &retentionDuration - } - // See if a subscription has already been created or is in the process of being created. - // If not, start creating one. - si := c.subInfo - if si == nil && !getAlreadyOpenOnly { - c.subInfo = &subInfo{} - si = c.subInfo - } - c.initLock.Unlock() - if si == nil { - return nil, fmt.Errorf("no already open subscription") - } - - // Make sure the subscription structure is initialized at most once. - si.once.Do(func() { - // Load the subscription. - var ok bool - sub := c.Client.Subscription(c.SubscriptionID) - ok, si.err = sub.Exists(ctx) - if si.err != nil { - return - } - // If subscription doesn't exist, create it. - if !ok { - if !c.AllowCreateSubscription { - si.err = fmt.Errorf("transport not allowed to create subscription %q", c.SubscriptionID) - return - } - - // Load the topic. - var topic *pubsub.Topic - topic, si.err = c.getOrCreateTopic(ctx, false) - if si.err != nil { - return - } - - // Create a new subscription to the previously created topic - // with the given name. - // TODO: allow to use push config + allow setting the SubscriptionConfig. - sub, si.err = c.Client.CreateSubscription(ctx, c.SubscriptionID, pubsub.SubscriptionConfig{ - Topic: topic, - AckDeadline: *c.AckDeadline, - RetentionDuration: *c.RetentionDuration, - }) - if si.err != nil { - return - } - - si.wasCreated = true - } - if c.ReceiveSettings == nil { - sub.ReceiveSettings = DefaultReceiveSettings - } else { - sub.ReceiveSettings = *c.ReceiveSettings - } - // Success. - si.sub = sub - }) - if si.sub == nil { - // Initialization failed, remove this attempt so that future callers - // will try to initialize again. - c.initLock.Lock() - if c.subInfo == si { - c.subInfo = nil - } - c.initLock.Unlock() - return nil, fmt.Errorf("unable to create subscription %q, %v", c.SubscriptionID, si.err) - } - return si, nil -} - -func (c *Connection) getOrCreateSubscription(ctx context.Context, getAlreadyOpenOnly bool) (*pubsub.Subscription, error) { - si, err := c.getOrCreateSubscriptionInfo(ctx, getAlreadyOpenOnly) - if si != nil { - return si.sub, nil - } else { - return nil, err - } -} - -// DeleteSubscription -func (c *Connection) DeleteSubscription(ctx context.Context) error { - si, err := c.getOrCreateSubscriptionInfo(ctx, true) - - if err != nil { - return errors.New("subscription not open") - } - - if !si.wasCreated { - return errors.New("subscription was not created by pubsub transport") - } - if err := si.sub.Delete(ctx); err != nil { - return err - } - - c.initLock.Lock() - if si == c.subInfo { - c.subInfo = nil - } - c.initLock.Unlock() - - return nil -} - -// Publish -func (c *Connection) Publish(ctx context.Context, msg *pubsub.Message) (*cloudevents.Event, error) { - topic, err := c.getOrCreateTopic(ctx, false) - if err != nil { - return nil, err - } - - r := topic.Publish(ctx, msg) - _, err = r.Get(ctx) - return nil, err -} - -// Start -// NOTE: This is a blocking call. -func (c *Connection) Receive(ctx context.Context, fn func(context.Context, *pubsub.Message)) error { - sub, err := c.getOrCreateSubscription(ctx, false) - if err != nil { - return err - } - // Ok, ready to start pulling. - return sub.Receive(ctx, func(ctx context.Context, m *pubsub.Message) { - ctx = pscontext.WithTransportContext(ctx, pscontext.NewTransportContext(c.ProjectID, c.TopicID, c.SubscriptionID, "pull", m)) - fn(ctx, m) - }) -} diff --git a/v1/cloudevents/transport/pubsub/internal/connection_test.go b/v1/cloudevents/transport/pubsub/internal/connection_test.go deleted file mode 100644 index b524a6890..000000000 --- a/v1/cloudevents/transport/pubsub/internal/connection_test.go +++ /dev/null @@ -1,730 +0,0 @@ -package internal - -import ( - "context" - "fmt" - "sync" - "testing" - "time" - - "cloud.google.com/go/pubsub" - "cloud.google.com/go/pubsub/pstest" - "github.com/google/go-cmp/cmp" - "google.golang.org/api/option" - "google.golang.org/grpc" -) - -type testPubsubClient struct { - srv *pstest.Server - conn *grpc.ClientConn -} - -type failPattern struct { - // Error to return. nil for no injected error - ErrReturn error - // Times to return this error (or non-error). 0 for infinite. - Count int - // Duration to block prior to making the call - Delay time.Duration -} - -// Create a pubsub client. If failureMap is provided, it gives a set of failures to induce in specific methods. -// failureMap is modified by the event processor and should not be read or modified after calling New() -func (pc *testPubsubClient) New(ctx context.Context, projectID string, failureMap map[string][]failPattern) (*pubsub.Client, error) { - pc.srv = pstest.NewServer() - var err error - var conn *grpc.ClientConn - if len(failureMap) == 0 { - conn, err = grpc.Dial(pc.srv.Addr, grpc.WithInsecure()) - } else { - conn, err = grpc.Dial(pc.srv.Addr, grpc.WithInsecure(), grpc.WithUnaryInterceptor(makeFailureIntercept(failureMap))) - - } - if err != nil { - return nil, err - } - pc.conn = conn - return pubsub.NewClient(ctx, projectID, option.WithGRPCConn(conn)) -} - -func (pc *testPubsubClient) Close() { - pc.srv.Close() - pc.conn.Close() -} - -// Make a grpc failure injector that failes the specified methods with the -// specified rates. -func makeFailureIntercept(failureMap map[string][]failPattern) grpc.UnaryClientInterceptor { - var lock sync.Mutex - return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { - var injectedErr error - var delay time.Duration - - lock.Lock() - if failureMap != nil { - fpArr := failureMap[method] - if len(fpArr) != 0 { - injectedErr = fpArr[0].ErrReturn - delay = fpArr[0].Delay - if fpArr[0].Count != 0 { - fpArr[0].Count-- - if fpArr[0].Count == 0 { - failureMap[method] = fpArr[1:] - } - } - } - } - lock.Unlock() - if delay != 0 { - time.Sleep(delay) - } - if injectedErr != nil { - return injectedErr - } else { - return invoker(ctx, method, req, reply, cc, opts...) - } - } - -} - -// Verify that the topic exists prior to the call, deleting it via psconn succeeds, and -// the topic does not exist after the call. -func verifyTopicDeleteWorks(t *testing.T, client *pubsub.Client, psconn *Connection, topicID string) { - ctx := context.Background() - - if ok, err := client.Topic(topicID).Exists(ctx); err != nil || !ok { - t.Errorf("topic id=%s got exists=%v want=true, err=%v", topicID, ok, err) - } - - if err := psconn.DeleteTopic(ctx); err != nil { - t.Errorf("delete topic failed: %v", err) - } - - if ok, err := client.Topic(topicID).Exists(ctx); err != nil || ok { - t.Errorf("topic id=%s got exists=%v want=false, err=%v", topicID, ok, err) - } -} - -// Verify that the topic exists before and after the call, and that deleting it via psconn fails -func verifyTopicDeleteFails(t *testing.T, client *pubsub.Client, psconn *Connection, topicID string) { - ctx := context.Background() - - if ok, err := client.Topic(topicID).Exists(ctx); err != nil || !ok { - t.Errorf("topic id=%s got exists=%v want=true, err=%v", topicID, ok, err) - } - - if err := psconn.DeleteTopic(ctx); err == nil { - t.Errorf("delete topic succeeded unexpectedly") - } - - if ok, err := client.Topic(topicID).Exists(ctx); err != nil || !ok { - t.Errorf("topic id=%s after delete got exists=%v want=true, err=%v", topicID, ok, err) - } -} - -// Test that publishing creates a topic -func TestPublishCreateTopic(t *testing.T) { - ctx := context.Background() - pc := &testPubsubClient{} - defer pc.Close() - - projectID, topicID, subID := "test-project", "test-topic", "test-sub" - - client, err := pc.New(ctx, projectID, nil) - if err != nil { - t.Fatalf("failed to create pubsub client: %v", err) - } - defer client.Close() - - psconn := &Connection{ - AllowCreateSubscription: true, - AllowCreateTopic: true, - Client: client, - ProjectID: projectID, - TopicID: topicID, - SubscriptionID: subID, - } - - msg := &pubsub.Message{ - ID: "msg-id-1", - Data: []byte("msg-data-1"), - } - if _, err := psconn.Publish(ctx, msg); err != nil { - t.Errorf("failed to publish message: %v", err) - } - - verifyTopicDeleteWorks(t, client, psconn, topicID) -} - -// Test that publishing to an already created topic works and doesn't allow topic deletion -func TestPublishExistingTopic(t *testing.T) { - for _, allowCreate := range []bool{true, false} { - t.Run(fmt.Sprintf("allowCreate_%v", allowCreate), func(t *testing.T) { - ctx := context.Background() - pc := &testPubsubClient{} - defer pc.Close() - - projectID, topicID, subID := "test-project", "test-topic", "test-sub" - - client, err := pc.New(ctx, projectID, nil) - if err != nil { - t.Fatalf("failed to create pubsub client: %v", err) - } - defer client.Close() - - psconn := &Connection{ - AllowCreateSubscription: true, - AllowCreateTopic: allowCreate, - Client: client, - ProjectID: projectID, - TopicID: topicID, - SubscriptionID: subID, - } - - topic, err := client.CreateTopic(ctx, topicID) - if err != nil { - t.Fatalf("failed to pre-create topic: %v", err) - } - topic.Stop() - - msg := &pubsub.Message{ - ID: "msg-id-1", - Data: []byte("msg-data-1"), - } - if _, err := psconn.Publish(ctx, msg); err != nil { - t.Errorf("failed to publish message: %v", err) - } - - verifyTopicDeleteFails(t, client, psconn, topicID) - }) - } -} - -// Make sure that Publishing works if the original publish failed due to an -// error in one of the pubsub calls. -func TestPublishAfterPublishFailure(t *testing.T) { - for _, failureMethod := range []string{ - "/google.pubsub.v1.Publisher/GetTopic", - "/google.pubsub.v1.Publisher/CreateTopic", - "/google.pubsub.v1.Publisher/Publish"} { - t.Run(failureMethod, func(t *testing.T) { - ctx := context.Background() - pc := &testPubsubClient{} - defer pc.Close() - - projectID, topicID, subID := "test-project", "test-topic", "test-sub" - - failureMap := make(map[string][]failPattern) - failureMap[failureMethod] = []failPattern{{ - ErrReturn: fmt.Errorf("Injected error"), - Count: 1, - Delay: 0}} - client, err := pc.New(ctx, projectID, failureMap) - if err != nil { - t.Fatalf("failed to create pubsub client: %v", err) - } - defer client.Close() - - psconn := &Connection{ - AllowCreateSubscription: true, - AllowCreateTopic: true, - Client: client, - ProjectID: projectID, - TopicID: topicID, - SubscriptionID: subID, - } - - msg := &pubsub.Message{ - ID: "msg-id-1", - Data: []byte("msg-data-1"), - } - // Fails due to injected failure - if _, err := psconn.Publish(ctx, msg); err == nil { - t.Errorf("Expected publish failure, but didn't see it: %v", err) - } - // Succeeds - if _, err := psconn.Publish(ctx, msg); err != nil { - t.Errorf("failed to publish message: %v", err) - } - verifyTopicDeleteWorks(t, client, psconn, topicID) - }) - } -} - -// Test Publishing after Deleting a first version of a topic -func TestPublishCreateTopicAfterDelete(t *testing.T) { - ctx := context.Background() - pc := &testPubsubClient{} - defer pc.Close() - - projectID, topicID, subID := "test-project", "test-topic", "test-sub" - - client, err := pc.New(ctx, projectID, nil) - if err != nil { - t.Fatalf("failed to create pubsub client: %v", err) - } - defer client.Close() - - psconn := &Connection{ - AllowCreateSubscription: true, - AllowCreateTopic: true, - Client: client, - ProjectID: projectID, - TopicID: topicID, - SubscriptionID: subID, - } - - msg := &pubsub.Message{ - ID: "msg-id-1", - Data: []byte("msg-data-1"), - } - if _, err := psconn.Publish(ctx, msg); err != nil { - t.Errorf("failed to publish message: %v", err) - } - - verifyTopicDeleteWorks(t, client, psconn, topicID) - - if _, err := psconn.Publish(ctx, msg); err != nil { - t.Errorf("failed to publish message: %v", err) - } - - verifyTopicDeleteWorks(t, client, psconn, topicID) -} - -// Test that publishing fails if a topic doesn't exist and topic creation isn't allowed -func TestPublishCreateTopicNotAllowedFails(t *testing.T) { - ctx := context.Background() - pc := &testPubsubClient{} - defer pc.Close() - - projectID, topicID, subID := "test-project", "test-topic", "test-sub" - - client, err := pc.New(ctx, projectID, nil) - if err != nil { - t.Fatalf("failed to create pubsub client: %v", err) - } - defer client.Close() - - psconn := &Connection{ - AllowCreateSubscription: true, - AllowCreateTopic: false, - Client: client, - ProjectID: projectID, - TopicID: topicID, - SubscriptionID: subID, - } - - msg := &pubsub.Message{ - ID: "msg-id-1", - Data: []byte("msg-data-1"), - } - if _, err := psconn.Publish(ctx, msg); err == nil { - t.Errorf("publish succeeded unexpectedly") - } - - if ok, err := client.Topic(topicID).Exists(ctx); err == nil && ok { - t.Errorf("topic id=%s got exists=%v want=false, err=%v", topicID, ok, err) - } -} - -// Test that failures of racing topic opens are reported out -func TestPublishParallelFailure(t *testing.T) { - // This test is racy since it relies on a delay on one goroutine to - // ensure a second hits a sync.Once while the other is still processing - // it. Optimistically try with a short delay, but retry with longer - // ones so a failure is almost certainly a real failure, not a race. - var overallError error - for _, delay := range []time.Duration{time.Second / 4, 2 * time.Second, 10 * time.Second, 40 * time.Second} { - overallError = func() error { - failureMethod := "/google.pubsub.v1.Publisher/GetTopic" - ctx := context.Background() - pc := &testPubsubClient{} - defer pc.Close() - - projectID, topicID, subID := "test-project", "test-topic", "test-sub" - - // Inject a failure, but also add a delay to the call sees the error - failureMap := make(map[string][]failPattern) - failureMap[failureMethod] = []failPattern{{ - ErrReturn: fmt.Errorf("Injected error"), - Count: 1, - Delay: delay}} - client, err := pc.New(ctx, projectID, failureMap) - if err != nil { - t.Fatalf("failed to create pubsub client: %v", err) - } - defer client.Close() - - psconn := &Connection{ - AllowCreateSubscription: true, - AllowCreateTopic: true, - Client: client, - ProjectID: projectID, - TopicID: topicID, - SubscriptionID: subID, - } - - msg := &pubsub.Message{ - ID: "msg-id-1", - Data: []byte("msg-data-1"), - } - resChan := make(chan error) - // Try a publish. We want this to be the first to try to create the channel - go func() { - _, err := psconn.Publish(ctx, msg) - resChan <- err - }() - - // Try a second publish. We hope the above has hit it's critical section before - // this starts so that this reports out the error returned above. - _, errPub1 := psconn.Publish(ctx, msg) - errPub2 := <-resChan - if errPub1 == nil || errPub2 == nil { - return fmt.Errorf("expected dual expected failure, saw (%v) (%v) last run", errPub1, errPub2) - } else if errPub1 == nil && errPub2 == nil { - t.Fatalf("Dual success when expecting at least one failure, delay %v", delay) - } - return nil - }() - // Saw a successfull run, no retry needed. - if overallError == nil { - break - } - // Failure. The loop will bump the delay and try again(unless we've hit the max reasonable delay) - } - if overallError != nil { - t.Errorf(overallError.Error()) - } -} - -// Test that creating a subscription also creates the topic and subscription -func TestReceiveCreateTopicAndSubscription(t *testing.T) { - ctx := context.Background() - pc := &testPubsubClient{} - defer pc.Close() - - projectID, topicID, subID := "test-project", "test-topic", "test-sub" - client, err := pc.New(ctx, projectID, nil) - if err != nil { - t.Fatalf("failed to create pubsub client: %v", err) - } - defer client.Close() - - psconn := &Connection{ - AllowCreateSubscription: true, - AllowCreateTopic: true, - Client: client, - ProjectID: projectID, - TopicID: topicID, - SubscriptionID: subID, - } - - ctx2, cancel := context.WithCancel(ctx) - go psconn.Receive(ctx2, func(_ context.Context, msg *pubsub.Message) { - msg.Ack() - }) - // Sleep waiting for the goroutine to create the topic and subscription - // If it takes over a minute, run the test anyway to get failure logging - for _, delay := range []time.Duration{time.Second / 4, time.Second, 20 * time.Second, 40 * time.Second} { - time.Sleep(delay) - ok, err := client.Subscription(subID).Exists(ctx) - if ok == true && err == nil { - break - } - } - - if ok, err := client.Topic(topicID).Exists(ctx); err != nil || !ok { - t.Errorf("topic id=%s got exists=%v want=true, err=%v", topicID, ok, err) - } - - if ok, err := client.Subscription(subID).Exists(ctx); err != nil || !ok { - t.Errorf("subscription id=%s got exists=%v want=true, err=%v", subID, ok, err) - } - - si, err := psconn.getOrCreateSubscriptionInfo(context.Background(), true) - if err != nil { - t.Errorf("error getting subscription info %v", err) - } - if si.sub.ReceiveSettings.NumGoroutines != DefaultReceiveSettings.NumGoroutines { - t.Errorf("subscription receive settings have NumGoroutines=%d, want %d", - si.sub.ReceiveSettings.NumGoroutines, DefaultReceiveSettings.NumGoroutines) - } - - cancel() - - if err := psconn.DeleteSubscription(ctx); err != nil { - t.Errorf("delete subscription failed: %v", err) - } - - if ok, err := client.Subscription(subID).Exists(ctx); err != nil || ok { - t.Errorf("subscription id=%s got exists=%v want=false, err=%v", subID, ok, err) - } - - verifyTopicDeleteWorks(t, client, psconn, topicID) -} - -// Test receive on an existing topic and subscription also works. -func TestReceiveExistingTopic(t *testing.T) { - for _, allow := range [](struct{ Sub, Topic bool }){{true, true}, {true, false}, {false, true}, {false, false}} { - t.Run(fmt.Sprintf("sub_%v__topic_%v", allow.Sub, allow.Topic), func(t *testing.T) { - - ctx := context.Background() - pc := &testPubsubClient{} - defer pc.Close() - - projectID, topicID, subID := "test-project", "test-topic", "test-sub" - client, err := pc.New(ctx, projectID, nil) - if err != nil { - t.Fatalf("failed to create pubsub client: %v", err) - } - defer client.Close() - - psconn := &Connection{ - AllowCreateSubscription: allow.Sub, - AllowCreateTopic: allow.Topic, - Client: client, - ProjectID: projectID, - TopicID: topicID, - SubscriptionID: subID, - } - - topic, err := client.CreateTopic(ctx, topicID) - if err != nil { - pc.Close() - t.Fatalf("failed to pre-create topic: %v", err) - } - - _, err = client.CreateSubscription(ctx, subID, pubsub.SubscriptionConfig{ - Topic: topic, - AckDeadline: DefaultAckDeadline, - RetentionDuration: DefaultRetentionDuration, - }) - topic.Stop() - if err != nil { - pc.Close() - t.Fatalf("failed to pre-createsubscription: %v", err) - } - - ctx2, cancel := context.WithCancel(ctx) - go psconn.Receive(ctx2, func(_ context.Context, msg *pubsub.Message) { - msg.Ack() - }) - // Block waiting for receive to succeed - si, err := psconn.getOrCreateSubscriptionInfo(context.Background(), false) - if err != nil { - t.Errorf("error getting subscription info %v", err) - } - if si.sub.ReceiveSettings.NumGoroutines != DefaultReceiveSettings.NumGoroutines { - t.Errorf("subscription receive settings have NumGoroutines=%d, want %d", - si.sub.ReceiveSettings.NumGoroutines, DefaultReceiveSettings.NumGoroutines) - } - - cancel() - - if err := psconn.DeleteSubscription(ctx); err == nil { - t.Errorf("delete subscription unexpectedly succeeded") - } - - if ok, err := client.Subscription(subID).Exists(ctx); err != nil || !ok { - t.Errorf("subscription id=%s got exists=%v want=true, err=%v", subID, ok, err) - } - - verifyTopicDeleteFails(t, client, psconn, topicID) - }) - } -} - -// Test that creating a subscription after a failed attempt to create a subsciption works -func TestReceiveCreateSubscriptionAfterFailure(t *testing.T) { - for _, failureMethod := range []string{ - "/google.pubsub.v1.Publisher/GetTopic", - "/google.pubsub.v1.Publisher/CreateTopic", - "/google.pubsub.v1.Subscriber/GetSubscription", - "/google.pubsub.v1.Subscriber/CreateSubscription"} { - t.Run(failureMethod, func(t *testing.T) { - - ctx := context.Background() - pc := &testPubsubClient{} - defer pc.Close() - - projectID, topicID, subID := "test-project", "test-topic", "test-sub" - failureMap := make(map[string][]failPattern) - failureMap[failureMethod] = []failPattern{{ - ErrReturn: fmt.Errorf("Injected error"), - Count: 1, - Delay: 0}} - client, err := pc.New(ctx, projectID, failureMap) - if err != nil { - t.Fatalf("failed to create pubsub client: %v", err) - } - defer client.Close() - - psconn := &Connection{ - AllowCreateSubscription: true, - AllowCreateTopic: true, - Client: client, - ProjectID: projectID, - TopicID: topicID, - SubscriptionID: subID, - } - - // We expect this receive to fail due to the injected error - ctx2, cancel := context.WithCancel(ctx) - errRet := make(chan error) - go func() { - errRet <- psconn.Receive(ctx2, func(_ context.Context, msg *pubsub.Message) { - msg.Ack() - }) - }() - - select { - case err := <-errRet: - if err == nil { - t.Fatalf("unexpected nil error from Receive") - } - case <-time.After(time.Minute): - cancel() - t.Fatalf("timeout waiting for receive error") - } - - // We expect this receive to succeed - errRet2 := make(chan error) - ctx2, cancel = context.WithCancel(context.Background()) - go func() { - errRet2 <- psconn.Receive(ctx2, func(_ context.Context, msg *pubsub.Message) { - msg.Ack() - }) - }() - // Sleep waiting for the goroutine to create the topic and subscription - // If it takes over a minute, run the test anyway to get failure logging - for _, delay := range []time.Duration{time.Second / 4, time.Second, 20 * time.Second, 40 * time.Second} { - time.Sleep(delay) - ok, err := client.Subscription(subID).Exists(ctx) - if ok == true && err == nil { - break - } - } - select { - case err := <-errRet2: - t.Errorf("unexpected error from Receive: %v", err) - default: - } - if ok, err := client.Topic(topicID).Exists(ctx); err != nil || !ok { - t.Errorf("topic id=%s got exists=%v want=true, err=%v", topicID, ok, err) - } - - if ok, err := client.Subscription(subID).Exists(ctx); err != nil || !ok { - t.Errorf("subscription id=%s got exists=%v want=true, err=%v", subID, ok, err) - } - - cancel() - }) - } -} - -// Test that lack of create privileges for topic or subscription causes a receive to fail for -// a non-existing subscription and topic -func TestReceiveCreateDisallowedFail(t *testing.T) { - ctx := context.Background() - pc := &testPubsubClient{} - defer pc.Close() - - for _, allow := range [](struct{ Sub, Topic bool }){{false, true}, {true, false}, {false, false}} { - t.Run(fmt.Sprintf("sub_%v__topic_%v", allow.Sub, allow.Topic), func(t *testing.T) { - - projectID, topicID, subID := "test-project", "test-topic", "test-sub" - client, err := pc.New(ctx, projectID, nil) - if err != nil { - t.Fatalf("failed to create pubsub client: %v", err) - } - defer client.Close() - - psconn := &Connection{ - AllowCreateSubscription: allow.Sub, - AllowCreateTopic: allow.Topic, - Client: client, - ProjectID: projectID, - TopicID: topicID, - SubscriptionID: subID, - } - - ctx2, cancel := context.WithCancel(ctx) - errRet := make(chan error) - go func() { - errRet <- psconn.Receive(ctx2, func(_ context.Context, msg *pubsub.Message) { - msg.Ack() - }) - }() - - select { - case err := <-errRet: - if err == nil { - t.Fatalf("unexpected nil error from Receive") - } - case <-time.After(time.Minute): - cancel() - t.Fatalf("timeout waiting for receive error") - } - cancel() - }) - } -} - -// Test a full round trip of a message -func TestPublishReceiveRoundtrip(t *testing.T) { - ctx := context.Background() - pc := &testPubsubClient{} - defer pc.Close() - - projectID, topicID, subID := "test-project", "test-topic", "test-sub" - client, err := pc.New(ctx, projectID, nil) - if err != nil { - t.Fatalf("failed to create pubsub client: %v", err) - } - defer client.Close() - - psconn := &Connection{ - AllowCreateSubscription: true, - AllowCreateTopic: true, - Client: client, - ProjectID: projectID, - TopicID: topicID, - SubscriptionID: subID, - } - - wantMsgs := make(map[string]string) - gotMsgs := make(map[string]string) - wg := &sync.WaitGroup{} - - ctx2, cancel := context.WithCancel(ctx) - mux := &sync.Mutex{} - // Pubsub will drop all messages if there is no subscription. - // Call Receive first so that subscription can be created before - // we publish any message. - go psconn.Receive(ctx2, func(_ context.Context, msg *pubsub.Message) { - mux.Lock() - defer mux.Unlock() - gotMsgs[string(msg.Data)] = string(msg.Data) - msg.Ack() - wg.Done() - }) - // Wait a little bit for the subscription creation to complete. - time.Sleep(time.Second) - - for i := 0; i < 10; i++ { - data := fmt.Sprintf("data-%d", i) - wantMsgs[data] = data - - if _, err := psconn.Publish(ctx, &pubsub.Message{Data: []byte(data)}); err != nil { - t.Errorf("failed to publish message: %v", err) - } - wg.Add(1) - } - - wg.Wait() - cancel() - - if diff := cmp.Diff(gotMsgs, wantMsgs); diff != "" { - t.Errorf("received unexpected messages (-want +got):\n%s", diff) - } -} diff --git a/v1/cloudevents/transport/pubsub/message.go b/v1/cloudevents/transport/pubsub/message.go deleted file mode 100644 index 5597e20b5..000000000 --- a/v1/cloudevents/transport/pubsub/message.go +++ /dev/null @@ -1,47 +0,0 @@ -package pubsub - -import ( - "encoding/json" - - "github.com/cloudevents/sdk-go/v1/cloudevents/transport" -) - -// type check that this transport message impl matches the contract -var _ transport.Message = (*Message)(nil) - -// Message represents a Pub/Sub message. -type Message struct { - // Data is the actual data in the message. - Data []byte - - // Attributes represents the key-value pairs the current message - // is labelled with. - Attributes map[string]string -} - -func (m Message) CloudEventsVersion() string { - // Check as Binary encoding first. - if m.Attributes != nil { - // Binary v0.3: - if s := m.Attributes[prefix+"specversion"]; s != "" { - return s - } - } - - // Now check as Structured encoding. - raw := make(map[string]json.RawMessage) - if err := json.Unmarshal(m.Data, &raw); err != nil { - return "" - } - - // structured v0.3 - if v, ok := raw["specversion"]; ok { - var version string - if err := json.Unmarshal(v, &version); err != nil { - return "" - } - return version - } - - return "" -} diff --git a/v1/cloudevents/transport/pubsub/options.go b/v1/cloudevents/transport/pubsub/options.go deleted file mode 100644 index a77adb246..000000000 --- a/v1/cloudevents/transport/pubsub/options.go +++ /dev/null @@ -1,207 +0,0 @@ -package pubsub - -import ( - "fmt" - "os" - - "cloud.google.com/go/pubsub" -) - -// Option is the function signature required to be considered an pubsub.Option. -type Option func(*Transport) error - -const ( - DefaultProjectEnvKey = "GOOGLE_CLOUD_PROJECT" - DefaultTopicEnvKey = "PUBSUB_TOPIC" - DefaultSubscriptionEnvKey = "PUBSUB_SUBSCRIPTION" -) - -// WithEncoding sets the encoding for pubsub transport. -func WithEncoding(encoding Encoding) Option { - return func(t *Transport) error { - 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("pubsub fn for DefaultEncodingSelector was 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("pubsub 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("pubsub structured encoding option can not set nil transport") - } - - t.DefaultEncodingSelectionFn = DefaultStructuredEncodingSelectionStrategy - return nil - } -} - -// WithClient sets the pubsub client for pubsub transport. Use this for explicit -// auth setup. Otherwise the env var 'GOOGLE_APPLICATION_CREDENTIALS' is used. -// See https://cloud.google.com/docs/authentication/production for more details. -func WithClient(client *pubsub.Client) Option { - return func(t *Transport) error { - t.client = client - return nil - } -} - -// WithProjectID sets the project ID for pubsub transport. -func WithProjectID(projectID string) Option { - return func(t *Transport) error { - t.projectID = projectID - return nil - } -} - -// WithProjectIDFromEnv sets the project ID for pubsub transport from a -// given environment variable name. -func WithProjectIDFromEnv(key string) Option { - return func(t *Transport) error { - v := os.Getenv(key) - if v == "" { - return fmt.Errorf("unable to load project id, %q environment variable not set", key) - } - t.projectID = v - return nil - } -} - -// WithProjectIDFromDefaultEnv sets the project ID for pubsub transport from -// the environment variable named 'GOOGLE_CLOUD_PROJECT'. -func WithProjectIDFromDefaultEnv() Option { - return WithProjectIDFromEnv(DefaultProjectEnvKey) -} - -// WithTopicID sets the topic ID for pubsub transport. -func WithTopicID(topicID string) Option { - return func(t *Transport) error { - t.topicID = topicID - return nil - } -} - -// WithTopicIDFromEnv sets the topic ID for pubsub transport from a given -// environment variable name. -func WithTopicIDFromEnv(key string) Option { - return func(t *Transport) error { - v := os.Getenv(key) - if v == "" { - return fmt.Errorf("unable to load topic id, %q environment variable not set", key) - } - t.topicID = v - return nil - } -} - -// WithTopicIDFromDefaultEnv sets the topic ID for pubsub transport from the -// environment variable named 'PUBSUB_TOPIC'. -func WithTopicIDFromDefaultEnv() Option { - return WithTopicIDFromEnv(DefaultTopicEnvKey) -} - -// WithSubscriptionID sets the subscription ID for pubsub transport. -// This option can be used multiple times. -func WithSubscriptionID(subscriptionID string) Option { - return func(t *Transport) error { - if t.subscriptions == nil { - t.subscriptions = make([]subscriptionWithTopic, 0) - } - t.subscriptions = append(t.subscriptions, subscriptionWithTopic{ - subscriptionID: subscriptionID, - }) - return nil - } -} - -// WithSubscriptionAndTopicID sets the subscription and topic IDs for pubsub transport. -// This option can be used multiple times. -func WithSubscriptionAndTopicID(subscriptionID, topicID string) Option { - return func(t *Transport) error { - if t.subscriptions == nil { - t.subscriptions = make([]subscriptionWithTopic, 0) - } - t.subscriptions = append(t.subscriptions, subscriptionWithTopic{ - subscriptionID: subscriptionID, - topicID: topicID, - }) - return nil - } -} - -// WithSubscriptionIDFromEnv sets the subscription ID for pubsub transport from -// a given environment variable name. -func WithSubscriptionIDFromEnv(key string) Option { - return func(t *Transport) error { - v := os.Getenv(key) - if v == "" { - return fmt.Errorf("unable to load subscription id, %q environment variable not set", key) - } - - opt := WithSubscriptionID(v) - return opt(t) - } -} - -// WithSubscriptionIDFromDefaultEnv sets the subscription ID for pubsub -// transport from the environment variable named 'PUBSUB_SUBSCRIPTION'. -func WithSubscriptionIDFromDefaultEnv() Option { - return WithSubscriptionIDFromEnv(DefaultSubscriptionEnvKey) -} - -// AllowCreateTopic sets if the transport can create a topic if it does not -// exist. -func AllowCreateTopic(allow bool) Option { - return func(t *Transport) error { - t.AllowCreateTopic = allow - return nil - } -} - -// AllowCreateSubscription sets if the transport can create a subscription if -// it does not exist. -func AllowCreateSubscription(allow bool) Option { - return func(t *Transport) error { - t.AllowCreateSubscription = allow - return nil - } -} - -// WithReceiveSettings sets the Pubsub ReceiveSettings for pull subscriptions. -func WithReceiveSettings(rs *pubsub.ReceiveSettings) Option { - return func(t *Transport) error { - t.ReceiveSettings = rs - return nil - } -} diff --git a/v1/cloudevents/transport/pubsub/options_test.go b/v1/cloudevents/transport/pubsub/options_test.go deleted file mode 100644 index 40fa1c782..000000000 --- a/v1/cloudevents/transport/pubsub/options_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package pubsub - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" -) - -func TestWithEncoding(t *testing.T) { - testCases := map[string]struct { - t *Transport - encoding Encoding - want *Transport - wantErr string - }{ - "valid encoding": { - t: &Transport{}, - encoding: StructuredV03, - want: &Transport{ - Encoding: StructuredV03, - }, - }, - } - for n, tc := range testCases { - t.Run(n, func(t *testing.T) { - - err := tc.t.applyOptions(WithEncoding(tc.encoding)) - - if tc.wantErr != "" || err != nil { - var gotErr string - if err != nil { - gotErr = err.Error() - } - if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - got := tc.t - - if diff := cmp.Diff(tc.want, got, - cmpopts.IgnoreUnexported(Transport{})); diff != "" { - t.Errorf("unexpected (-want, +got) = %v", diff) - } - }) - } -} diff --git a/v1/cloudevents/transport/pubsub/transport.go b/v1/cloudevents/transport/pubsub/transport.go deleted file mode 100644 index 36be548be..000000000 --- a/v1/cloudevents/transport/pubsub/transport.go +++ /dev/null @@ -1,317 +0,0 @@ -package pubsub - -import ( - "context" - "errors" - "fmt" - "strings" - "sync" - - "go.uber.org/zap" - - "cloud.google.com/go/pubsub" - - "github.com/cloudevents/sdk-go/v1/cloudevents" - cecontext "github.com/cloudevents/sdk-go/v1/cloudevents/context" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport" - "github.com/cloudevents/sdk-go/v1/cloudevents/transport/pubsub/internal" -) - -// Transport adheres to transport.Transport. -var _ transport.Transport = (*Transport)(nil) - -const ( - TransportName = "Pub/Sub" -) - -type subscriptionWithTopic struct { - topicID string - subscriptionID string -} - -// Transport acts as both a pubsub topic and a pubsub subscription . -type Transport struct { - // Encoding - Encoding Encoding - - // DefaultEncodingSelectionFn allows for other encoding selection strategies to be injected. - DefaultEncodingSelectionFn EncodingSelector - - codec transport.Codec - // Codec Mutex - coMu sync.Mutex - - // PubSub - - // ReceiveSettings is used to configure Pubsub pull subscription. - ReceiveSettings *pubsub.ReceiveSettings - - // AllowCreateTopic controls if the transport can create a topic if it does - // not exist. - AllowCreateTopic bool - - // AllowCreateSubscription controls if the transport can create a - // subscription if it does not exist. - AllowCreateSubscription bool - - projectID string - topicID string - subscriptionID string - - gccMux sync.Mutex - - subscriptions []subscriptionWithTopic - client *pubsub.Client - - connectionsBySubscription map[string]*internal.Connection - connectionsByTopic map[string]*internal.Connection - - // Receiver - Receiver transport.Receiver - - // Converter is invoked if the incoming transport receives an undecodable - // message. - Converter transport.Converter -} - -// New creates a new pubsub transport. -func New(ctx context.Context, opts ...Option) (*Transport, error) { - t := &Transport{} - if err := t.applyOptions(opts...); err != nil { - return nil, err - } - - if t.client == nil { - // Auth to pubsub. - client, err := pubsub.NewClient(ctx, t.projectID) - if err != nil { - return nil, err - } - // Success. - t.client = client - } - - if t.connectionsBySubscription == nil { - t.connectionsBySubscription = make(map[string]*internal.Connection, 0) - } - - if t.connectionsByTopic == nil { - t.connectionsByTopic = make(map[string]*internal.Connection, 0) - } - 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.coMu.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.coMu.Unlock() - } - return true -} - -func (t *Transport) getConnection(ctx context.Context, topic, subscription string) *internal.Connection { - if subscription != "" { - if conn, ok := t.connectionsBySubscription[subscription]; ok { - return conn - } - } - if topic != "" { - if conn, ok := t.connectionsByTopic[topic]; ok { - return conn - } - } - - return nil -} - -func (t *Transport) getOrCreateConnection(ctx context.Context, topic, subscription string) *internal.Connection { - t.gccMux.Lock() - defer t.gccMux.Unlock() - - // Get. - if conn := t.getConnection(ctx, topic, subscription); conn != nil { - return conn - } - // Create. - conn := &internal.Connection{ - AllowCreateSubscription: t.AllowCreateSubscription, - AllowCreateTopic: t.AllowCreateTopic, - ReceiveSettings: t.ReceiveSettings, - Client: t.client, - ProjectID: t.projectID, - TopicID: topic, - SubscriptionID: subscription, - } - // Save for later. - if subscription != "" { - t.connectionsBySubscription[subscription] = conn - } - if topic != "" { - t.connectionsByTopic[topic] = conn - } - - return conn -} - -// Send implements Transport.Send -func (t *Transport) Send(ctx context.Context, event cloudevents.Event) (context.Context, *cloudevents.Event, error) { - // TODO populate response context properly. - if ok := t.loadCodec(ctx); !ok { - return ctx, nil, fmt.Errorf("unknown encoding set on transport: %d", t.Encoding) - } - - topic := cecontext.TopicFrom(ctx) - if topic == "" { - topic = t.topicID - } - - conn := t.getOrCreateConnection(ctx, topic, "") - - msg, err := t.codec.Encode(ctx, event) - if err != nil { - return ctx, nil, err - } - - if m, ok := msg.(*Message); ok { - respEvent, err := conn.Publish(ctx, &pubsub.Message{ - Attributes: m.Attributes, - Data: m.Data, - }) - return ctx, respEvent, err - } - - return ctx, nil, fmt.Errorf("failed to encode Event into a Message") -} - -// 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 -} - -func (t *Transport) startSubscriber(ctx context.Context, sub subscriptionWithTopic, done func(error)) { - logger := cecontext.LoggerFrom(ctx) - logger.Infof("starting subscriber for Topic %q, Subscription %q", sub.topicID, sub.subscriptionID) - conn := t.getOrCreateConnection(ctx, sub.topicID, sub.subscriptionID) - - logger.Info("conn is", conn) - if conn == nil { - err := fmt.Errorf("failed to find connection for Topic: %q, Subscription: %q", sub.topicID, sub.subscriptionID) - done(err) - return - } - // Ok, ready to start pulling. - err := conn.Receive(ctx, func(ctx context.Context, m *pubsub.Message) { - msg := &Message{ - Attributes: m.Attributes, - Data: m.Data, - } - event, err := t.codec.Decode(ctx, msg) - // If codec returns and error, try with the converter if it is set. - if err != nil && t.HasConverter() { - event, err = t.Converter.Convert(ctx, msg, err) - } - if err != nil { - logger.Errorw("failed to decode message", zap.Error(err)) - m.Nack() - return - } - - if err := t.Receiver.Receive(ctx, *event, nil); err != nil { - logger.Warnw("pubsub receiver return err", zap.Error(err)) - m.Nack() - return - } - m.Ack() - }) - done(err) -} - -// StartReceiver implements Transport.StartReceiver -// NOTE: This is a blocking call. -func (t *Transport) StartReceiver(ctx context.Context) error { - // Load the codec. - if ok := t.loadCodec(ctx); !ok { - return fmt.Errorf("unknown encoding set on transport: %d", t.Encoding) - } - - cctx, cancel := context.WithCancel(ctx) - defer cancel() - n := len(t.subscriptions) - - // Make the channels for quit and errors. - quit := make(chan struct{}, n) - errc := make(chan error, n) - - // Start up each subscription. - for _, sub := range t.subscriptions { - go t.startSubscriber(cctx, sub, func(err error) { - if err != nil { - errc <- err - } else { - quit <- struct{}{} - } - }) - } - - // Collect errors and done calls until we have n of them. - errs := []string(nil) - for success := 0; success < n; success++ { - var err error - select { - case <-ctx.Done(): // Block for parent context to finish. - success-- - case err = <-errc: // Collect errors - case <-quit: - } - if cancel != nil { - // Stop all other subscriptions. - cancel() - cancel = nil - } - if err != nil { - errs = append(errs, err.Error()) - } - } - - close(quit) - close(errc) - - return errors.New(strings.Join(errs, "\n")) -} - -// HasTracePropagation implements Transport.HasTracePropagation -func (t *Transport) HasTracePropagation() bool { - return false -} diff --git a/v1/cloudevents/transport/transport.go b/v1/cloudevents/transport/transport.go deleted file mode 100644 index c9a574a3e..000000000 --- a/v1/cloudevents/transport/transport.go +++ /dev/null @@ -1,49 +0,0 @@ -package transport - -import ( - "context" - - "github.com/cloudevents/sdk-go/v1/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 - // HasTracePropagation is true when the transport implements - // in-band trace propagation. When false, the client receiver - // will propagate trace context from distributed tracing - // extension attributes when available. - HasTracePropagation() 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) -} diff --git a/v1/cloudevents/types/allocate.go b/v1/cloudevents/types/allocate.go deleted file mode 100644 index c38f71177..000000000 --- a/v1/cloudevents/types/allocate.go +++ /dev/null @@ -1,36 +0,0 @@ -package types - -import "reflect" - -// Allocate allocates a new instance of type t and returns: -// asPtr is of type t if t is a pointer type and of type &t otherwise -// asValue is a Value of type t pointing to the same data as asPtr -func Allocate(obj interface{}) (asPtr interface{}, asValue reflect.Value) { - if obj == nil { - return nil, reflect.Value{} - } - - switch t := reflect.TypeOf(obj); t.Kind() { - case reflect.Ptr: - reflectPtr := reflect.New(t.Elem()) - asPtr = reflectPtr.Interface() - asValue = reflectPtr - case reflect.Map: - reflectPtr := reflect.MakeMap(t) - asPtr = reflectPtr.Interface() - asValue = reflectPtr - case reflect.String: - reflectPtr := reflect.New(t) - asPtr = "" - asValue = reflectPtr.Elem() - case reflect.Slice: - reflectPtr := reflect.MakeSlice(t, 0, 0) - asPtr = reflectPtr.Interface() - asValue = reflectPtr - default: - reflectPtr := reflect.New(t) - asPtr = reflectPtr.Interface() - asValue = reflectPtr.Elem() - } - return -} diff --git a/v1/cloudevents/types/allocate_test.go b/v1/cloudevents/types/allocate_test.go deleted file mode 100644 index a092d10be..000000000 --- a/v1/cloudevents/types/allocate_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package types_test - -import ( - "testing" - "time" - - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/google/go-cmp/cmp" -) - -type DataExample struct { - AnInt int `json:"a,omitempty"` - AString string `json:"b,omitempty"` - AnArray []string `json:"c,omitempty"` - AMap map[string]map[string]int `json:"d,omitempty"` - ATime *time.Time `json:"e,omitempty"` -} - -func TestAllocate(t *testing.T) { - - emptyString := "" - exampleString := "howdy" - - testCases := map[string]struct { - obj interface{} - want interface{} - }{ - "nil": { - obj: nil, - want: nil, - }, - "map": { - obj: map[string]string{ - "test": "case", - }, - want: map[string]string{}, - }, - "slice": { - obj: []string{ - "test", - "case", - }, - want: []string{}, - }, - "string": { - obj: "hello", - want: "", - }, - "string ptr": { - obj: &exampleString, - want: &emptyString, - }, - "struct": { - obj: DataExample{ - AnInt: 42, - }, - want: &DataExample{}, - }, - "pointer": { - obj: &DataExample{ - AnInt: 42, - }, - want: &DataExample{}, - }, - } - for n, tc := range testCases { - tc := tc // Don't use range variable in func literal. - t.Run(n, func(t *testing.T) { - got, _ := types.Allocate(tc.obj) - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected object (-want, +got) = %v", diff) - } - }) - } -} diff --git a/v1/cloudevents/types/doc.go b/v1/cloudevents/types/doc.go deleted file mode 100644 index b1d9c29da..000000000 --- a/v1/cloudevents/types/doc.go +++ /dev/null @@ -1,41 +0,0 @@ -/* -Package types implements the CloudEvents type system. - -CloudEvents defines a set of abstract types for event context attributes. Each -type has a corresponding native Go type and a canonical string encoding. The -native Go types used to represent the CloudEvents types are: -bool, int32, string, []byte, *url.URL, time.Time - - +----------------+----------------+-----------------------------------+ - |CloudEvents Type|Native Type |Convertible From | - +================+================+===================================+ - |Bool |bool |bool | - +----------------+----------------+-----------------------------------+ - |Integer |int32 |Any numeric type with value in | - | | |range of int32 | - +----------------+----------------+-----------------------------------+ - |String |string |string | - +----------------+----------------+-----------------------------------+ - |Binary |[]byte |[]byte | - +----------------+----------------+-----------------------------------+ - |URI-Reference |*url.URL |url.URL, types.URIRef, types.URI | - +----------------+----------------+-----------------------------------+ - |URI |*url.URL |url.URL, types.URIRef, types.URI | - | | |Must be an absolute URI. | - +----------------+----------------+-----------------------------------+ - |Timestamp |time.Time |time.Time, types.Timestamp | - +----------------+----------------+-----------------------------------+ - -Extension attributes may be stored as a native type or a canonical string. The -To functions will convert to the desired from any convertible type -or from the canonical string form. - -The Parse and Format functions convert native types to/from -canonical strings. - -Note are no Parse or Format functions for URL or string. For URL use the -standard url.Parse() and url.URL.String(). The canonical string format of a -string is the string itself. - -*/ -package types diff --git a/v1/cloudevents/types/timestamp.go b/v1/cloudevents/types/timestamp.go deleted file mode 100644 index 3ae1c7def..000000000 --- a/v1/cloudevents/types/timestamp.go +++ /dev/null @@ -1,70 +0,0 @@ -package types - -import ( - "encoding/json" - "encoding/xml" - "fmt" - "time" -) - -// Timestamp wraps time.Time to normalize the time layout to RFC3339. It is -// intended to enforce compliance with the CloudEvents spec for their -// definition of Timestamp. Custom marshal methods are implemented to ensure -// the outbound Timestamp is a string in the RFC3339 layout. -type Timestamp struct { - time.Time -} - -// ParseTimestamp attempts to parse the given time assuming RFC3339 layout -func ParseTimestamp(s string) (*Timestamp, error) { - if s == "" { - return nil, nil - } - tt, err := ParseTime(s) - return &Timestamp{Time: tt}, err -} - -// MarshalJSON implements a custom json marshal method used when this type is -// marshaled using json.Marshal. -func (t *Timestamp) MarshalJSON() ([]byte, error) { - if t == nil || t.IsZero() { - return []byte(`""`), nil - } - return []byte(fmt.Sprintf("%q", t)), nil -} - -// UnmarshalJSON implements the json unmarshal method used when this type is -// unmarshaled using json.Unmarshal. -func (t *Timestamp) UnmarshalJSON(b []byte) error { - var timestamp string - if err := json.Unmarshal(b, ×tamp); err != nil { - return err - } - var err error - t.Time, err = ParseTime(timestamp) - return err -} - -// MarshalXML implements a custom xml marshal method used when this type is -// marshaled using xml.Marshal. -func (t *Timestamp) MarshalXML(e *xml.Encoder, start xml.StartElement) error { - if t == nil || t.IsZero() { - return e.EncodeElement(nil, start) - } - return e.EncodeElement(t.String(), start) -} - -// UnmarshalXML implements the xml unmarshal method used when this type is -// unmarshaled using xml.Unmarshal. -func (t *Timestamp) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { - var timestamp string - if err := d.DecodeElement(×tamp, &start); err != nil { - return err - } - var err error - t.Time, err = ParseTime(timestamp) - return err -} - -// String outputs the time using RFC3339 format. -func (t Timestamp) String() string { return FormatTime(t.Time) } diff --git a/v1/cloudevents/types/timestamp_test.go b/v1/cloudevents/types/timestamp_test.go deleted file mode 100644 index b6706c98a..000000000 --- a/v1/cloudevents/types/timestamp_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package types_test - -import ( - "encoding/json" - "encoding/xml" - "fmt" - "testing" - "time" - - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/stretchr/testify/assert" -) - -func TestTimestampParseString(t *testing.T) { - ok := func(s string, want time.Time) { - t.Helper() - got, err := types.ParseTimestamp(s) - assert.NoError(t, err) - assert.Equal(t, want, got.Time) - assert.Equal(t, s, got.String()) - } - ok("1984-02-28T15:04:05Z", time.Date(1984, 02, 28, 15, 04, 05, 0, time.UTC)) - ok("1984-02-28T15:04:05.999999999Z", time.Date(1984, 02, 28, 15, 04, 05, 999999999, time.UTC)) - - bad := func(s, wanterr string) { - t.Helper() - _, err := types.ParseTime(s) - assert.EqualError(t, err, wanterr) - } - bad("", "cannot convert \"\" to time.Time: not in RFC3339 format") - bad("2019-02-28", "cannot convert \"2019-02-28\" to time.Time: not in RFC3339 format") -} - -func TestJsonMarshalUnmarshalTimestamp(t *testing.T) { - ok := func(ts string) { - t.Helper() - tt, err := types.ParseTime(ts) - assert.NoError(t, err) - got, err := json.Marshal(types.Timestamp{Time: tt}) - assert.NoError(t, err) - assert.Equal(t, fmt.Sprintf(`"%s"`, ts), string(got)) - } - ok("1984-02-28T15:04:05Z") - ok("1984-02-28T15:04:05.999999999Z") - - bad := func(s, wanterr string) { - t.Helper() - var ts types.Timestamp - err := json.Unmarshal([]byte(s), &ts) - assert.EqualError(t, err, wanterr) - } - bad("", "unexpected end of JSON input") - bad("2019-02-28", "invalid character '-' after top-level value") -} - -func TestXMLMarshalUnmarshalTimestamp(t *testing.T) { - ok := func(tstr string) { - t.Helper() - tt, err := types.ParseTime(tstr) - assert.NoError(t, err) - got, err := xml.Marshal(types.Timestamp{Time: tt}) - assert.NoError(t, err) - assert.Equal(t, fmt.Sprintf("%s", tstr), string(got)) - var ts types.Timestamp - err = xml.Unmarshal(got, &ts) - assert.NoError(t, err) - assert.Equal(t, tt, ts.Time) - } - ok("1984-02-28T15:04:05Z") - ok("1984-02-28T15:04:05.999999999Z") - - bad := func(s, wanterr string) { - t.Helper() - var ts types.Timestamp - err := xml.Unmarshal([]byte(s), &ts) - assert.EqualError(t, err, wanterr) - } - bad("", "EOF") - bad("2019-02-28", "EOF") - bad("2019-02-28", "cannot convert \"2019-02-28\" to time.Time: not in RFC3339 format") - bad("", "cannot convert \"\" to time.Time: not in RFC3339 format") -} diff --git a/v1/cloudevents/types/uri.go b/v1/cloudevents/types/uri.go deleted file mode 100644 index 97248a24d..000000000 --- a/v1/cloudevents/types/uri.go +++ /dev/null @@ -1,77 +0,0 @@ -package types - -import ( - "encoding/json" - "encoding/xml" - "fmt" - "net/url" -) - -// URI is a wrapper to url.URL. It is intended to enforce compliance with -// the CloudEvents spec for their definition of URI. Custom -// marshal methods are implemented to ensure the outbound URI object -// is a flat string. -type URI struct { - url.URL -} - -// ParseURI attempts to parse the given string as a URI. -func ParseURI(u string) *URI { - if u == "" { - return nil - } - pu, err := url.Parse(u) - if err != nil { - return nil - } - return &URI{URL: *pu} -} - -// MarshalJSON implements a custom json marshal method used when this type is -// marshaled using json.Marshal. -func (u URI) 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 *URI) UnmarshalJSON(b []byte) error { - var ref string - if err := json.Unmarshal(b, &ref); err != nil { - return err - } - r := ParseURI(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 URI) 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 *URI) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { - var ref string - if err := d.DecodeElement(&ref, &start); err != nil { - return err - } - r := ParseURI(ref) - if r != nil { - *u = *r - } - return nil -} - -// String returns the full string representation of the URI-Reference. -func (u *URI) String() string { - if u == nil { - return "" - } - return u.URL.String() -} diff --git a/v1/cloudevents/types/uri_test.go b/v1/cloudevents/types/uri_test.go deleted file mode 100644 index 9b9725848..000000000 --- a/v1/cloudevents/types/uri_test.go +++ /dev/null @@ -1,292 +0,0 @@ -package types_test - -import ( - "encoding/xml" - "net/url" - "testing" - - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/google/go-cmp/cmp" -) - -func TestParseURL(t *testing.T) { - testCases := map[string]struct { - t string - want *types.URI - }{ - "empty": { - want: nil, - }, - "empty string": { - t: "", - want: nil, - }, - "invalid format": { - t: "💩://error", - want: nil, - }, - "relative": { - t: "/path/to/something", - want: func() *types.URI { - u, _ := url.Parse("/path/to/something") - return &types.URI{URL: *u} - }(), - }, - "url": { - t: "http://path/to/something", - want: func() *types.URI { - u, _ := url.Parse("http://path/to/something") - return &types.URI{URL: *u} - }(), - }, - "mailto": { - t: "mailto:cncf-wg-serverless@lists.cncf.io", - want: func() *types.URI { - u, _ := url.Parse("mailto:cncf-wg-serverless@lists.cncf.io") - return &types.URI{URL: *u} - }(), - }, - "urn": { - t: "urn:uuid:6e8bc430-9c3a-11d9-9669-0800200c9a66", - want: func() *types.URI { - u, _ := url.Parse("urn:uuid:6e8bc430-9c3a-11d9-9669-0800200c9a66") - return &types.URI{URL: *u} - }(), - }, - "id3": { - t: "1-555-123-4567", - want: func() *types.URI { - u, _ := url.Parse("1-555-123-4567") - return &types.URI{URL: *u} - }(), - }, - } - for n, tc := range testCases { - tc := tc // Don't use range variable in func literal. - t.Run(n, func(t *testing.T) { - - got := types.ParseURI(tc.t) - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected object (-want, +got) = %v", diff) - } - }) - } -} - -func TestJsonMarshalURL(t *testing.T) { - testCases := map[string]struct { - t string - want []byte - }{ - "empty": {}, - "empty string": { - t: "", - }, - "invalid url": { - t: "not a url", - want: []byte(`"not%20a%20url"`), - }, - "relative format": { - t: "/path/to/something", - want: []byte(`"/path/to/something"`), - }, - } - for n, tc := range testCases { - tc := tc // Don't use range variable in func literal. - t.Run(n, func(t *testing.T) { - - var got []byte - tt := types.ParseURI(tc.t) - if tt != nil { - got, _ = tt.MarshalJSON() - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Logf("got: %s", string(got)) - t.Errorf("unexpected object (-want, +got) = %v", diff) - } - }) - } -} - -func TestXMLMarshalURI(t *testing.T) { - testCases := map[string]struct { - t string - want []byte - }{ - "empty": {}, - "empty string": { - t: "", - }, - "invalid url": { - t: "not a url", - want: []byte(`not%20a%20url`), - }, - "relative format": { - t: "/path/to/something", - want: []byte(`/path/to/something`), - }, - } - for n, tc := range testCases { - tc := tc // Don't use range variable in func literal. - t.Run(n, func(t *testing.T) { - - var got []byte - tt := types.ParseURI(tc.t) - if tt != nil { - got, _ = xml.Marshal(tt) - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Logf("got: %s", string(got)) - t.Errorf("unexpected object (-want, +got) = %v", diff) - } - }) - } -} - -func TestJsonUnmarshalURI(t *testing.T) { - testCases := map[string]struct { - b []byte - want *types.URI - wantErr string - }{ - "empty": { - wantErr: "unexpected end of JSON input", - }, - "invalid format": { - b: []byte("%"), - wantErr: "invalid character '%' looking for beginning of value", - }, - "relative": { - b: []byte(`"/path/to/something"`), - want: func() *types.URI { - u, _ := url.Parse("/path/to/something") - return &types.URI{URL: *u} - }(), - }, - "url": { - b: []byte(`"http://path/to/something"`), - want: func() *types.URI { - u, _ := url.Parse("http://path/to/something") - return &types.URI{URL: *u} - }(), - }, - } - for n, tc := range testCases { - tc := tc // Don't use range variable in func literal. - t.Run(n, func(t *testing.T) { - - got := &types.URI{} - err := got.UnmarshalJSON(tc.b) - - if tc.wantErr != "" || err != nil { - var gotErr string - if err != nil { - gotErr = err.Error() - } - if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected object (-want, +got) = %v", diff) - } - }) - } -} - -func TestXMLUnmarshalURI(t *testing.T) { - testCases := map[string]struct { - b []byte - want *types.URI - wantErr string - }{ - "empty": { - wantErr: "EOF", - }, - "invalid format": { - b: []byte(`%`), - want: &types.URI{}, - }, - "bad xml": { - b: []byte(`%`), - wantErr: "XML syntax error on line 1: element closed by ", - }, - "relative": { - b: []byte(`/path/to/something`), - want: func() *types.URI { - u, _ := url.Parse("/path/to/something") - return &types.URI{URL: *u} - }(), - }, - "url": { - b: []byte(`http://path/to/something`), - want: func() *types.URI { - u, _ := url.Parse("http://path/to/something") - return &types.URI{URL: *u} - }(), - }, - } - for n, tc := range testCases { - tc := tc // Don't use range variable in func literal. - t.Run(n, func(t *testing.T) { - - got := &types.URI{} - - err := xml.Unmarshal(tc.b, got) - - if tc.wantErr != "" || err != nil { - var gotErr string - if err != nil { - gotErr = err.Error() - } - if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected object (-want, +got) = %v", diff) - } - }) - } -} - -func TestURIString(t *testing.T) { - testCases := map[string]struct { - t string - want string - }{ - "empty": { - want: "", - }, - "relative": { - t: "/path/to/something", - want: "/path/to/something", - }, - "url": { - t: "http://path/to/something", - want: "http://path/to/something", - }, - } - for n, tc := range testCases { - tc := tc // Don't use range variable in func literal. - t.Run(n, func(t *testing.T) { - - tt := types.ParseURI(tc.t) - got := tt.String() - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Logf("got: %s", got) - t.Errorf("unexpected string (-want, +got) = %v", diff) - } - }) - } -} - -// TODO: Test xml: MarshalXML(e *xml.Encoder, start xml.StartElement) error { diff --git a/v1/cloudevents/types/uriref.go b/v1/cloudevents/types/uriref.go deleted file mode 100644 index e19a1dbb7..000000000 --- a/v1/cloudevents/types/uriref.go +++ /dev/null @@ -1,77 +0,0 @@ -package types - -import ( - "encoding/json" - "encoding/xml" - "fmt" - "net/url" -) - -// URIRef 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 URIRef object is -// is a flat string. -type URIRef struct { - url.URL -} - -// ParseURIRef attempts to parse the given string as a URI-Reference. -func ParseURIRef(u string) *URIRef { - if u == "" { - return nil - } - pu, err := url.Parse(u) - if err != nil { - return nil - } - return &URIRef{URL: *pu} -} - -// MarshalJSON implements a custom json marshal method used when this type is -// marshaled using json.Marshal. -func (u URIRef) 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 *URIRef) UnmarshalJSON(b []byte) error { - var ref string - if err := json.Unmarshal(b, &ref); err != nil { - return err - } - r := ParseURIRef(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 URIRef) 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 *URIRef) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { - var ref string - if err := d.DecodeElement(&ref, &start); err != nil { - return err - } - r := ParseURIRef(ref) - if r != nil { - *u = *r - } - return nil -} - -// String returns the full string representation of the URI-Reference. -func (u *URIRef) String() string { - if u == nil { - return "" - } - return u.URL.String() -} diff --git a/v1/cloudevents/types/uriref_test.go b/v1/cloudevents/types/uriref_test.go deleted file mode 100644 index a2ce66769..000000000 --- a/v1/cloudevents/types/uriref_test.go +++ /dev/null @@ -1,271 +0,0 @@ -package types_test - -import ( - "encoding/xml" - "net/url" - "testing" - - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/google/go-cmp/cmp" -) - -func TestParseURIRef(t *testing.T) { - testCases := map[string]struct { - t string - want *types.URIRef - }{ - "empty": { - want: nil, - }, - "empty string": { - t: "", - want: nil, - }, - "invalid format": { - t: "💩://error", - want: nil, - }, - "relative": { - t: "/path/to/something", - want: func() *types.URIRef { - u, _ := url.Parse("/path/to/something") - return &types.URIRef{URL: *u} - }(), - }, - "url": { - t: "http://path/to/something", - want: func() *types.URIRef { - u, _ := url.Parse("http://path/to/something") - return &types.URIRef{URL: *u} - }(), - }, - } - for n, tc := range testCases { - tc := tc // Don't use range variable in func literal. - t.Run(n, func(t *testing.T) { - - got := types.ParseURIRef(tc.t) - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected object (-want, +got) = %v", diff) - } - }) - } -} - -func TestJsonMarshalURIRef(t *testing.T) { - testCases := map[string]struct { - t string - want []byte - }{ - "empty": {}, - "empty string": { - t: "", - }, - "invalid url": { - t: "not a url", - want: []byte(`"not%20a%20url"`), - }, - "relative format": { - t: "/path/to/something", - want: []byte(`"/path/to/something"`), - }, - } - for n, tc := range testCases { - tc := tc // Don't use range variable in func literal. - t.Run(n, func(t *testing.T) { - - var got []byte - tt := types.ParseURIRef(tc.t) - if tt != nil { - got, _ = tt.MarshalJSON() - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Logf("got: %s", string(got)) - t.Errorf("unexpected object (-want, +got) = %v", diff) - } - }) - } -} - -func TestXMLMarshalURIRef(t *testing.T) { - testCases := map[string]struct { - t string - want []byte - }{ - "empty": {}, - "empty string": { - t: "", - }, - "invalid url": { - t: "not a url", - want: []byte(`not%20a%20url`), - }, - "relative format": { - t: "/path/to/something", - want: []byte(`/path/to/something`), - }, - } - for n, tc := range testCases { - tc := tc // Don't use range variable in func literal. - t.Run(n, func(t *testing.T) { - - var got []byte - tt := types.ParseURIRef(tc.t) - if tt != nil { - got, _ = xml.Marshal(tt) - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Logf("got: %s", string(got)) - t.Errorf("unexpected object (-want, +got) = %v", diff) - } - }) - } -} - -func TestJsonUnmarshalURIRef(t *testing.T) { - testCases := map[string]struct { - b []byte - want *types.URIRef - wantErr string - }{ - "empty": { - wantErr: "unexpected end of JSON input", - }, - "invalid format": { - b: []byte("%"), - wantErr: "invalid character '%' looking for beginning of value", - }, - "relative": { - b: []byte(`"/path/to/something"`), - want: func() *types.URIRef { - u, _ := url.Parse("/path/to/something") - return &types.URIRef{URL: *u} - }(), - }, - "url": { - b: []byte(`"http://path/to/something"`), - want: func() *types.URIRef { - u, _ := url.Parse("http://path/to/something") - return &types.URIRef{URL: *u} - }(), - }, - } - for n, tc := range testCases { - tc := tc // Don't use range variable in func literal. - t.Run(n, func(t *testing.T) { - - got := &types.URIRef{} - err := got.UnmarshalJSON(tc.b) - - if tc.wantErr != "" || err != nil { - var gotErr string - if err != nil { - gotErr = err.Error() - } - if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected object (-want, +got) = %v", diff) - } - }) - } -} - -func TestXMLUnmarshalURIRef(t *testing.T) { - testCases := map[string]struct { - b []byte - want *types.URIRef - wantErr string - }{ - "empty": { - wantErr: "EOF", - }, - "invalid format": { - b: []byte(`%`), - want: &types.URIRef{}, - }, - "bad xml": { - b: []byte(`%`), - wantErr: "XML syntax error on line 1: element closed by ", - }, - "relative": { - b: []byte(`/path/to/something`), - want: func() *types.URIRef { - u, _ := url.Parse("/path/to/something") - return &types.URIRef{URL: *u} - }(), - }, - "url": { - b: []byte(`http://path/to/something`), - want: func() *types.URIRef { - u, _ := url.Parse("http://path/to/something") - return &types.URIRef{URL: *u} - }(), - }, - } - for n, tc := range testCases { - tc := tc // Don't use range variable in func literal. - t.Run(n, func(t *testing.T) { - - got := &types.URIRef{} - - err := xml.Unmarshal(tc.b, got) - - if tc.wantErr != "" || err != nil { - var gotErr string - if err != nil { - gotErr = err.Error() - } - if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected object (-want, +got) = %v", diff) - } - }) - } -} - -func TestURIRefString(t *testing.T) { - testCases := map[string]struct { - t string - want string - }{ - "empty": { - want: "", - }, - "relative": { - t: "/path/to/something", - want: "/path/to/something", - }, - "url": { - t: "http://path/to/something", - want: "http://path/to/something", - }, - } - for n, tc := range testCases { - tc := tc // Don't use range variable in func literal. - t.Run(n, func(t *testing.T) { - - tt := types.ParseURIRef(tc.t) - got := tt.String() - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Logf("got: %s", got) - t.Errorf("unexpected string (-want, +got) = %v", diff) - } - }) - } -} - -// TODO: Test xml: MarshalXML(e *xml.Encoder, start xml.StartElement) error { diff --git a/v1/cloudevents/types/urlref.go b/v1/cloudevents/types/urlref.go deleted file mode 100644 index 2578801cd..000000000 --- a/v1/cloudevents/types/urlref.go +++ /dev/null @@ -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() -} diff --git a/v1/cloudevents/types/urlref_test.go b/v1/cloudevents/types/urlref_test.go deleted file mode 100644 index 76450ae3b..000000000 --- a/v1/cloudevents/types/urlref_test.go +++ /dev/null @@ -1,271 +0,0 @@ -package types_test - -import ( - "encoding/xml" - "net/url" - "testing" - - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/google/go-cmp/cmp" -) - -func TestParseURLRef(t *testing.T) { - testCases := map[string]struct { - t string - want *types.URLRef - }{ - "empty": { - want: nil, - }, - "empty string": { - t: "", - want: nil, - }, - "invalid format": { - t: "💩://error", - want: nil, - }, - "relative": { - t: "/path/to/something", - want: func() *types.URLRef { - u, _ := url.Parse("/path/to/something") - return &types.URLRef{URL: *u} - }(), - }, - "url": { - t: "http://path/to/something", - want: func() *types.URLRef { - u, _ := url.Parse("http://path/to/something") - return &types.URLRef{URL: *u} - }(), - }, - } - for n, tc := range testCases { - tc := tc // Don't use range variable in func literal. - t.Run(n, func(t *testing.T) { - - got := types.ParseURLRef(tc.t) - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected object (-want, +got) = %v", diff) - } - }) - } -} - -func TestJsonMarshalURLRef(t *testing.T) { - testCases := map[string]struct { - t string - want []byte - }{ - "empty": {}, - "empty string": { - t: "", - }, - "invalid url": { - t: "not a url", - want: []byte(`"not%20a%20url"`), - }, - "relative format": { - t: "/path/to/something", - want: []byte(`"/path/to/something"`), - }, - } - for n, tc := range testCases { - tc := tc // Don't use range variable in func literal. - t.Run(n, func(t *testing.T) { - - var got []byte - tt := types.ParseURLRef(tc.t) - if tt != nil { - got, _ = tt.MarshalJSON() - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Logf("got: %s", string(got)) - t.Errorf("unexpected object (-want, +got) = %v", diff) - } - }) - } -} - -func TestXMLMarshalURLRef(t *testing.T) { - testCases := map[string]struct { - t string - want []byte - }{ - "empty": {}, - "empty string": { - t: "", - }, - "invalid url": { - t: "not a url", - want: []byte(`not%20a%20url`), - }, - "relative format": { - t: "/path/to/something", - want: []byte(`/path/to/something`), - }, - } - for n, tc := range testCases { - tc := tc // Don't use range variable in func literal. - t.Run(n, func(t *testing.T) { - - var got []byte - tt := types.ParseURLRef(tc.t) - if tt != nil { - got, _ = xml.Marshal(tt) - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Logf("got: %s", string(got)) - t.Errorf("unexpected object (-want, +got) = %v", diff) - } - }) - } -} - -func TestJsonUnmarshalURLRef(t *testing.T) { - testCases := map[string]struct { - b []byte - want *types.URLRef - wantErr string - }{ - "empty": { - wantErr: "unexpected end of JSON input", - }, - "invalid format": { - b: []byte("%"), - wantErr: "invalid character '%' looking for beginning of value", - }, - "relative": { - b: []byte(`"/path/to/something"`), - want: func() *types.URLRef { - u, _ := url.Parse("/path/to/something") - return &types.URLRef{URL: *u} - }(), - }, - "url": { - b: []byte(`"http://path/to/something"`), - want: func() *types.URLRef { - u, _ := url.Parse("http://path/to/something") - return &types.URLRef{URL: *u} - }(), - }, - } - for n, tc := range testCases { - tc := tc // Don't use range variable in func literal. - t.Run(n, func(t *testing.T) { - - got := &types.URLRef{} - err := got.UnmarshalJSON(tc.b) - - if tc.wantErr != "" || err != nil { - var gotErr string - if err != nil { - gotErr = err.Error() - } - if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected object (-want, +got) = %v", diff) - } - }) - } -} - -func TestXMLUnmarshalURLRef(t *testing.T) { - testCases := map[string]struct { - b []byte - want *types.URLRef - wantErr string - }{ - "empty": { - wantErr: "EOF", - }, - "invalid format": { - b: []byte(`%`), - want: &types.URLRef{}, - }, - "bad xml": { - b: []byte(`%`), - wantErr: "XML syntax error on line 1: element closed by ", - }, - "relative": { - b: []byte(`/path/to/something`), - want: func() *types.URLRef { - u, _ := url.Parse("/path/to/something") - return &types.URLRef{URL: *u} - }(), - }, - "url": { - b: []byte(`http://path/to/something`), - want: func() *types.URLRef { - u, _ := url.Parse("http://path/to/something") - return &types.URLRef{URL: *u} - }(), - }, - } - for n, tc := range testCases { - tc := tc // Don't use range variable in func literal. - t.Run(n, func(t *testing.T) { - - got := &types.URLRef{} - - err := xml.Unmarshal(tc.b, got) - - if tc.wantErr != "" || err != nil { - var gotErr string - if err != nil { - gotErr = err.Error() - } - if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { - t.Errorf("unexpected error (-want, +got) = %v", diff) - } - return - } - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("unexpected object (-want, +got) = %v", diff) - } - }) - } -} - -func TestURLRefString(t *testing.T) { - testCases := map[string]struct { - t string - want string - }{ - "empty": { - want: "", - }, - "relative": { - t: "/path/to/something", - want: "/path/to/something", - }, - "url": { - t: "http://path/to/something", - want: "http://path/to/something", - }, - } - for n, tc := range testCases { - tc := tc // Don't use range variable in func literal. - t.Run(n, func(t *testing.T) { - - tt := types.ParseURLRef(tc.t) - got := tt.String() - - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Logf("got: %s", got) - t.Errorf("unexpected string (-want, +got) = %v", diff) - } - }) - } -} - -// TODO: Test xml: MarshalXML(e *xml.Encoder, start xml.StartElement) error { diff --git a/v1/cloudevents/types/value.go b/v1/cloudevents/types/value.go deleted file mode 100644 index 4086808e6..000000000 --- a/v1/cloudevents/types/value.go +++ /dev/null @@ -1,269 +0,0 @@ -package types - -import ( - "encoding/base64" - "fmt" - "math" - "net/url" - "reflect" - "strconv" - "time" -) - -// FormatBool returns canonical string format: "true" or "false" -func FormatBool(v bool) string { return strconv.FormatBool(v) } - -// FormatInteger returns canonical string format: decimal notation. -func FormatInteger(v int32) string { return strconv.Itoa(int(v)) } - -// FormatBinary returns canonical string format: standard base64 encoding -func FormatBinary(v []byte) string { return base64.StdEncoding.EncodeToString(v) } - -// FormatTime returns canonical string format: RFC3339 with nanoseconds -func FormatTime(v time.Time) string { return v.UTC().Format(time.RFC3339Nano) } - -// ParseBool parse canonical string format: "true" or "false" -func ParseBool(v string) (bool, error) { return strconv.ParseBool(v) } - -// ParseInteger parse canonical string format: decimal notation. -func ParseInteger(v string) (int32, error) { - // Accept floating-point but truncate to int32 as per CE spec. - f, err := strconv.ParseFloat(v, 64) - if err != nil { - return 0, err - } - if f > math.MaxInt32 || f < math.MinInt32 { - return 0, rangeErr(v) - } - return int32(f), nil -} - -// ParseBinary parse canonical string format: standard base64 encoding -func ParseBinary(v string) ([]byte, error) { return base64.StdEncoding.DecodeString(v) } - -// ParseTime parse canonical string format: RFC3339 with nanoseconds -func ParseTime(v string) (time.Time, error) { - t, err := time.Parse(time.RFC3339Nano, v) - if err != nil { - err := convertErr(time.Time{}, v) - err.extra = ": not in RFC3339 format" - return time.Time{}, err - } - return t, nil -} - -// Format returns the canonical string format of v, where v can be -// any type that is convertible to a CloudEvents type. -func Format(v interface{}) (string, error) { - v, err := Validate(v) - if err != nil { - return "", err - } - switch v := v.(type) { - case bool: - return FormatBool(v), nil - case int32: - return FormatInteger(v), nil - case string: - return v, nil - case []byte: - return FormatBinary(v), nil - case URI: - return v.String(), nil - case URIRef: - // url.URL is often passed by pointer so allow both - return v.String(), nil - case Timestamp: - return FormatTime(v.Time), nil - default: - return "", fmt.Errorf("%T is not a CloudEvents type", v) - } -} - -// Validate v is a valid CloudEvents attribute value, convert it to one of: -// bool, int32, string, []byte, types.URI, types.URIRef, types.Timestamp -func Validate(v interface{}) (interface{}, error) { - switch v := v.(type) { - case bool, int32, string, []byte: - return v, nil // Already a CloudEvents type, no validation needed. - - case uint, uintptr, uint8, uint16, uint32, uint64: - u := reflect.ValueOf(v).Uint() - if u > math.MaxInt32 { - return nil, rangeErr(v) - } - return int32(u), nil - case int, int8, int16, int64: - i := reflect.ValueOf(v).Int() - if i > math.MaxInt32 || i < math.MinInt32 { - return nil, rangeErr(v) - } - return int32(i), nil - case float32, float64: - f := reflect.ValueOf(v).Float() - if f > math.MaxInt32 || f < math.MinInt32 { - return nil, rangeErr(v) - } - return int32(f), nil - - case *url.URL: - if v == nil { - break - } - return URI{*v}, nil - case url.URL: - return URI{v}, nil - case URIRef: - return v, nil - case URI: - return v, nil - case URLRef: - // Convert old type to new one - return URIRef{v.URL}, nil - case time.Time: - return Timestamp{v}, nil - case *time.Time: - if v == nil { - break - } - return Timestamp{*v}, nil - case Timestamp: - return v, nil - } - rx := reflect.ValueOf(v) - if rx.Kind() == reflect.Ptr && !rx.IsNil() { - // Allow pointers-to convertible types - return Validate(rx.Elem().Interface()) - } - return nil, fmt.Errorf("invalid CloudEvents value: %#v", v) -} - -// ToBool accepts a bool value or canonical "true"/"false" string. -func ToBool(v interface{}) (bool, error) { - v, err := Validate(v) - if err != nil { - return false, err - } - switch v := v.(type) { - case bool: - return v, nil - case string: - return ParseBool(v) - default: - return false, convertErr(true, v) - } -} - -// ToInteger accepts any numeric value in int32 range, or canonical string. -func ToInteger(v interface{}) (int32, error) { - v, err := Validate(v) - if err != nil { - return 0, err - } - switch v := v.(type) { - case int32: - return v, nil - case string: - return ParseInteger(v) - default: - return 0, convertErr(int32(0), v) - } -} - -// ToString returns a string value unaltered. -// -// This function does not perform canonical string encoding, use one of the -// Format functions for that. -func ToString(v interface{}) (string, error) { - v, err := Validate(v) - if err != nil { - return "", err - } - switch v := v.(type) { - case string: - return v, nil - default: - return "", convertErr("", v) - } -} - -// ToBinary returns a []byte value, decoding from base64 string if necessary. -func ToBinary(v interface{}) ([]byte, error) { - v, err := Validate(v) - if err != nil { - return nil, err - } - switch v := v.(type) { - case []byte: - return v, nil - case string: - return base64.StdEncoding.DecodeString(v) - default: - return nil, convertErr([]byte(nil), v) - } -} - -// ToURL returns a *url.URL value, parsing from string if necessary. -func ToURL(v interface{}) (*url.URL, error) { - v, err := Validate(v) - if err != nil { - return nil, err - } - switch v := v.(type) { - case URI: - return &v.URL, nil - case URIRef: - return &v.URL, nil - case string: - u, err := url.Parse(v) - if err != nil { - return nil, err - } - return u, nil - default: - return nil, convertErr((*url.URL)(nil), v) - } -} - -// ToTime returns a time.Time value, parsing from RFC3339 string if necessary. -func ToTime(v interface{}) (time.Time, error) { - v, err := Validate(v) - if err != nil { - return time.Time{}, err - } - switch v := v.(type) { - case Timestamp: - return v.Time, nil - case string: - ts, err := time.Parse(time.RFC3339Nano, v) - if err != nil { - return time.Time{}, err - } - return ts, nil - default: - return time.Time{}, convertErr(time.Time{}, v) - } -} - -type ConvertErr struct { - // Value being converted - Value interface{} - // Type of attempted conversion - Type reflect.Type - - extra string -} - -func (e *ConvertErr) Error() string { - return fmt.Sprintf("cannot convert %#v to %s%s", e.Value, e.Type, e.extra) -} - -func convertErr(target, v interface{}) *ConvertErr { - return &ConvertErr{Value: v, Type: reflect.TypeOf(target)} -} - -func rangeErr(v interface{}) error { - e := convertErr(int32(0), v) - e.extra = ": out of range" - return e -} diff --git a/v1/cloudevents/types/value_test.go b/v1/cloudevents/types/value_test.go deleted file mode 100644 index 75cb64e41..000000000 --- a/v1/cloudevents/types/value_test.go +++ /dev/null @@ -1,247 +0,0 @@ -package types_test - -import ( - "fmt" - "math" - "net/url" - "reflect" - "strconv" - "testing" - "time" - - "github.com/cloudevents/sdk-go/v1/cloudevents/types" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func Example() { - // Handle a time value that may be in native or canonical string form. - printTime := func(v interface{}) { - t, err := types.ToTime(v) - fmt.Printf("%v %v\n", t, err) - } - printTime(time.Date(1969, 3, 21, 12, 24, 0, 0, time.UTC)) - printTime("2020-03-21T12:34:56.78Z") - - // Convert numeric values to common 32-bit integer form - printInt := func(v interface{}) { - i, err := types.ToInteger(v) - fmt.Printf("%v %v\n", i, err) - } - printInt(123.456) - printInt("456") - printInt(int64(99999)) - // But not illegal or out-of-range values - printInt(math.MaxInt32 + 1) - printInt("not an int") - - // OUTPUT: - // 1969-03-21 12:24:00 +0000 UTC - // 2020-03-21 12:34:56.78 +0000 UTC - // 123 - // 456 - // 99999 - // 0 cannot convert 2147483648 to int32: out of range - // 0 strconv.ParseFloat: parsing "not an int": invalid syntax -} - -var ( - testURL = &url.URL{Scheme: "http", Host: "example.com", Path: "/foo"} - testURLstr = "http://example.com/foo" - timeStr = "2020-03-21T12:34:56.78Z" - someTime = time.Date(2020, 3, 21, 12, 34, 56, 780000000, time.UTC) -) - -type valueTester struct { - testing.TB - convertFn interface{} -} - -// Call types.To... function, use reflection since return types differ. -func (t valueTester) convert(v interface{}) (interface{}, error) { - rf := reflect.ValueOf(t.convertFn) - args := []reflect.Value{reflect.ValueOf(v)} - if v == nil { - args[0] = reflect.Zero(rf.Type().In(0)) // Avoid the zero argument reflection trap. - } - result := rf.Call(args) - err, _ := result[1].Interface().(error) - return result[0].Interface(), err -} - -// Verify round trip: convertible -> wrapped -> string -> wrapped -func (t *valueTester) ok(in, want interface{}, wantStr string) { - t.Helper() - got, err := types.Validate(in) - require.NoError(t, err) - assert.Equal(t, want, got) - - gotStr, err := types.Format(in) - require.NoError(t, err) - assert.Equal(t, wantStr, gotStr) - - x, err := t.convert(gotStr) - assert.NoError(t, err) - x2, err := types.Validate(x) - assert.NoError(t, err) - assert.Equal(t, want, x2) -} - -// Verify round trip with exception: convertible -> wrapped -> string -> different wrapped -func (t *valueTester) okWithDifferentFromString(in, want interface{}, wantStr string, wantAfterStr interface{}) { - t.Helper() - got, err := types.Validate(in) - require.NoError(t, err) - assert.Equal(t, want, got) - - gotStr, err := types.Format(in) - require.NoError(t, err) - assert.Equal(t, wantStr, gotStr) - - x, err := t.convert(gotStr) - assert.NoError(t, err) - x2, err := types.Validate(x) - assert.NoError(t, err) - assert.Equal(t, wantAfterStr, x2) -} - -// Verify expected error. -func (t *valueTester) err(in interface{}, wantErr string) { - t.Helper() - _, err := t.convert(in) - assert.EqualError(t, err, wantErr) -} - -// Verify string->value conversion. -func (t *valueTester) str(str string, want interface{}) { - t.Helper() - got, err := t.convert(str) - assert.NoError(t, err) - assert.Equal(t, want, got) -} - -func TestBool(t *testing.T) { - x := valueTester{t, types.ToBool} - x.ok(true, true, "true") - x.ok(false, false, "false") - - x.err("notabool", "strconv.ParseBool: parsing \"notabool\": invalid syntax") - x.err(0, "cannot convert 0 to bool") - x.err(nil, "invalid CloudEvents value: ") -} - -func TestInteger(t *testing.T) { - x := valueTester{t, types.ToInteger} - x.ok(42, int32(42), "42") - x.ok(int8(-8), int32(-8), "-8") - x.ok(int16(-16), int32(-16), "-16") - x.ok(int32(-32), int32(-32), "-32") - x.ok(int64(-64), int32(-64), "-64") - x.ok(uint(1), int32(1), "1") - x.ok(uint8(8), int32(8), "8") - x.ok(uint16(16), int32(16), "16") - x.ok(uint32(32), int32(32), "32") - x.ok(uint64(64), int32(64), "64") - x.ok(float32(123.4), int32(123), "123") - x.ok(float64(-567.8), int32(-567), "-567") - i := new(uint16) - *i = 24 // non-nil pointers allowed - x.ok(i, int32(24), "24") - - x.ok(math.MaxInt32, int32(math.MaxInt32), strconv.Itoa(math.MaxInt32)) - x.ok(math.MinInt32, int32(math.MinInt32), strconv.Itoa(math.MinInt32)) - x.ok(int64(math.MinInt32), int32(math.MinInt32), strconv.Itoa(math.MinInt32)) - x.ok(uint32(math.MaxInt32), int32(math.MaxInt32), strconv.Itoa(math.MaxInt32)) - x.ok(uint64(math.MaxInt32), int32(math.MaxInt32), strconv.Itoa(math.MaxInt32)) - x.ok(float64(math.MaxInt32), int32(math.MaxInt32), strconv.Itoa(math.MaxInt32)) - x.ok(float64(math.MinInt32), int32(math.MinInt32), strconv.Itoa(math.MinInt32)) - - x.str("123.456", int32(123)) - x.str("-123.456", int32(-123)) - x.str(".9", int32(0)) - x.str("-.9", int32(0)) - - x.err(math.MaxInt32+1, "cannot convert 2147483648 to int32: out of range") - x.err(uint32(math.MaxInt32+1), "cannot convert 0x80000000 to int32: out of range") - x.err(int64(math.MaxInt32+1), "cannot convert 2147483648 to int32: out of range") - x.err(int64(math.MinInt32-1), "cannot convert -2147483649 to int32: out of range") - x.err(float64(math.MinInt32-1), "cannot convert -2.147483649e+09 to int32: out of range") - x.err(float64(math.MaxInt32+1), "cannot convert 2.147483648e+09 to int32: out of range") - // Float32 doesn't keep all the bits of an int32 so we need to exaggerate fof range error. - x.err(float64(2*math.MinInt32), "cannot convert -4.294967296e+09 to int32: out of range") - x.err(float64(-2*math.MaxInt32), "cannot convert -4.294967294e+09 to int32: out of range") - - x.err("X", "strconv.ParseFloat: parsing \"X\": invalid syntax") - x.err(true, "cannot convert true to int32") - x.err(nil, "invalid CloudEvents value: ") -} - -func TestString(t *testing.T) { - x := valueTester{t, types.ToString} - x.ok("hello", "hello", "hello") - s := new(string) - *s = "foo" // non-nil pointers allowed - x.ok(s, "foo", "foo") -} - -func TestBinary(t *testing.T) { - x := valueTester{t, types.ToBinary} - x.ok([]byte("hello"), []byte("hello"), "aGVsbG8=") - x.ok([]byte{}, []byte{}, "") - // Asymmetic case: ToBinary([]byte(nil)) returns []byte(nil), - // but ToBinary("") returns []byte{} - // Logically equivalent but not assert.Equal(). - x.str("", []byte{}) - - x.err("XXX", "illegal base64 data at input byte 0") - x.err(nil, "invalid CloudEvents value: ") -} - -func TestURL(t *testing.T) { - t.Skip("Fails on Golang 1.14") - - x := valueTester{t, types.ToURL} - x.ok(testURL, types.URI{*testURL}, testURLstr) - x.ok(*testURL, types.URI{*testURL}, testURLstr) - x.okWithDifferentFromString(types.URLRef{URL: *testURL}, types.URIRef{*testURL}, testURLstr, types.URI{*testURL}) - x.okWithDifferentFromString(&types.URLRef{URL: *testURL}, types.URIRef{*testURL}, testURLstr, types.URI{*testURL}) - x.okWithDifferentFromString(types.URIRef{URL: *testURL}, types.URIRef{*testURL}, testURLstr, types.URI{*testURL}) - x.okWithDifferentFromString(&types.URIRef{URL: *testURL}, types.URIRef{*testURL}, testURLstr, types.URI{*testURL}) - x.ok(types.URI{URL: *testURL}, types.URI{*testURL}, testURLstr) - x.ok(&types.URI{URL: *testURL}, types.URI{*testURL}, testURLstr) - - x.str("http://hello/world", &url.URL{Scheme: "http", Host: "hello", Path: "/world"}) - x.str("/world", &url.URL{Path: "/world"}) - x.str("world", &url.URL{Path: "world"}) - - x.err("%bad %url", "parse %bad %url: invalid URL escape \"%ur\"") - x.err(nil, "invalid CloudEvents value: ") - x.err((*url.URL)(nil), "invalid CloudEvents value: (*url.URL)(nil)") - x.err((*types.URIRef)(nil), "invalid CloudEvents value: (*types.URIRef)(nil)") -} - -func TestTime(t *testing.T) { - x := valueTester{t, types.ToTime} - x.ok(someTime, types.Timestamp{someTime}, timeStr) - x.ok(&someTime, types.Timestamp{someTime}, timeStr) - x.ok(types.Timestamp{someTime}, types.Timestamp{someTime}, timeStr) - x.ok(&types.Timestamp{someTime}, types.Timestamp{someTime}, timeStr) - - x.str(timeStr, someTime) - - x.err(nil, "invalid CloudEvents value: ") - x.err(5, "cannot convert 5 to time.Time") - x.err((*time.Time)(nil), "invalid CloudEvents value: (*time.Time)(nil)") - x.err((*types.Timestamp)(nil), "invalid CloudEvents value: (*types.Timestamp)(nil)") - x.err("not a time", "parsing time \"not a time\" as \"2006-01-02T15:04:05.999999999Z07:00\": cannot parse \"not a time\" as \"2006\"") -} - -func TestIncompatible(t *testing.T) { - // Values that won't convert at all. - x := valueTester{t, types.Validate} - x.err(nil, "invalid CloudEvents value: ") - x.err(complex(0, 0), "invalid CloudEvents value: (0+0i)") - x.err(map[string]interface{}{}, "invalid CloudEvents value: map[string]interface {}{}") - x.err(struct{ i int }{i: 9}, "invalid CloudEvents value: struct { i int }{i:9}") - x.err((*int32)(nil), "invalid CloudEvents value: (*int32)(nil)") -} diff --git a/v1/go.mod b/v1/go.mod deleted file mode 100644 index ca81f80c9..000000000 --- a/v1/go.mod +++ /dev/null @@ -1,34 +0,0 @@ -module github.com/cloudevents/sdk-go/v1 - -require ( - cloud.google.com/go v0.40.0 - github.com/Azure/azure-sdk-for-go v30.1.0+incompatible // indirect - github.com/Azure/go-autorest/autorest v0.2.0 // indirect - github.com/Azure/go-autorest/autorest/to v0.2.0 // indirect - github.com/Azure/go-autorest/autorest/validation v0.1.0 // indirect - github.com/Shopify/sarama v1.19.0 - github.com/fortytw2/leaktest v1.3.0 // indirect - github.com/google/go-cmp v0.4.0 - github.com/google/uuid v1.1.1 - github.com/kr/pretty v0.2.0 // indirect - github.com/lightstep/tracecontext.go v0.0.0-20181129014701-1757c391b1ac - github.com/nats-io/nats-server/v2 v2.1.2 - github.com/nats-io/nats.go v1.9.1 - github.com/pkg/errors v0.8.1 - github.com/stretchr/testify v1.5.1 - github.com/valyala/bytebufferpool v1.0.0 - go.opencensus.io v0.22.0 - go.uber.org/atomic v1.4.0 // indirect - go.uber.org/multierr v1.1.0 // indirect - go.uber.org/zap v1.10.0 - golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72 // indirect - golang.org/x/net v0.0.0-20200202094626-16171245cfb2 // indirect - golang.org/x/sync v0.0.0-20190423024810-112230192c58 - google.golang.org/api v0.15.0 - google.golang.org/grpc v1.26.0 - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect - gopkg.in/yaml.v2 v2.2.8 // indirect - pack.ag/amqp v0.11.0 -) - -go 1.13 diff --git a/v1/go.sum b/v1/go.sum deleted file mode 100644 index aba7f5878..000000000 --- a/v1/go.sum +++ /dev/null @@ -1,296 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.40.0 h1:FjSY7bOj+WzJe6TZRVtXI2b9kAYvtNg4lMbcH2+MUkk= -cloud.google.com/go v0.40.0/go.mod h1:Tk58MuI9rbLMKlAjeO/bDnteAx7tX2gJIXw4T5Jwlro= -contrib.go.opencensus.io/exporter/ocagent v0.4.12 h1:jGFvw3l57ViIVEPKKEUXPcLYIXJmQxLUh6ey1eJhwyc= -contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA= -github.com/Azure/azure-sdk-for-go v30.1.0+incompatible h1:HyYPft8wXpxMd0kfLtXo6etWcO+XuPbLkcgx9g2cqxU= -github.com/Azure/azure-sdk-for-go v30.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/go-autorest/autorest v0.2.0 h1:zBtSTOQTtjzHVRe+mhkiHvHwRTKHhjBEyo1m6DfI3So= -github.com/Azure/go-autorest/autorest v0.2.0/go.mod h1:AKyIcETwSUFxIcs/Wnq/C+kwCtlEYGUVd7FPNb2slmg= -github.com/Azure/go-autorest/autorest/adal v0.1.0 h1:RSw/7EAullliqwkZvgIGDYZWQm1PGKXI8c4aY/87yuU= -github.com/Azure/go-autorest/autorest/adal v0.1.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E= -github.com/Azure/go-autorest/autorest/date v0.1.0 h1:YGrhWfrgtFs84+h0o46rJrlmsZtyZRg470CqAXTZaGM= -github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= -github.com/Azure/go-autorest/autorest/mocks v0.1.0 h1:Kx+AUU2Te+A3JIyYn6Dfs+cFgx5XorQKuIXrZGoq/SI= -github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= -github.com/Azure/go-autorest/autorest/to v0.2.0 h1:nQOZzFCudTh+TvquAtCRjM01VEYx85e9qbwt5ncW4L8= -github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc= -github.com/Azure/go-autorest/autorest/validation v0.1.0 h1:ISSNzGUh+ZSzizJWOWzs8bwpXIePbGLW4z/AmUFGH5A= -github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8= -github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY= -github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= -github.com/Azure/go-autorest/tracing v0.1.0 h1:TRBxC5Pj/fIuh4Qob0ZpkggbfT8RC0SubHbpV3p4/Vc= -github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Shopify/sarama v1.19.0 h1:9oksLxC6uxVPHPVYUmq6xhr1BOF/hHobWH2UzO67z1s= -github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= -github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= -github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/census-instrumentation/opencensus-proto v0.2.0 h1:LzQXZOgg4CQfE6bFvXGM30YZL1WW/M337pXml+GrcZ4= -github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/eapache/go-resiliency v1.1.0 h1:1NtRmCAqadE2FN4ZcN6g90TP3uk8cg9rn9eNK2197aU= -github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= -github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4 h1:hU4mGcQI4DaAYW+IbTun+2qEZVFxK0ySjQLTbS0VQKc= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= -github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk= -github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/grpc-ecosystem/grpc-gateway v1.8.5 h1:2+KSC78XiO6Qy0hIjfc1OD9H+hsaJdJlb8Kqsd41CTE= -github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lightstep/tracecontext.go v0.0.0-20181129014701-1757c391b1ac h1:+2b6iGRJe3hvV/yVXrd41yVEjxuFHxasJqDhkIjS4gk= -github.com/lightstep/tracecontext.go v0.0.0-20181129014701-1757c391b1ac/go.mod h1:Frd2bnT3w5FB5q49ENTfVlztJES+1k/7lyWX2+9gq/M= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= -github.com/nats-io/jwt v0.3.2 h1:+RB5hMpXUUA2dfxuhBTEkMOrYmM+gKIZYS1KjSostMI= -github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= -github.com/nats-io/nats-server/v2 v2.1.2 h1:i2Ly0B+1+rzNZHHWtD4ZwKi+OU5l+uQo1iDHZ2PmiIc= -github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= -github.com/nats-io/nats.go v1.9.1 h1:ik3HbLhZ0YABLto7iX80pZLPw/6dx3T+++MZJwLnMrQ= -github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= -github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nkeys v0.1.3 h1:6JrEfig+HzTH85yxzhSVbjHRJv9cn0p6n3IngIcM5/k= -github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= -github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829 h1:D+CiwcpGTW6pL6bv6KI3KbyEyCKyS+1JWS2h8PNDnGA= -github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f h1:BVwpUVJDADN2ufcGik7W992pyps0wZ888b/y9GXcLTU= -github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.2.0 h1:kUZDBDTdBVBYBj5Tmh2NZLlF60mfjA27rM34b+cVwNU= -github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1 h1:/K3IL0Z1quvmJ7X0A1AwNEK7CRkVK3YwfOU/QAL4WGg= -github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a h1:9ZKAASQSHhDYGoxY8uLVpewe1GDZ2vu2Tr/vTdVAkFQ= -github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= -go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72 h1:+ELyKg6m8UBf0nPFSqD0mi7zUfwPyXo23HNjMnXPz7w= -golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e h1:D5TXcfTk7xF7hvieo4QErS3qqCB4teTffacDWr7CI+0= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.6.0/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4= -google.golang.org/api v0.15.0 h1:yzlyyDW/J0w8yNFJIhiAJy4kq74S+1DOLdawELNxFMA= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -pack.ag/amqp v0.11.0 h1:ot/IA0enDkt4/c8xfbCO7AZzjM4bHys/UffnFmnHUnU= -pack.ag/amqp v0.11.0/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/v2/test/conformance/run_test.go b/v2/test/conformance/run_test.go index 37714511f..5e12ced90 100644 --- a/v2/test/conformance/run_test.go +++ b/v2/test/conformance/run_test.go @@ -1,3 +1,5 @@ +// +build conformance + package conformance import (