aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--account.go23
-rw-r--r--account_test.go32
-rw-r--r--container.go28
-rw-r--r--container_test.go2
-rw-r--r--errors.go12
-rw-r--r--headers.go322
-rw-r--r--headers/base.go25
-rw-r--r--headers/errors.go31
-rw-r--r--headers/headers.go91
-rw-r--r--headers/headers_test.go (renamed from metadata_test.go)46
-rw-r--r--headers/metadata.go52
-rw-r--r--headers/string.go60
-rw-r--r--headers/uint64.go120
-rw-r--r--headers_test.go19
-rw-r--r--metadata.go89
-rw-r--r--request.go18
16 files changed, 558 insertions, 412 deletions
diff --git a/account.go b/account.go
index 8fb4845..4438ce8 100644
--- a/account.go
+++ b/account.go
@@ -96,10 +96,11 @@ func (a *Account) Headers() (AccountHeaders, error) {
return AccountHeaders{}, err
}
- var headers AccountHeaders
- err = parseHeaders(resp.Header, &headers)
+ headers := NewAccountHeaders()
+ headers.FromHTTP(resp.Header)
+ err = headers.Validate()
if err != nil {
- return AccountHeaders{}, err
+ return headers, err
}
a.headers = &headers
return *a.headers, nil
@@ -111,14 +112,15 @@ func (a *Account) Invalidate() {
a.headers = nil
}
-//Update updates the account using a POST request. To set arbitrary request
-//headers (and to add URL parameters), pass a non-nil *RequestOptions.
+//Update updates the account using a POST request. To add URL parameters, pass
+//a non-nil *RequestOptions.
//
//A successful POST request implies Invalidate() since it may change metadata.
func (a *Account) Update(headers AccountHeaders, opts *RequestOptions) error {
_, err := Request{
Method: "POST",
- Options: compileHeaders(&headers, opts),
+ Headers: headers.ToHTTP(),
+ Options: opts,
ExpectStatusCodes: []int{204},
}.Do(a.client)
if err == nil {
@@ -127,8 +129,8 @@ func (a *Account) Update(headers AccountHeaders, opts *RequestOptions) error {
return err
}
-//Create creates the account using a PUT request. To set arbitrary request
-//headers (and to add URL parameters), pass a non-nil *RequestOptions.
+//Create creates the account using a PUT request. To add URL parameters, pass
+//a non-nil *RequestOptions.
//
//Note that this operation is only available to reseller admins, not to regular
//users.
@@ -136,8 +138,9 @@ func (a *Account) Update(headers AccountHeaders, opts *RequestOptions) error {
//A successful PUT request implies Invalidate() since it may change metadata.
func (a *Account) Create(headers AccountHeaders, opts *RequestOptions) error {
_, err := Request{
- Method: "POST",
- Options: compileHeaders(&headers, opts),
+ Method: "PUT",
+ Headers: headers.ToHTTP(),
+ Options: opts,
ExpectStatusCodes: []int{201, 202},
}.Do(a.client)
if err == nil {
diff --git a/account_test.go b/account_test.go
index 5515777..6f44e89 100644
--- a/account_test.go
+++ b/account_test.go
@@ -32,24 +32,22 @@ func TestAccountBasic(t *testing.T) {
//Headers() does not fail, i.e. everything parses correctly), but
//Content-Type is going to be text/plain because GET on an account lists
//the container names as plain text.
- expectString(t, hdr.Raw.Get("Content-Type"), "text/plain; charset=utf-8")
+ expectString(t, hdr.Get("Content-Type"), "text/plain; charset=utf-8")
})
}
func TestAccountMetadata(t *testing.T) {
testWithAccount(t, func(a *Account) {
//test creating some metadata
- err := a.Update(AccountHeaders{
- Metadata: NewMetadata(
- "schwift-test1", "first",
- "schwift-test2", "second",
- ),
- }, nil)
+ hdr := NewAccountHeaders()
+ hdr.Metadata.Set("schwift-test1", "first")
+ hdr.Metadata.Set("schwift-test2", "second")
+ err := a.Update(hdr, nil)
if !expectError(t, err, "") {
t.FailNow()
}
- hdr, err := a.Headers()
+ hdr, err = a.Headers()
if !expectError(t, err, "") {
t.FailNow()
}
@@ -57,11 +55,9 @@ func TestAccountMetadata(t *testing.T) {
expectString(t, hdr.Metadata.Get("schwift-test2"), "second")
//test deleting some metadata
- m := make(Metadata)
- m.Clear("schwift-test1")
- err = a.Update(AccountHeaders{
- Metadata: m,
- }, nil)
+ hdr = NewAccountHeaders()
+ hdr.Metadata.Clear("schwift-test1")
+ err = a.Update(hdr, nil)
if !expectError(t, err, "") {
t.FailNow()
}
@@ -74,11 +70,11 @@ func TestAccountMetadata(t *testing.T) {
expectString(t, hdr.Metadata.Get("schwift-test2"), "second")
//test updating some metadata
- m = make(Metadata)
- m.Set("schwift-test2", "changed")
- err = a.Update(AccountHeaders{
- Metadata: m,
- }, nil)
+ hdr = NewAccountHeaders()
+ hdr.Metadata.Set("schwift-test1", "will not be set")
+ hdr.Metadata.Del("schwift-test1")
+ hdr.Metadata.Set("schwift-test2", "changed")
+ err = a.Update(hdr, nil)
if !expectError(t, err, "") {
t.FailNow()
}
diff --git a/container.go b/container.go
index 9fcfe2b..e3b95d8 100644
--- a/container.go
+++ b/container.go
@@ -79,17 +79,18 @@ func (c *Container) Headers() (ContainerHeaders, error) {
return ContainerHeaders{}, err
}
- var headers ContainerHeaders
- err = parseHeaders(resp.Header, &headers)
+ headers := NewContainerHeaders()
+ headers.FromHTTP(resp.Header)
+ err = headers.Validate()
if err != nil {
- return ContainerHeaders{}, err
+ return headers, err
}
c.headers = &headers
return *c.headers, nil
}
-//Update updates the container using a POST request. To set arbitrary request
-//headers (and to add URL parameters), pass a non-nil *RequestOptions.
+//Update updates the container using a POST request. To add URL parameters, pass
+//a non-nil *RequestOptions.
//
//If you are not sure whether the container exists, use Create() instead.
//
@@ -98,7 +99,8 @@ func (c *Container) Update(headers ContainerHeaders, opts *RequestOptions) error
_, err := Request{
Method: "POST",
ContainerName: c.name,
- Options: compileHeaders(&headers, opts),
+ Headers: headers.ToHTTP(),
+ Options: opts,
ExpectStatusCodes: []int{204},
}.Do(c.a.client)
if err == nil {
@@ -107,8 +109,8 @@ func (c *Container) Update(headers ContainerHeaders, opts *RequestOptions) error
return err
}
-//Create creates the container using a PUT request. To set arbitrary request
-//headers (and to add URL parameters), pass a non-nil *RequestOptions.
+//Create creates the container using a PUT request. To add URL parameters, pass
+//a non-nil *RequestOptions.
//
//This function can be used regardless of whether the container exists or not.
//
@@ -117,7 +119,8 @@ func (c *Container) Create(headers ContainerHeaders, opts *RequestOptions) error
_, err := Request{
Method: "PUT",
ContainerName: c.name,
- Options: compileHeaders(&headers, opts),
+ Headers: headers.ToHTTP(),
+ Options: opts,
ExpectStatusCodes: []int{201, 202},
}.Do(c.a.client)
if err == nil {
@@ -126,8 +129,8 @@ func (c *Container) Create(headers ContainerHeaders, opts *RequestOptions) error
return err
}
-//Delete deletes the container using a DELETE request. To set arbitrary request
-//headers (and to add URL parameters), pass a non-nil *RequestOptions.
+//Delete deletes the container using a DELETE request. To add URL parameters,
+//pass a non-nil *RequestOptions.
//
//This operation fails with http.StatusConflict if the container is not empty.
//
@@ -138,7 +141,8 @@ func (c *Container) Delete(headers ContainerHeaders, opts *RequestOptions) error
_, err := Request{
Method: "DELETE",
ContainerName: c.name,
- Options: compileHeaders(&headers, opts),
+ Headers: headers.ToHTTP(),
+ Options: opts,
ExpectStatusCodes: []int{204},
}.Do(c.a.client)
if err == nil {
diff --git a/container_test.go b/container_test.go
index 7c67ed5..bbb2deb 100644
--- a/container_test.go
+++ b/container_test.go
@@ -38,7 +38,7 @@ func TestContainerExistence(t *testing.T) {
expectBool(t, Is(err, http.StatusNotFound), true)
expectBool(t, Is(err, http.StatusNoContent), false)
- err = c.Create(ContainerHeaders{}, nil)
+ err = c.Create(NewContainerHeaders(), nil)
expectError(t, err, "")
exists, err = c.Exists()
diff --git a/errors.go b/errors.go
index 7a7d40d..df6b706 100644
--- a/errors.go
+++ b/errors.go
@@ -76,15 +76,3 @@ func Is(err error, code int) bool {
}
return false
}
-
-//MalformedHeaderError is generated when a response from Swift contains a
-//malformed header.
-type MalformedHeaderError struct {
- Key string
- ParseError error
-}
-
-//Error implements the builtin/error interface.
-func (e MalformedHeaderError) Error() string {
- return "Bad header " + e.Key + ": " + e.ParseError.Error()
-}
diff --git a/headers.go b/headers.go
index 7f8cfd1..44d0607 100644
--- a/headers.go
+++ b/headers.go
@@ -19,268 +19,126 @@
package schwift
import (
- "fmt"
- "net/http"
- "net/textproto"
"reflect"
- "strconv"
- "strings"
+
+ "github.com/majewsky/schwift/headers"
)
-//AccountHeaders contains the headers for an account. The Raw attribute
-//contains the original set of headers returned from a HEAD or GET request on
-//the account. The other attributes contain the parsed values of common
-//headers, as noted in the tags next to each field. Well-known metadata headers
-//can be accessed in a type-safe way using the methods on this type.
+//AccountHeaders contains the headers for an account. The Headers attribute
+//contains the actual set of headers that was returned from a HEAD or GET
+//request on the account, and will be sent by a PUT or POST request. The other
+//attributes allow type-safe access to well-known headers, as noted in the tags
+//next to each field.
type AccountHeaders struct {
- BytesUsed uint64 `schwift:"ro,X-Account-Bytes-Used"`
- ContainerCount uint64 `schwift:"ro,X-Account-Container-Count"`
- ObjectCount uint64 `schwift:"ro,X-Account-Object-Count"`
- Metadata Metadata `schwift:"rw,X-Account-Meta-"`
- Raw http.Header
-}
-
-//QuotaBytes returns a handle to read or write the X-Account-Meta-Quota-Bytes field.
-func (a AccountHeaders) QuotaBytes() UnsignedIntField {
- return UnsignedIntField{
- a.Metadata,
- "X-Account-Meta-", "Quota-Bytes",
- }
-}
-
-//TempURLKey returns a handle to read or write the X-Account-Meta-Temp-URL-Key field.
-func (a AccountHeaders) TempURLKey() StringField {
- return StringField{
- a.Metadata,
- "X-Account-Meta-", "Temp-URL-Key",
- }
-}
-
-//TempURLKey2 returns a handle to read or write the X-Account-Meta-Temp-URL-Key-2 field.
-func (a AccountHeaders) TempURLKey2() StringField {
- return StringField{
- a.Metadata,
- "X-Account-Meta-", "Temp-URL-Key-2",
- }
-}
-
-//ContainerHeaders contains the headers for an account. The Raw attribute
-//contains the original set of headers returned from a HEAD or GET request on
-//the account. The other attributes contain the parsed values of common
-//headers, as noted in the tags next to each field. Well-known metadata headers
-//can be accessed in a type-safe way using the methods on this type.
+ headers.Headers
+ BytesUsed headers.Uint64Readonly `schwift:"X-Account-Bytes-Used"`
+ ContainerCount headers.Uint64Readonly `schwift:"X-Account-Container-Count"`
+ Metadata headers.Metadata `schwift:"X-Account-Meta-"`
+ ObjectCount headers.Uint64Readonly `schwift:"X-Account-Object-Count"`
+ QuotaBytes headers.Uint64 `schwift:"X-Account-Meta-Quota-Bytes"`
+ TempURLKey headers.String `schwift:"X-Account-Meta-Temp-URL-Key"`
+ TempURLKey2 headers.String `schwift:"X-Account-Meta-Temp-URL-Key-2"`
+ //forbid initialization as struct literal (must use NewAccountHeaders)
+ private struct{}
+}
+
+//NewAccountHeaders prepares a new AccountHeaders instance.
+func NewAccountHeaders() AccountHeaders {
+ var ah AccountHeaders
+ ah.Headers = make(headers.Headers)
+ initializeByReflection(&ah)
+ return ah
+}
+
+//Validate returns headers.MalformedHeaderError if the value of any well-known
+//header does not conform to its data type. This is called automatically by
+//Schwift when preparing an AccountHeaders instance from a GET/HEAD response,
+//so you usually do not need to do it yourself. You will get the validation error
+//from the Account method doing the request, e.g. Headers() or List().
+func (ah AccountHeaders) Validate() error {
+ return validateByReflection(&ah)
+}
+
+//ContainerHeaders contains the headers for a container. The Headers attribute
+//contains the actual set of headers that was returned from a HEAD or GET
+//request on the container, and will be sent by a PUT or POST request. The
+//other attributes allow type-safe access to well-known headers, as noted in
+//the tags next to each field.
type ContainerHeaders struct {
- Metadata Metadata `schwift:"rw,X-Container-Meta-"`
- Raw http.Header
+ headers.Headers
+ Metadata headers.Metadata `schwift:"X-Container-Meta-"`
//TODO map well-known headers
+ //forbid initialization as struct literal (must use NewContainerHeaders)
+ private struct{}
}
-////////////////////////////////////////////////////////////////////////////////
-// field types
-
-//StringField is a helper type used in the interface of AccountHeaders,
-//ContainerHeaders and ObjectHeaders. For example:
-//
-// var headers AccountHeaders
-// ...
-// value := headers.TempURLKey().Get()
-// headers.TempURLKey().Set(value + " changed")
-// headers.TempURLKey().Clear()
-type StringField struct {
- metadata Metadata
- prefix string
- key string
-}
-
-//Get returns the value for this key, or the empty string if the key does not exist.
-func (f StringField) Get() string {
- return f.metadata.Get(f.key)
+//NewContainerHeaders prepares a new ContainerHeaders instance.
+func NewContainerHeaders() ContainerHeaders {
+ var ch ContainerHeaders
+ ch.Headers = make(headers.Headers)
+ initializeByReflection(&ch)
+ return ch
}
-//Set writes a new value for this key into the original AccountHeaders,
-//ContainerHeaders or ObjectHeaders instance.
-func (f StringField) Set(value string) {
- f.metadata.Set(f.key, value)
+//Validate returns headers.MalformedHeaderError if the value of any well-known
+//header does not conform to its data type. This is called automatically by
+//Schwift when preparing an ContainerHeaders instance from a GET/HEAD response,
+//so you usually do not need to do it yourself. You will get the validation error
+//from the Container method doing the request, e.g. Headers() or List().
+func (ch ContainerHeaders) Validate() error {
+ return validateByReflection(&ch)
}
-//Clear removes this key from the original AccountHeaders, ContainerHeaders or
-//ObjectHeaders instance.
-func (f StringField) Clear() {
- f.metadata.Clear(f.key)
-}
-
-//UnsignedIntField is a helper type used in the interface of AccountHeaders,
-//ContainerHeaders and ObjectHeaders. For example:
-//
-// var headers AccountHeaders
-// ...
-// if headers.QuotaBytes.Exists() {
-// value, err := headers.QuotaBytes().Get()
-// headers.QuotaBytes().Set(value * 2)
-// }
-// ....
-// headers.QuotaBytes().Clear()
-type UnsignedIntField struct {
- metadata Metadata
- prefix string
- key string
-}
-
-//Exists returns whether there is a value for this key.
-func (f UnsignedIntField) Exists() bool {
- return f.metadata.Get(f.key) != ""
-}
-
-//Get returns the value for this key, or 0 if the key does not exist.
-func (f UnsignedIntField) Get() (uint64, error) {
- str := f.metadata.Get(f.key)
- if str == "" {
- return 0, nil
- }
- value, err := strconv.ParseUint(str, 10, 64)
- if err != nil {
- err = MalformedHeaderError{Key: f.prefix + f.key, ParseError: err}
- }
- return value, err
-}
-
-//Set writes a new value for this key into the original AccountHeaders,
-//ContainerHeaders or ObjectHeaders instance.
-func (f UnsignedIntField) Set(value uint64) {
- f.metadata.Set(f.key, strconv.FormatUint(value, 10))
-}
-
-//Clear removes this key from the original AccountHeaders, ContainerHeaders or
-//ObjectHeaders instance.
-func (f UnsignedIntField) Clear() {
- f.metadata.Clear(f.key)
+type fieldInfo struct {
+ FieldName string
+ HeaderName string
}
-////////////////////////////////////////////////////////////////////////////////
-// generic parsing functions
-
-func parseHeaders(hdr http.Header, target interface{}) error {
- return foreachField(target, func(fieldPtr interface{}, info fieldInfo) error {
- //populate the .Raw field that all input types share
- if info.FieldName == "Raw" {
- *(fieldPtr.(*http.Header)) = hdr
- return nil
- }
-
- //skip over fields without schwift field tag
- if info.HeaderName == "" {
- return nil
- }
-
- //decode header value into field depending on type
- switch fieldPtr := fieldPtr.(type) {
- case *string:
- *fieldPtr = hdr.Get(info.HeaderName)
- case *uint64:
- value, err := strconv.ParseUint(hdr.Get(info.HeaderName), 10, 64)
- if err != nil {
- return MalformedHeaderError{info.HeaderName, err}
- }
- *fieldPtr = value
- case *Metadata:
- //collect all headers with a prefix equal to `headerName`
- values := make(Metadata)
- for key, value := range hdr {
- key = textproto.CanonicalMIMEHeaderKey(key)
- if strings.HasPrefix(key, info.HeaderName) {
- key = strings.TrimPrefix(key, info.HeaderName)
- values[key] = value[0]
- }
- }
- *fieldPtr = values
- default:
- panic(fmt.Sprintf("parseHeaders: cannot handle field type %T", fieldPtr))
- }
+func initializeByReflection(value interface{}) {
+ rv := reflect.ValueOf(value).Elem()
+ hdrs := rv.FieldByName("Headers").Interface().(headers.Headers)
+ foreachTaggedField(value, func(fieldPtr interface{}, info fieldInfo) error {
+ base := reflect.ValueOf(fieldPtr).Elem().FieldByName("Base").Addr().Interface().(*headers.Base)
+ base.H = hdrs
+ base.K = info.HeaderName
return nil
})
}
-func compileHeaders(headers interface{}, opts *RequestOptions) RequestOptions {
- hdr := make(http.Header)
-
- foreachField(headers, func(fieldPtr interface{}, info fieldInfo) error {
- //skip over fields without schwift field tag, and readonly fields
- if info.HeaderName == "" || info.Access != "rw" {
- return nil
- }
+type validator interface {
+ Validate() error
+}
- //decode header value into field depending on type
- switch fieldPtr := fieldPtr.(type) {
- case *string:
- hdr.Set(info.HeaderName, *fieldPtr)
- case *uint64:
- hdr.Set(info.HeaderName, strconv.FormatUint(*fieldPtr, 10))
- case *Metadata:
- for key, value := range *fieldPtr {
- //empty string means that this key shall be removed
- if value == "" {
- //for object metadata, a key is removed by just omitting it...
- if info.HeaderName != "X-Object-Meta-" {
- //...for container and account metadata, a key is removed by
- //setting its value to the empty string
- hdr.Set(info.HeaderName+key, "")
- }
- } else {
- //NOTE: The spec says that `value` needs to be percent-encoded, but
- //neither python-swiftclient nor ncw/swift do so. If in doubt, we
- //follow the de-facto standards rather than the spec.
- hdr.Set(info.HeaderName+key, value)
- }
+func validateByReflection(value interface{}) error {
+ return foreachTaggedField(value, func(fieldPtr interface{}, info fieldInfo) error {
+ if validator, ok := fieldPtr.(validator); ok {
+ err := validator.Validate()
+ if err != nil {
+ return err
}
- default:
- panic(fmt.Sprintf("compileHeaders: cannot handle field type %T", fieldPtr))
}
return nil
})
-
- //contents of `opts` overrides contents of `headers`
- result := RequestOptions{Headers: hdr}
- if opts != nil {
- result.Values = opts.Values
- for k, v := range opts.Headers {
- result.Headers[k] = v
- }
- }
- return result
-}
-
-type fieldInfo struct {
- FieldName string
- Access string
- HeaderName string
}
-func foreachField(value interface{}, callback func(fieldPtr interface{}, info fieldInfo) error) error {
- rv := reflect.ValueOf(value)
- //unpack pointer type if necessary
- if rv.Type().Kind() == reflect.Ptr {
- rv = rv.Elem()
- }
+func foreachTaggedField(value interface{}, callback func(fieldPtr interface{}, info fieldInfo) error) error {
+ rv := reflect.ValueOf(value).Elem()
//iterate over the struct fields
for idx := 0; idx < rv.NumField(); idx++ {
fieldType := rv.Type().Field(idx)
- fieldPtr := rv.Field(idx).Addr().Interface()
-
- //decode schwift:"<access>,<header-name>" tag
- tagValues := strings.SplitN(fieldType.Tag.Get("schwift"), ",", 2)
- fieldInfo := fieldInfo{
- FieldName: fieldType.Name,
- }
- if len(tagValues) == 2 {
- fieldInfo.Access = tagValues[0]
- fieldInfo.HeaderName = tagValues[1]
- }
-
- err := callback(fieldPtr, fieldInfo)
- if err != nil {
- return err
+ headerName := fieldType.Tag.Get("schwift")
+
+ if headerName != "" {
+ fieldPtr := rv.Field(idx).Addr().Interface()
+ err := callback(fieldPtr, fieldInfo{
+ FieldName: fieldType.Name,
+ HeaderName: headerName,
+ })
+ if err != nil {
+ return err
+ }
}
}
diff --git a/headers/base.go b/headers/base.go
new file mode 100644
index 0000000..1357e82
--- /dev/null
+++ b/headers/base.go
@@ -0,0 +1,25 @@
+/******************************************************************************
+*
+* Copyright 2018 Stefan Majewsky <majewsky@gmx.net>
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*
+******************************************************************************/
+
+package headers
+
+//Base is an implementation detail.
+type Base struct {
+ H Headers
+ K string
+}
diff --git a/headers/errors.go b/headers/errors.go
new file mode 100644
index 0000000..6c1f5ab
--- /dev/null
+++ b/headers/errors.go
@@ -0,0 +1,31 @@
+/******************************************************************************
+*
+* Copyright 2018 Stefan Majewsky <majewsky@gmx.net>
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*
+******************************************************************************/
+
+package headers
+
+//MalformedHeaderError is generated when a response from Swift contains a
+//malformed header.
+type MalformedHeaderError struct {
+ Key string
+ ParseError error
+}
+
+//Error implements the builtin/error interface.
+func (e MalformedHeaderError) Error() string {
+ return "Bad header " + e.Key + ": " + e.ParseError.Error()
+}
diff --git a/headers/headers.go b/headers/headers.go
new file mode 100644
index 0000000..33127a8
--- /dev/null
+++ b/headers/headers.go
@@ -0,0 +1,91 @@
+/******************************************************************************
+*
+* Copyright 2018 Stefan Majewsky <majewsky@gmx.net>
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*
+******************************************************************************/
+
+//Package headers contains helper types for the type-safe representation of
+//headers on Swift accounts/containers/objects.
+package headers
+
+import (
+ "net/http"
+ "net/textproto"
+)
+
+//Headers works like http.Header, but does not allow multiple values per key.
+//
+//If you write the map directly, without using the provided methods, you must
+//normalize all keys with textproto.CanonicalMIMEHeaderKey(). Otherwise, the
+//results are undefined.
+type Headers map[string]string
+
+//Clear sets the value for the specified header to the empty string. When the
+//Headers instance is then sent to the server with Update(), the server will
+//delete the value for that header; cf. Del().
+func (h Headers) Clear(key string) {
+ h.Set(key, "")
+}
+
+//Del deletes a key from the Headers instance. When the Headers instance
+//is then sent to the server with Update(), Del() has different effects
+//depending on context because of Swift's inconsistent API:
+//
+//For most writable attributes, a key which has been deleted with Del() will
+//remain unchanged on the server. To remove the key on the server, use Clear()
+//instead.
+//
+//For object metadata (but not other object attributes), deleting a key will
+//cause that key to be deleted on the server. Del() is identical to Clear() in
+//this case.
+func (h Headers) Del(key string) {
+ k := textproto.CanonicalMIMEHeaderKey(key)
+ delete(h, k)
+}
+
+//Get returns the value for the specified header.
+func (h Headers) Get(key string) string {
+ if h == nil {
+ return ""
+ }
+ k := textproto.CanonicalMIMEHeaderKey(key)
+ return h[k]
+}
+
+//Set sets a new value for the specified header, possibly overwriting a
+//previous value.
+func (h Headers) Set(key, value string) {
+ k := textproto.CanonicalMIMEHeaderKey(key)
+ h[k] = value
+}
+
+//ToHTTP converts this map into a http.Header.
+func (h Headers) ToHTTP() http.Header {
+ dest := make(http.Header, len(h))
+ for k, v := range h {
+ dest.Set(k, v)
+ }
+ return dest
+}
+
+//FromHTTP populates this map with the headers in the given http.Header. When a
+//header has multiple values, every value but the first one will be discarded.
+func (h Headers) FromHTTP(src http.Header) {
+ for k, v := range src {
+ if len(v) > 0 {
+ h.Set(k, v[0])
+ }
+ }
+}
diff --git a/metadata_test.go b/headers/headers_test.go
index 8977747..9724435 100644
--- a/metadata_test.go
+++ b/headers/headers_test.go
@@ -16,55 +16,61 @@
*
******************************************************************************/
-package schwift
+package headers
import "testing"
-func TestMetadata(t *testing.T) {
- m := NewMetadata(
- "first", "value1",
- "second-thing", "value2",
- )
+func TestHeaders(t *testing.T) {
+ h := make(Headers)
+ h.Set("first", "value1")
+ h.Set("second-thing", "value2")
- expectMetadata(t, m, map[string]string{
+ expectHeaders(t, h, map[string]string{
"First": "value1",
"Second-Thing": "value2",
})
- expectString(t, m.Get("first"), "value1")
- expectString(t, m.Get("First"), "value1")
- expectString(t, m.Get("FIRST"), "value1")
+ expectString(t, h.Get("first"), "value1")
+ expectString(t, h.Get("First"), "value1")
+ expectString(t, h.Get("FIRST"), "value1")
- m.Set("first", "changed")
- m.Set("third", "")
+ h.Set("first", "changed")
+ h.Set("third", "")
- expectMetadata(t, m, map[string]string{
+ expectHeaders(t, h, map[string]string{
"First": "changed",
"Second-Thing": "value2",
"Third": "",
})
- m.Clear("second-thing")
- m.Clear("fourth-thing")
+ h.Clear("second-thing")
+ h.Clear("fourth-thing")
- expectMetadata(t, m, map[string]string{
+ expectHeaders(t, h, map[string]string{
"First": "changed",
"Second-Thing": "",
"Third": "",
"Fourth-Thing": "",
})
- m.Del("FIRST")
- m.Del("second-Thing")
+ h.Del("FIRST")
+ h.Del("second-Thing")
- expectMetadata(t, m, map[string]string{
+ expectHeaders(t, h, map[string]string{
"Third": "",
"Fourth-Thing": "",
})
}
-func expectMetadata(t *testing.T, actual Metadata, expected map[string]string) {
+func expectString(t *testing.T, actual string, expected string) {
+ t.Helper()
+ if actual != expected {
+ t.Errorf("expected value %q, got %q instead\n", expected, actual)
+ }
+}
+
+func expectHeaders(t *testing.T, actual Headers, expected map[string]string) {
t.Helper()
reported := make(map[string]bool)
diff --git a/headers/metadata.go b/headers/metadata.go
new file mode 100644
index 0000000..8f07e89
--- /dev/null
+++ b/headers/metadata.go
@@ -0,0 +1,52 @@
+/******************************************************************************
+*
+* Copyright 2018 Stefan Majewsky <majewsky@gmx.net>
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*
+******************************************************************************/
+
+package headers
+
+//Metadata is a helper type that provides safe access to the metadata headers
+//in a schwift.Headers instance. It cannot be directly constructed, but each
+//subtype of schwift.Headers has a field "Metadata" of this type. For example:
+//
+// var hdr ObjectHeaders
+// //the following two statements are equivalent
+// hdr.Set("X-Object-Meta-Access", "strictly confidential")
+// hdr.Metadata.Set("Access", "strictly confidential")
+// //because hdr.Metadata is a headers.Metadata instance
+type Metadata struct {
+ Base
+}
+
+//Clear works like Headers.Clear(), but prepends the metadata prefix to the key.
+func (m Metadata) Clear(key string) {
+ m.H.Clear(m.K + key)
+}
+
+//Del works like Headers.Del(), but prepends the metadata prefix to the key.
+func (m Metadata) Del(key string) {
+ m.H.Del(m.K + key)
+}
+
+//Get works like Headers.Get(), but prepends the metadata prefix to the key.
+func (m Metadata) Get(key string) string {
+ return m.H.Get(m.K + key)
+}
+
+//Set works like Headers.Set(), but prepends the metadata prefix to the key.
+func (m Metadata) Set(key, value string) {
+ m.H.Set(m.K+key, value)
+}
diff --git a/headers/string.go b/headers/string.go
new file mode 100644
index 0000000..9979aef
--- /dev/null
+++ b/headers/string.go
@@ -0,0 +1,60 @@
+/******************************************************************************
+*
+* Copyright 2018 Stefan Majewsky <majewsky@gmx.net>
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*
+******************************************************************************/
+
+package headers
+
+//String is a helper type that provides type-safe access to a Swift header key
+//whose value is a string. It cannot be directly constructed, but some subtypes
+//of schwift.Headers have fields of this type. For example:
+//
+// var hdr AccountHeaders
+// //the following two statements are equivalent:
+// hdr.Set("X-Container-Read", ".r:*,.rlistings")
+// hdr.ReadACL.Set(".r:*,.rlistings")
+// //because hdr.ReadACL is a headers.String instance
+type String struct {
+ Base
+}
+
+//Exists checks whether there is a value for this header.
+func (f String) Exists() bool {
+ return f.H.Get(f.K) != ""
+}
+
+//Get returns the value for this header, or the empty string if there is no value.
+func (f String) Get() string {
+ return f.H.Get(f.K)
+}
+
+//Set writes a new value for this header into the corresponding schwift.Headers
+//instance.
+func (f String) Set(value string) {
+ f.H.Set(f.K, value)
+}
+
+//Del removes this key from the original schwift.Headers instance, so that the
+//key will remain unchanged on the server during Update().
+func (f String) Del() {
+ f.H.Del(f.K)
+}
+
+//Clear sets this key to an empty string in the original schwift.Headers
+//instance, so that the key will be removed on the server during Update().
+func (f String) Clear() {
+ f.H.Clear(f.K)
+}
diff --git a/headers/uint64.go b/headers/uint64.go
new file mode 100644
index 0000000..6e8668b
--- /dev/null
+++ b/headers/uint64.go
@@ -0,0 +1,120 @@
+/******************************************************************************
+*
+* Copyright 2018 Stefan Majewsky <majewsky@gmx.net>
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*
+******************************************************************************/
+
+package headers
+
+import (
+ "strconv"
+)
+
+//Uint64 is a helper type that provides type-safe access to a Swift header
+//whose value is an unsigned integer. It cannot be directly constructed, but
+//some subtypes of schwift.Headers have fields of this type. For example:
+//
+// var hdr AccountHeaders
+// //the following two statements are equivalent:
+// hdr.Set("X-Account-Meta-Quota-Bytes", "1048576")
+// hdr.QuotaBytes.Set(1 << 20)
+// //because hdr.QuotaBytes is a headers.Uint64 instance
+type Uint64 struct {
+ Base
+}
+
+//Exists checks whether there is a value for this header.
+func (f Uint64) Exists() bool {
+ return f.H.Get(f.K) != ""
+}
+
+//Get returns the value for this header, or 0 if there is no value (or if it is
+//not a valid uint64).
+func (f Uint64) Get() uint64 {
+ v, err := strconv.ParseUint(f.H.Get(f.K), 10, 64)
+ if err != nil {
+ return 0
+ }
+ return v
+}
+
+//Set writes a new value for this header into the corresponding schwift.Headers
+//instance.
+func (f Uint64) Set(value uint64) {
+ f.H.Set(f.K, strconv.FormatUint(value, 10))
+}
+
+//Del removes this key from the original schwift.Headers instance, so that the
+//key will remain unchanged on the server during Update().
+func (f Uint64) Del() {
+ f.H.Del(f.K)
+}
+
+//Clear sets this key to an empty string in the original schwift.Headers
+//instance, so that the key will be removed on the server during Update().
+func (f Uint64) Clear() {
+ f.H.Clear(f.K)
+}
+
+//Validate is only used internally, but needs to be exported to cross package
+//boundaries.
+func (f Uint64) Validate() error {
+ val := f.H.Get(f.K)
+ if val == "" {
+ return nil
+ }
+ _, err := strconv.ParseUint(val, 10, 64)
+ if err == nil {
+ return nil
+ }
+ return MalformedHeaderError{f.K, err}
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+//Uint64Readonly is a readonly variant of Uint64. It is used for fields that
+//cannot be set by the client.
+type Uint64Readonly struct {
+ Base
+}
+
+//Exists checks whether there is a value for this header.
+func (f Uint64Readonly) Exists() bool {
+ return f.H.Get(f.K) != ""
+}
+
+//Get returns the value for this header, or 0 if there is no value (or if it is
+//not a valid uint64).
+func (f Uint64Readonly) Get() uint64 {
+ v, err := strconv.ParseUint(f.H.Get(f.K), 10, 64)
+ if err != nil {
+ return 0
+ }
+ return v
+}
+
+//Validate is only used internally, but needs to be exported to cross package
+//boundaries.
+func (f Uint64Readonly) Validate() error {
+ val := f.H.Get(f.K)
+ if val == "" {
+ return nil
+ }
+ _, err := strconv.ParseUint(val, 10, 64)
+ if err == nil {
+ return nil
+ }
+ return MalformedHeaderError{f.K, err}
+}
diff --git a/headers_test.go b/headers_test.go
index b54181c..66a2c36 100644
--- a/headers_test.go
+++ b/headers_test.go
@@ -24,23 +24,20 @@ import (
)
func TestParseAccountHeadersSuccess(t *testing.T) {
- var headers AccountHeaders
- err := parseHeaders(http.Header{
+ headers := NewAccountHeaders()
+ headers.FromHTTP(http.Header{
"X-Account-Bytes-Used": {"1234"},
"X-Account-Object-Count": {"42"},
"X-Account-Container-Count": {"23"},
"X-Account-Meta-Quota-Bytes": {"1048576"},
"X-Account-Meta-foo": {"bar"},
- }, &headers)
+ })
- expectError(t, err, "")
- expectUint64(t, headers.BytesUsed, 1234)
- expectUint64(t, headers.ContainerCount, 23)
- expectUint64(t, headers.ObjectCount, 42)
-
- value, err := headers.QuotaBytes().Get()
- expectError(t, err, "")
- expectUint64(t, value, 1048576)
+ expectError(t, headers.Validate(), "")
+ expectUint64(t, headers.BytesUsed.Get(), 1234)
+ expectUint64(t, headers.ContainerCount.Get(), 23)
+ expectUint64(t, headers.ObjectCount.Get(), 42)
+ expectUint64(t, headers.QuotaBytes.Get(), 1048576)
expectString(t, headers.Metadata.Get("foo"), "bar")
expectString(t, headers.Metadata.Get("Foo"), "bar")
diff --git a/metadata.go b/metadata.go
deleted file mode 100644
index 5f5f63f..0000000
--- a/metadata.go
+++ /dev/null
@@ -1,89 +0,0 @@
-/******************************************************************************
-*
-* Copyright 2018 Stefan Majewsky <majewsky@gmx.net>
-*
-* Licensed under the Apache License, Version 2.0 (the "License");
-* you may not use this file except in compliance with the License.
-* You may obtain a copy of the License at
-*
-* http://www.apache.org/licenses/LICENSE-2.0
-*
-* Unless required by applicable law or agreed to in writing, software
-* distributed under the License is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-* See the License for the specific language governing permissions and
-* limitations under the License.
-*
-******************************************************************************/
-
-package schwift
-
-import "net/textproto"
-
-//Metadata works like http.Header, but does not allow multiple values per key.
-type Metadata map[string]string
-
-//NewMetadata constructs a Metadata instance from a list of key-value pairs
-//with compact syntax. It is recommended over a map literal since it correctly
-//formats keys with textproto.CanonicalMIMEHeaderKey(). For example:
-//
-// m = NewMetadata(
-// "color", "blue",
-// "size", "large",
-// )
-//
-// //...is equivalent to...
-//
-// m = make(Metadata)
-// m.Set("color", "blue")
-// m.Set("size", "large")
-//
-//NewMetadata panics if it is called with an odd number of arguments.
-func NewMetadata(args ...string) Metadata {
- if len(args)%2 == 1 {
- panic("NewMetadata called with an odd number of arguments")
- }
- m := make(Metadata)
- for idx := 0; idx < len(args); idx += 2 {
- m.Set(args[idx], args[idx+1])
- }
- return m
-}
-
-//Clear sets the value to this key to the empty string, such that a Post() with
-//this Metadata will remove the existing value from this metadata key on the server.
-func (m Metadata) Clear(key string) {
- m.Set(key, "")
-}
-
-//Del works just like http.Header.Del().
-//
-//Del deletes a key from the Metadata instance. When the Metadata instance
-//is then sent to the server with Post(), Del() has different effects depending
-//on context because of Swift's inconsistent API:
-//
-//For account or container metadata, a key which has been deleted with Del() will
-//remain unchanged on the server. To remove the key on the server, use Clear()
-//instead.
-//
-//For object metadata, deleting a key will cause that key to be deleted on the
-//server. Del() is identical to Clear() in this case.
-func (m Metadata) Del(key string) {
- k := textproto.CanonicalMIMEHeaderKey(key)
- delete(m, k)
-}
-
-//Get works just like http.Header.Get().
-func (m Metadata) Get(key string) string {
- if m == nil {
- return ""
- }
- k := textproto.CanonicalMIMEHeaderKey(key)
- return m[k]
-}
-
-//Set works just like http.Header.Set().
-func (m Metadata) Set(key, value string) {
- k := textproto.CanonicalMIMEHeaderKey(key)
- m[k] = value
-}
diff --git a/request.go b/request.go
index 79a0296..0106d38 100644
--- a/request.go
+++ b/request.go
@@ -47,7 +47,8 @@ type Request struct {
Method string //"GET", "HEAD", "PUT", "POST" or "DELETE"
ContainerName string //empty for requests on accounts
ObjectName string //empty for requests on accounts/containers
- Options RequestOptions
+ Headers http.Header
+ Options *RequestOptions
Body io.Reader
//ExpectStatusCodes can be left empty to disable this check, otherwise
//schwift.UnexpectedStatusCodeError may be returned.
@@ -56,8 +57,7 @@ type Request struct {
//RequestOptions contains additional headers and values for request.
type RequestOptions struct {
- Headers http.Header
- Values url.Values
+ Values url.Values
}
//URL returns the full URL for this request.
@@ -94,7 +94,11 @@ func (r Request) do(client *gophercloud.ServiceClient, afterReauth bool) (*http.
provider := client.ProviderClient
//build URL
- uri, err := r.URL(client, r.Options.Values)
+ var values url.Values
+ if r.Options != nil {
+ values = r.Options.Values
+ }
+ uri, err := r.URL(client, values)
if err != nil {
return nil, err
}
@@ -105,10 +109,10 @@ func (r Request) do(client *gophercloud.ServiceClient, afterReauth bool) (*http.
return nil, err
}
- req.Header.Set("User-Agent", provider.UserAgent.Join())
- for key, values := range r.Options.Headers {
- req.Header[key] = values
+ for k, v := range r.Headers {
+ req.Header[k] = v
}
+ req.Header.Set("User-Agent", provider.UserAgent.Join())
for key, value := range provider.AuthenticatedHeaders() {
req.Header.Set(key, value)
}