From 4739e13a1d3244b1875703fbd8d9e7a1ee592262 Mon Sep 17 00:00:00 2001 From: "Trifonova.Antonia" Date: Mon, 24 Jan 2022 16:50:09 +0200 Subject: [PATCH 1/8] Headers improvements - timeout is changed to time.Duration - usage of 'comma ok' idiom - add constant for ditto specific content type Signed-off-by: Antonia Trifonova antonia.trifonova@bosch.io --- protocol/headers.go | 170 ++++++---- protocol/headers_opts.go | 41 ++- protocol/headers_opts_test.go | 45 ++- protocol/headers_test.go | 615 ++++++++++++++++++++++++---------- 4 files changed, 625 insertions(+), 246 deletions(-) diff --git a/protocol/headers.go b/protocol/headers.go index 1ad9d44..b5be111 100644 --- a/protocol/headers.go +++ b/protocol/headers.go @@ -13,24 +13,43 @@ package protocol import ( "encoding/json" + "fmt" + "strconv" + "time" ) -// Ditto-specific headers constants. const ( - HeaderCorrelationID = "correlation-id" + // ContentTypeDitto defines the Ditto JSON 'content-type' header value for Ditto Protocol messages. + ContentTypeDitto = "application/vnd.eclipse.ditto+json" + + // HeaderCorrelationID represents 'correlation-id' header. + HeaderCorrelationID = "correlation-id" + // HeaderResponseRequired represents 'response-required' header. HeaderResponseRequired = "response-required" - HeaderChannel = "ditto-channel" - HeaderDryRun = "ditto-dry-run" - HeaderOrigin = "origin" - HeaderOriginator = "ditto-originator" - HeaderETag = "ETag" - HeaderIfMatch = "If-Match" - HeaderIfNoneMatch = "If-None-Match" - HeaderReplyTarget = "ditto-reply-target" - HeaderReplyTo = "reply-to" - HeaderTimeout = "timeout" - HeaderSchemaVersion = "version" - HeaderContentType = "content-type" + // HeaderChannel represents 'ditto-channel' header. + HeaderChannel = "ditto-channel" + // HeaderDryRun represents 'ditto-dry-run' header. + HeaderDryRun = "ditto-dry-run" + // HeaderOrigin represents 'origin' header. + HeaderOrigin = "origin" + // HeaderOriginator represents 'ditto-originator' header. + HeaderOriginator = "ditto-originator" + // HeaderETag represents 'ETag' header. + HeaderETag = "ETag" + // HeaderIfMatch represents 'If-Match' header. + HeaderIfMatch = "If-Match" + // HeaderIfNoneMatch represents 'If-None-March' header. + HeaderIfNoneMatch = "If-None-Match" + // HeaderReplyTarget represents 'ditto-reply-target' header. + HeaderReplyTarget = "ditto-reply-target" + // HeaderReplyTo represents 'reply-to' header. + HeaderReplyTo = "reply-to" + // HeaderTimeout represents 'timeout' header. + HeaderTimeout = "timeout" + // HeaderSchemaVersion represents 'version' header. + HeaderSchemaVersion = "version" + // HeaderContentType represents 'content-type' header. + HeaderContentType = "content-type" ) // Headers represents all Ditto-specific headers along with additional HTTP/etc. headers @@ -42,114 +61,151 @@ type Headers struct { // CorrelationID returns the 'correlation-id' header value or empty string if not set. func (h *Headers) CorrelationID() string { - if h.Values[HeaderCorrelationID] == nil { - return "" + if value, ok := h.Values[HeaderCorrelationID]; ok && value != nil { + return value.(string) } - return h.Values[HeaderCorrelationID].(string) + return "" } // Timeout returns the 'timeout' header value or empty string if not set. -func (h *Headers) Timeout() string { - if h.Values[HeaderTimeout] == nil { - return "" +func (h *Headers) Timeout() time.Duration { + if value, ok := h.Values[HeaderTimeout]; ok { + if duration, err := parseTimeout(value.(string)); err == nil { + return duration + } + } + return 60 * time.Second +} + +func parseTimeout(timeout string) (time.Duration, error) { + l := len(timeout) + if l > 0 { + t := time.Duration(-1) + switch timeout[l-1] { + case 'm': + if i, err := strconv.Atoi(timeout[:l-1]); err == nil { + t = time.Duration(i) * time.Minute + } + case 's': + if timeout[l-2] == 'm' { + if i, err := strconv.Atoi(timeout[:l-2]); err == nil { + t = time.Duration(i) * time.Millisecond + } + } else { + if i, err := strconv.Atoi(timeout[:l-1]); err == nil { + t = time.Duration(i) * time.Second + } + } + default: + if i, err := strconv.Atoi(timeout); err == nil { + t = time.Duration(i) * time.Second + } + } + if inTimeoutRange(t) { + return t, nil + } } - return h.Values[HeaderTimeout].(string) + return -1, fmt.Errorf("invalid timeout '%s'", timeout) +} + +func inTimeoutRange(timeout time.Duration) bool { + return timeout >= 0 && timeout < time.Hour } // IsResponseRequired returns the 'response-required' header value or empty string if not set. func (h *Headers) IsResponseRequired() bool { - if h.Values[HeaderResponseRequired] == nil { - return false + if value, ok := h.Values[HeaderResponseRequired]; ok && value != nil { + return value.(bool) } - return h.Values[HeaderResponseRequired].(bool) + return false } // Channel returns the 'ditto-channel' header value or empty string if not set. func (h *Headers) Channel() string { - if h.Values[HeaderChannel] == nil { - return "" + if value, ok := h.Values[HeaderChannel]; ok && value != nil { + return value.(string) } - return h.Values[HeaderChannel].(string) + return "" } // IsDryRun returns the 'ditto-dry-run' header value or empty string if not set. func (h *Headers) IsDryRun() bool { - if h.Values[HeaderDryRun] == nil { - return false + if value, ok := h.Values[HeaderDryRun]; ok && value != nil { + return value.(bool) } - return h.Values[HeaderDryRun].(bool) + return false } // Origin returns the 'origin' header value or empty string if not set. func (h *Headers) Origin() string { - if h.Values[HeaderOrigin] == nil { - return "" + if value, ok := h.Values[HeaderOrigin]; ok && value != nil { + return value.(string) } - return h.Values[HeaderOrigin].(string) + return "" } // Originator returns the 'ditto-originator' header value or empty string if not set. func (h *Headers) Originator() string { - if h.Values[HeaderOriginator] == nil { - return "" + if value, ok := h.Values[HeaderOriginator]; ok && value != nil { + return value.(string) } - return h.Values[HeaderOriginator].(string) + return "" } // ETag returns the 'ETag' header value or empty string if not set. func (h *Headers) ETag() string { - if h.Values[HeaderETag] == nil { - return "" + if value, ok := h.Values[HeaderETag]; ok && value != nil { + return value.(string) } - return h.Values[HeaderETag].(string) + return "" } // IfMatch returns the 'If-Match' header value or empty string if not set. func (h *Headers) IfMatch() string { - if h.Values[HeaderIfMatch] == nil { - return "" + if value, ok := h.Values[HeaderIfMatch]; ok && value != nil { + return value.(string) } - return h.Values[HeaderIfMatch].(string) + return "" } // IfNoneMatch returns the 'If-None-Match' header value or empty string if not set. func (h *Headers) IfNoneMatch() string { - if h.Values[HeaderIfNoneMatch] == nil { - return "" + if value, ok := h.Values[HeaderIfNoneMatch]; ok && value != nil { + return value.(string) } - return h.Values[HeaderIfNoneMatch].(string) + return "" } // ReplyTarget returns the 'ditto-reply-target' header value or empty string if not set. func (h *Headers) ReplyTarget() int64 { - if h.Values[HeaderReplyTarget] == nil { - return 0 + if value, ok := h.Values[HeaderReplyTarget]; ok && value != nil { + return value.(int64) } - return h.Values[HeaderReplyTarget].(int64) + return 0 } // ReplyTo returns the 'reply-to' header value or empty string if not set. func (h *Headers) ReplyTo() string { - if h.Values[HeaderReplyTo] == nil { - return "" + if value, ok := h.Values[HeaderReplyTo]; ok && value != nil { + return value.(string) } - return h.Values[HeaderReplyTo].(string) + return "" } // Version returns the 'version' header value or empty string if not set. func (h *Headers) Version() int64 { - if h.Values[HeaderSchemaVersion] == nil { - return 0 + if value, ok := h.Values[HeaderSchemaVersion]; ok && value != nil { + return value.(int64) } - return h.Values[HeaderSchemaVersion].(int64) + return 0 } // ContentType returns the 'content-type' header value or empty string if not set. func (h *Headers) ContentType() string { - if h.Values[HeaderContentType] == nil { - return "" + if value, ok := h.Values[HeaderContentType]; ok && value != nil { + return value.(string) } - return h.Values[HeaderContentType].(string) + return "" } // Generic returns the value of the provided key header and if a header with such key is present. diff --git a/protocol/headers_opts.go b/protocol/headers_opts.go index d33fac0..28e5628 100644 --- a/protocol/headers_opts.go +++ b/protocol/headers_opts.go @@ -11,6 +11,11 @@ package protocol +import ( + "strconv" + "time" +) + // HeaderOpt represents a specific Headers option that can be applied to the Headers instance // resulting in changing the value of a specific header of a set of headers. type HeaderOpt func(headers *Headers) error @@ -140,9 +145,41 @@ func WithIfNoneMatch(ifNoneMatch string) HeaderOpt { } // WithTimeout sets the 'timeout' header value. -func WithTimeout(timeout string) HeaderOpt { +// +// The provided value should be a non-negative duration in Millisecond, Second or Minute unit. +// The change results in timeout header string value containing the duration +// and the unit symbol (ms, s or m), e.g. '45s' or '250ms' or '1m'. +// +// The default value is '60s'. +// If a negative duration or duration of an hour or more is provided, the timeout header value +// is removed, i.e. the default one is used. +func WithTimeout(timeout time.Duration) HeaderOpt { return func(headers *Headers) error { - headers.Values[HeaderTimeout] = timeout + if inTimeoutRange(timeout) { + var value string + + if timeout > time.Second { + div := int64(timeout / time.Second) + rem := timeout % time.Second + if rem == 0 { + value = strconv.FormatInt(div, 10) + } else { + value = strconv.FormatInt(div+1, 10) + } + } else { + div := int64(timeout / time.Millisecond) + rem := timeout % time.Millisecond + if rem == 0 { + value = strconv.FormatInt(div, 10) + "ms" + } else { + value = strconv.FormatInt(div+1, 10) + "ms" + } + } + + headers.Values[HeaderTimeout] = value + } else { + delete(headers.Values, HeaderTimeout) + } return nil } } diff --git a/protocol/headers_opts_test.go b/protocol/headers_opts_test.go index 48c53b0..b3a700b 100644 --- a/protocol/headers_opts_test.go +++ b/protocol/headers_opts_test.go @@ -14,6 +14,7 @@ package protocol import ( "errors" "testing" + "time" "github.com/eclipse/ditto-clients-golang/internal" ) @@ -270,12 +271,46 @@ func TestWithIfNoneMatch(t *testing.T) { } func TestWithTimeout(t *testing.T) { - t.Run("TestWithTimeout", func(t *testing.T) { - tmo := "10" + tests := map[string]struct { + arg time.Duration + want time.Duration + }{ + "test_with_seconds": { + arg: 10 * time.Second, + want: 10 * time.Second, + }, + "test_with_milliseconds": { + arg: 500 * time.Millisecond, + want: 500 * time.Millisecond, + }, + "test_with_minute": { + arg: 1 * time.Minute, + want: time.Minute, + }, + "test_with_zero": { + arg: 0, + want: 0 * time.Second, + }, + "test_without_unit": { + arg: 5, + want: 1 * time.Millisecond, + }, + "test_with_invalid_timeout": { + arg: -1, + want: 60 * time.Second, + }, + "test_with_1_hour_timeout": { + arg: time.Hour, + want: 60 * time.Second, + }, + } - got := NewHeaders(WithTimeout(tmo)) - internal.AssertEqual(t, tmo, got.Timeout()) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := NewHeaders(WithTimeout(testCase.arg)) + internal.AssertEqual(t, testCase.want, got.Timeout()) + }) + } } func TestWithSchemaVersion(t *testing.T) { diff --git a/protocol/headers_test.go b/protocol/headers_test.go index e6cfe85..d823c65 100644 --- a/protocol/headers_test.go +++ b/protocol/headers_test.go @@ -12,248 +12,499 @@ package protocol import ( + "encoding/json" "testing" + "time" "github.com/eclipse/ditto-clients-golang/internal" ) func TestHeadersCorrelationID(t *testing.T) { - t.Run("TestHeadersCorrelationID", func(t *testing.T) { - arg := make(map[string]interface{}) - arg[HeaderCorrelationID] = "correlation-id" - h := &Headers{ - Values: arg, - } - - got := h.CorrelationID() - internal.AssertEqual(t, "correlation-id", got) - - arg[HeaderCorrelationID] = nil + tests := map[string]struct { + testHeader *Headers + want string + }{ + "test_with_correlation_id": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderCorrelationID: "correlation-id"}, + }, + want: "correlation-id", + }, + "test_correlation_id_value_nil": { + testHeader: &Headers{ + Values: map[string]interface{}{ + HeaderCorrelationID: nil, + }, + }, + want: "", + }, + "test_without_correlation_id": { + testHeader: &Headers{}, + want: "", + }, + } - got = h.CorrelationID() - internal.AssertEqual(t, "", got) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := testCase.testHeader.CorrelationID() + internal.AssertEqual(t, testCase.want, got) + }) + } } func TestHeadersTimeout(t *testing.T) { - t.Run("TestHeadersTimeout", func(t *testing.T) { - arg := make(map[string]interface{}) - arg[HeaderTimeout] = "10" - h := &Headers{ - Values: arg, - } + tests := map[string]struct { + testHeader *Headers + want time.Duration + }{ + "test_with_timeout": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderTimeout: "10s"}, + }, + want: 10 * time.Second, + }, + "test_timeout_value_nil": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderTimeout: ""}, + }, + want: 60 * time.Second, + }, + "test_without_timeout": { + testHeader: &Headers{}, + want: 60 * time.Second, + }, + } - got := h.Timeout() - internal.AssertEqual(t, "10", got) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := testCase.testHeader.Timeout() + internal.AssertEqual(t, testCase.want, got) + }) + } +} - arg[HeaderTimeout] = nil - got = h.Timeout() - internal.AssertEqual(t, "", got) - }) +func TestTimeout(t *testing.T) { + tests := map[string]struct { + data string + want time.Duration + }{ + "test_seconds": { + data: `{ "timeout": "10s" }`, + want: 10 * time.Second, + }, + "test_milliseconds": { + data: `{ "timeout": "500ms" }`, + want: 500 * time.Millisecond, + }, + "test_minute": { + data: `{ "timeout": "1m" }`, + want: time.Minute, + }, + "test_without_unit_symbol": { + data: `{ "timeout": "10" }`, + want: 10 * time.Second, + }, + "test_with_60_m_timeout": { + data: `{ "timeout": "60m" }`, + want: 60 * time.Second, + }, + "test_with_3600_s_timeout": { + data: `{ "timeout": "3600" }`, + want: 60 * time.Second, + }, + "test_with_negative_timeout": { + data: `{ "timeout": "-5" }`, + want: 60 * time.Second, + }, + "test_with_empty_timeout": { + data: `{ "timeout": "" }`, + want: 60 * time.Second, + }, + "test_with_invalid_timeout": { + data: `{ "timeout": "invalid" }`, + want: 60 * time.Second, + }, + } + + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + var headers Headers + json.Unmarshal([]byte(testCase.data), &headers) + internal.AssertEqual(t, testCase.want, headers.Timeout()) + }) + } } func TestHeadersIsResponseRequired(t *testing.T) { - t.Run("TestHeadersIsResponseRequired", func(t *testing.T) { - arg := make(map[string]interface{}) - arg[HeaderResponseRequired] = false - h := &Headers{ - Values: arg, - } - - got := h.IsResponseRequired() - internal.AssertFalse(t, got) + tests := map[string]struct { + testHeader *Headers + want bool + }{ + "test_with_response_required": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderResponseRequired: true}, + }, + want: true, + }, + "test_response_required_value_nil": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderResponseRequired: nil}, + }, + want: false, + }, + "test_without_response_required": { + testHeader: &Headers{}, + want: false, + }, + } - arg[HeaderResponseRequired] = nil - got = h.IsResponseRequired() - internal.AssertFalse(t, got) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := testCase.testHeader.IsResponseRequired() + internal.AssertEqual(t, testCase.want, got) + }) + } } func TestHeadersChannel(t *testing.T) { - t.Run("TestHeadersChannel", func(t *testing.T) { - arg := make(map[string]interface{}) - arg[HeaderChannel] = "1" - h := &Headers{ - Values: arg, - } - - got := h.Channel() - internal.AssertEqual(t, "1", got) + tests := map[string]struct { + testHeader *Headers + want string + }{ + "test_with_channel": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderChannel: "1"}, + }, + want: "1", + }, + "test_channel_value_nil": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderChannel: nil}, + }, + want: "", + }, + "test_without_channel": { + testHeader: &Headers{}, + want: "", + }, + } - arg[HeaderChannel] = nil - got = h.Channel() - internal.AssertEqual(t, "", got) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := testCase.testHeader.Channel() + internal.AssertEqual(t, testCase.want, got) + }) + } } func TestHeadersIsDryRun(t *testing.T) { - t.Run("TestHeadersIsDryRun", func(t *testing.T) { - arg := make(map[string]interface{}) - arg[HeaderDryRun] = false - h := &Headers{ - Values: arg, - } - - got := h.IsDryRun() - internal.AssertFalse(t, got) + tests := map[string]struct { + testHeader *Headers + want bool + }{ + "test_with_dry_run": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderDryRun: true}, + }, + want: true, + }, + "test_dry_run_value_nil": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderDryRun: nil}, + }, + want: false, + }, + "test_without_dry_run": { + testHeader: &Headers{}, + want: false, + }, + } - arg[HeaderDryRun] = nil - got = h.IsDryRun() - internal.AssertFalse(t, got) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := testCase.testHeader.IsDryRun() + internal.AssertEqual(t, testCase.want, got) + }) + } } func TestHeadersOrigin(t *testing.T) { - t.Run("TestHeadersOrigin", func(t *testing.T) { - arg := make(map[string]interface{}) - arg[HeaderOrigin] = "origin" - h := &Headers{ - Values: arg, - } - - got := h.Origin() - internal.AssertEqual(t, "origin", got) + tests := map[string]struct { + testHeader *Headers + want string + }{ + "test_with_origin": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderOrigin: "origin"}, + }, + want: "origin", + }, + "test_origin_value_nil": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderOrigin: nil}, + }, + want: "", + }, + "test_without_origin": { + testHeader: &Headers{}, + want: "", + }, + } - arg[HeaderOrigin] = nil - got = h.Origin() - internal.AssertEqual(t, "", got) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := testCase.testHeader.Origin() + internal.AssertEqual(t, testCase.want, got) + }) + } } func TestHeadersOriginator(t *testing.T) { - t.Run("TestHeadersOriginator", func(t *testing.T) { - arg := make(map[string]interface{}) - arg[HeaderOriginator] = "ditto-originator" - h := &Headers{ - Values: arg, - } - - got := h.Originator() - internal.AssertEqual(t, "ditto-originator", got) + tests := map[string]struct { + testHeader *Headers + want string + }{ + "test_with_ditto_originator": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderOriginator: "ditto-originator"}, + }, + want: "ditto-originator", + }, + "test_ditto_originator_value_nil": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderOriginator: nil}, + }, + want: "", + }, + "test_without_ditto_originator": { + testHeader: &Headers{}, + want: "", + }, + } - arg[HeaderOriginator] = nil - got = h.Originator() - internal.AssertEqual(t, "", got) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := testCase.testHeader.Originator() + internal.AssertEqual(t, testCase.want, got) + }) + } } func TestHeadersETag(t *testing.T) { - t.Run("TestHeadersETag", func(t *testing.T) { - arg := make(map[string]interface{}) - arg[HeaderETag] = "1" - h := &Headers{ - Values: arg, - } - - got := h.ETag() - internal.AssertEqual(t, "1", got) + tests := map[string]struct { + testHeader *Headers + want string + }{ + "test_with_etag": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderETag: "test-etag"}, + }, + want: "test-etag", + }, + "test_etag_value_nil": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderETag: nil}, + }, + want: "", + }, + "test_without_etag": { + testHeader: &Headers{}, + want: "", + }, + } - arg[HeaderETag] = nil - got = h.ETag() - internal.AssertEqual(t, "", got) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := testCase.testHeader.ETag() + internal.AssertEqual(t, testCase.want, got) + }) + } } func TestHeadersIfMatch(t *testing.T) { - t.Run("TestHeadersIfMatch", func(t *testing.T) { - arg := make(map[string]interface{}) - arg[HeaderIfMatch] = "HeaderIfMatch" - h := &Headers{ - Values: arg, - } - - got := h.IfMatch() - internal.AssertEqual(t, "HeaderIfMatch", got) + tests := map[string]struct { + testHeader *Headers + want string + }{ + "test_with_if_match": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderIfMatch: "HeaderIfMatch"}, + }, + want: "HeaderIfMatch", + }, + "test_if_match_value_nil": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderIfMatch: nil}, + }, + want: "", + }, + "test_without_if_match": { + testHeader: &Headers{}, + want: "", + }, + } - arg[HeaderIfMatch] = nil - got = h.IfMatch() - internal.AssertEqual(t, "", got) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := testCase.testHeader.IfMatch() + internal.AssertEqual(t, testCase.want, got) + }) + } } func TestHeadersIfNoneMatch(t *testing.T) { - t.Run("TestHeadersIfNoneMatch", func(t *testing.T) { - arg := make(map[string]interface{}) - arg[HeaderIfNoneMatch] = "123" - h := &Headers{ - Values: arg, - } - - got := h.IfNoneMatch() - internal.AssertEqual(t, "123", got) + tests := map[string]struct { + testHeader *Headers + want string + }{ + "test_with_if_none_match": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderIfNoneMatch: "HeaderIfNoneMatch"}, + }, + want: "HeaderIfNoneMatch", + }, + "test_if_none_match_value_nil": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderIfNoneMatch: nil}, + }, + want: "", + }, + "test_without_if_none_match": { + testHeader: &Headers{}, + want: "", + }, + } - arg[HeaderIfNoneMatch] = nil - got = h.IfNoneMatch() - internal.AssertEqual(t, "", got) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := testCase.testHeader.IfNoneMatch() + internal.AssertEqual(t, testCase.want, got) + }) + } } func TestHeadersReplyTarget(t *testing.T) { - t.Run("TestHeadersReplyTarget", func(t *testing.T) { - arg := make(map[string]interface{}) - arg[HeaderReplyTarget] = int64(123) - h := &Headers{ - Values: arg, - } - - got := h.ReplyTarget() - internal.AssertEqual(t, int64(123), got) + tests := map[string]struct { + testHeader *Headers + want int64 + }{ + "test_with_reply_target": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderReplyTarget: int64(123)}, + }, + want: int64(123), + }, + "test_reply_target_value_nil": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderReplyTarget: nil}, + }, + want: 0, + }, + "test_without_reply_target": { + testHeader: &Headers{}, + want: 0, + }, + } - arg[HeaderReplyTarget] = nil - got = h.ReplyTarget() - internal.AssertEqual(t, int64(0), got) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := testCase.testHeader.ReplyTarget() + internal.AssertEqual(t, testCase.want, got) + }) + } } func TestHeadersReplyTo(t *testing.T) { - t.Run("TestHeadersReplyTo", func(t *testing.T) { - arg := make(map[string]interface{}) - arg[HeaderReplyTo] = "someone" - h := &Headers{ - Values: arg, - } - - got := h.ReplyTo() - internal.AssertEqual(t, "someone", got) + tests := map[string]struct { + testHeader *Headers + want string + }{ + "test_with_reply_to": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderReplyTo: "someone"}, + }, + want: "someone", + }, + "test_reply_to_value_nil": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderReplyTo: nil}, + }, + want: "", + }, + "test_without_reply_to": { + testHeader: &Headers{}, + want: "", + }, + } - arg[HeaderReplyTo] = nil - got = h.ReplyTo() - internal.AssertEqual(t, "", got) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := testCase.testHeader.ReplyTo() + internal.AssertEqual(t, testCase.want, got) + }) + } } func TestHeadersVersion(t *testing.T) { - t.Run("TestHeadersVersion", func(t *testing.T) { - arg := make(map[string]interface{}) - arg[HeaderSchemaVersion] = int64(1111) - h := &Headers{ - Values: arg, - } - - got := h.Version() - internal.AssertEqual(t, int64(1111), got) + tests := map[string]struct { + testHeader *Headers + want int64 + }{ + "test_with_version": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderSchemaVersion: int64(123)}, + }, + want: int64(123), + }, + "test_version_value_nil": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderSchemaVersion: nil}, + }, + want: 0, + }, + "test_without_version": { + testHeader: &Headers{}, + want: 0, + }, + } - arg[HeaderSchemaVersion] = nil - got = h.Version() - internal.AssertEqual(t, int64(0), got) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := testCase.testHeader.Version() + internal.AssertEqual(t, testCase.want, got) + }) + } } func TestHeadersContentType(t *testing.T) { - t.Run("TestHeadersContentType", func(t *testing.T) { - arg := make(map[string]interface{}) - arg[HeaderContentType] = "HeaderContentType" - h := &Headers{ - Values: arg, - } - - got := h.ContentType() - internal.AssertEqual(t, "HeaderContentType", got) + tests := map[string]struct { + testHeader *Headers + want string + }{ + "test_with_content_type": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderContentType: "HeaderContentType"}, + }, + want: "HeaderContentType", + }, + "test_content_type_value_nil": { + testHeader: &Headers{ + Values: map[string]interface{}{HeaderContentType: nil}, + }, + want: "", + }, + "test_without_content_type": { + testHeader: &Headers{}, + want: "", + }, + } - arg[HeaderContentType] = nil - got = h.ContentType() - internal.AssertEqual(t, "", got) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := testCase.testHeader.ContentType() + internal.AssertEqual(t, testCase.want, got) + }) + } } func TestHeadersGeneric(t *testing.T) { From aacdd8ba53a4660c742c30a7f327b48402d22d16 Mon Sep 17 00:00:00 2001 From: "Trifonova.Antonia" Date: Tue, 25 Jan 2022 14:57:56 +0200 Subject: [PATCH 2/8] Headers improvements - Update the doc according the refactoring - Convert timeout in time.Duration after unmarshaling JSON - Add more tests scenarios Signed-off-by: Antonia Trifonova antonia.trifonova@bosch.io --- protocol/headers.go | 19 +++++++---- protocol/headers_test.go | 68 ++++++++++++++++++++++++++-------------- 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/protocol/headers.go b/protocol/headers.go index b5be111..83b7f65 100644 --- a/protocol/headers.go +++ b/protocol/headers.go @@ -67,7 +67,7 @@ func (h *Headers) CorrelationID() string { return "" } -// Timeout returns the 'timeout' header value or empty string if not set. +// Timeout returns the 'timeout' header value or duration of 60 seconds if not set. func (h *Headers) Timeout() time.Duration { if value, ok := h.Values[HeaderTimeout]; ok { if duration, err := parseTimeout(value.(string)); err == nil { @@ -112,12 +112,12 @@ func inTimeoutRange(timeout time.Duration) bool { return timeout >= 0 && timeout < time.Hour } -// IsResponseRequired returns the 'response-required' header value or empty string if not set. +// IsResponseRequired returns the 'response-required' header value or true if not set. func (h *Headers) IsResponseRequired() bool { if value, ok := h.Values[HeaderResponseRequired]; ok && value != nil { return value.(bool) } - return false + return true } // Channel returns the 'ditto-channel' header value or empty string if not set. @@ -220,10 +220,17 @@ func (h *Headers) MarshalJSON() ([]byte, error) { // UnmarshalJSON unmarshels Headers. func (h *Headers) UnmarshalJSON(data []byte) error { - var v map[string]interface{} - if err := json.Unmarshal(data, &v); err != nil { + var m map[string]interface{} + if err := json.Unmarshal(data, &m); err != nil { return err } - h.Values = v + + if value, ok := m[HeaderTimeout]; ok { + if _, err := parseTimeout(value.(string)); err != nil { + return err + } + } + + h.Values = m return nil } diff --git a/protocol/headers_test.go b/protocol/headers_test.go index d823c65..8603dc7 100644 --- a/protocol/headers_test.go +++ b/protocol/headers_test.go @@ -12,7 +12,7 @@ package protocol import ( - "encoding/json" + "errors" "testing" "time" @@ -128,8 +128,8 @@ func TestTimeout(t *testing.T) { for testName, testCase := range tests { t.Run(testName, func(t *testing.T) { - var headers Headers - json.Unmarshal([]byte(testCase.data), &headers) + headers := NewHeaders() + headers.UnmarshalJSON([]byte(testCase.data)) internal.AssertEqual(t, testCase.want, headers.Timeout()) }) } @@ -142,19 +142,19 @@ func TestHeadersIsResponseRequired(t *testing.T) { }{ "test_with_response_required": { testHeader: &Headers{ - Values: map[string]interface{}{HeaderResponseRequired: true}, + Values: map[string]interface{}{HeaderResponseRequired: false}, }, - want: true, + want: false, }, "test_response_required_value_nil": { testHeader: &Headers{ Values: map[string]interface{}{HeaderResponseRequired: nil}, }, - want: false, + want: true, }, "test_without_response_required": { testHeader: &Headers{}, - want: false, + want: true, }, } @@ -560,19 +560,49 @@ func TestHeadersMarshalJSON(t *testing.T) { } func TestHeadersUnmarshalJSON(t *testing.T) { - ct := "application/json" - tests := map[string]struct { data string - wantErr bool + wantErr error }{ - "test_headers_unmarshal_JSON_ok": { - data: "{\"content-type\":\"application/json\"}", - wantErr: false, + "test_headers_unmarshal_JSON_with_one_heder": { + data: `{"content-type":"application/json"}`, + wantErr: nil, + }, + "test_headers_unmarshal_JSON_with_many_headers": { + data: `{ + "content-type": "application/json", + "response-required": false, + "timeout": "30ms" + }`, + wantErr: nil, + }, + "test_headers_unmarshal_JSON_negative_timeout": { + data: `{ + "timeout": "-30" + }`, + wantErr: errors.New("invalid timeout '-30'"), + }, + "test_headers_unmarshal_JSON_invalid_timeout": { + data: `{ + "timeout": "1h" + }`, + wantErr: errors.New("invalid timeout '1h'"), + }, + "test_headers_unmarshal_JSON_empty_timeout": { + data: `{ + "timeout": "" + }`, + wantErr: errors.New("invalid timeout ''"), + }, + "test_headers_unmarshal_JSON_zero_timeout": { + data: `{ + "timeout": "0" + }`, + wantErr: nil, }, "test_headers_unmarshal_JSON_err": { data: "", - wantErr: true, + wantErr: errors.New("unexpected end of JSON input"), }, } @@ -580,15 +610,7 @@ func TestHeadersUnmarshalJSON(t *testing.T) { t.Run(testName, func(t *testing.T) { got := NewHeaders() err := got.UnmarshalJSON([]byte(testCase.data)) - if testCase.wantErr { - if err == nil { - t.Errorf("Headers.UnmarshalJSON() error must not be nil") - } - } else { - if got.ContentType() != ct { - t.Errorf("Headers.UnmarshalJSON() got = %v, want %v", got.ContentType(), ct) - } - } + internal.AssertError(t, testCase.wantErr, err) }) } } From 16a0ee7ef51dafe6040890ed081e86342c3eaf05 Mon Sep 17 00:00:00 2001 From: "Trifonova.Antonia" Date: Wed, 30 Mar 2022 15:50:03 +0300 Subject: [PATCH 3/8] Poc Headers improvements: - change header type to map - provide the functionality for case-insensitive - change timeout getter and setter method to expect time.Duration type - improve the getter methods to return default value (if this value is presented) - improve the test scenarios Signed-off-by: Antonia Trifonova antonia.trifonova@bosch.io --- protocol/envelope.go | 4 +- protocol/headers.go | 325 ++++++++++++++++++++++--------- protocol/headers_opts.go | 131 ++++++------- protocol/headers_opts_test.go | 93 +++------ protocol/headers_test.go | 321 ++++++++++-------------------- protocol/things/commands_test.go | 12 +- protocol/things/events_test.go | 12 +- protocol/things/messages_test.go | 12 +- 8 files changed, 442 insertions(+), 468 deletions(-) diff --git a/protocol/envelope.go b/protocol/envelope.go index 798b7d4..5616687 100644 --- a/protocol/envelope.go +++ b/protocol/envelope.go @@ -15,7 +15,7 @@ package protocol // payload, the structure is to be used as a ready to use Ditto message. type Envelope struct { Topic *Topic `json:"topic"` - Headers *Headers `json:"headers,omitempty"` + Headers Headers `json:"headers,omitempty"` Path string `json:"path"` Value interface{} `json:"value,omitempty"` Fields string `json:"fields,omitempty"` @@ -32,7 +32,7 @@ func (msg *Envelope) WithTopic(topic *Topic) *Envelope { } // WithHeaders sets the Headers of the Envelope. -func (msg *Envelope) WithHeaders(headers *Headers) *Envelope { +func (msg *Envelope) WithHeaders(headers Headers) *Envelope { msg.Headers = headers return msg } diff --git a/protocol/headers.go b/protocol/headers.go index 83b7f65..21383ad 100644 --- a/protocol/headers.go +++ b/protocol/headers.go @@ -12,9 +12,9 @@ package protocol import ( - "encoding/json" "fmt" "strconv" + "strings" "time" ) @@ -22,56 +22,89 @@ const ( // ContentTypeDitto defines the Ditto JSON 'content-type' header value for Ditto Protocol messages. ContentTypeDitto = "application/vnd.eclipse.ditto+json" + // ContentTypeJSON defines the JSON 'content-type' header value for Ditto Protocol messages. + ContentTypeJSON = "application/json" + + // ContentTypeJSONMerge defines the JSON merge patch 'content-type' header value for Ditto Protocol messages, + // as specified with RFC 7396 (https://datatracker.ietf.org/doc/html/rfc7396). + ContentTypeJSONMerge = "application/merge-patch+json" + // HeaderCorrelationID represents 'correlation-id' header. HeaderCorrelationID = "correlation-id" + // HeaderResponseRequired represents 'response-required' header. HeaderResponseRequired = "response-required" + // HeaderChannel represents 'ditto-channel' header. HeaderChannel = "ditto-channel" + // HeaderDryRun represents 'ditto-dry-run' header. HeaderDryRun = "ditto-dry-run" + // HeaderOrigin represents 'origin' header. HeaderOrigin = "origin" + // HeaderOriginator represents 'ditto-originator' header. HeaderOriginator = "ditto-originator" - // HeaderETag represents 'ETag' header. - HeaderETag = "ETag" - // HeaderIfMatch represents 'If-Match' header. - HeaderIfMatch = "If-Match" - // HeaderIfNoneMatch represents 'If-None-March' header. - HeaderIfNoneMatch = "If-None-Match" + + // HeaderETag represents 'etag' header. + HeaderETag = "etag" + + // HeaderIfMatch represents 'if-match' header. + HeaderIfMatch = "if-match" + + // HeaderIfNoneMatch represents 'if-none-march' header. + HeaderIfNoneMatch = "if-none-match" + // HeaderReplyTarget represents 'ditto-reply-target' header. HeaderReplyTarget = "ditto-reply-target" + // HeaderReplyTo represents 'reply-to' header. HeaderReplyTo = "reply-to" + // HeaderTimeout represents 'timeout' header. HeaderTimeout = "timeout" + // HeaderSchemaVersion represents 'version' header. HeaderSchemaVersion = "version" + // HeaderContentType represents 'content-type' header. HeaderContentType = "content-type" ) // Headers represents all Ditto-specific headers along with additional HTTP/etc. headers // that can be applied depending on the transport used. +// For the pre-defined headers, the values are in the row type. The getter methods are provided +// to get the header value in specified type. // See https://www.eclipse.org/ditto/protocol-specification.html -type Headers struct { - Values map[string]interface{} -} +type Headers map[string]interface{} -// CorrelationID returns the 'correlation-id' header value or empty string if not set. -func (h *Headers) CorrelationID() string { - if value, ok := h.Values[HeaderCorrelationID]; ok && value != nil { - return value.(string) +// CorrelationID returns the 'correlation-id' header value if it is presented. +// If the header value is not presented, the CorrelationID returns empty string. +// +// If there are two headers differing only in capitalization CorrelationID returns the first value. +// To use the provided key to get the value, access the map directly. +func (h Headers) CorrelationID() string { + for k, v := range h { + if strings.EqualFold(k, HeaderCorrelationID) { + return v.(string) + } } return "" } -// Timeout returns the 'timeout' header value or duration of 60 seconds if not set. -func (h *Headers) Timeout() time.Duration { - if value, ok := h.Values[HeaderTimeout]; ok { - if duration, err := parseTimeout(value.(string)); err == nil { - return duration +// Timeout returns the 'timeout' header value if it is presented +// The default and maximum value is duration of 60 seconds. +// If the header value is not presented, the Timout returns the default value. +// +// If there are two headers differing only in capitalization, the Timeout returns the first value. +// To use the provided key to get the value, access the map directly. +func (h Headers) Timeout() time.Duration { + for k := range h { + if strings.EqualFold(k, HeaderTimeout) { + if duration, err := parseTimeout(h[k].(string)); err == nil { + return duration + } } } return 60 * time.Second @@ -101,136 +134,246 @@ func parseTimeout(timeout string) (time.Duration, error) { t = time.Duration(i) * time.Second } } - if inTimeoutRange(t) { + if t >= 0 && t < time.Hour { return t, nil } } - return -1, fmt.Errorf("invalid timeout '%s'", timeout) + return 60 * time.Second, fmt.Errorf("invalid timeout '%s'", timeout) } -func inTimeoutRange(timeout time.Duration) bool { - return timeout >= 0 && timeout < time.Hour -} - -// IsResponseRequired returns the 'response-required' header value or true if not set. -func (h *Headers) IsResponseRequired() bool { - if value, ok := h.Values[HeaderResponseRequired]; ok && value != nil { - return value.(bool) +// IsResponseRequired returns the 'response-required' header value if it is presented. +// The default value is true. +// If the header value is not presented, the IsResponseRequired returns the default value. +// +// If there are two headers differing only in capitalization, the IsResponseRequired returns the first value. +// To use the provided key to get the value, access the map directly. +func (h Headers) IsResponseRequired() bool { + for k, v := range h { + if strings.EqualFold(k, HeaderResponseRequired) { + return v.(bool) + } } return true } -// Channel returns the 'ditto-channel' header value or empty string if not set. -func (h *Headers) Channel() string { - if value, ok := h.Values[HeaderChannel]; ok && value != nil { - return value.(string) +// Channel returns the 'ditto-channel' header value. +// If the header value is not presented, the Channel returns empty string. +// +// If there are two headers differing only in capitalization, the Channel returns the first value. +// To use the provided key to get the value, access the map directly. +func (h Headers) Channel() string { + for k, v := range h { + if strings.EqualFold(k, HeaderChannel) { + return v.(string) + } } return "" } -// IsDryRun returns the 'ditto-dry-run' header value or empty string if not set. -func (h *Headers) IsDryRun() bool { - if value, ok := h.Values[HeaderDryRun]; ok && value != nil { - return value.(bool) +// IsDryRun returns the 'ditto-dry-run' header value if it is presented. +// The default value is false. +// If the header value is not presented, the IsDryRun returns the default value. +// +// If there are two headers differing only in capitalization, the IsDryRun returns the first value. +// To use the provided key to get the value, access the map directly. +func (h Headers) IsDryRun() bool { + for k, v := range h { + if strings.EqualFold(k, HeaderDryRun) { + return v.(bool) + } } return false } -// Origin returns the 'origin' header value or empty string if not set. -func (h *Headers) Origin() string { - if value, ok := h.Values[HeaderOrigin]; ok && value != nil { - return value.(string) +// Origin returns the 'origin' header value if it is presented. +// If the header value is not presented, the Origin returns the empty string. +// +// If there are two headers differing only in capitalization, the Origin returns the first value. +// To use the provided key to get the value, access the map directly. +func (h Headers) Origin() string { + for k, v := range h { + if strings.EqualFold(k, HeaderOrigin) { + return v.(string) + } } return "" } -// Originator returns the 'ditto-originator' header value or empty string if not set. -func (h *Headers) Originator() string { - if value, ok := h.Values[HeaderOriginator]; ok && value != nil { - return value.(string) +// Originator returns the 'ditto-originator' header value if it is presented. +// If the header value is not presented, the Originator returns the empty string. +// +// If there are two headers differing only in capitalization, the Originator returns the first value. +// To use the provided key to get the value, access the map directly. +func (h Headers) Originator() string { + for k, v := range h { + if strings.EqualFold(k, HeaderOriginator) { + return v.(string) + } } return "" } -// ETag returns the 'ETag' header value or empty string if not set. -func (h *Headers) ETag() string { - if value, ok := h.Values[HeaderETag]; ok && value != nil { - return value.(string) +// ETag returns the 'etag' header value if it is presented. +// If the header value is not presented, the ETag returns the empty string. +// +// If there are two headers differing only in capitalization, the ETag returns the first value. +// To use the provided key to get the value, access the map directly. +func (h Headers) ETag() string { + for k, v := range h { + if strings.EqualFold(k, HeaderETag) { + return v.(string) + } } return "" } -// IfMatch returns the 'If-Match' header value or empty string if not set. -func (h *Headers) IfMatch() string { - if value, ok := h.Values[HeaderIfMatch]; ok && value != nil { - return value.(string) +// IfMatch returns the 'if-match' header value if it is presented. +// If the header value is not presented, the IfMatch returns the empty string. +// +// If there are two headers differing only in capitalization, the IfMatch returns the first value. +// To use the provided key to get the value, access the map directly. +func (h Headers) IfMatch() string { + for k, v := range h { + if strings.EqualFold(k, HeaderIfMatch) { + return v.(string) + } } return "" } -// IfNoneMatch returns the 'If-None-Match' header value or empty string if not set. -func (h *Headers) IfNoneMatch() string { - if value, ok := h.Values[HeaderIfNoneMatch]; ok && value != nil { - return value.(string) +// IfNoneMatch returns the 'if-none-match' header value if it is presented. +// If the header value is not presented, the IfNoneMatch returns the empty string. +// +// If there are two headers differing only in capitalization, the IfNoneMatch returns the first value. +// To use the provided key to get the value, access the map directly. +func (h Headers) IfNoneMatch() string { + for k, v := range h { + if strings.EqualFold(k, HeaderIfNoneMatch) { + return v.(string) + } } return "" } -// ReplyTarget returns the 'ditto-reply-target' header value or empty string if not set. -func (h *Headers) ReplyTarget() int64 { - if value, ok := h.Values[HeaderReplyTarget]; ok && value != nil { - return value.(int64) +// ReplyTarget returns the 'ditto-reply-target' header value if it is presented. +// If the header value is not presented, the ReplyTarget returns 0. +// +// If there are two headers differing only in capitalization, the ReplyTarget returns the first value. +// To use the provided key to get the value, access the map directly. +func (h Headers) ReplyTarget() int64 { + for k, v := range h { + if strings.EqualFold(k, HeaderReplyTarget) { + return v.(int64) + } } return 0 } -// ReplyTo returns the 'reply-to' header value or empty string if not set. -func (h *Headers) ReplyTo() string { - if value, ok := h.Values[HeaderReplyTo]; ok && value != nil { - return value.(string) +// ReplyTo returns the 'reply-to' header value if it is presented. +// If the header value is not presented, the ReplyTo returns the empty string. +// +// If there are two headers differing only in capitalization, the ReplyTo returns the first value. +// To use the provided key to get the value, access the map directly. +func (h Headers) ReplyTo() string { + for k, v := range h { + if strings.EqualFold(k, HeaderReplyTo) { + return v.(string) + } } return "" } -// Version returns the 'version' header value or empty string if not set. -func (h *Headers) Version() int64 { - if value, ok := h.Values[HeaderSchemaVersion]; ok && value != nil { - return value.(int64) +// Version returns the 'version' header value if it is presented. +// If the header value is not presented, the Version returns 0. +// +// If there are two headers differing only in capitalization, the Version returns the first value. +// To use the provided key to get the value, access the map directly. +func (h Headers) Version() int64 { + for k, v := range h { + if strings.EqualFold(k, HeaderSchemaVersion) { + return v.(int64) + } } return 0 } -// ContentType returns the 'content-type' header value or empty string if not set. -func (h *Headers) ContentType() string { - if value, ok := h.Values[HeaderContentType]; ok && value != nil { - return value.(string) +// ContentType returns the 'content-type' header value if it is presented. +// If the header value is not presented, the ContentType returns the empty string. +// +// If there are two headers differing only in capitalization, the ContentType returns the first value. +// To use the provided key to get the value, access the map directly. +func (h Headers) ContentType() string { + for k, v := range h { + if strings.EqualFold(k, HeaderContentType) { + return v.(string) + } } return "" } -// Generic returns the value of the provided key header and if a header with such key is present. -func (h *Headers) Generic(id string) interface{} { - return h.Values[id] +// Generic returns the first value of the provided key header. Capitalization of header names does not affect the header map. +// If there are no provided value, Generic returns nil. +// +// If there are two headers differing only in capitalization Generic returns the first value. +// To use the provided key to get the value, access the map directly. +func (h Headers) Generic(id string) interface{} { + for k, v := range h { + if strings.EqualFold(k, id) { + return v + } + } + return nil } -// MarshalJSON marshels Headers. -func (h *Headers) MarshalJSON() ([]byte, error) { - return json.Marshal(h.Values) -} +// // MarshalJSON marshels Headers. +// func (h *Headers) MarshalJSON() ([]byte, error) { +// // TODO validation +// // convert - timeout - ditto timeout string +// // error for invalid values +// return json.Marshal(h.Values) +// } // UnmarshalJSON unmarshels Headers. -func (h *Headers) UnmarshalJSON(data []byte) error { - var m map[string]interface{} - if err := json.Unmarshal(data, &m); err != nil { - return err - } +// func (h *Headers) UnmarshalJSON(data []byte) error { +// var m map[string]interface{} - if value, ok := m[HeaderTimeout]; ok { - if _, err := parseTimeout(value.(string)); err != nil { - return err - } +// if err := json.Unmarshal(data, &m); err != nil { +// return err +// } + +// for k := range m { +// // TODO for all headers +// // error for ivalid values +// if strings.EqualFold(k, HeaderTimeout) && m[k] != nil { +// m[k] = parseTimeout(m[k].(string)) +// } +// } + +// h.Values = m + +// return nil +// } + +// UnmarshalJSON unmarshels Headers. +// func (h *Headers) UnmarshalJSON(data []byte) error { +// temp := make(map[string]interface{}) +// if err := json.Unmarshal(data, &temp); err != nil { +// return err +// } +// *h = temp +// return nil +// } + +// With sets new Headers to the existing. +func (h Headers) With(opts ...HeaderOpt) Headers { + res := make(map[string]interface{}) + + for key, value := range h { + res[key] = value } - h.Values = m - return nil + if err := applyOptsHeader(res, opts...); err != nil { + return nil + } + return res } diff --git a/protocol/headers_opts.go b/protocol/headers_opts.go index 28e5628..9759daf 100644 --- a/protocol/headers_opts.go +++ b/protocol/headers_opts.go @@ -18,9 +18,9 @@ import ( // HeaderOpt represents a specific Headers option that can be applied to the Headers instance // resulting in changing the value of a specific header of a set of headers. -type HeaderOpt func(headers *Headers) error +type HeaderOpt func(headers Headers) error -func applyOptsHeader(headers *Headers, opts ...HeaderOpt) error { +func applyOptsHeader(headers Headers, opts ...HeaderOpt) error { for _, o := range opts { if err := o(headers); err != nil { return err @@ -30,9 +30,8 @@ func applyOptsHeader(headers *Headers, opts ...HeaderOpt) error { } // NewHeaders returns a new Headers instance. -func NewHeaders(opts ...HeaderOpt) *Headers { - res := &Headers{} - res.Values = make(map[string]interface{}) +func NewHeaders(opts ...HeaderOpt) Headers { + res := Headers{} if err := applyOptsHeader(res, opts...); err != nil { return nil } @@ -40,16 +39,16 @@ func NewHeaders(opts ...HeaderOpt) *Headers { } // NewHeadersFrom returns a new Headers instance using the provided header. -func NewHeadersFrom(orig *Headers, opts ...HeaderOpt) *Headers { +func NewHeadersFrom(orig Headers, opts ...HeaderOpt) Headers { if orig == nil { return NewHeaders(opts...) } - res := &Headers{ - Values: make(map[string]interface{}), - } - for key, value := range orig.Values { - res.Values[key] = value + res := Headers{} + + for key, value := range orig { + res[key] = value } + if err := applyOptsHeader(res, opts...); err != nil { return nil } @@ -58,152 +57,140 @@ func NewHeadersFrom(orig *Headers, opts ...HeaderOpt) *Headers { // WithCorrelationID sets the 'correlation-id' header value. func WithCorrelationID(correlationID string) HeaderOpt { - return func(headers *Headers) error { - headers.Values[HeaderCorrelationID] = correlationID + return func(headers Headers) error { + headers[HeaderCorrelationID] = correlationID return nil } } // WithReplyTo sets the 'reply-to' header value. func WithReplyTo(replyTo string) HeaderOpt { - return func(headers *Headers) error { - headers.Values[HeaderReplyTo] = replyTo + return func(headers Headers) error { + headers[HeaderReplyTo] = replyTo return nil } } // WithReplyTarget sets the 'ditto-reply-target' header value. func WithReplyTarget(replyTarget string) HeaderOpt { - return func(headers *Headers) error { - headers.Values[HeaderReplyTarget] = replyTarget + return func(headers Headers) error { + headers[HeaderReplyTarget] = replyTarget return nil } } // WithChannel sets the 'ditto-channel' header value. func WithChannel(channel string) HeaderOpt { - return func(headers *Headers) error { - headers.Values[HeaderChannel] = channel + return func(headers Headers) error { + headers[HeaderChannel] = channel return nil } } // WithResponseRequired sets the 'response-required' header value. func WithResponseRequired(isResponseRequired bool) HeaderOpt { - return func(headers *Headers) error { - headers.Values[HeaderResponseRequired] = isResponseRequired + return func(headers Headers) error { + headers[HeaderResponseRequired] = isResponseRequired return nil } } // WithOriginator sets the 'ditto-originator' header value. func WithOriginator(dittoOriginator string) HeaderOpt { - return func(headers *Headers) error { - headers.Values[HeaderOriginator] = dittoOriginator + return func(headers Headers) error { + headers[HeaderOriginator] = dittoOriginator return nil } } // WithOrigin sets the 'origin' header value. func WithOrigin(origin string) HeaderOpt { - return func(headers *Headers) error { - headers.Values[HeaderOrigin] = origin + return func(headers Headers) error { + headers[HeaderOrigin] = origin return nil } } // WithDryRun sets the 'ditto-dry-run' header value. func WithDryRun(isDryRun bool) HeaderOpt { - return func(headers *Headers) error { - headers.Values[HeaderDryRun] = isDryRun + return func(headers Headers) error { + headers[HeaderDryRun] = isDryRun return nil } } -// WithETag sets the 'ETag' header value. +// WithETag sets the 'etag' header value. func WithETag(eTag string) HeaderOpt { - return func(headers *Headers) error { - headers.Values[HeaderETag] = eTag + return func(headers Headers) error { + headers[HeaderETag] = eTag return nil } } -// WithIfMatch sets the 'If-Match' header value. +// WithIfMatch sets the 'if-match' header value. func WithIfMatch(ifMatch string) HeaderOpt { - return func(headers *Headers) error { - headers.Values[HeaderIfMatch] = ifMatch + return func(headers Headers) error { + headers[HeaderIfMatch] = ifMatch return nil } } -// WithIfNoneMatch sets the 'If-None-Match' header value. +// WithIfNoneMatch sets the 'if-none-match' header value. func WithIfNoneMatch(ifNoneMatch string) HeaderOpt { - return func(headers *Headers) error { - headers.Values[HeaderIfNoneMatch] = ifNoneMatch + return func(headers Headers) error { + headers[HeaderIfNoneMatch] = ifNoneMatch return nil } } // WithTimeout sets the 'timeout' header value. -// -// The provided value should be a non-negative duration in Millisecond, Second or Minute unit. -// The change results in timeout header string value containing the duration -// and the unit symbol (ms, s or m), e.g. '45s' or '250ms' or '1m'. -// -// The default value is '60s'. -// If a negative duration or duration of an hour or more is provided, the timeout header value -// is removed, i.e. the default one is used. func WithTimeout(timeout time.Duration) HeaderOpt { - return func(headers *Headers) error { - if inTimeoutRange(timeout) { - var value string - - if timeout > time.Second { - div := int64(timeout / time.Second) - rem := timeout % time.Second - if rem == 0 { - value = strconv.FormatInt(div, 10) - } else { - value = strconv.FormatInt(div+1, 10) - } + return func(headers Headers) error { + var value string + + if timeout > time.Second { + div := int64(timeout / time.Second) + rem := timeout % time.Second + if rem == 0 { + value = strconv.FormatInt(div, 10) } else { - div := int64(timeout / time.Millisecond) - rem := timeout % time.Millisecond - if rem == 0 { - value = strconv.FormatInt(div, 10) + "ms" - } else { - value = strconv.FormatInt(div+1, 10) + "ms" - } + value = strconv.FormatInt(div+1, 10) } - - headers.Values[HeaderTimeout] = value } else { - delete(headers.Values, HeaderTimeout) + div := int64(timeout / time.Millisecond) + rem := timeout % time.Millisecond + if rem == 0 { + value = strconv.FormatInt(div, 10) + "ms" + } else { + value = strconv.FormatInt(div+1, 10) + "ms" + } } + + headers[HeaderTimeout] = value return nil } } // WithSchemaVersion sets the 'version' header value. func WithSchemaVersion(schemaVersion string) HeaderOpt { - return func(headers *Headers) error { - headers.Values[HeaderSchemaVersion] = schemaVersion + return func(headers Headers) error { + headers[HeaderSchemaVersion] = schemaVersion return nil } } // WithContentType sets the 'content-type' header value. func WithContentType(contentType string) HeaderOpt { - return func(headers *Headers) error { - headers.Values[HeaderContentType] = contentType + return func(headers Headers) error { + headers[HeaderContentType] = contentType return nil } } // WithGeneric sets the value of the provided key header. func WithGeneric(headerID string, value interface{}) HeaderOpt { - return func(headers *Headers) error { - headers.Values[headerID] = value + return func(headers Headers) error { + headers[headerID] = value return nil } } diff --git a/protocol/headers_opts_test.go b/protocol/headers_opts_test.go index b3a700b..41d3b75 100644 --- a/protocol/headers_opts_test.go +++ b/protocol/headers_opts_test.go @@ -20,7 +20,7 @@ import ( ) func WithError() HeaderOpt { - return func(headers *Headers) error { + return func(headers Headers) error { return errors.New("this is an error example") } } @@ -41,16 +41,15 @@ func TestApplyOptsHeader(t *testing.T) { } for testName, testCase := range tests { t.Run(testName, func(t *testing.T) { - res := make(map[string]interface{}) - res[HeaderChannel] = "somethingBefore" - headers := &Headers{res} + headers := Headers{HeaderChannel: "somethingBefore"} + err := applyOptsHeader(headers, testCase.opts...) if testCase.wantErr { if err == nil { t.Errorf("applyOptsHeader() must rise an error") } - } else if headers.Values[HeaderChannel] != "somethingNow" { - t.Errorf("applyOptsHeader() Header want = \"somethingNow\" got %v", headers.Values[HeaderChannel]) + } else if headers[HeaderChannel] != "somethingNow" { + t.Errorf("applyOptsHeader() Header want = \"somethingNow\" got %v", headers[HeaderChannel]) } }) } @@ -59,15 +58,11 @@ func TestApplyOptsHeader(t *testing.T) { func TestNewHeaders(t *testing.T) { tests := map[string]struct { opts []HeaderOpt - want *Headers + want Headers }{ "test_new_headers": { opts: []HeaderOpt{WithChannel("someChannel")}, - want: &Headers{ - Values: map[string]interface{}{ - HeaderChannel: "someChannel", - }, - }, + want: Headers{HeaderChannel: "someChannel"}, }, "test_new_headers_error": { opts: []HeaderOpt{WithError()}, @@ -75,9 +70,7 @@ func TestNewHeaders(t *testing.T) { }, "test_new_headers_without_opts": { opts: nil, - want: &Headers{ - Values: make(map[string]interface{}), - }, + want: Headers{}, }, } for testName, testCase := range tests { @@ -90,76 +83,52 @@ func TestNewHeaders(t *testing.T) { func TestNewHeadersFrom(t *testing.T) { tests := map[string]struct { - arg1 *Headers + arg1 Headers arg2 []HeaderOpt - want *Headers + want Headers }{ "test_copy_existing_empty_header_with_new_value": { - arg1: &Headers{}, + arg1: Headers{}, arg2: []HeaderOpt{WithCorrelationID("test-correlation-id")}, - want: &Headers{ - Values: map[string]interface{}{ - HeaderCorrelationID: "test-correlation-id", - }, - }, + want: Headers{HeaderCorrelationID: "test-correlation-id"}, }, "test_copy_existing_not_empty_haeder_with_new_value": { - arg1: &Headers{ - Values: map[string]interface{}{HeaderCorrelationID: "test-correlation-id"}, - }, + arg1: Headers{HeaderCorrelationID: "test-correlation-id"}, arg2: []HeaderOpt{WithContentType("application/json")}, - want: &Headers{ - Values: map[string]interface{}{ - HeaderCorrelationID: "test-correlation-id", - HeaderContentType: "application/json", - }, + want: Headers{ + HeaderCorrelationID: "test-correlation-id", + HeaderContentType: "application/json", }, }, "test_copy_existing_not_empty_header_nil_value": { - arg1: &Headers{ - Values: map[string]interface{}{HeaderCorrelationID: "test-correlation-id"}, - }, + arg1: Headers{HeaderCorrelationID: "test-correlation-id"}, arg2: nil, - want: &Headers{ - Values: map[string]interface{}{HeaderCorrelationID: "test-correlation-id"}, - }, + want: Headers{HeaderCorrelationID: "test-correlation-id"}, }, "test_copy_existing_not_empty_header_empty_value": { - arg1: &Headers{ - Values: map[string]interface{}{HeaderCorrelationID: "test-correlation-id"}, - }, + arg1: Headers{HeaderCorrelationID: "test-correlation-id"}, arg2: []HeaderOpt{}, - want: &Headers{ - Values: map[string]interface{}{HeaderCorrelationID: "test-correlation-id"}, - }, + want: Headers{HeaderCorrelationID: "test-correlation-id"}, }, "test_copy_existing_empty_header_nil_value": { - arg1: &Headers{}, + arg1: Headers{}, arg2: nil, - want: &Headers{ - Values: make(map[string]interface{}), - }, + want: Headers{}, }, "test_copy_nil_header_with_values": { arg1: nil, arg2: []HeaderOpt{WithCorrelationID("correlation-id")}, - want: &Headers{ - Values: map[string]interface{}{HeaderCorrelationID: "correlation-id"}, - }, + want: Headers{HeaderCorrelationID: "correlation-id"}, }, "test_copy_nil_header_nil_value": { arg1: nil, arg2: nil, - want: &Headers{ - make(map[string]interface{}), - }, + want: Headers{}, }, "test_copy_nil_header_empty_value": { arg1: nil, arg2: []HeaderOpt{}, - want: &Headers{ - Values: make(map[string]interface{}), - }, + want: Headers{}, }, } @@ -194,7 +163,7 @@ func TestWithReplyTarget(t *testing.T) { rt := "11111" got := NewHeaders(WithReplyTarget(rt)) - internal.AssertEqual(t, rt, got.Values[HeaderReplyTarget]) + internal.AssertEqual(t, rt, got[HeaderReplyTarget]) }) } @@ -291,14 +260,6 @@ func TestWithTimeout(t *testing.T) { arg: 0, want: 0 * time.Second, }, - "test_without_unit": { - arg: 5, - want: 1 * time.Millisecond, - }, - "test_with_invalid_timeout": { - arg: -1, - want: 60 * time.Second, - }, "test_with_1_hour_timeout": { arg: time.Hour, want: 60 * time.Second, @@ -318,7 +279,7 @@ func TestWithSchemaVersion(t *testing.T) { sv := "123456789" got := NewHeaders(WithSchemaVersion("123456789")) - internal.AssertEqual(t, sv, got.Values[HeaderSchemaVersion]) + internal.AssertEqual(t, sv, got[HeaderSchemaVersion]) }) } diff --git a/protocol/headers_test.go b/protocol/headers_test.go index 8603dc7..5adc90b 100644 --- a/protocol/headers_test.go +++ b/protocol/headers_test.go @@ -12,7 +12,7 @@ package protocol import ( - "errors" + "encoding/json" "testing" "time" @@ -21,25 +21,20 @@ import ( func TestHeadersCorrelationID(t *testing.T) { tests := map[string]struct { - testHeader *Headers + testHeader Headers want string }{ "test_with_correlation_id": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderCorrelationID: "correlation-id"}, - }, - want: "correlation-id", - }, - "test_correlation_id_value_nil": { - testHeader: &Headers{ - Values: map[string]interface{}{ - HeaderCorrelationID: nil, - }, - }, - want: "", + testHeader: Headers{HeaderCorrelationID: "correlation-id"}, + want: "correlation-id", }, "test_without_correlation_id": { - testHeader: &Headers{}, + testHeader: Headers{}, + want: "", + }, + + "test_empty_correlation_id": { + testHeader: Headers{HeaderCorrelationID: ""}, want: "", }, } @@ -54,23 +49,15 @@ func TestHeadersCorrelationID(t *testing.T) { func TestHeadersTimeout(t *testing.T) { tests := map[string]struct { - testHeader *Headers + testHeader Headers want time.Duration }{ "test_with_timeout": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderTimeout: "10s"}, - }, - want: 10 * time.Second, - }, - "test_timeout_value_nil": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderTimeout: ""}, - }, - want: 60 * time.Second, + testHeader: Headers{HeaderTimeout: "10s"}, + want: 10 * time.Second, }, "test_without_timeout": { - testHeader: &Headers{}, + testHeader: Headers{}, want: 60 * time.Second, }, } @@ -129,7 +116,7 @@ func TestTimeout(t *testing.T) { for testName, testCase := range tests { t.Run(testName, func(t *testing.T) { headers := NewHeaders() - headers.UnmarshalJSON([]byte(testCase.data)) + json.Unmarshal([]byte(testCase.data), &headers) internal.AssertEqual(t, testCase.want, headers.Timeout()) }) } @@ -137,23 +124,19 @@ func TestTimeout(t *testing.T) { func TestHeadersIsResponseRequired(t *testing.T) { tests := map[string]struct { - testHeader *Headers + testHeader Headers want bool }{ - "test_with_response_required": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderResponseRequired: false}, - }, - want: false, + "test_with_response_required_false": { + testHeader: Headers{HeaderResponseRequired: false}, + want: false, }, - "test_response_required_value_nil": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderResponseRequired: nil}, - }, - want: true, + "test_with_response_required_true": { + testHeader: Headers{HeaderResponseRequired: true}, + want: true, }, "test_without_response_required": { - testHeader: &Headers{}, + testHeader: Headers{}, want: true, }, } @@ -168,23 +151,15 @@ func TestHeadersIsResponseRequired(t *testing.T) { func TestHeadersChannel(t *testing.T) { tests := map[string]struct { - testHeader *Headers + testHeader Headers want string }{ "test_with_channel": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderChannel: "1"}, - }, - want: "1", - }, - "test_channel_value_nil": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderChannel: nil}, - }, - want: "", + testHeader: Headers{HeaderChannel: "1"}, + want: "1", }, "test_without_channel": { - testHeader: &Headers{}, + testHeader: Headers{}, want: "", }, } @@ -199,23 +174,15 @@ func TestHeadersChannel(t *testing.T) { func TestHeadersIsDryRun(t *testing.T) { tests := map[string]struct { - testHeader *Headers + testHeader Headers want bool }{ "test_with_dry_run": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderDryRun: true}, - }, - want: true, - }, - "test_dry_run_value_nil": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderDryRun: nil}, - }, - want: false, + testHeader: Headers{HeaderDryRun: true}, + want: true, }, "test_without_dry_run": { - testHeader: &Headers{}, + testHeader: Headers{}, want: false, }, } @@ -230,23 +197,15 @@ func TestHeadersIsDryRun(t *testing.T) { func TestHeadersOrigin(t *testing.T) { tests := map[string]struct { - testHeader *Headers + testHeader Headers want string }{ "test_with_origin": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderOrigin: "origin"}, - }, - want: "origin", - }, - "test_origin_value_nil": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderOrigin: nil}, - }, - want: "", + testHeader: Headers{HeaderOrigin: "origin"}, + want: "origin", }, "test_without_origin": { - testHeader: &Headers{}, + testHeader: Headers{}, want: "", }, } @@ -261,23 +220,15 @@ func TestHeadersOrigin(t *testing.T) { func TestHeadersOriginator(t *testing.T) { tests := map[string]struct { - testHeader *Headers + testHeader Headers want string }{ "test_with_ditto_originator": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderOriginator: "ditto-originator"}, - }, - want: "ditto-originator", - }, - "test_ditto_originator_value_nil": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderOriginator: nil}, - }, - want: "", + testHeader: Headers{HeaderOriginator: "ditto-originator"}, + want: "ditto-originator", }, "test_without_ditto_originator": { - testHeader: &Headers{}, + testHeader: Headers{}, want: "", }, } @@ -292,23 +243,15 @@ func TestHeadersOriginator(t *testing.T) { func TestHeadersETag(t *testing.T) { tests := map[string]struct { - testHeader *Headers + testHeader Headers want string }{ "test_with_etag": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderETag: "test-etag"}, - }, - want: "test-etag", - }, - "test_etag_value_nil": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderETag: nil}, - }, - want: "", + testHeader: Headers{HeaderETag: "test-etag"}, + want: "test-etag", }, "test_without_etag": { - testHeader: &Headers{}, + testHeader: Headers{}, want: "", }, } @@ -323,23 +266,15 @@ func TestHeadersETag(t *testing.T) { func TestHeadersIfMatch(t *testing.T) { tests := map[string]struct { - testHeader *Headers + testHeader Headers want string }{ "test_with_if_match": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderIfMatch: "HeaderIfMatch"}, - }, - want: "HeaderIfMatch", - }, - "test_if_match_value_nil": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderIfMatch: nil}, - }, - want: "", + testHeader: Headers{HeaderIfMatch: "HeaderIfMatch"}, + want: "HeaderIfMatch", }, "test_without_if_match": { - testHeader: &Headers{}, + testHeader: Headers{}, want: "", }, } @@ -354,23 +289,15 @@ func TestHeadersIfMatch(t *testing.T) { func TestHeadersIfNoneMatch(t *testing.T) { tests := map[string]struct { - testHeader *Headers + testHeader Headers want string }{ "test_with_if_none_match": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderIfNoneMatch: "HeaderIfNoneMatch"}, - }, - want: "HeaderIfNoneMatch", - }, - "test_if_none_match_value_nil": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderIfNoneMatch: nil}, - }, - want: "", + testHeader: Headers{HeaderIfNoneMatch: "HeaderIfNoneMatch"}, + want: "HeaderIfNoneMatch", }, "test_without_if_none_match": { - testHeader: &Headers{}, + testHeader: Headers{}, want: "", }, } @@ -385,23 +312,15 @@ func TestHeadersIfNoneMatch(t *testing.T) { func TestHeadersReplyTarget(t *testing.T) { tests := map[string]struct { - testHeader *Headers + testHeader Headers want int64 }{ "test_with_reply_target": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderReplyTarget: int64(123)}, - }, - want: int64(123), - }, - "test_reply_target_value_nil": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderReplyTarget: nil}, - }, - want: 0, + testHeader: Headers{HeaderReplyTarget: int64(123)}, + want: int64(123), }, "test_without_reply_target": { - testHeader: &Headers{}, + testHeader: Headers{}, want: 0, }, } @@ -416,23 +335,15 @@ func TestHeadersReplyTarget(t *testing.T) { func TestHeadersReplyTo(t *testing.T) { tests := map[string]struct { - testHeader *Headers + testHeader Headers want string }{ "test_with_reply_to": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderReplyTo: "someone"}, - }, - want: "someone", - }, - "test_reply_to_value_nil": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderReplyTo: nil}, - }, - want: "", + testHeader: Headers{HeaderReplyTo: "someone"}, + want: "someone", }, "test_without_reply_to": { - testHeader: &Headers{}, + testHeader: Headers{}, want: "", }, } @@ -447,23 +358,15 @@ func TestHeadersReplyTo(t *testing.T) { func TestHeadersVersion(t *testing.T) { tests := map[string]struct { - testHeader *Headers + testHeader Headers want int64 }{ "test_with_version": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderSchemaVersion: int64(123)}, - }, - want: int64(123), - }, - "test_version_value_nil": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderSchemaVersion: nil}, - }, - want: 0, + testHeader: Headers{HeaderSchemaVersion: int64(123)}, + want: int64(123), }, "test_without_version": { - testHeader: &Headers{}, + testHeader: Headers{}, want: 0, }, } @@ -478,23 +381,15 @@ func TestHeadersVersion(t *testing.T) { func TestHeadersContentType(t *testing.T) { tests := map[string]struct { - testHeader *Headers + testHeader Headers want string }{ "test_with_content_type": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderContentType: "HeaderContentType"}, - }, - want: "HeaderContentType", - }, - "test_content_type_value_nil": { - testHeader: &Headers{ - Values: map[string]interface{}{HeaderContentType: nil}, - }, - want: "", + testHeader: Headers{HeaderContentType: "HeaderContentType"}, + want: "HeaderContentType", }, "test_without_content_type": { - testHeader: &Headers{}, + testHeader: Headers{}, want: "", }, } @@ -511,9 +406,7 @@ func TestHeadersGeneric(t *testing.T) { t.Run("TestHeadersGeneric", func(t *testing.T) { arg := make(map[string]interface{}) arg[HeaderContentType] = "HeaderContentType" - h := &Headers{ - Values: arg, - } + h := Headers{HeaderContentType: "HeaderContentType"} got := h.Generic(HeaderContentType) internal.AssertEqual(t, arg[HeaderContentType], got) @@ -544,8 +437,7 @@ func TestHeadersMarshalJSON(t *testing.T) { for testName, testCase := range tests { t.Run(testName, func(t *testing.T) { - h := &Headers{testCase.data} - got, err := h.MarshalJSON() + got, err := json.Marshal(testCase.data) if testCase.wantErr { if err == nil { t.Errorf("Headers.MarshalJSON() error must not be nil") @@ -561,56 +453,59 @@ func TestHeadersMarshalJSON(t *testing.T) { func TestHeadersUnmarshalJSON(t *testing.T) { tests := map[string]struct { - data string - wantErr error + data string + want Headers }{ "test_headers_unmarshal_JSON_with_one_heder": { - data: `{"content-type":"application/json"}`, - wantErr: nil, + data: `{"content-type":"application/json"}`, + want: Headers{HeaderContentType: "application/json"}, }, "test_headers_unmarshal_JSON_with_many_headers": { data: `{ - "content-type": "application/json", - "response-required": false, - "timeout": "30ms" + "content-type":"application/json", + "timeout": "30ms", + "response-required":false }`, - wantErr: nil, - }, - "test_headers_unmarshal_JSON_negative_timeout": { - data: `{ - "timeout": "-30" - }`, - wantErr: errors.New("invalid timeout '-30'"), - }, - "test_headers_unmarshal_JSON_invalid_timeout": { - data: `{ - "timeout": "1h" - }`, - wantErr: errors.New("invalid timeout '1h'"), - }, - "test_headers_unmarshal_JSON_empty_timeout": { - data: `{ - "timeout": "" - }`, - wantErr: errors.New("invalid timeout ''"), - }, - "test_headers_unmarshal_JSON_zero_timeout": { - data: `{ - "timeout": "0" - }`, - wantErr: nil, + want: Headers{ + HeaderContentType: "application/json", + HeaderTimeout: "30ms", + HeaderResponseRequired: false, + }, }, "test_headers_unmarshal_JSON_err": { - data: "", - wantErr: errors.New("unexpected end of JSON input"), + data: "", + want: Headers{}, }, } for testName, testCase := range tests { t.Run(testName, func(t *testing.T) { got := NewHeaders() - err := got.UnmarshalJSON([]byte(testCase.data)) - internal.AssertError(t, testCase.wantErr, err) + json.Unmarshal([]byte(testCase.data), &got) + internal.AssertEqual(t, testCase.want, got) }) } } + +func TestCaseInsensitiveKey(t *testing.T) { + headers := Headers{HeaderCorrelationID: "correlation-id-1"} + envelope := &Envelope{ + Headers: headers, + } + // override correlation-id instead of custom header - header key is the last set one + internal.AssertEqual(t, "correlation-id-1", envelope.Headers.Generic("correlation-ID")) + + envelope.WithHeaders(NewHeaders(WithGeneric("coRRelation-ID", "correlation-id-2"))) + + // return the first correlation-id (side effect from unmarshal JSON) + res := envelope.Headers.CorrelationID() + internal.AssertEqual(t, "correlation-id-2", res) + + json.Marshal(envelope.Headers) + + data := `{ + "correlation-iD":"correlation-id-3" + }` + json.Unmarshal([]byte(data), &envelope.Headers) + internal.AssertEqual(t, "correlation-id-3", envelope.Headers["correlation-iD"]) +} diff --git a/protocol/things/commands_test.go b/protocol/things/commands_test.go index 156a789..ebc7987 100644 --- a/protocol/things/commands_test.go +++ b/protocol/things/commands_test.go @@ -355,14 +355,10 @@ func TestEnvelope(t *testing.T) { protocol.WithChannel("testChannel"), }, want: &protocol.Envelope{ - Topic: cmd.Topic, - Path: cmd.Path, - Value: cmd.Payload, - Headers: &protocol.Headers{ - Values: map[string]interface{}{ - protocol.HeaderChannel: "testChannel", - }, - }, + Topic: cmd.Topic, + Path: cmd.Path, + Value: cmd.Payload, + Headers: protocol.Headers{protocol.HeaderChannel: "testChannel"}, }, }, } diff --git a/protocol/things/events_test.go b/protocol/things/events_test.go index 2606c3a..84144c8 100644 --- a/protocol/things/events_test.go +++ b/protocol/things/events_test.go @@ -269,14 +269,10 @@ func TestEventEnvelope(t *testing.T) { protocol.WithChannel("testChannel"), }, want: &protocol.Envelope{ - Topic: event.Topic, - Path: event.Path, - Value: event.Payload, - Headers: &protocol.Headers{ - Values: map[string]interface{}{ - protocol.HeaderChannel: "testChannel", - }, - }, + Topic: event.Topic, + Path: event.Path, + Value: event.Payload, + Headers: protocol.Headers{protocol.HeaderChannel: "testChannel"}, }, }, } diff --git a/protocol/things/messages_test.go b/protocol/things/messages_test.go index 6eb25a9..83d2c57 100644 --- a/protocol/things/messages_test.go +++ b/protocol/things/messages_test.go @@ -117,14 +117,10 @@ func TestMessageEnvelope(t *testing.T) { protocol.WithChannel("testChannel"), }, want: &protocol.Envelope{ - Topic: msg.Topic, - Path: fmt.Sprintf(pathMessagesFormat, msg.AddressedPartOfThing, msg.Mailbox, msg.Subject), - Value: msg.Payload, - Headers: &protocol.Headers{ - Values: map[string]interface{}{ - protocol.HeaderChannel: "testChannel", - }, - }, + Topic: msg.Topic, + Path: fmt.Sprintf(pathMessagesFormat, msg.AddressedPartOfThing, msg.Mailbox, msg.Subject), + Value: msg.Payload, + Headers: protocol.Headers{protocol.HeaderChannel: "testChannel"}, }, }, } From 79f4756ff71ccd3b41184a3961452dafb0d77ae4 Mon Sep 17 00:00:00 2001 From: "Trifonova.Antonia" Date: Mon, 4 Apr 2022 17:41:57 +0300 Subject: [PATCH 4/8] Poc Headers improvements Signed-off-by: Antonia Trifonova antonia.trifonova@bosch.io --- protocol/headers.go | 78 +++++++++++++++++----------------------- protocol/headers_opts.go | 10 ++++++ protocol/headers_test.go | 23 +++++++++--- 3 files changed, 61 insertions(+), 50 deletions(-) diff --git a/protocol/headers.go b/protocol/headers.go index 21383ad..fd5f576 100644 --- a/protocol/headers.go +++ b/protocol/headers.go @@ -12,10 +12,13 @@ package protocol import ( + "encoding/json" "fmt" "strconv" "strings" "time" + + "github.com/google/uuid" ) const ( @@ -72,15 +75,16 @@ const ( HeaderContentType = "content-type" ) -// Headers represents all Ditto-specific headers along with additional HTTP/etc. headers +// Headers represents all Ditto-specific headers along with additional HTTP/etc. Headers // that can be applied depending on the transport used. -// For the pre-defined headers, the values are in the row type. The getter methods are provided -// to get the header value in specified type. +// +// The header values in this map should be serialized. +// The provided getter methods returns the header values which is associated with this definition's key. // See https://www.eclipse.org/ditto/protocol-specification.html type Headers map[string]interface{} // CorrelationID returns the 'correlation-id' header value if it is presented. -// If the header value is not presented, the CorrelationID returns empty string. +// If the header value is not presented, the 'correlation-id' header value will be generated in UUID format. // // If there are two headers differing only in capitalization CorrelationID returns the first value. // To use the provided key to get the value, access the map directly. @@ -90,10 +94,10 @@ func (h Headers) CorrelationID() string { return v.(string) } } - return "" + return uuid.New().String() } -// Timeout returns the 'timeout' header value if it is presented +// Timeout returns the 'timeout' header value if it is presented. // The default and maximum value is duration of 60 seconds. // If the header value is not presented, the Timout returns the default value. // @@ -134,7 +138,7 @@ func parseTimeout(timeout string) (time.Duration, error) { t = time.Duration(i) * time.Second } } - if t >= 0 && t < time.Hour { + if t >= 0 && t <= 60*time.Second { return t, nil } } @@ -284,7 +288,7 @@ func (h Headers) ReplyTo() string { } // Version returns the 'version' header value if it is presented. -// If the header value is not presented, the Version returns 0. +// If the header value is not presented, the Version returns 2. // // If there are two headers differing only in capitalization, the Version returns the first value. // To use the provided key to get the value, access the map directly. @@ -294,7 +298,7 @@ func (h Headers) Version() int64 { return v.(int64) } } - return 0 + return 2 } // ContentType returns the 'content-type' header value if it is presented. @@ -325,44 +329,26 @@ func (h Headers) Generic(id string) interface{} { return nil } -// // MarshalJSON marshels Headers. -// func (h *Headers) MarshalJSON() ([]byte, error) { -// // TODO validation -// // convert - timeout - ditto timeout string -// // error for invalid values -// return json.Marshal(h.Values) -// } - -// UnmarshalJSON unmarshels Headers. -// func (h *Headers) UnmarshalJSON(data []byte) error { -// var m map[string]interface{} - -// if err := json.Unmarshal(data, &m); err != nil { -// return err -// } - -// for k := range m { -// // TODO for all headers -// // error for ivalid values -// if strings.EqualFold(k, HeaderTimeout) && m[k] != nil { -// m[k] = parseTimeout(m[k].(string)) -// } -// } - -// h.Values = m - -// return nil -// } - // UnmarshalJSON unmarshels Headers. -// func (h *Headers) UnmarshalJSON(data []byte) error { -// temp := make(map[string]interface{}) -// if err := json.Unmarshal(data, &temp); err != nil { -// return err -// } -// *h = temp -// return nil -// } +// +// The header names are case-insensitive and case-preserving. +// If there is the same header name as the provided and the difference is +// in capitalization the new header name will be set. +func (h *Headers) UnmarshalJSON(data []byte) error { + temp := make(map[string]interface{}) + if err := json.Unmarshal(data, &temp); err != nil { + return err + } + for k := range temp { + for k1 := range *h { + if strings.EqualFold(k, k1) { + delete(*h, k1) + } + } + } + *h = temp + return nil +} // With sets new Headers to the existing. func (h Headers) With(opts ...HeaderOpt) Headers { diff --git a/protocol/headers_opts.go b/protocol/headers_opts.go index 9759daf..a136a7f 100644 --- a/protocol/headers_opts.go +++ b/protocol/headers_opts.go @@ -13,6 +13,7 @@ package protocol import ( "strconv" + "strings" "time" ) @@ -188,8 +189,17 @@ func WithContentType(contentType string) HeaderOpt { } // WithGeneric sets the value of the provided key header. +// +// The header names are case-insensitive and case-preserving. +// If there is the same header name as the provided and the difference is +// in capitalization the new header name will be set. func WithGeneric(headerID string, value interface{}) HeaderOpt { return func(headers Headers) error { + for k := range headers { + if strings.EqualFold(k, headerID) { + delete(headers, k) + } + } headers[headerID] = value return nil } diff --git a/protocol/headers_test.go b/protocol/headers_test.go index 5adc90b..e422767 100644 --- a/protocol/headers_test.go +++ b/protocol/headers_test.go @@ -30,9 +30,7 @@ func TestHeadersCorrelationID(t *testing.T) { }, "test_without_correlation_id": { testHeader: Headers{}, - want: "", }, - "test_empty_correlation_id": { testHeader: Headers{HeaderCorrelationID: ""}, want: "", @@ -42,7 +40,11 @@ func TestHeadersCorrelationID(t *testing.T) { for testName, testCase := range tests { t.Run(testName, func(t *testing.T) { got := testCase.testHeader.CorrelationID() - internal.AssertEqual(t, testCase.want, got) + if testCase.testHeader[HeaderCorrelationID] == nil { + internal.AssertNotNil(t, got) + } else { + internal.AssertEqual(t, testCase.want, got) + } }) } } @@ -367,7 +369,7 @@ func TestHeadersVersion(t *testing.T) { }, "test_without_version": { testHeader: Headers{}, - want: 0, + want: 2, }, } @@ -509,3 +511,16 @@ func TestCaseInsensitiveKey(t *testing.T) { json.Unmarshal([]byte(data), &envelope.Headers) internal.AssertEqual(t, "correlation-id-3", envelope.Headers["correlation-iD"]) } + +func TestCasePreservedKey(t *testing.T) { + headers := Headers{HeaderCorrelationID: "correlation-id-1"} + + data := `{ + "correlation-iD":"correlation-id-2" + }` + + want := Headers{"correlation-iD": "correlation-id-2"} + + headers.UnmarshalJSON([]byte(data)) + internal.AssertEqual(t, want, headers) +} From 311a9d0a6099fd93e375786287e67e88fd473433 Mon Sep 17 00:00:00 2001 From: "Trifonova.Antonia" Date: Fri, 8 Apr 2022 09:16:22 +0300 Subject: [PATCH 5/8] Poc Headers improvements Signed-off-by: Antonia Trifonova antonia.trifonova@bosch.io --- protocol/headers.go | 57 +++++++++++++--------------------------- protocol/headers_opts.go | 10 ------- protocol/headers_test.go | 37 ++++++++++---------------- 3 files changed, 32 insertions(+), 72 deletions(-) diff --git a/protocol/headers.go b/protocol/headers.go index fd5f576..718f577 100644 --- a/protocol/headers.go +++ b/protocol/headers.go @@ -12,7 +12,6 @@ package protocol import ( - "encoding/json" "fmt" "strconv" "strings" @@ -86,7 +85,7 @@ type Headers map[string]interface{} // CorrelationID returns the 'correlation-id' header value if it is presented. // If the header value is not presented, the 'correlation-id' header value will be generated in UUID format. // -// If there are two headers differing only in capitalization CorrelationID returns the first value. +// If there are more than one headers differing only in capitalization, the CorrelationID returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) CorrelationID() string { for k, v := range h { @@ -94,14 +93,15 @@ func (h Headers) CorrelationID() string { return v.(string) } } - return uuid.New().String() + h[HeaderCorrelationID] = uuid.New().String() + return h[HeaderCorrelationID].(string) } // Timeout returns the 'timeout' header value if it is presented. // The default and maximum value is duration of 60 seconds. // If the header value is not presented, the Timout returns the default value. // -// If there are two headers differing only in capitalization, the Timeout returns the first value. +// If there are more than one headers differing only in capitalization, the Timeout returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) Timeout() time.Duration { for k := range h { @@ -149,7 +149,7 @@ func parseTimeout(timeout string) (time.Duration, error) { // The default value is true. // If the header value is not presented, the IsResponseRequired returns the default value. // -// If there are two headers differing only in capitalization, the IsResponseRequired returns the first value. +// If there are more than one headers differing only in capitalization, the IsResponseRequired returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) IsResponseRequired() bool { for k, v := range h { @@ -163,7 +163,7 @@ func (h Headers) IsResponseRequired() bool { // Channel returns the 'ditto-channel' header value. // If the header value is not presented, the Channel returns empty string. // -// If there are two headers differing only in capitalization, the Channel returns the first value. +// If there are more than one headers differing only in capitalization, the Channel returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) Channel() string { for k, v := range h { @@ -178,7 +178,7 @@ func (h Headers) Channel() string { // The default value is false. // If the header value is not presented, the IsDryRun returns the default value. // -// If there are two headers differing only in capitalization, the IsDryRun returns the first value. +// If there are more than one headers differing only in capitalization, the IsDryRun returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) IsDryRun() bool { for k, v := range h { @@ -192,7 +192,7 @@ func (h Headers) IsDryRun() bool { // Origin returns the 'origin' header value if it is presented. // If the header value is not presented, the Origin returns the empty string. // -// If there are two headers differing only in capitalization, the Origin returns the first value. +// If there are more than one headers differing only in capitalization, the Origin returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) Origin() string { for k, v := range h { @@ -206,7 +206,7 @@ func (h Headers) Origin() string { // Originator returns the 'ditto-originator' header value if it is presented. // If the header value is not presented, the Originator returns the empty string. // -// If there are two headers differing only in capitalization, the Originator returns the first value. +// If there are more than one headers differing only in capitalization, the Originator returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) Originator() string { for k, v := range h { @@ -220,7 +220,7 @@ func (h Headers) Originator() string { // ETag returns the 'etag' header value if it is presented. // If the header value is not presented, the ETag returns the empty string. // -// If there are two headers differing only in capitalization, the ETag returns the first value. +// If there are more than one headers differing only in capitalization, the ETag returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) ETag() string { for k, v := range h { @@ -234,7 +234,7 @@ func (h Headers) ETag() string { // IfMatch returns the 'if-match' header value if it is presented. // If the header value is not presented, the IfMatch returns the empty string. // -// If there are two headers differing only in capitalization, the IfMatch returns the first value. +// If there are more than one headers differing only in capitalization, the IfMatch returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) IfMatch() string { for k, v := range h { @@ -248,7 +248,7 @@ func (h Headers) IfMatch() string { // IfNoneMatch returns the 'if-none-match' header value if it is presented. // If the header value is not presented, the IfNoneMatch returns the empty string. // -// If there are two headers differing only in capitalization, the IfNoneMatch returns the first value. +// If there are more than one headers differing only in capitalization, the IfNoneMatch returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) IfNoneMatch() string { for k, v := range h { @@ -262,7 +262,7 @@ func (h Headers) IfNoneMatch() string { // ReplyTarget returns the 'ditto-reply-target' header value if it is presented. // If the header value is not presented, the ReplyTarget returns 0. // -// If there are two headers differing only in capitalization, the ReplyTarget returns the first value. +// If there are more than one headers differing only in capitalization, the ReplyTarget returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) ReplyTarget() int64 { for k, v := range h { @@ -276,7 +276,7 @@ func (h Headers) ReplyTarget() int64 { // ReplyTo returns the 'reply-to' header value if it is presented. // If the header value is not presented, the ReplyTo returns the empty string. // -// If there are two headers differing only in capitalization, the ReplyTo returns the first value. +// If there are more than one headers differing only in capitalization, the ReplyTo returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) ReplyTo() string { for k, v := range h { @@ -290,7 +290,7 @@ func (h Headers) ReplyTo() string { // Version returns the 'version' header value if it is presented. // If the header value is not presented, the Version returns 2. // -// If there are two headers differing only in capitalization, the Version returns the first value. +// If there are more than one headers differing only in capitalization, the Version returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) Version() int64 { for k, v := range h { @@ -304,7 +304,7 @@ func (h Headers) Version() int64 { // ContentType returns the 'content-type' header value if it is presented. // If the header value is not presented, the ContentType returns the empty string. // -// If there are two headers differing only in capitalization, the ContentType returns the first value. +// If there are more than one headers differing only in capitalization, the ContentType returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) ContentType() string { for k, v := range h { @@ -316,9 +316,9 @@ func (h Headers) ContentType() string { } // Generic returns the first value of the provided key header. Capitalization of header names does not affect the header map. -// If there are no provided value, Generic returns nil. +// If there are no provided value, the Generic returns nil. // -// If there are two headers differing only in capitalization Generic returns the first value. +// If there are more than one headers differing only in capitalization Generic returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) Generic(id string) interface{} { for k, v := range h { @@ -329,27 +329,6 @@ func (h Headers) Generic(id string) interface{} { return nil } -// UnmarshalJSON unmarshels Headers. -// -// The header names are case-insensitive and case-preserving. -// If there is the same header name as the provided and the difference is -// in capitalization the new header name will be set. -func (h *Headers) UnmarshalJSON(data []byte) error { - temp := make(map[string]interface{}) - if err := json.Unmarshal(data, &temp); err != nil { - return err - } - for k := range temp { - for k1 := range *h { - if strings.EqualFold(k, k1) { - delete(*h, k1) - } - } - } - *h = temp - return nil -} - // With sets new Headers to the existing. func (h Headers) With(opts ...HeaderOpt) Headers { res := make(map[string]interface{}) diff --git a/protocol/headers_opts.go b/protocol/headers_opts.go index a136a7f..9759daf 100644 --- a/protocol/headers_opts.go +++ b/protocol/headers_opts.go @@ -13,7 +13,6 @@ package protocol import ( "strconv" - "strings" "time" ) @@ -189,17 +188,8 @@ func WithContentType(contentType string) HeaderOpt { } // WithGeneric sets the value of the provided key header. -// -// The header names are case-insensitive and case-preserving. -// If there is the same header name as the provided and the difference is -// in capitalization the new header name will be set. func WithGeneric(headerID string, value interface{}) HeaderOpt { return func(headers Headers) error { - for k := range headers { - if strings.EqualFold(k, headerID) { - delete(headers, k) - } - } headers[headerID] = value return nil } diff --git a/protocol/headers_test.go b/protocol/headers_test.go index e422767..26757c7 100644 --- a/protocol/headers_test.go +++ b/protocol/headers_test.go @@ -21,29 +21,33 @@ import ( func TestHeadersCorrelationID(t *testing.T) { tests := map[string]struct { - testHeader Headers - want string + testHeader Headers + want string + hasCorrelationID bool }{ "test_with_correlation_id": { - testHeader: Headers{HeaderCorrelationID: "correlation-id"}, - want: "correlation-id", + testHeader: Headers{HeaderCorrelationID: "correlation-id"}, + want: "correlation-id", + hasCorrelationID: true, }, "test_without_correlation_id": { - testHeader: Headers{}, + testHeader: Headers{}, + hasCorrelationID: false, }, "test_empty_correlation_id": { - testHeader: Headers{HeaderCorrelationID: ""}, - want: "", + testHeader: Headers{HeaderCorrelationID: ""}, + want: "", + hasCorrelationID: true, }, } for testName, testCase := range tests { t.Run(testName, func(t *testing.T) { got := testCase.testHeader.CorrelationID() - if testCase.testHeader[HeaderCorrelationID] == nil { - internal.AssertNotNil(t, got) - } else { + if testCase.hasCorrelationID { internal.AssertEqual(t, testCase.want, got) + } else { + internal.AssertNotNil(t, got) } }) } @@ -511,16 +515,3 @@ func TestCaseInsensitiveKey(t *testing.T) { json.Unmarshal([]byte(data), &envelope.Headers) internal.AssertEqual(t, "correlation-id-3", envelope.Headers["correlation-iD"]) } - -func TestCasePreservedKey(t *testing.T) { - headers := Headers{HeaderCorrelationID: "correlation-id-1"} - - data := `{ - "correlation-iD":"correlation-id-2" - }` - - want := Headers{"correlation-iD": "correlation-id-2"} - - headers.UnmarshalJSON([]byte(data)) - internal.AssertEqual(t, want, headers) -} From f5c76b004365bb7ad802ce66febe58d89a4faa4e Mon Sep 17 00:00:00 2001 From: "Trifonova.Antonia" Date: Mon, 18 Apr 2022 17:58:22 +0300 Subject: [PATCH 6/8] Poc Headers improvements Signed-off-by: Antonia Trifonova antonia.trifonova@bosch.io --- protocol/headers.go | 295 ++++++++++++++------ protocol/headers_opts.go | 189 ++++++++++--- protocol/headers_opts_test.go | 416 +++++++++++++++++++++++----- protocol/headers_test.go | 508 ++++++++++++++++++++++++++++++++-- 4 files changed, 1201 insertions(+), 207 deletions(-) diff --git a/protocol/headers.go b/protocol/headers.go index 718f577..20d4375 100644 --- a/protocol/headers.go +++ b/protocol/headers.go @@ -13,6 +13,7 @@ package protocol import ( "fmt" + "sort" "strconv" "strings" "time" @@ -67,8 +68,8 @@ const ( // HeaderTimeout represents 'timeout' header. HeaderTimeout = "timeout" - // HeaderSchemaVersion represents 'version' header. - HeaderSchemaVersion = "version" + // HeaderVersion represents 'version' header. + HeaderVersion = "version" // HeaderContentType represents 'content-type' header. HeaderContentType = "content-type" @@ -82,93 +83,86 @@ const ( // See https://www.eclipse.org/ditto/protocol-specification.html type Headers map[string]interface{} -// CorrelationID returns the 'correlation-id' header value if it is presented. +// CorrelationID returns the 'correlation-id' header value if it is presented and true if the type is string. +// CorrelationID returns an empty string and false if the header is presented, but the type is not a string. +// // If the header value is not presented, the 'correlation-id' header value will be generated in UUID format. // // If there are more than one headers differing only in capitalization, the CorrelationID returns the first met value. // To use the provided key to get the value, access the map directly. -func (h Headers) CorrelationID() string { - for k, v := range h { +func (h Headers) CorrelationID() (string, bool) { + if value, ok := h[HeaderCorrelationID]; ok { + return h.stringValue(value, "") + } + keys := sortHeadersKey(h) + for _, k := range keys { if strings.EqualFold(k, HeaderCorrelationID) { - return v.(string) + return h.stringValue(h[k], "") } } h[HeaderCorrelationID] = uuid.New().String() - return h[HeaderCorrelationID].(string) + return h[HeaderCorrelationID].(string), true } // Timeout returns the 'timeout' header value if it is presented. // The default and maximum value is duration of 60 seconds. +// // If the header value is not presented, the Timout returns the default value. +// If the header value is presented, but the type is not a string or the value is not valid, the Timeout returns the default value. // // If there are more than one headers differing only in capitalization, the Timeout returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) Timeout() time.Duration { - for k := range h { + if value, ok := h[HeaderTimeout]; ok { + return h.timeoutValue(value) + } + keys := sortHeadersKey(h) + for _, k := range keys { if strings.EqualFold(k, HeaderTimeout) { - if duration, err := parseTimeout(h[k].(string)); err == nil { - return duration - } + return h.timeoutValue(h[k]) } } return 60 * time.Second } -func parseTimeout(timeout string) (time.Duration, error) { - l := len(timeout) - if l > 0 { - t := time.Duration(-1) - switch timeout[l-1] { - case 'm': - if i, err := strconv.Atoi(timeout[:l-1]); err == nil { - t = time.Duration(i) * time.Minute - } - case 's': - if timeout[l-2] == 'm' { - if i, err := strconv.Atoi(timeout[:l-2]); err == nil { - t = time.Duration(i) * time.Millisecond - } - } else { - if i, err := strconv.Atoi(timeout[:l-1]); err == nil { - t = time.Duration(i) * time.Second - } - } - default: - if i, err := strconv.Atoi(timeout); err == nil { - t = time.Duration(i) * time.Second - } - } - if t >= 0 && t <= 60*time.Second { - return t, nil - } - } - return 60 * time.Second, fmt.Errorf("invalid timeout '%s'", timeout) -} - // IsResponseRequired returns the 'response-required' header value if it is presented. // The default value is true. +// // If the header value is not presented, the IsResponseRequired returns the default value. +// If the header value is presented, but the type is not a bool, the IsResponseRequired returns the default value. // // If there are more than one headers differing only in capitalization, the IsResponseRequired returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) IsResponseRequired() bool { - for k, v := range h { + if value, ok := h[HeaderResponseRequired]; ok { + return h.boolValue(value, true) + } + keys := sortHeadersKey(h) + for _, k := range keys { if strings.EqualFold(k, HeaderResponseRequired) { - return v.(bool) + return h.boolValue(h[k], true) } } return true } // Channel returns the 'ditto-channel' header value. -// If the header value is not presented, the Channel returns empty string. +// +// If the header value is not presented, the Channel returns the empty string. +// If the header value is presented, but the type is not a string, the Cannel returns the empty string. // // If there are more than one headers differing only in capitalization, the Channel returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) Channel() string { - for k, v := range h { + if value, ok := h[HeaderChannel]; ok { + str, _ := h.stringValue(value, "") + return str + } + keys := sortHeadersKey(h) + for _, k := range keys { if strings.EqualFold(k, HeaderChannel) { - return v.(string) + str, _ := h.stringValue(h[k], "") + return str } } return "" @@ -176,157 +170,224 @@ func (h Headers) Channel() string { // IsDryRun returns the 'ditto-dry-run' header value if it is presented. // The default value is false. +// // If the header value is not presented, the IsDryRun returns the default value. +// If the header value is presented, but the type is not a bool, the IsDryRun returns the default value. // // If there are more than one headers differing only in capitalization, the IsDryRun returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) IsDryRun() bool { - for k, v := range h { + if value, ok := h[HeaderDryRun]; ok { + return h.boolValue(value, false) + } + keys := sortHeadersKey(h) + for _, k := range keys { if strings.EqualFold(k, HeaderDryRun) { - return v.(bool) + return h.boolValue(h[k], false) } } return false } // Origin returns the 'origin' header value if it is presented. +// // If the header value is not presented, the Origin returns the empty string. +// If the header value is presented, but the value is not a string, the Origin returns the empty string. // // If there are more than one headers differing only in capitalization, the Origin returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) Origin() string { - for k, v := range h { + if value, ok := h[HeaderOrigin]; ok { + str, _ := h.stringValue(value, "") + return str + } + keys := sortHeadersKey(h) + for _, k := range keys { if strings.EqualFold(k, HeaderOrigin) { - return v.(string) + str, _ := h.stringValue(h[k], "") + return str } } return "" } // Originator returns the 'ditto-originator' header value if it is presented. +// // If the header value is not presented, the Originator returns the empty string. +// If the header value is presented, but the type is not a string, the Originator returns the empty string. // // If there are more than one headers differing only in capitalization, the Originator returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) Originator() string { - for k, v := range h { + if value, ok := h[HeaderOriginator]; ok { + str, _ := h.stringValue(value, "") + return str + } + keys := sortHeadersKey(h) + for _, k := range keys { if strings.EqualFold(k, HeaderOriginator) { - return v.(string) + str, _ := h.stringValue(h[k], "") + return str } } return "" } // ETag returns the 'etag' header value if it is presented. +// // If the header value is not presented, the ETag returns the empty string. +// If the header value is presented, but the type is not a string, the ETag returns the empty string. // -// If there are more than one headers differing only in capitalization, the ETag returns the first met value. +// If there are more than one headers for 'etag' differing only in capitalization +// the ETag returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) ETag() string { - for k, v := range h { + if value, ok := h[HeaderETag]; ok { + str, _ := h.stringValue(value, "") + return str + } + keys := sortHeadersKey(h) + for _, k := range keys { if strings.EqualFold(k, HeaderETag) { - return v.(string) + str, _ := h.stringValue(h[k], "") + return str } } return "" } // IfMatch returns the 'if-match' header value if it is presented. +// // If the header value is not presented, the IfMatch returns the empty string. +// If the header value is presented, but the type is not a string, the IfMatch returns the empty string. // // If there are more than one headers differing only in capitalization, the IfMatch returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) IfMatch() string { - for k, v := range h { + if value, ok := h[HeaderIfMatch]; ok { + str, _ := h.stringValue(value, "") + return str + } + keys := sortHeadersKey(h) + for _, k := range keys { if strings.EqualFold(k, HeaderIfMatch) { - return v.(string) + str, _ := h.stringValue(h[k], "") + return str } } return "" } // IfNoneMatch returns the 'if-none-match' header value if it is presented. +// // If the header value is not presented, the IfNoneMatch returns the empty string. +// If the header value is presented, but the type is not a string, the IfNonMatch returns the empty string. // // If there are more than one headers differing only in capitalization, the IfNoneMatch returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) IfNoneMatch() string { - for k, v := range h { + if value, ok := h[HeaderIfNoneMatch]; ok { + str, _ := h.stringValue(value, "") + return str + } + keys := sortHeadersKey(h) + for _, k := range keys { if strings.EqualFold(k, HeaderIfNoneMatch) { - return v.(string) + str, _ := h.stringValue(h[k], "") + return str } } return "" } // ReplyTarget returns the 'ditto-reply-target' header value if it is presented. +// // If the header value is not presented, the ReplyTarget returns 0. +// If the header value is presented, but the type is not an int64, the ReplyTarget returns 0. // // If there are more than one headers differing only in capitalization, the ReplyTarget returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) ReplyTarget() int64 { - for k, v := range h { + if value, ok := h[HeaderReplyTarget]; ok { + return h.intValue(value, 0) + } + keys := sortHeadersKey(h) + for _, k := range keys { if strings.EqualFold(k, HeaderReplyTarget) { - return v.(int64) + return h.intValue(h[k], 0) } } return 0 } // ReplyTo returns the 'reply-to' header value if it is presented. +// // If the header value is not presented, the ReplyTo returns the empty string. +// If the header value is presented, but the type is not a sting, the ReplyTo returns the empty string. // // If there are more than one headers differing only in capitalization, the ReplyTo returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) ReplyTo() string { - for k, v := range h { + if value, ok := h[HeaderReplyTo]; ok { + str, _ := h.stringValue(value, "") + return str + } + keys := sortHeadersKey(h) + for _, k := range keys { if strings.EqualFold(k, HeaderReplyTo) { - return v.(string) + str, _ := h.stringValue(h[k], "") + return str } } return "" } // Version returns the 'version' header value if it is presented. -// If the header value is not presented, the Version returns 2. +// The default value is 2. +// +// If the header value is not presented, the Version returns the default value. +// If the header value is presented, but the type is not an int64, he Version returns the default value. // // If there are more than one headers differing only in capitalization, the Version returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) Version() int64 { - for k, v := range h { - if strings.EqualFold(k, HeaderSchemaVersion) { - return v.(int64) + if value, ok := h[HeaderVersion]; ok { + return h.intValue(value, int64(2)) + } + keys := sortHeadersKey(h) + for _, k := range keys { + if strings.EqualFold(k, HeaderVersion) { + return h.intValue(h[k], int64(2)) } } - return 2 + return int64(2) } // ContentType returns the 'content-type' header value if it is presented. +// +// If the header value is not presented, the ContentType returns the empty string. // If the header value is not presented, the ContentType returns the empty string. // // If there are more than one headers differing only in capitalization, the ContentType returns the first met value. // To use the provided key to get the value, access the map directly. func (h Headers) ContentType() string { - for k, v := range h { + if value, ok := h[HeaderContentType]; ok { + str, _ := h.stringValue(value, "") + return str + } + keys := sortHeadersKey(h) + for _, k := range keys { if strings.EqualFold(k, HeaderContentType) { - return v.(string) + str, _ := h.stringValue(h[k], "") + return str } } return "" } -// Generic returns the first value of the provided key header. Capitalization of header names does not affect the header map. -// If there are no provided value, the Generic returns nil. -// -// If there are more than one headers differing only in capitalization Generic returns the first met value. -// To use the provided key to get the value, access the map directly. +// Generic returns the value of the provided key header. func (h Headers) Generic(id string) interface{} { - for k, v := range h { - if strings.EqualFold(k, id) { - return v - } - } - return nil + return h[id] } // With sets new Headers to the existing. @@ -342,3 +403,73 @@ func (h Headers) With(opts ...HeaderOpt) Headers { } return res } + +func (h Headers) stringValue(headerValue interface{}, defValue string) (string, bool) { + if value, ok := headerValue.(string); ok { + return value, true + } + return defValue, false +} + +func (h Headers) timeoutValue(headerValue interface{}) time.Duration { + if value, ok := headerValue.(string); ok { + if duration, err := parseTimeout(value); err == nil { + return duration + } + } + return 60 * time.Second +} + +func (h Headers) intValue(headerValue interface{}, defValue int64) int64 { + if value, ok := headerValue.(int64); ok { + return value + } + return defValue +} + +func (h Headers) boolValue(headerValue interface{}, defValue bool) bool { + if value, ok := headerValue.(bool); ok { + return value + } + return defValue +} + +func sortHeadersKey(h Headers) []string { + var keys []string + for k := range h { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +func parseTimeout(timeout string) (time.Duration, error) { + l := len(timeout) + if l > 0 { + t := time.Duration(-1) + switch timeout[l-1] { + case 'm': + if i, err := strconv.Atoi(timeout[:l-1]); err == nil { + t = time.Duration(i) * time.Minute + } + case 's': + if timeout[l-2] == 'm' { + if i, err := strconv.Atoi(timeout[:l-2]); err == nil { + t = time.Duration(i) * time.Millisecond + } + } else { + if i, err := strconv.Atoi(timeout[:l-1]); err == nil { + t = time.Duration(i) * time.Second + } + } + default: + if i, err := strconv.Atoi(timeout); err == nil { + t = time.Duration(i) * time.Second + } + } + if t >= 0 && t <= 60*time.Second { + return t, nil + } + } + return 60 * time.Second, fmt.Errorf("invalid timeout '%s'", timeout) +} diff --git a/protocol/headers_opts.go b/protocol/headers_opts.go index 9759daf..b12042b 100644 --- a/protocol/headers_opts.go +++ b/protocol/headers_opts.go @@ -13,6 +13,7 @@ package protocol import ( "strconv" + "strings" "time" ) @@ -55,95 +56,191 @@ func NewHeadersFrom(orig Headers, opts ...HeaderOpt) Headers { return res } -// WithCorrelationID sets the 'correlation-id' header value. +// WithCorrelationID sets a new value for header key 'correlation-id' if it is provided. +// +// If header key 'correlation-id' is not provided and there are more than one headers for 'correlation-id' +// differing only in capitalization, WithCorrelationID sets a new value for the first met header. +// +// If there aren't any headers for 'correlation-id', WithCorrelationID sets a new header with +// key 'correlation-id' and the provided value. +// +// To use the provided key to set a new value, access the map directly. func WithCorrelationID(correlationID string) HeaderOpt { return func(headers Headers) error { - headers[HeaderCorrelationID] = correlationID + setNewValue(headers, HeaderCorrelationID, correlationID) return nil } } -// WithReplyTo sets the 'reply-to' header value. +// WithReplyTo sets a new value for header key 'reply-to' if it is provided. +// +// If header key 'reply-to' is not provided and there are more than one headers for 'reply-to' +// differing only in capitalization, WithReplyTo sets a new value for the first met header. +// +// If there aren't any headers for 'reply-to', WithReplyTo sets a new header with +// key 'reply-to' and the provided value. +// +// To use the provided key to set a new value, access the map directly. func WithReplyTo(replyTo string) HeaderOpt { return func(headers Headers) error { - headers[HeaderReplyTo] = replyTo + setNewValue(headers, HeaderReplyTo, replyTo) return nil } } -// WithReplyTarget sets the 'ditto-reply-target' header value. -func WithReplyTarget(replyTarget string) HeaderOpt { +// WithReplyTarget sets a new value for header key 'ditto-reply-target' if it is provided. +// +// If header key 'ditto-reply-target' is not provided and there are more than one headers for 'ditto-reply-target' +// differing only in capitalization, WithReplyTarget sets a new value for the first met header. +// +// If there aren't any headers for 'ditto-reply-target', WithReplyTarget sets a new header with +// key 'ditto-reply-target' and the provided value. +// +// To use the provided key to set a new value, access the map directly. +func WithReplyTarget(replyTarget int64) HeaderOpt { return func(headers Headers) error { - headers[HeaderReplyTarget] = replyTarget + setNewValue(headers, HeaderReplyTarget, replyTarget) return nil } } -// WithChannel sets the 'ditto-channel' header value. +// WithChannel sets a new value for header key 'ditto-channel' if it is provided. +// +// If header key 'ditto-channel' is not provided and there are more than one headers for 'ditto-channel' +// differing only in capitalization, WithChannel sets a new value for the first met header. +// +// If there aren't any headers for 'ditto-channel', WithChannel sets a new header with +// key 'ditto-channel' and the provided value. +// +// To use the provided key to set a new value, access the map directly. func WithChannel(channel string) HeaderOpt { return func(headers Headers) error { - headers[HeaderChannel] = channel + setNewValue(headers, HeaderChannel, channel) return nil } } -// WithResponseRequired sets the 'response-required' header value. +// WithResponseRequired sets a new value for header key 'response-required' if it is provided. +// +// If header key 'response-required' is not provided and there are more than one headers for 'response-required' +// differing only in capitalization, WithResponseRequired sets a new value for the first met header. +// +// If there aren't any headers for 'response-required', WithResponseRequired sets a new header with +// key 'response-required' and the provided value. +// +// To use the provided key to set a new value, access the map directly. func WithResponseRequired(isResponseRequired bool) HeaderOpt { return func(headers Headers) error { - headers[HeaderResponseRequired] = isResponseRequired + setNewValue(headers, HeaderResponseRequired, isResponseRequired) return nil } } -// WithOriginator sets the 'ditto-originator' header value. +// WithOriginator sets a new value for header key 'ditto-originator' if it is provided. +// +// If header key 'ditto-originator' is not provided and there are more than one headers for 'ditto-originator' +// differing only in capitalization, WithOriginator sets a new value for the first met header. +// +// If there aren't any headers for 'ditto-originator', WithOriginator sets a new header with +// key 'ditto-originator' and the provided value. +// +// To use the provided key to set a new value, access the map directly. func WithOriginator(dittoOriginator string) HeaderOpt { return func(headers Headers) error { - headers[HeaderOriginator] = dittoOriginator + setNewValue(headers, HeaderOriginator, dittoOriginator) return nil } } -// WithOrigin sets the 'origin' header value. +// WithOrigin sets a new value for header key 'origin' if it is provided. +// +// If header key 'origin' is not provided and there are more than one headers for 'origin' +// differing only in capitalization, WithOrigin sets a new value for the first met header. +// +// If there aren't any headers for 'origin', WithOrigin sets a new header with +// key 'origin' and the provided value. +// +// To use the provided key to set a new value, access the map directly. func WithOrigin(origin string) HeaderOpt { return func(headers Headers) error { - headers[HeaderOrigin] = origin + setNewValue(headers, HeaderOrigin, origin) return nil } } -// WithDryRun sets the 'ditto-dry-run' header value. +// WithDryRun sets a new value for header key 'ditto-dry-run' if it is provided. +// +// If header key 'ditto-dry-run' is not provided and there are more than one headers for 'ditto-dry-run' +// differing only in capitalization, WithDryRun sets a new value for the first met header. +// +// If there aren't any headers for 'ditto-dry-run', WithDryRun sets a new header with +// key 'ditto-dry-run' and the provided value. +// +// To use the provided key to set a new value, access the map directly. func WithDryRun(isDryRun bool) HeaderOpt { return func(headers Headers) error { - headers[HeaderDryRun] = isDryRun + setNewValue(headers, HeaderDryRun, isDryRun) return nil } } -// WithETag sets the 'etag' header value. +// WithETag sets a new value for header key 'etag' if it is provided. +// +// If header key 'etag' is not provided and there are more than one headers for 'etag' +// differing only in capitalization, WithETag sets a new value for the first met header. +// +// If there aren't any headers for 'etag', WithETag sets a new header with +// key 'etag' and the provided value. +// +// To use the provided key to set a new value, access the map directly. func WithETag(eTag string) HeaderOpt { return func(headers Headers) error { - headers[HeaderETag] = eTag + setNewValue(headers, HeaderETag, eTag) return nil } } -// WithIfMatch sets the 'if-match' header value. +// WithIfMatch sets a new value for header key 'if-match' if it is provided. +// +// If header key 'if-match' is not provided and there are more than one headers for 'if-match' +// differing only in capitalization, WithIfMatch sets a new value for the first met header. +// +// If there aren't any headers for 'if-match', WithIfMatch sets a new header with +// key 'if-match' and the provided value. +// +// To use the provided key to set a new value, access the map directly. func WithIfMatch(ifMatch string) HeaderOpt { return func(headers Headers) error { - headers[HeaderIfMatch] = ifMatch + setNewValue(headers, HeaderIfMatch, ifMatch) return nil } } -// WithIfNoneMatch sets the 'if-none-match' header value. +// WithIfNoneMatch sets a new value for header key 'if-none-match' if it is provided. +// +// If header key 'if-none-match' is not provided and there are more than one headers for 'if-none-match' +// differing only in capitalization, WithIfNoneMatch sets a new value for the first met header. +// +// If there aren't any headers for 'if-none-match', WithIfNoneMatch sets a new header with +// key 'if-none-match' and the provided value. +// +// To use the provided key to set a new value, access the map directly. func WithIfNoneMatch(ifNoneMatch string) HeaderOpt { return func(headers Headers) error { - headers[HeaderIfNoneMatch] = ifNoneMatch + setNewValue(headers, HeaderIfNoneMatch, ifNoneMatch) return nil } } -// WithTimeout sets the 'timeout' header value. +// WithTimeout sets a new value for header key 'timeout' if it is provided. +// +// If header key 'timeout' is not provided and there are more than one headers for 'timeout' +// differing only in capitalization, WithTimeout sets a new value for the first met header. +// +// If there aren't any headers for 'timeout', WithTimeout sets a new header with +// key 'timeout' and the provided value. +// +// To use the provided key to set a new value, access the map directly. func WithTimeout(timeout time.Duration) HeaderOpt { return func(headers Headers) error { var value string @@ -165,24 +262,39 @@ func WithTimeout(timeout time.Duration) HeaderOpt { value = strconv.FormatInt(div+1, 10) + "ms" } } - - headers[HeaderTimeout] = value + setNewValue(headers, HeaderTimeout, value) return nil } } -// WithSchemaVersion sets the 'version' header value. -func WithSchemaVersion(schemaVersion string) HeaderOpt { +// WithVersion sets a new value for header key 'version' if it is provided. +// +// If header key 'version' is not provided and there are more than one headers for 'version' +// differing only in capitalization, WithVersion sets a new value for the first met header. +// +// If there aren't any headers for 'version', WithVersion sets a new header with +// key 'version' and the provided value. +// +// To use the provided key to set a new value, access the map directly. +func WithVersion(version int64) HeaderOpt { return func(headers Headers) error { - headers[HeaderSchemaVersion] = schemaVersion + setNewValue(headers, HeaderVersion, version) return nil } } -// WithContentType sets the 'content-type' header value. +// WithContentType sets a new value for header key 'content-type' if it is provided. +// +// If header key 'content-type' is not provided and there are more than one headers for 'content-type' +// differing only in capitalization, WithContentType sets a new value for the first met header. +// +// If there aren't any headers for 'content-type', WithContentType sets a new header with +// key 'content-type' and the provided value. +// +// To use the provided key to set a new value, access the map directly. func WithContentType(contentType string) HeaderOpt { return func(headers Headers) error { - headers[HeaderContentType] = contentType + setNewValue(headers, HeaderContentType, contentType) return nil } } @@ -194,3 +306,18 @@ func WithGeneric(headerID string, value interface{}) HeaderOpt { return nil } } + +func setNewValue(headers Headers, headerKey string, headerValue interface{}) { + if _, ok := headers[headerKey]; ok { + headers[headerKey] = headerValue + return + } + keys := sortHeadersKey(headers) + for _, k := range keys { + if strings.EqualFold(k, headerKey) { + headers[k] = headerValue + return + } + } + headers[headerKey] = headerValue +} diff --git a/protocol/headers_opts_test.go b/protocol/headers_opts_test.go index 41d3b75..079d26e 100644 --- a/protocol/headers_opts_test.go +++ b/protocol/headers_opts_test.go @@ -141,102 +141,344 @@ func TestNewHeadersFrom(t *testing.T) { } func TestWithCorrelationID(t *testing.T) { - t.Run("TestWithCorrelationID", func(t *testing.T) { - cid := "correlationId" + tests := map[string]struct { + testHeader Headers + arg string + }{ + "test_change_existing_correlation_id": { + testHeader: Headers{HeaderCorrelationID: "correlation-id"}, + arg: "new-correlation-id", + }, + "test_change_first_met_correlation_id": { + testHeader: Headers{ + "Correlation-ID": "correlation-id-1", + "CORRELATION-ID": "correlation-id-2", + }, + arg: "correlation-id-3", + }, + "test_set_new_correation_id": { + testHeader: NewHeaders(), + arg: "new-correlation-id", + }, + } - got := NewHeaders(WithCorrelationID(cid)) - internal.AssertEqual(t, cid, got.CorrelationID()) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := NewHeadersFrom(testCase.testHeader, WithCorrelationID(testCase.arg)) + want, _ := got.CorrelationID() + internal.AssertEqual(t, testCase.arg, want) + }) + } } func TestWithReplyTo(t *testing.T) { - t.Run("TestWithReplyTo", func(t *testing.T) { - rto := "replyto" + tests := map[string]struct { + testHeader Headers + arg string + }{ + "test_change_existing_reply_to": { + testHeader: Headers{HeaderReplyTo: "reply-to"}, + arg: "new-reply-to", + }, + "test_change_first_met_reply-to": { + testHeader: Headers{ + "Reply-To": "reply-to-1", + "REPLY-TO": "reply-to-2", + }, + arg: "reply-to-3", + }, + "test_set_new_reply-to": { + testHeader: NewHeaders(), + arg: "new-reply-to", + }, + } - got := NewHeaders(WithReplyTo(rto)) - internal.AssertEqual(t, rto, got.ReplyTo()) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := NewHeadersFrom(testCase.testHeader, WithReplyTo(testCase.arg)) + want := got.ReplyTo() + internal.AssertEqual(t, testCase.arg, want) + }) + } } func TestWithReplyTarget(t *testing.T) { - t.Run("TestWithReplyTarget", func(t *testing.T) { - rt := "11111" + tests := map[string]struct { + testHeader Headers + arg int64 + }{ + "test_change_existing_ditto_reply_target": { + testHeader: Headers{HeaderReplyTarget: 1}, + arg: 2, + }, + "test_change_first_met_ditto_reply_target": { + testHeader: Headers{ + "Ditto-Reply-Target": 1, + "DITTO-REPLY-TARGET": 2, + }, + arg: 3, + }, + "test_set_new_reply-target": { + testHeader: NewHeaders(), + arg: 1, + }, + } - got := NewHeaders(WithReplyTarget(rt)) - internal.AssertEqual(t, rt, got[HeaderReplyTarget]) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := NewHeadersFrom(testCase.testHeader, WithReplyTarget(testCase.arg)) + want := got.ReplyTarget() + internal.AssertEqual(t, testCase.arg, want) + }) + } } func TestWithChannel(t *testing.T) { - t.Run("TestWithChannel", func(t *testing.T) { - cha := "channel" + tests := map[string]struct { + testHeader Headers + arg string + }{ + "test_change_existing_ditto_channel": { + testHeader: Headers{HeaderChannel: "test-ditto-channel"}, + arg: "new-ditto-channel", + }, + "test_change_first_met_ditto_channel": { + testHeader: Headers{ + "Ditto-Channel": "test-ditto-channel-1", + "DITTO-CHANNEL": "test-ditto-channel-2", + }, + arg: "test-ditto-channel-3", + }, + "test_set_new_ditto-channel": { + testHeader: NewHeaders(), + arg: "test-ditto-channel", + }, + } - got := NewHeaders(WithChannel(cha)) - internal.AssertEqual(t, cha, got.Channel()) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := NewHeadersFrom(testCase.testHeader, WithChannel(testCase.arg)) + want := got.Channel() + internal.AssertEqual(t, testCase.arg, want) + }) + } } func TestWithResponseRequired(t *testing.T) { - t.Run("TestWithResponseRequired", func(t *testing.T) { - rrq := true + tests := map[string]struct { + testHeader Headers + arg bool + }{ + "test_change_existing_response_required": { + testHeader: Headers{HeaderResponseRequired: true}, + arg: false, + }, + "test_change_first_met_response_required": { + testHeader: Headers{ + "Response-Required": true, + "RESPONSE-REQUIRED": true, + }, + arg: false, + }, + "test_set_new_response_required": { + testHeader: NewHeaders(), + arg: false, + }, + } - got := NewHeaders(WithResponseRequired(rrq)) - internal.AssertEqual(t, rrq, got.IsResponseRequired()) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := NewHeadersFrom(testCase.testHeader, WithResponseRequired(testCase.arg)) + want := got.IsResponseRequired() + internal.AssertEqual(t, testCase.arg, want) + }) + } } func TestWithOriginator(t *testing.T) { - t.Run("TestWithOriginator", func(t *testing.T) { - org := "originator" + tests := map[string]struct { + testHeader Headers + arg string + }{ + "test_change_existing_ditto_originator": { + testHeader: Headers{HeaderOriginator: "test-ditto-originator"}, + arg: "new-ditto-originator", + }, + "test_change_first_met_ditto_originator": { + testHeader: Headers{ + "Ditto-Originator": "ditto-originator-1", + "DITTO-ORIGINATOR": "ditto-originator-2", + }, + arg: "ditto-originator-3", + }, + "test_set_new_ditto_originator": { + testHeader: NewHeaders(), + arg: "test-ditto-originator", + }, + } - got := NewHeaders(WithOriginator(org)) - internal.AssertEqual(t, org, got.Originator()) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := NewHeadersFrom(testCase.testHeader, WithOriginator(testCase.arg)) + want := got.Originator() + internal.AssertEqual(t, testCase.arg, want) + }) + } } func TestWithOrigin(t *testing.T) { - t.Run("TestWithOrigin", func(t *testing.T) { - org := "origin" + tests := map[string]struct { + testHeader Headers + arg string + }{ + "test_change_existing_origin": { + testHeader: Headers{HeaderOrigin: "test-origin"}, + arg: "new-origin", + }, + "test_change_first_met_origin": { + testHeader: Headers{ + "Origin": "origin-1", + "ORIGIN": "origin-2", + }, + arg: "origin-3", + }, + "test_set_new_origin": { + testHeader: NewHeaders(), + arg: "test-origin", + }, + } - got := NewHeaders(WithOrigin(org)) - internal.AssertEqual(t, org, got.Origin()) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := NewHeadersFrom(testCase.testHeader, WithOrigin(testCase.arg)) + want := got.Origin() + internal.AssertEqual(t, testCase.arg, want) + }) + } } func TestWithDryRun(t *testing.T) { - t.Run("TestWithDryRun", func(t *testing.T) { - dry := true + tests := map[string]struct { + testHeader Headers + arg bool + }{ + "test_change_existing_dry_run": { + testHeader: Headers{HeaderDryRun: true}, + arg: false, + }, + "test_change_first_met_dry_run": { + testHeader: Headers{ + "Dry-Run": true, + "DRY-RUN": true, + }, + arg: false, + }, + "test_set_new_dry_run": { + testHeader: NewHeaders(), + arg: true, + }, + } - got := NewHeaders(WithDryRun(dry)) - internal.AssertEqual(t, dry, got.IsDryRun()) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := NewHeadersFrom(testCase.testHeader, WithDryRun(testCase.arg)) + want := got.IsDryRun() + internal.AssertEqual(t, testCase.arg, want) + }) + } } func TestWithETag(t *testing.T) { - t.Run("TestWithETag", func(t *testing.T) { - et := "etag" + tests := map[string]struct { + testHeader Headers + arg string + }{ + "test_change_existing_etag": { + testHeader: Headers{HeaderETag: "test-etag"}, + arg: "new-etag", + }, + "test_change_first_met_etag": { + testHeader: Headers{ + "ETag": "etag-1", + "ETAG": "etag-2", + }, + arg: "etag-3", + }, + "test_set_new_etag": { + testHeader: NewHeaders(), + arg: "test-etag", + }, + } - got := NewHeaders(WithETag(et)) - internal.AssertEqual(t, et, got.ETag()) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := NewHeadersFrom(testCase.testHeader, WithETag(testCase.arg)) + want := got.ETag() + internal.AssertEqual(t, testCase.arg, want) + }) + } } func TestWithIfMatch(t *testing.T) { - t.Run("TestWithIfMatch", func(t *testing.T) { - im := "ifMatch" + tests := map[string]struct { + testHeader Headers + arg string + }{ + "test_change_existing_if_match": { + testHeader: Headers{HeaderIfMatch: "test-if-match"}, + arg: "new-if-match", + }, + "test_change_first_met_if_match": { + testHeader: Headers{ + "If-Match": "if-match-1", + "IF-MATCH": "if-match-2", + }, + arg: "if-match-3", + }, + "test_set_new_if_match": { + testHeader: NewHeaders(), + arg: "test-if-match", + }, + } - got := NewHeaders(WithIfMatch(im)) - internal.AssertEqual(t, im, got.IfMatch()) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := NewHeadersFrom(testCase.testHeader, WithIfMatch(testCase.arg)) + want := got.IfMatch() + internal.AssertEqual(t, testCase.arg, want) + }) + } } func TestWithIfNoneMatch(t *testing.T) { - t.Run("TestWithIfNoneMatch", func(t *testing.T) { - inm := "ifNoneMatch" + tests := map[string]struct { + testHeader Headers + arg string + }{ + "test_change_existing_if_none_match": { + testHeader: Headers{HeaderIfNoneMatch: "test-if-none-match"}, + arg: "new-if-none-match", + }, + "test_change_first_met_if_none_match": { + testHeader: Headers{ + "If-None-Match": "if-none-match-1", + "IF-NONE-MATCH": "if-none-match-2", + }, + arg: "if-none-match-3", + }, + "test_set_new_if_none_match": { + testHeader: NewHeaders(), + arg: "test-if_none_match", + }, + } - got := NewHeaders(WithIfNoneMatch(inm)) - internal.AssertEqual(t, inm, got.IfNoneMatch()) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := NewHeadersFrom(testCase.testHeader, WithIfNoneMatch(testCase.arg)) + want := got.IfNoneMatch() + internal.AssertEqual(t, testCase.arg, want) + }) + } } func TestWithTimeout(t *testing.T) { @@ -275,21 +517,65 @@ func TestWithTimeout(t *testing.T) { } func TestWithSchemaVersion(t *testing.T) { - t.Run("TestWithSchemaVersion", func(t *testing.T) { - sv := "123456789" + tests := map[string]struct { + testHeader Headers + arg int64 + }{ + "test_change_existing_version": { + testHeader: Headers{HeaderVersion: 1}, + arg: int64(2), + }, + "test_change_first_met_version": { + testHeader: Headers{ + "Version": 0, + "VERSION": int64(1), + }, + arg: 2, + }, + "test_set_new_etag": { + testHeader: NewHeaders(), + arg: int64(2), + }, + } - got := NewHeaders(WithSchemaVersion("123456789")) - internal.AssertEqual(t, sv, got[HeaderSchemaVersion]) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := NewHeadersFrom(testCase.testHeader, WithVersion(testCase.arg)) + want := got.Version() + internal.AssertEqual(t, testCase.arg, want) + }) + } } func TestWithContentType(t *testing.T) { - t.Run("TestWithContentType", func(t *testing.T) { - hct := "contentType" + tests := map[string]struct { + testHeader Headers + arg string + }{ + "test_change_existing_content_type": { + testHeader: Headers{HeaderContentType: "test-content-type"}, + arg: "new-content-type", + }, + "test_change_first_met_content_type": { + testHeader: Headers{ + "Content-Type": "content-type-1", + "CONTENT-TYPE": "content-type-2", + }, + arg: "content-type-3", + }, + "test_set_new_content_type": { + testHeader: NewHeaders(), + arg: "test-content-type", + }, + } - got := NewHeaders(WithContentType(hct)) - internal.AssertEqual(t, hct, got.ContentType()) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + got := NewHeadersFrom(testCase.testHeader, WithContentType(testCase.arg)) + want := got.ContentType() + internal.AssertEqual(t, testCase.arg, want) + }) + } } func TestWithGeneric(t *testing.T) { diff --git a/protocol/headers_test.go b/protocol/headers_test.go index 26757c7..3706407 100644 --- a/protocol/headers_test.go +++ b/protocol/headers_test.go @@ -21,33 +21,60 @@ import ( func TestHeadersCorrelationID(t *testing.T) { tests := map[string]struct { - testHeader Headers - want string - hasCorrelationID bool + testHeader Headers + want string + isValidType bool }{ "test_with_correlation_id": { - testHeader: Headers{HeaderCorrelationID: "correlation-id"}, - want: "correlation-id", - hasCorrelationID: true, - }, - "test_without_correlation_id": { - testHeader: Headers{}, - hasCorrelationID: false, + testHeader: Headers{HeaderCorrelationID: "correlation-id"}, + want: "correlation-id", + isValidType: true, }, "test_empty_correlation_id": { - testHeader: Headers{HeaderCorrelationID: ""}, - want: "", - hasCorrelationID: true, + testHeader: Headers{HeaderCorrelationID: ""}, + want: "", + isValidType: true, + }, + "test_corrlation_id_number": { + testHeader: Headers{HeaderCorrelationID: 1}, + want: "", + isValidType: false, + }, + "test_same_corrlation_ids_invalid_value": { + testHeader: Headers{ + HeaderCorrelationID: 1, + "CORRELATION-ID": "test", + }, + want: "", + isValidType: false, + }, + "test_same_corrlation_ids_valid_value": { + testHeader: Headers{ + HeaderCorrelationID: "1", + "CORRELATION-ID": "test", + }, + want: "1", + isValidType: true, + }, + "test_same_corrlation_ids": { + testHeader: Headers{ + "correlation-ID": "1", + "CORRELATION-ID": "test", + }, + want: "test", + isValidType: true, }, } for testName, testCase := range tests { t.Run(testName, func(t *testing.T) { - got := testCase.testHeader.CorrelationID() - if testCase.hasCorrelationID { - internal.AssertEqual(t, testCase.want, got) + got, ok := testCase.testHeader.CorrelationID() + internal.AssertEqual(t, testCase.want, got) + if testCase.isValidType { + internal.AssertTrue(t, ok) } else { - internal.AssertNotNil(t, got) + internal.AssertFalse(t, ok) + internal.AssertEqual(t, 1, testCase.testHeader[HeaderCorrelationID]) } }) } @@ -66,6 +93,35 @@ func TestHeadersTimeout(t *testing.T) { testHeader: Headers{}, want: 60 * time.Second, }, + "test_empty_timeout": { + testHeader: Headers{HeaderTimeout: ""}, + want: 60 * time.Second, + }, + "test_timeout_number": { + testHeader: Headers{HeaderTimeout: 1}, + want: 60 * time.Second, + }, + "test_same_timeout_invalid_value": { + testHeader: Headers{ + HeaderTimeout: 1, + "TIMEOUT": "10s", + }, + want: 60 * time.Second, + }, + "test_same_timeout_valid_value": { + testHeader: Headers{ + HeaderTimeout: "1s", + "TIMEOUT": "1s", + }, + want: time.Second, + }, + "test_same_timeout": { + testHeader: Headers{ + "Timeout": "1", + "TIMEOUT": "5s", + }, + want: 5 * time.Second, + }, } for testName, testCase := range tests { @@ -76,7 +132,7 @@ func TestHeadersTimeout(t *testing.T) { } } -func TestTimeout(t *testing.T) { +func TestTimeoutValue(t *testing.T) { tests := map[string]struct { data string want time.Duration @@ -132,17 +188,50 @@ func TestHeadersIsResponseRequired(t *testing.T) { tests := map[string]struct { testHeader Headers want bool + valueInMap interface{} }{ "test_with_response_required_false": { testHeader: Headers{HeaderResponseRequired: false}, want: false, + valueInMap: false, }, "test_with_response_required_true": { testHeader: Headers{HeaderResponseRequired: true}, want: true, + valueInMap: true, }, "test_without_response_required": { testHeader: Headers{}, + valueInMap: nil, + want: true, + }, + "test_response_required_number": { + testHeader: Headers{HeaderResponseRequired: 1}, + valueInMap: 1, + want: true, + }, + "test_same_response_required_invalid_value": { + testHeader: Headers{ + HeaderResponseRequired: 1, + "RESPONSE-REQUIRED": false, + }, + valueInMap: 1, + want: true, + }, + "test_same_response_required_valid_value": { + testHeader: Headers{ + HeaderResponseRequired: false, + "RESPONSE-REQUIRED": true, + }, + valueInMap: false, + want: false, + }, + "test_same_response_required": { + testHeader: Headers{ + "Response-required": false, + "RESPONSE-REQUIRED": true, + }, + valueInMap: nil, want: true, }, } @@ -151,6 +240,7 @@ func TestHeadersIsResponseRequired(t *testing.T) { t.Run(testName, func(t *testing.T) { got := testCase.testHeader.IsResponseRequired() internal.AssertEqual(t, testCase.want, got) + internal.AssertEqual(t, testCase.valueInMap, testCase.testHeader[HeaderResponseRequired]) }) } } @@ -159,14 +249,46 @@ func TestHeadersChannel(t *testing.T) { tests := map[string]struct { testHeader Headers want string + valueInMap interface{} }{ "test_with_channel": { testHeader: Headers{HeaderChannel: "1"}, want: "1", + valueInMap: "1", }, "test_without_channel": { testHeader: Headers{}, want: "", + valueInMap: nil, + }, + "test_channel_number": { + testHeader: Headers{HeaderChannel: 1}, + valueInMap: 1, + want: "", + }, + "test_same_channel_invalid_value": { + testHeader: Headers{ + HeaderChannel: 1, + "DITTO-CHANNEL": "test-channel", + }, + valueInMap: 1, + want: "", + }, + "test_same_channel_valid_value": { + testHeader: Headers{ + HeaderChannel: "test-channel", + "DITTO-CHANNEL": "new-test-channel", + }, + valueInMap: "test-channel", + want: "test-channel", + }, + "test_same_channel": { + testHeader: Headers{ + "Ditto-Channel": "test-channel", + "DITTO-CHANNEL": "new-test-channel", + }, + valueInMap: nil, + want: "new-test-channel", }, } @@ -174,6 +296,7 @@ func TestHeadersChannel(t *testing.T) { t.Run(testName, func(t *testing.T) { got := testCase.testHeader.Channel() internal.AssertEqual(t, testCase.want, got) + internal.AssertEqual(t, testCase.valueInMap, testCase.testHeader[HeaderChannel]) }) } } @@ -182,13 +305,45 @@ func TestHeadersIsDryRun(t *testing.T) { tests := map[string]struct { testHeader Headers want bool + valueInMap interface{} }{ "test_with_dry_run": { testHeader: Headers{HeaderDryRun: true}, + valueInMap: true, want: true, }, "test_without_dry_run": { testHeader: Headers{}, + valueInMap: nil, + want: false, + }, + "test_dry_run_number": { + testHeader: Headers{HeaderDryRun: 1}, + valueInMap: 1, + want: false, + }, + "test_same_dry_run_invalid_value": { + testHeader: Headers{ + HeaderDryRun: 1, + "DITTO-DRY-RUN": "false", + }, + valueInMap: 1, + want: false, + }, + "test_same_dry_run_valid_value": { + testHeader: Headers{ + HeaderDryRun: true, + "DITTO-DRY-RUN": true, + }, + valueInMap: true, + want: true, + }, + "test_same_dry_run": { + testHeader: Headers{ + "Ditto-Dry-Run": true, + "DITTO-DRY-RUN": false, + }, + valueInMap: nil, want: false, }, } @@ -197,6 +352,7 @@ func TestHeadersIsDryRun(t *testing.T) { t.Run(testName, func(t *testing.T) { got := testCase.testHeader.IsDryRun() internal.AssertEqual(t, testCase.want, got) + internal.AssertEqual(t, testCase.valueInMap, testCase.testHeader[HeaderDryRun]) }) } } @@ -204,22 +360,55 @@ func TestHeadersIsDryRun(t *testing.T) { func TestHeadersOrigin(t *testing.T) { tests := map[string]struct { testHeader Headers + valueInMap interface{} want string }{ "test_with_origin": { testHeader: Headers{HeaderOrigin: "origin"}, + valueInMap: "origin", want: "origin", }, "test_without_origin": { testHeader: Headers{}, + valueInMap: nil, + want: "", + }, + "test_origin_number": { + testHeader: Headers{HeaderOrigin: 1}, + valueInMap: 1, want: "", }, + "test_same_origin_invalid_value": { + testHeader: Headers{ + HeaderOrigin: 1, + "ORIGIN": "test-origin", + }, + valueInMap: 1, + want: "", + }, + "test_same_origin_valid_value": { + testHeader: Headers{ + HeaderOrigin: "test-origin", + "ORIGIN": "test-new-origin", + }, + valueInMap: "test-origin", + want: "test-origin", + }, + "test_same_origin": { + testHeader: Headers{ + "Origin": "test-origin", + "ORIGIN": "test-new-origin", + }, + valueInMap: nil, + want: "test-new-origin", + }, } for testName, testCase := range tests { t.Run(testName, func(t *testing.T) { got := testCase.testHeader.Origin() internal.AssertEqual(t, testCase.want, got) + internal.AssertEqual(t, testCase.valueInMap, testCase.testHeader[HeaderOrigin]) }) } } @@ -227,22 +416,55 @@ func TestHeadersOrigin(t *testing.T) { func TestHeadersOriginator(t *testing.T) { tests := map[string]struct { testHeader Headers + valueInMap interface{} want string }{ "test_with_ditto_originator": { testHeader: Headers{HeaderOriginator: "ditto-originator"}, + valueInMap: "ditto-originator", want: "ditto-originator", }, "test_without_ditto_originator": { testHeader: Headers{}, + valueInMap: nil, + want: "", + }, + "test_ditto_originator_number": { + testHeader: Headers{HeaderOriginator: 1}, + valueInMap: 1, + want: "", + }, + "test_same_ditto_originator_invalid_value": { + testHeader: Headers{ + HeaderOriginator: 1, + "DITTO-ORIGINATOR": "test-ditto-originator", + }, + valueInMap: 1, want: "", }, + "test_same_ditto_originator_valid_value": { + testHeader: Headers{ + HeaderOriginator: "test-ditto-originator", + "DITTO-ORIGINATOR": "test-new-ditto-originator", + }, + valueInMap: "test-ditto-originator", + want: "test-ditto-originator", + }, + "test_same_ditto_originator": { + testHeader: Headers{ + "Ditto-Originator": "test-ditto-originator", + "DITTO-ORIGINATOR": "test-new-ditto-originator", + }, + valueInMap: nil, + want: "test-new-ditto-originator", + }, } for testName, testCase := range tests { t.Run(testName, func(t *testing.T) { got := testCase.testHeader.Originator() internal.AssertEqual(t, testCase.want, got) + internal.AssertEqual(t, testCase.valueInMap, testCase.testHeader[HeaderOriginator]) }) } } @@ -250,22 +472,55 @@ func TestHeadersOriginator(t *testing.T) { func TestHeadersETag(t *testing.T) { tests := map[string]struct { testHeader Headers + valueInMap interface{} want string }{ "test_with_etag": { testHeader: Headers{HeaderETag: "test-etag"}, + valueInMap: "test-etag", want: "test-etag", }, "test_without_etag": { testHeader: Headers{}, + valueInMap: nil, + want: "", + }, + "test_etag_number": { + testHeader: Headers{HeaderETag: 1}, + valueInMap: 1, + want: "", + }, + "test_same_etag_invalid_value": { + testHeader: Headers{ + HeaderETag: 1, + "ETAG": "test-etag", + }, + valueInMap: 1, want: "", }, + "test_same_etag_valid_value": { + testHeader: Headers{ + HeaderETag: "test-etag", + "ETAG": "test-new-etag", + }, + valueInMap: "test-etag", + want: "test-etag", + }, + "test_same_etag": { + testHeader: Headers{ + "ETag": "test-etag", + "ETAG": "test-new-etag", + }, + valueInMap: nil, + want: "test-new-etag", + }, } for testName, testCase := range tests { t.Run(testName, func(t *testing.T) { got := testCase.testHeader.ETag() internal.AssertEqual(t, testCase.want, got) + internal.AssertEqual(t, testCase.valueInMap, testCase.testHeader[HeaderETag]) }) } } @@ -273,22 +528,55 @@ func TestHeadersETag(t *testing.T) { func TestHeadersIfMatch(t *testing.T) { tests := map[string]struct { testHeader Headers + valueInMap interface{} want string }{ "test_with_if_match": { - testHeader: Headers{HeaderIfMatch: "HeaderIfMatch"}, - want: "HeaderIfMatch", + testHeader: Headers{HeaderIfMatch: "test-if-match"}, + valueInMap: "test-if-match", + want: "test-if-match", }, "test_without_if_match": { testHeader: Headers{}, + valueInMap: nil, + want: "", + }, + "test_if_match_number": { + testHeader: Headers{HeaderIfMatch: 1}, + valueInMap: 1, want: "", }, + "test_same_if_match_invalid_value": { + testHeader: Headers{ + HeaderIfMatch: 1, + "IF-MATCH": "test-if-match", + }, + valueInMap: 1, + want: "", + }, + "test_same_if_match_valid_value": { + testHeader: Headers{ + HeaderIfMatch: "test-if-match", + "IF-MATCH": "test-new-if-match", + }, + valueInMap: "test-if-match", + want: "test-if-match", + }, + "test_same_if_match": { + testHeader: Headers{ + "If-Match": "test-if-match", + "IF-MATCH": "test-new-if-match", + }, + valueInMap: nil, + want: "test-new-if-match", + }, } for testName, testCase := range tests { t.Run(testName, func(t *testing.T) { got := testCase.testHeader.IfMatch() internal.AssertEqual(t, testCase.want, got) + internal.AssertEqual(t, testCase.valueInMap, testCase.testHeader[HeaderIfMatch]) }) } } @@ -296,22 +584,55 @@ func TestHeadersIfMatch(t *testing.T) { func TestHeadersIfNoneMatch(t *testing.T) { tests := map[string]struct { testHeader Headers + valueInMap interface{} want string }{ "test_with_if_none_match": { - testHeader: Headers{HeaderIfNoneMatch: "HeaderIfNoneMatch"}, - want: "HeaderIfNoneMatch", + testHeader: Headers{HeaderIfNoneMatch: "test-if-none-match"}, + valueInMap: "test-if-none-match", + want: "test-if-none-match", }, "test_without_if_none_match": { testHeader: Headers{}, + valueInMap: nil, + want: "", + }, + "test_if_none_match_number": { + testHeader: Headers{HeaderIfNoneMatch: 1}, + valueInMap: 1, + want: "", + }, + "test_same_if_none_match_invalid_value": { + testHeader: Headers{ + HeaderIfNoneMatch: 1, + "IF-NONE-MATCH": "test-if-none-match", + }, + valueInMap: 1, want: "", }, + "test_same_if_none_match_valid_value": { + testHeader: Headers{ + HeaderIfNoneMatch: "test-if-none-match", + "IF-NONE-mATCH": "test-new-if-none-match", + }, + valueInMap: "test-if-none-match", + want: "test-if-none-match", + }, + "test_same_if_none_match": { + testHeader: Headers{ + "If-None-Match": "test-if-none-match", + "IF-NONE-MATCH": "test-new-if-none-match", + }, + valueInMap: nil, + want: "test-new-if-none-match", + }, } for testName, testCase := range tests { t.Run(testName, func(t *testing.T) { got := testCase.testHeader.IfNoneMatch() internal.AssertEqual(t, testCase.want, got) + internal.AssertEqual(t, testCase.valueInMap, testCase.testHeader[HeaderIfNoneMatch]) }) } } @@ -319,22 +640,55 @@ func TestHeadersIfNoneMatch(t *testing.T) { func TestHeadersReplyTarget(t *testing.T) { tests := map[string]struct { testHeader Headers + valueInMap interface{} want int64 }{ "test_with_reply_target": { testHeader: Headers{HeaderReplyTarget: int64(123)}, + valueInMap: int64(123), want: int64(123), }, "test_without_reply_target": { testHeader: Headers{}, + valueInMap: nil, want: 0, }, + "test_reply_target_string": { + testHeader: Headers{HeaderReplyTarget: "1"}, + valueInMap: "1", + want: 0, + }, + "test_same_reply_target_invalid_value": { + testHeader: Headers{ + HeaderReplyTarget: "1", + "DITTO-REPLY-TARGET": 1, + }, + valueInMap: "1", + want: 0, + }, + "test_same_reply_target_valid_value": { + testHeader: Headers{ + HeaderReplyTarget: int64(1), + "DITTO-REPLY-TARGET": "1", + }, + valueInMap: int64(1), + want: int64(1), + }, + "test_same_reply_target": { + testHeader: Headers{ + "Ditto-Reply-Target": int64(1), + "DITTO-REPLY-TARGET": int64(2), + }, + valueInMap: nil, + want: int64(2), + }, } for testName, testCase := range tests { t.Run(testName, func(t *testing.T) { got := testCase.testHeader.ReplyTarget() internal.AssertEqual(t, testCase.want, got) + internal.AssertEqual(t, testCase.valueInMap, testCase.testHeader[HeaderReplyTarget]) }) } } @@ -342,22 +696,55 @@ func TestHeadersReplyTarget(t *testing.T) { func TestHeadersReplyTo(t *testing.T) { tests := map[string]struct { testHeader Headers + valueInMap interface{} want string }{ "test_with_reply_to": { testHeader: Headers{HeaderReplyTo: "someone"}, + valueInMap: "someone", want: "someone", }, "test_without_reply_to": { testHeader: Headers{}, + valueInMap: nil, + want: "", + }, + "test_reply_to_number": { + testHeader: Headers{HeaderReplyTo: 1}, + valueInMap: 1, + want: "", + }, + "test_same_reply_to_invalid_value": { + testHeader: Headers{ + HeaderReplyTo: 1, + "REPLY-TO": "test-reply-to", + }, + valueInMap: 1, want: "", }, + "test_same_reply_to_valid_value": { + testHeader: Headers{ + HeaderReplyTo: "test-reply-to", + "REPLY-TO": "test-new-reply-to", + }, + valueInMap: "test-reply-to", + want: "test-reply-to", + }, + "test_same_reply-to": { + testHeader: Headers{ + "Reply-To": "test-reply-to", + "REPLY-TO": "test-new-reply-to", + }, + valueInMap: nil, + want: "test-new-reply-to", + }, } for testName, testCase := range tests { t.Run(testName, func(t *testing.T) { got := testCase.testHeader.ReplyTo() internal.AssertEqual(t, testCase.want, got) + internal.AssertEqual(t, testCase.valueInMap, testCase.testHeader[HeaderReplyTo]) }) } } @@ -365,15 +752,47 @@ func TestHeadersReplyTo(t *testing.T) { func TestHeadersVersion(t *testing.T) { tests := map[string]struct { testHeader Headers + valueInMap interface{} want int64 }{ "test_with_version": { - testHeader: Headers{HeaderSchemaVersion: int64(123)}, + testHeader: Headers{HeaderVersion: int64(123)}, + valueInMap: int64(123), want: int64(123), }, "test_without_version": { testHeader: Headers{}, - want: 2, + valueInMap: nil, + want: int64(2), + }, + "test_version_string": { + testHeader: Headers{HeaderVersion: "1"}, + valueInMap: "1", + want: int64(2), + }, + "test_same_version_invalid_value": { + testHeader: Headers{ + HeaderVersion: "1", + "VERSION": int64(1), + }, + valueInMap: "1", + want: int64(2), + }, + "test_same_version_valid_value": { + testHeader: Headers{ + HeaderVersion: int64(1), + "VERSION": int64(2), + }, + valueInMap: int64(1), + want: int64(1), + }, + "test_same_version": { + testHeader: Headers{ + "Version": int64(12), + "VERSION": int64(3), + }, + valueInMap: nil, + want: int64(3), }, } @@ -381,6 +800,7 @@ func TestHeadersVersion(t *testing.T) { t.Run(testName, func(t *testing.T) { got := testCase.testHeader.Version() internal.AssertEqual(t, testCase.want, got) + internal.AssertEqual(t, testCase.valueInMap, testCase.testHeader[HeaderVersion]) }) } } @@ -388,16 +808,48 @@ func TestHeadersVersion(t *testing.T) { func TestHeadersContentType(t *testing.T) { tests := map[string]struct { testHeader Headers + valueInMap interface{} want string }{ "test_with_content_type": { - testHeader: Headers{HeaderContentType: "HeaderContentType"}, - want: "HeaderContentType", + testHeader: Headers{HeaderContentType: "test-content-type"}, + valueInMap: "test-content-type", + want: "test-content-type", }, "test_without_content_type": { testHeader: Headers{}, + valueInMap: nil, + want: "", + }, + "test_content_type_number": { + testHeader: Headers{HeaderContentType: 1}, + valueInMap: 1, + want: "", + }, + "test_same_content_type_invalid_value": { + testHeader: Headers{ + HeaderContentType: 1, + "CONTENT-TYPE": "test-content-type", + }, + valueInMap: 1, want: "", }, + "test_same_content_type_valid_value": { + testHeader: Headers{ + HeaderContentType: "test-content-type", + "CONTENT-TYPE": "test-new-content-type", + }, + valueInMap: "test-content-type", + want: "test-content-type", + }, + "test_same_content_type": { + testHeader: Headers{ + "Content-Type": "test-content-type", + "CONTENT-TYPE": "test-new-content-type", + }, + valueInMap: nil, + want: "test-new-content-type", + }, } for testName, testCase := range tests { @@ -498,13 +950,11 @@ func TestCaseInsensitiveKey(t *testing.T) { envelope := &Envelope{ Headers: headers, } - // override correlation-id instead of custom header - header key is the last set one - internal.AssertEqual(t, "correlation-id-1", envelope.Headers.Generic("correlation-ID")) envelope.WithHeaders(NewHeaders(WithGeneric("coRRelation-ID", "correlation-id-2"))) // return the first correlation-id (side effect from unmarshal JSON) - res := envelope.Headers.CorrelationID() + res, _ := envelope.Headers.CorrelationID() internal.AssertEqual(t, "correlation-id-2", res) json.Marshal(envelope.Headers) From 3bf8be7ba3b3d1687a69af4e3ef724711827bd64 Mon Sep 17 00:00:00 2001 From: "Trifonova.Antonia" Date: Mon, 18 Apr 2022 18:06:50 +0300 Subject: [PATCH 7/8] Format issues Signed-off-by: Antonia Trifonova antonia.trifonova@bosch.io --- protocol/headers_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/protocol/headers_test.go b/protocol/headers_test.go index 3706407..21d6009 100644 --- a/protocol/headers_test.go +++ b/protocol/headers_test.go @@ -773,7 +773,7 @@ func TestHeadersVersion(t *testing.T) { "test_same_version_invalid_value": { testHeader: Headers{ HeaderVersion: "1", - "VERSION": int64(1), + "VERSION": int64(1), }, valueInMap: "1", want: int64(2), @@ -781,7 +781,7 @@ func TestHeadersVersion(t *testing.T) { "test_same_version_valid_value": { testHeader: Headers{ HeaderVersion: int64(1), - "VERSION": int64(2), + "VERSION": int64(2), }, valueInMap: int64(1), want: int64(1), From 72f7beb2225bb594620ae5df8fcd4f417441de0f Mon Sep 17 00:00:00 2001 From: "Trifonova.Antonia" Date: Tue, 19 Apr 2022 20:01:46 +0300 Subject: [PATCH 8/8] PoC headers Signed-off-by: Antonia Trifonova antonia.trifonova@bosch.io --- protocol/headers.go | 309 ++++++++++++++-------------------- protocol/headers_opts.go | 154 ++++++----------- protocol/headers_opts_test.go | 3 +- protocol/headers_test.go | 48 +++--- 4 files changed, 200 insertions(+), 314 deletions(-) diff --git a/protocol/headers.go b/protocol/headers.go index 20d4375..e6ae976 100644 --- a/protocol/headers.go +++ b/protocol/headers.go @@ -83,35 +83,39 @@ const ( // See https://www.eclipse.org/ditto/protocol-specification.html type Headers map[string]interface{} -// CorrelationID returns the 'correlation-id' header value if it is presented and true if the type is string. -// CorrelationID returns an empty string and false if the header is presented, but the type is not a string. +// CorrelationID returns the HeaderCorrelationID header value if it is presented. // -// If the header value is not presented, the 'correlation-id' header value will be generated in UUID format. +// If there is no HeaderCorrelationID value, but there is at least one value which key differs only in capitalization, +// the CorrelationID returns the value corresponding to the first such key(sorted in increasing order). // -// If there are more than one headers differing only in capitalization, the CorrelationID returns the first met value. -// To use the provided key to get the value, access the map directly. -func (h Headers) CorrelationID() (string, bool) { +// If there is no match about for this header, the CorrelationID will generate HeaderCorrelationID value in UUID format. +// +// If the type of the HeaderCorrelationID header (or the first met header) is not a string, the CorrelationID returns the empty string. +// +// Use Generic or access the map directly to get a value to a specific key in regard to capitalization. +func (h Headers) CorrelationID() string { if value, ok := h[HeaderCorrelationID]; ok { - return h.stringValue(value, "") + return getStr(value, "") } keys := sortHeadersKey(h) for _, k := range keys { if strings.EqualFold(k, HeaderCorrelationID) { - return h.stringValue(h[k], "") + return getStr(h[k], "") } } h[HeaderCorrelationID] = uuid.New().String() - return h[HeaderCorrelationID].(string), true + return h[HeaderCorrelationID].(string) } -// Timeout returns the 'timeout' header value if it is presented. +// Timeout returns the HeaderTimeout header value if it is presented. // The default and maximum value is duration of 60 seconds. // -// If the header value is not presented, the Timout returns the default value. -// If the header value is presented, but the type is not a string or the value is not valid, the Timeout returns the default value. +// If there is no HeaderTimeout value, but there is at least one value which key differs only in capitalization, +// the Timeout returns the value corresponding to the first such key(sorted in increasing order). +// +// If the type of the HeaderTimeout header (or the first met header) is not a string, the Timeout returns the default value. // -// If there are more than one headers differing only in capitalization, the Timeout returns the first met value. -// To use the provided key to get the value, access the map directly. +// Use Generic or access the map directly to get a value to a specific key in regard to capitalization. func (h Headers) Timeout() time.Duration { if value, ok := h[HeaderTimeout]; ok { return h.timeoutValue(value) @@ -125,14 +129,16 @@ func (h Headers) Timeout() time.Duration { return 60 * time.Second } -// IsResponseRequired returns the 'response-required' header value if it is presented. +// IsResponseRequired returns the HeaderResponseRequired header value if it is presented. // The default value is true. // -// If the header value is not presented, the IsResponseRequired returns the default value. -// If the header value is presented, but the type is not a bool, the IsResponseRequired returns the default value. +// If there is no HeaderResponseRequired value, but there is at least one value which key differs only in capitalization, +// the IsResponseRequired returns the value corresponding to the first such key(sorted in increasing order). // -// If there are more than one headers differing only in capitalization, the IsResponseRequired returns the first met value. -// To use the provided key to get the value, access the map directly. +// If the type of the HeaderResponseRequired header (or the first met header) is not a bool, the IsResponseRequired +// returns the default value. +// +// Use Generic or access the map directly to get a value to a specific key in regard to capitalization. func (h Headers) IsResponseRequired() bool { if value, ok := h[HeaderResponseRequired]; ok { return h.boolValue(value, true) @@ -146,36 +152,28 @@ func (h Headers) IsResponseRequired() bool { return true } -// Channel returns the 'ditto-channel' header value. +// Channel returns the HeaderChannel header value. +// +// If there is no HeaderChannel value, but there is at least one value which key differs only in capitalization, +// the Channel returns the value corresponding to the first such key(sorted in increasing order). // -// If the header value is not presented, the Channel returns the empty string. -// If the header value is presented, but the type is not a string, the Cannel returns the empty string. +// If the type of the HeaderChannel header (or the first met header) is not a string, the Channel returns the empty string. // -// If there are more than one headers differing only in capitalization, the Channel returns the first met value. -// To use the provided key to get the value, access the map directly. +// Use Generic or access the map directly to get a value to a specific key in regard to capitalization. func (h Headers) Channel() string { - if value, ok := h[HeaderChannel]; ok { - str, _ := h.stringValue(value, "") - return str - } - keys := sortHeadersKey(h) - for _, k := range keys { - if strings.EqualFold(k, HeaderChannel) { - str, _ := h.stringValue(h[k], "") - return str - } - } - return "" + return h.stringValue(HeaderChannel, "") + } -// IsDryRun returns the 'ditto-dry-run' header value if it is presented. +// IsDryRun returns the HeaderDryRun header value if it is presented. // The default value is false. // -// If the header value is not presented, the IsDryRun returns the default value. -// If the header value is presented, but the type is not a bool, the IsDryRun returns the default value. +// If there is no HeaderDryRun value, but there is at least one value which key differs only in capitalization, +// the IsDryRun returns the value corresponding to the first such key(sorted in increasing order). +// +// If the type of the HeaderDryRun header (or the first met header) is not a bool, the IsDryRun returns the default value. // -// If there are more than one headers differing only in capitalization, the IsDryRun returns the first met value. -// To use the provided key to get the value, access the map directly. +// Use Generic or access the map directly to get a value to a specific key in regard to capitalization. func (h Headers) IsDryRun() bool { if value, ok := h[HeaderDryRun]; ok { return h.boolValue(value, false) @@ -189,124 +187,79 @@ func (h Headers) IsDryRun() bool { return false } -// Origin returns the 'origin' header value if it is presented. +// Origin returns the HeaderOrigin header value if it is presented. // -// If the header value is not presented, the Origin returns the empty string. -// If the header value is presented, but the value is not a string, the Origin returns the empty string. +// If there is no HeaderOrigin value, but there is at least one value which key differs only in capitalization, +// the Origin returns the value corresponding to the first such key(sorted in increasing order). // -// If there are more than one headers differing only in capitalization, the Origin returns the first met value. -// To use the provided key to get the value, access the map directly. +// If the type of the HeaderOrigin header (or the first met header) is not a string, the Origin returns the empty string. +// +// Use Generic or access the map directly to get a value to a specific key in regard to capitalization. func (h Headers) Origin() string { - if value, ok := h[HeaderOrigin]; ok { - str, _ := h.stringValue(value, "") - return str - } - keys := sortHeadersKey(h) - for _, k := range keys { - if strings.EqualFold(k, HeaderOrigin) { - str, _ := h.stringValue(h[k], "") - return str - } - } - return "" + return h.stringValue(HeaderOrigin, "") + } -// Originator returns the 'ditto-originator' header value if it is presented. +// Originator returns the HeaderOriginator header value if it is presented. +// +// If there is no HeaderOriginator value, but there is at least one value which key differs only in capitalization, +// the Originator returns the value corresponding to the first such key(sorted in increasing order). // -// If the header value is not presented, the Originator returns the empty string. -// If the header value is presented, but the type is not a string, the Originator returns the empty string. +// If the type of the HeaderOriginator header (or the first met header) is not a string, the Originator returns the empty string. // -// If there are more than one headers differing only in capitalization, the Originator returns the first met value. -// To use the provided key to get the value, access the map directly. +// Use Generic or access the map directly to get a value to a specific key in regard to capitalization. func (h Headers) Originator() string { - if value, ok := h[HeaderOriginator]; ok { - str, _ := h.stringValue(value, "") - return str - } - keys := sortHeadersKey(h) - for _, k := range keys { - if strings.EqualFold(k, HeaderOriginator) { - str, _ := h.stringValue(h[k], "") - return str - } - } - return "" + return h.stringValue(HeaderOriginator, "") + } -// ETag returns the 'etag' header value if it is presented. +// ETag returns the HeaderETag header value if it is presented. +// +// If there is no HeaderETag value, but there is at least one value which key differs only in capitalization, +// the ETag returns the value corresponding to the first such key(sorted in increasing order). // -// If the header value is not presented, the ETag returns the empty string. -// If the header value is presented, but the type is not a string, the ETag returns the empty string. +// If the type of the HeaderETag header (or the first met header) is not a string, the ETag returns the empty string. // -// If there are more than one headers for 'etag' differing only in capitalization -// the ETag returns the first met value. -// To use the provided key to get the value, access the map directly. +// Use Generic or access the map directly to get a value to a specific key in regard to capitalization. func (h Headers) ETag() string { - if value, ok := h[HeaderETag]; ok { - str, _ := h.stringValue(value, "") - return str - } - keys := sortHeadersKey(h) - for _, k := range keys { - if strings.EqualFold(k, HeaderETag) { - str, _ := h.stringValue(h[k], "") - return str - } - } - return "" + return h.stringValue(HeaderETag, "") + } -// IfMatch returns the 'if-match' header value if it is presented. +// IfMatch returns the HeaderIfMatch header value if it is presented. // -// If the header value is not presented, the IfMatch returns the empty string. -// If the header value is presented, but the type is not a string, the IfMatch returns the empty string. +// If there is no HeaderIfMatch value, but there is at least one value which key differs only in capitalization, +// the IfMatch returns the value corresponding to the first such key(sorted in increasing order). // -// If there are more than one headers differing only in capitalization, the IfMatch returns the first met value. -// To use the provided key to get the value, access the map directly. +// If the type of the HeaderIfMatch header (or the first met header) is not a string, the IfMatch returns the empty string. +// +// Use Generic or access the map directly to get a value to a specific key in regard to capitalization. func (h Headers) IfMatch() string { - if value, ok := h[HeaderIfMatch]; ok { - str, _ := h.stringValue(value, "") - return str - } - keys := sortHeadersKey(h) - for _, k := range keys { - if strings.EqualFold(k, HeaderIfMatch) { - str, _ := h.stringValue(h[k], "") - return str - } - } - return "" + return h.stringValue(HeaderIfMatch, "") + } -// IfNoneMatch returns the 'if-none-match' header value if it is presented. +// IfNoneMatch returns the HeaderIfNoneMatch header value if it is presented. +// +// If there is no HeaderIfNoneMatch value, but there is at least one value which key differs only in capitalization, +// the IfNoneMatch returns the value corresponding to the first such key(sorted in increasing order). // -// If the header value is not presented, the IfNoneMatch returns the empty string. -// If the header value is presented, but the type is not a string, the IfNonMatch returns the empty string. +// If the type of the HeaderIfNoneMatch header (or the first met header) is not a string, the IfNoneMatch returns the empty string. // -// If there are more than one headers differing only in capitalization, the IfNoneMatch returns the first met value. -// To use the provided key to get the value, access the map directly. +// Use Generic or access the map directly to get a value to a specific key in regard to capitalization. func (h Headers) IfNoneMatch() string { - if value, ok := h[HeaderIfNoneMatch]; ok { - str, _ := h.stringValue(value, "") - return str - } - keys := sortHeadersKey(h) - for _, k := range keys { - if strings.EqualFold(k, HeaderIfNoneMatch) { - str, _ := h.stringValue(h[k], "") - return str - } - } - return "" + return h.stringValue(HeaderIfNoneMatch, "") + } -// ReplyTarget returns the 'ditto-reply-target' header value if it is presented. +// ReplyTarget returns the HeaderReplyTarget header value if it is presented. +// +// If there is no HeaderReplyTarget value, but there is at least one value which key differs only in capitalization, +// the ReplyTarget returns the value corresponding to the first such key(sorted in increasing order). // -// If the header value is not presented, the ReplyTarget returns 0. -// If the header value is presented, but the type is not an int64, the ReplyTarget returns 0. +// If the type of the HeaderReplyTarget header (or the first met header) is not an int64, the ReplyTarget returns 0. // -// If there are more than one headers differing only in capitalization, the ReplyTarget returns the first met value. -// To use the provided key to get the value, access the map directly. +// Use Generic or access the map directly to get a value to a specific key in regard to capitalization. func (h Headers) ReplyTarget() int64 { if value, ok := h[HeaderReplyTarget]; ok { return h.intValue(value, 0) @@ -320,36 +273,27 @@ func (h Headers) ReplyTarget() int64 { return 0 } -// ReplyTo returns the 'reply-to' header value if it is presented. +// ReplyTo returns the HeaderReplyTo header value if it is presented. // -// If the header value is not presented, the ReplyTo returns the empty string. -// If the header value is presented, but the type is not a sting, the ReplyTo returns the empty string. +// If there is no HeaderReplyTo value, but there is at least one value which key differs only in capitalization, +// the ReplyTo returns the value corresponding to the first such key(sorted in increasing order). // -// If there are more than one headers differing only in capitalization, the ReplyTo returns the first met value. -// To use the provided key to get the value, access the map directly. +// If the type of the HeaderReplyTo header (or the first met header) is not a string, the ReplyTo returns the empty string. +// +// Use Generic or access the map directly to get a value to a specific key in regard to capitalization. func (h Headers) ReplyTo() string { - if value, ok := h[HeaderReplyTo]; ok { - str, _ := h.stringValue(value, "") - return str - } - keys := sortHeadersKey(h) - for _, k := range keys { - if strings.EqualFold(k, HeaderReplyTo) { - str, _ := h.stringValue(h[k], "") - return str - } - } - return "" + return h.stringValue(HeaderReplyTo, "") } -// Version returns the 'version' header value if it is presented. +// Version returns the HeaderVersion header value if it is presented. // The default value is 2. // -// If the header value is not presented, the Version returns the default value. -// If the header value is presented, but the type is not an int64, he Version returns the default value. +// If there is no HeaderVersion value, but there is at least one value which key differs only in capitalization, +// the Version returns the value corresponding to the first such key(sorted in increasing order). +// +// If the type of the HeaderVersion header (or the first met header) is not an int 64, the Version returns the default value. // -// If there are more than one headers differing only in capitalization, the Version returns the first met value. -// To use the provided key to get the value, access the map directly. +// Use Generic or access the map directly to get a value to a specific key in regard to capitalization. func (h Headers) Version() int64 { if value, ok := h[HeaderVersion]; ok { return h.intValue(value, int64(2)) @@ -363,26 +307,16 @@ func (h Headers) Version() int64 { return int64(2) } -// ContentType returns the 'content-type' header value if it is presented. +// ContentType returns the HeaderContentType header value if it is presented. // -// If the header value is not presented, the ContentType returns the empty string. -// If the header value is not presented, the ContentType returns the empty string. +// If there is no HeaderContentType value, but there is at least one value which key differs only in capitalization, +// the ContentType returns the value corresponding to the first such key(sorted in increasing order). // -// If there are more than one headers differing only in capitalization, the ContentType returns the first met value. -// To use the provided key to get the value, access the map directly. +// If the type of the HeaderContentType header (or the first met header) is not a string, the ContentType returns the empty string. +// +// Use Generic or access the map directly to get a value to a specific key in regard to capitalization. func (h Headers) ContentType() string { - if value, ok := h[HeaderContentType]; ok { - str, _ := h.stringValue(value, "") - return str - } - keys := sortHeadersKey(h) - for _, k := range keys { - if strings.EqualFold(k, HeaderContentType) { - str, _ := h.stringValue(h[k], "") - return str - } - } - return "" + return h.stringValue(HeaderContentType, "") } // Generic returns the value of the provided key header. @@ -390,25 +324,24 @@ func (h Headers) Generic(id string) interface{} { return h[id] } -// With sets new Headers to the existing. -func (h Headers) With(opts ...HeaderOpt) Headers { - res := make(map[string]interface{}) - - for key, value := range h { - res[key] = value +func (h Headers) stringValue(headerKey, defValue string) string { + if value, ok := h[headerKey]; ok { + return getStr(value, defValue) } - - if err := applyOptsHeader(res, opts...); err != nil { - return nil + keys := sortHeadersKey(h) + for _, k := range keys { + if strings.EqualFold(k, headerKey) { + return getStr(h[k], defValue) + } } - return res + return defValue } -func (h Headers) stringValue(headerValue interface{}, defValue string) (string, bool) { - if value, ok := headerValue.(string); ok { - return value, true +func getStr(value interface{}, defValue string) string { + if str, ok := value.(string); ok { + return str } - return defValue, false + return defValue } func (h Headers) timeoutValue(headerValue interface{}) time.Duration { @@ -435,9 +368,11 @@ func (h Headers) boolValue(headerValue interface{}, defValue bool) bool { } func sortHeadersKey(h Headers) []string { - var keys []string + keys := make([]string, len(h)) + i := 0 for k := range h { - keys = append(keys, k) + keys[i] = k + i++ } sort.Strings(keys) return keys diff --git a/protocol/headers_opts.go b/protocol/headers_opts.go index b12042b..0e35616 100644 --- a/protocol/headers_opts.go +++ b/protocol/headers_opts.go @@ -56,15 +56,12 @@ func NewHeadersFrom(orig Headers, opts ...HeaderOpt) Headers { return res } -// WithCorrelationID sets a new value for header key 'correlation-id' if it is provided. +// WithCorrelationID sets the HeaderCorrelationID value. // -// If header key 'correlation-id' is not provided and there are more than one headers for 'correlation-id' -// differing only in capitalization, WithCorrelationID sets a new value for the first met header. +// If there is no HeaderCorrelationID value, but there is at least one which key differs only in capitalization, +// than the value would be set to the first such key(sorted in increasing order). // -// If there aren't any headers for 'correlation-id', WithCorrelationID sets a new header with -// key 'correlation-id' and the provided value. -// -// To use the provided key to set a new value, access the map directly. +// Use WithGeneric to set a value to a specific key in regard to capitalization. func WithCorrelationID(correlationID string) HeaderOpt { return func(headers Headers) error { setNewValue(headers, HeaderCorrelationID, correlationID) @@ -72,15 +69,12 @@ func WithCorrelationID(correlationID string) HeaderOpt { } } -// WithReplyTo sets a new value for header key 'reply-to' if it is provided. -// -// If header key 'reply-to' is not provided and there are more than one headers for 'reply-to' -// differing only in capitalization, WithReplyTo sets a new value for the first met header. +// WithReplyTo sets the HeaderReplyTo value. // -// If there aren't any headers for 'reply-to', WithReplyTo sets a new header with -// key 'reply-to' and the provided value. +// If there is no HeaderReplyTo value, but there is at least one which key differs only in capitalization, +// than the value would be set to the first such key(sorted in increasing order). // -// To use the provided key to set a new value, access the map directly. +// Use WithGeneric to set a value to a specific key in regard to capitalization. func WithReplyTo(replyTo string) HeaderOpt { return func(headers Headers) error { setNewValue(headers, HeaderReplyTo, replyTo) @@ -88,15 +82,12 @@ func WithReplyTo(replyTo string) HeaderOpt { } } -// WithReplyTarget sets a new value for header key 'ditto-reply-target' if it is provided. +// WithReplyTarget sets the HeaderReplyTarget value. // -// If header key 'ditto-reply-target' is not provided and there are more than one headers for 'ditto-reply-target' -// differing only in capitalization, WithReplyTarget sets a new value for the first met header. +// If there is no HeaderReplyTarget value, but there is at least one which key differs only in capitalization, +// than the value would be set to the first such key(sorted in increasing order). // -// If there aren't any headers for 'ditto-reply-target', WithReplyTarget sets a new header with -// key 'ditto-reply-target' and the provided value. -// -// To use the provided key to set a new value, access the map directly. +// Use WithGeneric to set a value to a specific key in regard to capitalization. func WithReplyTarget(replyTarget int64) HeaderOpt { return func(headers Headers) error { setNewValue(headers, HeaderReplyTarget, replyTarget) @@ -104,15 +95,12 @@ func WithReplyTarget(replyTarget int64) HeaderOpt { } } -// WithChannel sets a new value for header key 'ditto-channel' if it is provided. -// -// If header key 'ditto-channel' is not provided and there are more than one headers for 'ditto-channel' -// differing only in capitalization, WithChannel sets a new value for the first met header. +// WithChannel sets the HeaderChannel value. // -// If there aren't any headers for 'ditto-channel', WithChannel sets a new header with -// key 'ditto-channel' and the provided value. +// If there is no HeaderChannel value, but there is at least one which key differs only in capitalization, +// than the value would be set to the first such key(sorted in increasing order). // -// To use the provided key to set a new value, access the map directly. +// Use WithGeneric to set a value to a specific key in regard to capitalization. func WithChannel(channel string) HeaderOpt { return func(headers Headers) error { setNewValue(headers, HeaderChannel, channel) @@ -120,15 +108,12 @@ func WithChannel(channel string) HeaderOpt { } } -// WithResponseRequired sets a new value for header key 'response-required' if it is provided. +// WithResponseRequired sets the HeaderResponseRequired value. // -// If header key 'response-required' is not provided and there are more than one headers for 'response-required' -// differing only in capitalization, WithResponseRequired sets a new value for the first met header. +// If there is no HeaderResponseRequired value, but there is at least one which key differs only in capitalization, +// than the value would be set to the first such key(sorted in increasing order). // -// If there aren't any headers for 'response-required', WithResponseRequired sets a new header with -// key 'response-required' and the provided value. -// -// To use the provided key to set a new value, access the map directly. +// Use WithGeneric to set a value to a specific key in regard to capitalization. func WithResponseRequired(isResponseRequired bool) HeaderOpt { return func(headers Headers) error { setNewValue(headers, HeaderResponseRequired, isResponseRequired) @@ -136,15 +121,12 @@ func WithResponseRequired(isResponseRequired bool) HeaderOpt { } } -// WithOriginator sets a new value for header key 'ditto-originator' if it is provided. -// -// If header key 'ditto-originator' is not provided and there are more than one headers for 'ditto-originator' -// differing only in capitalization, WithOriginator sets a new value for the first met header. +// WithOriginator sets the HeaderOriginator value. // -// If there aren't any headers for 'ditto-originator', WithOriginator sets a new header with -// key 'ditto-originator' and the provided value. +// If there is no HeaderOriginator value, but there is at least one which key differs only in capitalization, +// than the value would be set to the first such key(sorted in increasing order). // -// To use the provided key to set a new value, access the map directly. +// Use WithGeneric to set a value to a specific key in regard to capitalization. func WithOriginator(dittoOriginator string) HeaderOpt { return func(headers Headers) error { setNewValue(headers, HeaderOriginator, dittoOriginator) @@ -152,15 +134,12 @@ func WithOriginator(dittoOriginator string) HeaderOpt { } } -// WithOrigin sets a new value for header key 'origin' if it is provided. -// -// If header key 'origin' is not provided and there are more than one headers for 'origin' -// differing only in capitalization, WithOrigin sets a new value for the first met header. +// WithOrigin sets the HeaderOrigin value. // -// If there aren't any headers for 'origin', WithOrigin sets a new header with -// key 'origin' and the provided value. +// If there is no HeaderOrigin value, but there is at least one which key differs only in capitalization, +// than the value would be set to the first such key(sorted in increasing order). // -// To use the provided key to set a new value, access the map directly. +// Use WithGeneric to set a value to a specific key in regard to capitalization. func WithOrigin(origin string) HeaderOpt { return func(headers Headers) error { setNewValue(headers, HeaderOrigin, origin) @@ -168,15 +147,12 @@ func WithOrigin(origin string) HeaderOpt { } } -// WithDryRun sets a new value for header key 'ditto-dry-run' if it is provided. +// WithDryRun sets the HeaderDryRun value. // -// If header key 'ditto-dry-run' is not provided and there are more than one headers for 'ditto-dry-run' -// differing only in capitalization, WithDryRun sets a new value for the first met header. +// If there is no HeaderDryRun value, but there is at least one which key differs only in capitalization, +// than the value would be set to the first such key(sorted in increasing order). // -// If there aren't any headers for 'ditto-dry-run', WithDryRun sets a new header with -// key 'ditto-dry-run' and the provided value. -// -// To use the provided key to set a new value, access the map directly. +// Use WithGeneric to set a value to a specific key in regard to capitalization. func WithDryRun(isDryRun bool) HeaderOpt { return func(headers Headers) error { setNewValue(headers, HeaderDryRun, isDryRun) @@ -184,15 +160,12 @@ func WithDryRun(isDryRun bool) HeaderOpt { } } -// WithETag sets a new value for header key 'etag' if it is provided. -// -// If header key 'etag' is not provided and there are more than one headers for 'etag' -// differing only in capitalization, WithETag sets a new value for the first met header. +// WithETag sets the HeaderETag value. // -// If there aren't any headers for 'etag', WithETag sets a new header with -// key 'etag' and the provided value. +// If there is no HeaderETag value, but there is at least one which key differs only in capitalization, +// than the value would be set to the first such key(sorted in increasing order). // -// To use the provided key to set a new value, access the map directly. +// Use WithGeneric to set a value to a specific key in regard to capitalization. func WithETag(eTag string) HeaderOpt { return func(headers Headers) error { setNewValue(headers, HeaderETag, eTag) @@ -200,15 +173,12 @@ func WithETag(eTag string) HeaderOpt { } } -// WithIfMatch sets a new value for header key 'if-match' if it is provided. +// WithIfMatch sets the HeaderIfMatch value. // -// If header key 'if-match' is not provided and there are more than one headers for 'if-match' -// differing only in capitalization, WithIfMatch sets a new value for the first met header. +// If there is no HeaderIfMatch value, but there is at least one which key differs only in capitalization, +// than the value would be set to the first such key(sorted in increasing order). // -// If there aren't any headers for 'if-match', WithIfMatch sets a new header with -// key 'if-match' and the provided value. -// -// To use the provided key to set a new value, access the map directly. +// Use WithGeneric to set a value to a specific key in regard to capitalization. func WithIfMatch(ifMatch string) HeaderOpt { return func(headers Headers) error { setNewValue(headers, HeaderIfMatch, ifMatch) @@ -216,15 +186,12 @@ func WithIfMatch(ifMatch string) HeaderOpt { } } -// WithIfNoneMatch sets a new value for header key 'if-none-match' if it is provided. -// -// If header key 'if-none-match' is not provided and there are more than one headers for 'if-none-match' -// differing only in capitalization, WithIfNoneMatch sets a new value for the first met header. +// WithIfNoneMatch sets the HeaderIfNoneMatch value. // -// If there aren't any headers for 'if-none-match', WithIfNoneMatch sets a new header with -// key 'if-none-match' and the provided value. +// If there is no HeaderIfNoneMatch value, but there is at least one which key differs only in capitalization, +// than the value would be set to the first such key(sorted in increasing order). // -// To use the provided key to set a new value, access the map directly. +// Use WithGeneric to set a value to a specific key in regard to capitalization. func WithIfNoneMatch(ifNoneMatch string) HeaderOpt { return func(headers Headers) error { setNewValue(headers, HeaderIfNoneMatch, ifNoneMatch) @@ -232,15 +199,12 @@ func WithIfNoneMatch(ifNoneMatch string) HeaderOpt { } } -// WithTimeout sets a new value for header key 'timeout' if it is provided. +// WithTimeout sets the HeaderTimeout value. // -// If header key 'timeout' is not provided and there are more than one headers for 'timeout' -// differing only in capitalization, WithTimeout sets a new value for the first met header. +// If there is no HeaderTimeout value, but there is at least one which key differs only in capitalization, +// than the value would be set to the first such key(sorted in increasing order). // -// If there aren't any headers for 'timeout', WithTimeout sets a new header with -// key 'timeout' and the provided value. -// -// To use the provided key to set a new value, access the map directly. +// Use WithGeneric to set a value to a specific key in regard to capitalization. func WithTimeout(timeout time.Duration) HeaderOpt { return func(headers Headers) error { var value string @@ -267,15 +231,12 @@ func WithTimeout(timeout time.Duration) HeaderOpt { } } -// WithVersion sets a new value for header key 'version' if it is provided. -// -// If header key 'version' is not provided and there are more than one headers for 'version' -// differing only in capitalization, WithVersion sets a new value for the first met header. +// WithVersion sets the HeaderVersion value. // -// If there aren't any headers for 'version', WithVersion sets a new header with -// key 'version' and the provided value. +// If there is no HeaderVersion value, but there is at least one which key differs only in capitalization, +// than the value would be set to the first such key(sorted in increasing order). // -// To use the provided key to set a new value, access the map directly. +// Use WithGeneric to set a value to a specific key in regard to capitalization. func WithVersion(version int64) HeaderOpt { return func(headers Headers) error { setNewValue(headers, HeaderVersion, version) @@ -283,15 +244,12 @@ func WithVersion(version int64) HeaderOpt { } } -// WithContentType sets a new value for header key 'content-type' if it is provided. -// -// If header key 'content-type' is not provided and there are more than one headers for 'content-type' -// differing only in capitalization, WithContentType sets a new value for the first met header. +// WithContentType sets the HeaderContentType value. // -// If there aren't any headers for 'content-type', WithContentType sets a new header with -// key 'content-type' and the provided value. +// If there is no HeaderContentType value, but there is at least one which key differs only in capitalization, +// than the value would be set to the first such key(sorted in increasing order). // -// To use the provided key to set a new value, access the map directly. +// Use WithGeneric to set a value to a specific key in regard to capitalization. func WithContentType(contentType string) HeaderOpt { return func(headers Headers) error { setNewValue(headers, HeaderContentType, contentType) diff --git a/protocol/headers_opts_test.go b/protocol/headers_opts_test.go index 079d26e..719fcda 100644 --- a/protocol/headers_opts_test.go +++ b/protocol/headers_opts_test.go @@ -165,8 +165,7 @@ func TestWithCorrelationID(t *testing.T) { for testName, testCase := range tests { t.Run(testName, func(t *testing.T) { got := NewHeadersFrom(testCase.testHeader, WithCorrelationID(testCase.arg)) - want, _ := got.CorrelationID() - internal.AssertEqual(t, testCase.arg, want) + internal.AssertEqual(t, testCase.arg, got.CorrelationID()) }) } } diff --git a/protocol/headers_test.go b/protocol/headers_test.go index 21d6009..14d45cd 100644 --- a/protocol/headers_test.go +++ b/protocol/headers_test.go @@ -21,61 +21,56 @@ import ( func TestHeadersCorrelationID(t *testing.T) { tests := map[string]struct { - testHeader Headers - want string - isValidType bool + testHeader Headers + valueInMap interface{} + want string }{ "test_with_correlation_id": { - testHeader: Headers{HeaderCorrelationID: "correlation-id"}, - want: "correlation-id", - isValidType: true, + testHeader: Headers{HeaderCorrelationID: "correlation-id"}, + valueInMap: "correlation-id", + want: "correlation-id", }, "test_empty_correlation_id": { - testHeader: Headers{HeaderCorrelationID: ""}, - want: "", - isValidType: true, + testHeader: Headers{HeaderCorrelationID: ""}, + valueInMap: "", + want: "", }, "test_corrlation_id_number": { - testHeader: Headers{HeaderCorrelationID: 1}, - want: "", - isValidType: false, + testHeader: Headers{HeaderCorrelationID: 1}, + valueInMap: 1, + want: "", }, "test_same_corrlation_ids_invalid_value": { testHeader: Headers{ HeaderCorrelationID: 1, "CORRELATION-ID": "test", }, - want: "", - isValidType: false, + valueInMap: 1, + want: "", }, "test_same_corrlation_ids_valid_value": { testHeader: Headers{ HeaderCorrelationID: "1", "CORRELATION-ID": "test", }, - want: "1", - isValidType: true, + valueInMap: "1", + want: "1", }, "test_same_corrlation_ids": { testHeader: Headers{ "correlation-ID": "1", "CORRELATION-ID": "test", }, - want: "test", - isValidType: true, + valueInMap: nil, + want: "test", }, } for testName, testCase := range tests { t.Run(testName, func(t *testing.T) { - got, ok := testCase.testHeader.CorrelationID() + got := testCase.testHeader.CorrelationID() internal.AssertEqual(t, testCase.want, got) - if testCase.isValidType { - internal.AssertTrue(t, ok) - } else { - internal.AssertFalse(t, ok) - internal.AssertEqual(t, 1, testCase.testHeader[HeaderCorrelationID]) - } + internal.AssertEqual(t, testCase.valueInMap, testCase.testHeader[HeaderCorrelationID]) }) } } @@ -954,8 +949,7 @@ func TestCaseInsensitiveKey(t *testing.T) { envelope.WithHeaders(NewHeaders(WithGeneric("coRRelation-ID", "correlation-id-2"))) // return the first correlation-id (side effect from unmarshal JSON) - res, _ := envelope.Headers.CorrelationID() - internal.AssertEqual(t, "correlation-id-2", res) + internal.AssertEqual(t, "correlation-id-2", envelope.Headers.CorrelationID()) json.Marshal(envelope.Headers)