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 1ad9d44..e6ae976 100644 --- a/protocol/headers.go +++ b/protocol/headers.go @@ -12,162 +12,399 @@ package protocol import ( - "encoding/json" + "fmt" + "sort" + "strconv" + "strings" + "time" + + "github.com/google/uuid" ) -// 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" + + // 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 = "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" + + // HeaderVersion represents 'version' header. + HeaderVersion = "version" + + // HeaderContentType represents 'content-type' header. + 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. +// +// 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 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 h.Values[HeaderCorrelationID] == nil { - return "" +// CorrelationID returns the HeaderCorrelationID header value if it is presented. +// +// 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 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 getStr(value, "") } - return h.Values[HeaderCorrelationID].(string) + keys := sortHeadersKey(h) + for _, k := range keys { + if strings.EqualFold(k, HeaderCorrelationID) { + return getStr(h[k], "") + } + } + h[HeaderCorrelationID] = uuid.New().String() + return h[HeaderCorrelationID].(string) } -// Timeout returns the 'timeout' header value or empty string if not set. -func (h *Headers) Timeout() string { - if h.Values[HeaderTimeout] == nil { - return "" +// Timeout returns the HeaderTimeout header value if it is presented. +// The default and maximum value is duration of 60 seconds. +// +// 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. +// +// 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) + } + keys := sortHeadersKey(h) + for _, k := range keys { + if strings.EqualFold(k, HeaderTimeout) { + return h.timeoutValue(h[k]) + } } - return h.Values[HeaderTimeout].(string) + return 60 * time.Second } -// 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 +// IsResponseRequired returns the HeaderResponseRequired header value if it is presented. +// The default value is true. +// +// 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 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) + } + keys := sortHeadersKey(h) + for _, k := range keys { + if strings.EqualFold(k, HeaderResponseRequired) { + return h.boolValue(h[k], true) + } } - return h.Values[HeaderResponseRequired].(bool) + return true } -// Channel returns the 'ditto-channel' header value or empty string if not set. -func (h *Headers) Channel() string { - if h.Values[HeaderChannel] == nil { - return "" - } - return h.Values[HeaderChannel].(string) +// 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 type of the HeaderChannel header (or the first met header) is not a string, the Channel 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) Channel() string { + return h.stringValue(HeaderChannel, "") + } -// 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 +// IsDryRun returns the HeaderDryRun header value if it is presented. +// The default value is false. +// +// 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. +// +// 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) + } + keys := sortHeadersKey(h) + for _, k := range keys { + if strings.EqualFold(k, HeaderDryRun) { + return h.boolValue(h[k], false) + } } - 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 "" - } - return h.Values[HeaderOrigin].(string) +// Origin returns the HeaderOrigin header value if it is presented. +// +// 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 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 { + return h.stringValue(HeaderOrigin, "") + } -// Originator returns the 'ditto-originator' header value or empty string if not set. -func (h *Headers) Originator() string { - if h.Values[HeaderOriginator] == nil { - return "" - } - return h.Values[HeaderOriginator].(string) +// 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 type of the HeaderOriginator header (or the first met header) is not a string, the Originator 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) Originator() string { + return h.stringValue(HeaderOriginator, "") + } -// ETag returns the 'ETag' header value or empty string if not set. -func (h *Headers) ETag() string { - if h.Values[HeaderETag] == nil { - return "" - } - return h.Values[HeaderETag].(string) +// 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 type of the HeaderETag header (or the first met header) is not a string, the ETag 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) ETag() string { + return h.stringValue(HeaderETag, "") + } -// IfMatch returns the 'If-Match' header value or empty string if not set. -func (h *Headers) IfMatch() string { - if h.Values[HeaderIfMatch] == nil { - return "" +// IfMatch returns the HeaderIfMatch header value if it is presented. +// +// 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 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 { + return h.stringValue(HeaderIfMatch, "") + +} + +// 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 type of the HeaderIfNoneMatch header (or the first met header) is not a string, the IfNoneMatch 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) IfNoneMatch() string { + return h.stringValue(HeaderIfNoneMatch, "") + +} + +// 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 type of the HeaderReplyTarget header (or the first met header) is not an int64, the ReplyTarget returns 0. +// +// 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) + } + keys := sortHeadersKey(h) + for _, k := range keys { + if strings.EqualFold(k, HeaderReplyTarget) { + return h.intValue(h[k], 0) + } } - return h.Values[HeaderIfMatch].(string) + return 0 } -// 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 "" +// ReplyTo returns the HeaderReplyTo header value if it is presented. +// +// 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 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 { + return h.stringValue(HeaderReplyTo, "") +} + +// Version returns the HeaderVersion header value if it is presented. +// The default value is 2. +// +// 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. +// +// 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)) } - return h.Values[HeaderIfNoneMatch].(string) + keys := sortHeadersKey(h) + for _, k := range keys { + if strings.EqualFold(k, HeaderVersion) { + return h.intValue(h[k], int64(2)) + } + } + return int64(2) +} + +// ContentType returns the HeaderContentType header value if it is presented. +// +// 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 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 { + return h.stringValue(HeaderContentType, "") } -// 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 +// Generic returns the value of the provided key header. +func (h Headers) Generic(id string) interface{} { + return h[id] +} + +func (h Headers) stringValue(headerKey, defValue string) string { + if value, ok := h[headerKey]; ok { + return getStr(value, defValue) + } + keys := sortHeadersKey(h) + for _, k := range keys { + if strings.EqualFold(k, headerKey) { + return getStr(h[k], defValue) + } } - return h.Values[HeaderReplyTarget].(int64) + return defValue } -// ReplyTo returns the 'reply-to' header value or empty string if not set. -func (h *Headers) ReplyTo() string { - if h.Values[HeaderReplyTo] == nil { - return "" +func getStr(value interface{}, defValue string) string { + if str, ok := value.(string); ok { + return str } - return h.Values[HeaderReplyTo].(string) + return defValue } -// Version returns the 'version' header value or empty string if not set. -func (h *Headers) Version() int64 { - if h.Values[HeaderSchemaVersion] == nil { - return 0 +func (h Headers) timeoutValue(headerValue interface{}) time.Duration { + if value, ok := headerValue.(string); ok { + if duration, err := parseTimeout(value); err == nil { + return duration + } } - return h.Values[HeaderSchemaVersion].(int64) + return 60 * time.Second } -// ContentType returns the 'content-type' header value or empty string if not set. -func (h *Headers) ContentType() string { - if h.Values[HeaderContentType] == nil { - return "" +func (h Headers) intValue(headerValue interface{}, defValue int64) int64 { + if value, ok := headerValue.(int64); ok { + return value } - return h.Values[HeaderContentType].(string) + return defValue } -// 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] +func (h Headers) boolValue(headerValue interface{}, defValue bool) bool { + if value, ok := headerValue.(bool); ok { + return value + } + return defValue } -// MarshalJSON marshels Headers. -func (h *Headers) MarshalJSON() ([]byte, error) { - return json.Marshal(h.Values) +func sortHeadersKey(h Headers) []string { + keys := make([]string, len(h)) + i := 0 + for k := range h { + keys[i] = k + i++ + } + sort.Strings(keys) + return keys } -// UnmarshalJSON unmarshels Headers. -func (h *Headers) UnmarshalJSON(data []byte) error { - var v map[string]interface{} - if err := json.Unmarshal(data, &v); err != nil { - return err +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 + } } - h.Values = v - return nil + return 60 * time.Second, fmt.Errorf("invalid timeout '%s'", timeout) } diff --git a/protocol/headers_opts.go b/protocol/headers_opts.go index d33fac0..0e35616 100644 --- a/protocol/headers_opts.go +++ b/protocol/headers_opts.go @@ -11,11 +11,17 @@ package protocol +import ( + "strconv" + "strings" + "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 +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 @@ -25,9 +31,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 } @@ -35,138 +40,242 @@ 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 } return res } -// WithCorrelationID sets the 'correlation-id' header value. +// WithCorrelationID sets the HeaderCorrelationID value. +// +// 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). +// +// Use WithGeneric to set a value to a specific key in regard to capitalization. func WithCorrelationID(correlationID string) HeaderOpt { - return func(headers *Headers) error { - headers.Values[HeaderCorrelationID] = correlationID + return func(headers Headers) error { + setNewValue(headers, HeaderCorrelationID, correlationID) return nil } } -// WithReplyTo sets the 'reply-to' header value. +// WithReplyTo sets the HeaderReplyTo 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). +// +// Use WithGeneric to set a value to a specific key in regard to capitalization. func WithReplyTo(replyTo string) HeaderOpt { - return func(headers *Headers) error { - headers.Values[HeaderReplyTo] = replyTo + return func(headers Headers) error { + setNewValue(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 +// WithReplyTarget sets the HeaderReplyTarget value. +// +// 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). +// +// 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) return nil } } -// WithChannel sets the 'ditto-channel' header value. +// WithChannel sets the HeaderChannel 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). +// +// Use WithGeneric to set a value to a specific key in regard to capitalization. func WithChannel(channel string) HeaderOpt { - return func(headers *Headers) error { - headers.Values[HeaderChannel] = channel + return func(headers Headers) error { + setNewValue(headers, HeaderChannel, channel) return nil } } -// WithResponseRequired sets the 'response-required' header value. +// WithResponseRequired sets the HeaderResponseRequired value. +// +// 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). +// +// Use WithGeneric to set a value to a specific key in regard to capitalization. func WithResponseRequired(isResponseRequired bool) HeaderOpt { - return func(headers *Headers) error { - headers.Values[HeaderResponseRequired] = isResponseRequired + return func(headers Headers) error { + setNewValue(headers, HeaderResponseRequired, isResponseRequired) return nil } } -// WithOriginator sets the 'ditto-originator' header value. +// WithOriginator sets the HeaderOriginator 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). +// +// Use WithGeneric to set a value to a specific key in regard to capitalization. func WithOriginator(dittoOriginator string) HeaderOpt { - return func(headers *Headers) error { - headers.Values[HeaderOriginator] = dittoOriginator + return func(headers Headers) error { + setNewValue(headers, HeaderOriginator, dittoOriginator) return nil } } -// WithOrigin sets the 'origin' header value. +// WithOrigin sets the HeaderOrigin 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). +// +// Use WithGeneric to set a value to a specific key in regard to capitalization. func WithOrigin(origin string) HeaderOpt { - return func(headers *Headers) error { - headers.Values[HeaderOrigin] = origin + return func(headers Headers) error { + setNewValue(headers, HeaderOrigin, origin) return nil } } -// WithDryRun sets the 'ditto-dry-run' header value. +// WithDryRun sets the HeaderDryRun value. +// +// 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). +// +// Use WithGeneric to set a value to a specific key in regard to capitalization. func WithDryRun(isDryRun bool) HeaderOpt { - return func(headers *Headers) error { - headers.Values[HeaderDryRun] = isDryRun + return func(headers Headers) error { + setNewValue(headers, HeaderDryRun, isDryRun) return nil } } -// WithETag sets the 'ETag' header value. +// WithETag sets the HeaderETag 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). +// +// Use WithGeneric to set a value to a specific key in regard to capitalization. func WithETag(eTag string) HeaderOpt { - return func(headers *Headers) error { - headers.Values[HeaderETag] = eTag + return func(headers Headers) error { + setNewValue(headers, HeaderETag, eTag) return nil } } -// WithIfMatch sets the 'If-Match' header value. +// WithIfMatch sets the HeaderIfMatch value. +// +// 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). +// +// Use WithGeneric to set a value to a specific key in regard to capitalization. func WithIfMatch(ifMatch string) HeaderOpt { - return func(headers *Headers) error { - headers.Values[HeaderIfMatch] = ifMatch + return func(headers Headers) error { + setNewValue(headers, HeaderIfMatch, ifMatch) return nil } } -// WithIfNoneMatch sets the 'If-None-Match' header value. +// WithIfNoneMatch sets the HeaderIfNoneMatch 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). +// +// Use WithGeneric to set a value to a specific key in regard to capitalization. func WithIfNoneMatch(ifNoneMatch string) HeaderOpt { - return func(headers *Headers) error { - headers.Values[HeaderIfNoneMatch] = ifNoneMatch + return func(headers Headers) error { + setNewValue(headers, HeaderIfNoneMatch, ifNoneMatch) return nil } } -// WithTimeout sets the 'timeout' header value. -func WithTimeout(timeout string) HeaderOpt { - return func(headers *Headers) error { - headers.Values[HeaderTimeout] = timeout +// WithTimeout sets the HeaderTimeout value. +// +// 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). +// +// 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 + + 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" + } + } + setNewValue(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 +// WithVersion sets the HeaderVersion 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). +// +// 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) return nil } } -// WithContentType sets the 'content-type' header value. +// WithContentType sets the HeaderContentType 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). +// +// Use WithGeneric to set a value to a specific key in regard to capitalization. func WithContentType(contentType string) HeaderOpt { - return func(headers *Headers) error { - headers.Values[HeaderContentType] = contentType + return func(headers Headers) error { + setNewValue(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 } } + +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 48c53b0..719fcda 100644 --- a/protocol/headers_opts_test.go +++ b/protocol/headers_opts_test.go @@ -14,12 +14,13 @@ package protocol import ( "errors" "testing" + "time" "github.com/eclipse/ditto-clients-golang/internal" ) func WithError() HeaderOpt { - return func(headers *Headers) error { + return func(headers Headers) error { return errors.New("this is an error example") } } @@ -40,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]) } }) } @@ -58,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()}, @@ -74,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 { @@ -89,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{}, }, } @@ -171,129 +141,440 @@ 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)) + internal.AssertEqual(t, testCase.arg, got.CorrelationID()) + }) + } } 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.Values[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) { - 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_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) { - 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.Values[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 e6cfe85..14d45cd 100644 --- a/protocol/headers_test.go +++ b/protocol/headers_test.go @@ -12,257 +12,854 @@ 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 + valueInMap interface{} + want string + }{ + "test_with_correlation_id": { + testHeader: Headers{HeaderCorrelationID: "correlation-id"}, + valueInMap: "correlation-id", + want: "correlation-id", + }, + "test_empty_correlation_id": { + testHeader: Headers{HeaderCorrelationID: ""}, + valueInMap: "", + want: "", + }, + "test_corrlation_id_number": { + testHeader: Headers{HeaderCorrelationID: 1}, + valueInMap: 1, + want: "", + }, + "test_same_corrlation_ids_invalid_value": { + testHeader: Headers{ + HeaderCorrelationID: 1, + "CORRELATION-ID": "test", + }, + valueInMap: 1, + want: "", + }, + "test_same_corrlation_ids_valid_value": { + testHeader: Headers{ + HeaderCorrelationID: "1", + "CORRELATION-ID": "test", + }, + valueInMap: "1", + want: "1", + }, + "test_same_corrlation_ids": { + testHeader: Headers{ + "correlation-ID": "1", + "CORRELATION-ID": "test", + }, + valueInMap: nil, + want: "test", + }, + } - 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) + internal.AssertEqual(t, testCase.valueInMap, testCase.testHeader[HeaderCorrelationID]) + }) + } } 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{HeaderTimeout: "10s"}, + want: 10 * time.Second, + }, + "test_without_timeout": { + 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 { + t.Run(testName, func(t *testing.T) { + got := testCase.testHeader.Timeout() + internal.AssertEqual(t, testCase.want, got) + }) + } +} - got := h.Timeout() - internal.AssertEqual(t, "10", got) +func TestTimeoutValue(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, + }, + } - arg[HeaderTimeout] = nil - got = h.Timeout() - internal.AssertEqual(t, "", got) - }) + for testName, testCase := range tests { + t.Run(testName, func(t *testing.T) { + headers := NewHeaders() + 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 + 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, + }, + } - 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) + internal.AssertEqual(t, testCase.valueInMap, testCase.testHeader[HeaderResponseRequired]) + }) + } } 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 + 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", + }, + } - 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) + internal.AssertEqual(t, testCase.valueInMap, testCase.testHeader[HeaderChannel]) + }) + } } 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 + 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, + }, + } - 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) + internal.AssertEqual(t, testCase.valueInMap, testCase.testHeader[HeaderDryRun]) + }) + } } 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 + 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", + }, + } - 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) + internal.AssertEqual(t, testCase.valueInMap, testCase.testHeader[HeaderOrigin]) + }) + } } 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 + 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", + }, + } - 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) + internal.AssertEqual(t, testCase.valueInMap, testCase.testHeader[HeaderOriginator]) + }) + } } 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 + 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", + }, + } - 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) + internal.AssertEqual(t, testCase.valueInMap, testCase.testHeader[HeaderETag]) + }) + } } 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 + valueInMap interface{} + want string + }{ + "test_with_if_match": { + 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", + }, + } - 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) + internal.AssertEqual(t, testCase.valueInMap, testCase.testHeader[HeaderIfMatch]) + }) + } } 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 + valueInMap interface{} + want string + }{ + "test_with_if_none_match": { + 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", + }, + } - 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) + internal.AssertEqual(t, testCase.valueInMap, testCase.testHeader[HeaderIfNoneMatch]) + }) + } } 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 + 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), + }, + } - 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) + internal.AssertEqual(t, testCase.valueInMap, testCase.testHeader[HeaderReplyTarget]) + }) + } } 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 + 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", + }, + } - 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) + internal.AssertEqual(t, testCase.valueInMap, testCase.testHeader[HeaderReplyTo]) + }) + } } 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 + valueInMap interface{} + want int64 + }{ + "test_with_version": { + testHeader: Headers{HeaderVersion: int64(123)}, + valueInMap: int64(123), + want: int64(123), + }, + "test_without_version": { + testHeader: Headers{}, + 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), + }, + } - 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) + internal.AssertEqual(t, testCase.valueInMap, testCase.testHeader[HeaderVersion]) + }) + } } 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 + valueInMap interface{} + want string + }{ + "test_with_content_type": { + 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", + }, + } - 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) { 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) @@ -293,8 +890,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") @@ -309,35 +905,57 @@ func TestHeadersMarshalJSON(t *testing.T) { } func TestHeadersUnmarshalJSON(t *testing.T) { - ct := "application/json" - tests := map[string]struct { - data string - wantErr bool + data string + want Headers }{ - "test_headers_unmarshal_JSON_ok": { - data: "{\"content-type\":\"application/json\"}", - wantErr: false, + "test_headers_unmarshal_JSON_with_one_heder": { + data: `{"content-type":"application/json"}`, + want: Headers{HeaderContentType: "application/json"}, + }, + "test_headers_unmarshal_JSON_with_many_headers": { + data: `{ + "content-type":"application/json", + "timeout": "30ms", + "response-required":false + }`, + want: Headers{ + HeaderContentType: "application/json", + HeaderTimeout: "30ms", + HeaderResponseRequired: false, + }, }, "test_headers_unmarshal_JSON_err": { - data: "", - wantErr: true, + data: "", + want: Headers{}, }, } for testName, testCase := range tests { 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) - } - } + 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, + } + + envelope.WithHeaders(NewHeaders(WithGeneric("coRRelation-ID", "correlation-id-2"))) + + // return the first correlation-id (side effect from unmarshal JSON) + internal.AssertEqual(t, "correlation-id-2", envelope.Headers.CorrelationID()) + + 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"}, }, }, }