diff options
| -rw-r--r-- | account.go | 23 | ||||
| -rw-r--r-- | account_test.go | 32 | ||||
| -rw-r--r-- | container.go | 28 | ||||
| -rw-r--r-- | container_test.go | 2 | ||||
| -rw-r--r-- | errors.go | 12 | ||||
| -rw-r--r-- | headers.go | 322 | ||||
| -rw-r--r-- | headers/base.go | 25 | ||||
| -rw-r--r-- | headers/errors.go | 31 | ||||
| -rw-r--r-- | headers/headers.go | 91 | ||||
| -rw-r--r-- | headers/headers_test.go (renamed from metadata_test.go) | 46 | ||||
| -rw-r--r-- | headers/metadata.go | 52 | ||||
| -rw-r--r-- | headers/string.go | 60 | ||||
| -rw-r--r-- | headers/uint64.go | 120 | ||||
| -rw-r--r-- | headers_test.go | 19 | ||||
| -rw-r--r-- | metadata.go | 89 | ||||
| -rw-r--r-- | request.go | 18 |
16 files changed, 558 insertions, 412 deletions
@@ -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() @@ -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() -} @@ -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 -} @@ -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) } |
