-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Headers improvements #23
base: master
Are you sure you want to change the base?
Changes from 2 commits
4739e13
aacdd8b
16a0ee7
79f4756
311a9d0
f5c76b0
3bf8be7
72f7beb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 "" | ||
// 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 | ||
} | ||
} | ||
return 60 * time.Second | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the client supposed to handle default values of the headers? Or by spec, the cloud backend is supposed to interpret missing values to their defaults? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this case the value is only returned/read and not set explicitly.
I'm totally agree with this statement, so it's good all getters/readers to return the default value no matter it's explicitly set or not (as it's done for the "response-required" value). For advanced users the Values map is exported so they can check any header existence or set it with its default value. |
||
} | ||
|
||
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 | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a specific reasoning behind implementing a custom duration string parsing instead of using the one from the Go SDK - i.e. time.ParseDuration? |
||
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 | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we are about to perform header values validation here - are we about to introduce them for all known headers limitations and expected values - e.g. requested-acks header value validation, etc.? Otherwise, it is not really consistent. |
||
|
||
// 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 h.Values[HeaderResponseRequired] == nil { | ||
return false | ||
if value, ok := h.Values[HeaderResponseRequired]; ok && value != nil { | ||
return value.(bool) | ||
} | ||
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 "" | ||
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. | ||
|
@@ -164,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 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we decide to change the API from string to time.Duration - the value in the Headers.Values map must be of type time.Duration as well - not a string one parsed every time. Otherwise, we introduce an inconsistency.
This of course, will requires some marshaling/unmarshaling customizations but it's important to keep the usage of the header the same regardless how the value was accessed - via the getter or via the Headers.Values map directly.