pax_global_header00006660000000000000000000000064136545261660014530gustar00rootroot0000000000000052 comment=895b231a883c043bfff93926c5228930ea253c8e httprequest-1.2.1/000077500000000000000000000000001365452616600141215ustar00rootroot00000000000000httprequest-1.2.1/.travis.yml000066400000000000000000000001561365452616600162340ustar00rootroot00000000000000language: go go_import_path: "gopkg.in/httprequest.v1" go: - "1.11.x" script: GO111MODULE=on go test ./... httprequest-1.2.1/LICENSE000066400000000000000000000210531365452616600151270ustar00rootroot00000000000000This software is licensed under the LGPLv3, included below. As a special exception to the GNU Lesser General Public License version 3 ("LGPL3"), the copyright holders of this Library give you permission to convey to a third party a Combined Work that links statically or dynamically to this Library without providing any Minimal Corresponding Source or Minimal Application Code as set out in 4d or providing the installation information set out in section 4e, provided that you comply with the other provisions of LGPL3 and provided that you meet, for the Application the terms and conditions of the license(s) which apply to the Application. Except as stated in this special exception, the provisions of LGPL3 will continue to comply in full to this Library. If you modify this Library, you may apply this exception to your version of this Library, but you are not obliged to do so. If you do not wish to do so, delete this exception statement from your version. This exception does not (and cannot) modify any license terms which apply to the Application, with which you must still comply. GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. httprequest-1.2.1/README.md000066400000000000000000000524171365452616600154110ustar00rootroot00000000000000# httprequest -- import "gopkg.in/httprequest.v1" Package httprequest provides functionality for marshaling unmarshaling HTTP request parameters into a struct type. It also provides a way to define methods as HTTP routes using the same approach. It requires at least Go 1.7, and Go 1.9 is required if the importing program also uses golang.org/x/net/context. ## Usage ```go const ( CodeBadRequest = "bad request" CodeUnauthorized = "unauthorized" CodeForbidden = "forbidden" CodeNotFound = "not found" ) ``` These constants are recognized by DefaultErrorMapper as mapping to the similarly named HTTP status codes. ```go var ( ErrUnmarshal = errgo.New("httprequest unmarshal error") ErrBadUnmarshalType = errgo.New("httprequest bad unmarshal type") ) ``` ```go var DefaultErrorMapper = defaultErrorMapper ``` DefaultErrorMapper is used by Server when ErrorMapper is nil. It maps all errors to RemoteError instances; if an error implements the ErrorCoder interface, the Code field will be set accordingly; some codes will map to specific HTTP status codes (for example, if ErrorCode returns CodeBadRequest, the resulting HTTP status will be http.StatusBadRequest). ```go var DefaultErrorUnmarshaler = ErrorUnmarshaler(new(RemoteError)) ``` DefaultErrorUnmarshaler is the default error unmarshaler used by Client. #### func AddHandlers ```go func AddHandlers(r *httprouter.Router, hs []Handler) ``` AddHandlers adds all the handlers in the given slice to r. #### func ErrorUnmarshaler ```go func ErrorUnmarshaler(template error) func(*http.Response) error ``` ErrorUnmarshaler returns a function which will unmarshal error responses into new values of the same type as template. The argument must be a pointer. A new instance of it is created every time the returned function is called. If the error cannot by unmarshaled, the function will return an *HTTPResponseError holding the response from the request. #### func Marshal ```go func Marshal(baseURL, method string, x interface{}) (*http.Request, error) ``` Marshal is the counterpart of Unmarshal. It takes information from x, which must be a pointer to a struct, and returns an HTTP request using the given method that holds all of the information. The Body field in the returned request will always be of type BytesReaderCloser. If x implements the HeaderSetter interface, its SetHeader method will be called to add additional headers to the HTTP request after it has been marshaled. If x is pointer to a CustomHeader object then Marshal will use its Body member to create the HTTP request. The HTTP request will use the given method. Named fields in the given baseURL will be filled out from "path"-tagged fields in x to form the URL path in the returned request. These are specified as for httprouter. If a field in baseURL is a suffix of the form "*var" (a trailing wildcard element that holds the rest of the path), the marshaled string must begin with a "/". This matches the httprouter convention that it always returns such fields with a "/" prefix. If a field is of type string or []string, the value of the field will be used directly; otherwise if implements encoding.TextMarshaler, that will be used to marshal the field, otherwise fmt.Sprint will be used. An "omitempty" attribute on a form or header field specifies that if the form or header value is zero, the form or header entry will be omitted. If the field is a nil pointer, it will be omitted; otherwise if the field type implements IsZeroer, that method will be used to determine whether the value is zero, otherwise if the value is comparable, it will be compared with the zero value for its type, otherwise the value will never be omitted. One notable implementation of IsZeroer is time.Time. An "inbody" attribute on a form field specifies that the field will be marshaled as part of an application/x-www-form-urlencoded body. Note that the field may still be unmarshaled from either a URL query parameter or a form-encoded body. For example, this code: type UserDetails struct { Age int } type Test struct { Username string `httprequest:"user,path"` ContextId int64 `httprequest:"context,form"` Extra string `httprequest:"context,form,omitempty"` Details UserDetails `httprequest:",body"` } req, err := Marshal("http://example.com/users/:user/details", "GET", &Test{ Username: "bob", ContextId: 1234, Details: UserDetails{ Age: 36, } }) if err != nil { ... } will produce an HTTP request req with a URL of http://example.com/users/bob/details?context=1234 and a JSON-encoded body holding `{"Age":36}`. It is an error if there is a field specified in the URL that is not found in x. #### func ToHTTP ```go func ToHTTP(h httprouter.Handle) http.Handler ``` ToHTTP converts an httprouter.Handle into an http.Handler. It will pass no path variables to h. #### func Unmarshal ```go func Unmarshal(p Params, x interface{}) error ``` Unmarshal takes values from given parameters and fills out fields in x, which must be a pointer to a struct. Tags on the struct's fields determine where each field is filled in from. Similar to encoding/json and other encoding packages, the tag holds a comma-separated list. The first item in the list is an alternative name for the field (the field name itself will be used if this is empty). The next item specifies where the field is filled in from. It may be: "path" - the field is taken from a parameter in p.PathVar with a matching field name. "form" - the field is taken from the given name in p.Request.Form (note that this covers both URL query parameters and POST form parameters). "header" - the field is taken from the given name in p.Request.Header. "body" - the field is filled in by parsing the request body as JSON. For path and form parameters, the field will be filled out from the field in p.PathVar or p.Form using one of the following methods (in descending order of preference): - if the type is string, it will be set from the first value. - if the type is []string, it will be filled out using all values for that field (allowed only for form) - if the type implements encoding.TextUnmarshaler, its UnmarshalText method will be used - otherwise fmt.Sscan will be used to set the value. When the unmarshaling fails, Unmarshal returns an error with an ErrUnmarshal cause. If the type of x is inappropriate, it returns an error with an ErrBadUnmarshalType cause. #### func UnmarshalJSONResponse ```go func UnmarshalJSONResponse(resp *http.Response, x interface{}) error ``` UnmarshalJSONResponse unmarshals the given HTTP response into x, which should be a pointer to the result to be unmarshaled into. If the response cannot be unmarshaled, an error of type *DecodeResponseError will be returned. #### func WriteJSON ```go func WriteJSON(w http.ResponseWriter, code int, val interface{}) error ``` WriteJSON writes the given value to the ResponseWriter and sets the HTTP status to the given code. If val implements the HeaderSetter interface, the SetHeader method will be called to add additional headers to the HTTP response. It is called after the Content-Type header has been added, so can be used to override the content type if required. #### type BytesReaderCloser ```go type BytesReaderCloser struct { *bytes.Reader } ``` BytesReaderCloser is a bytes.Reader which implements io.Closer with a no-op Close method. #### func (BytesReaderCloser) Close ```go func (BytesReaderCloser) Close() error ``` Close implements io.Closer.Close. #### type Client ```go type Client struct { // BaseURL holds the base URL to use when making // HTTP requests. BaseURL string // Doer holds a value that will be used to actually // make the HTTP request. If it is nil, http.DefaultClient // will be used instead. If Doer implements DoerWithContext, // DoWithContext will be used instead. Doer Doer // If a request returns an HTTP response that signifies an // error, UnmarshalError is used to unmarshal the response into // an appropriate error. See ErrorUnmarshaler for a convenient // way to create an UnmarshalError function for a given type. If // this is nil, DefaultErrorUnmarshaler will be used. UnmarshalError func(resp *http.Response) error } ``` Client represents a client that can invoke httprequest endpoints. #### func (*Client) Call ```go func (c *Client) Call(ctx context.Context, params, resp interface{}) error ``` Call invokes the endpoint implied by the given params, which should be of the form accepted by the ArgT argument to a function passed to Handle, and unmarshals the response into the given response parameter, which should be a pointer to the response value. If params implements the HeaderSetter interface, its SetHeader method will be called to add additional headers to the HTTP request. If resp is nil, the response will be ignored if the request was successful. If resp is of type **http.Response, instead of unmarshaling into it, its element will be set to the returned HTTP response directly and the caller is responsible for closing its Body field. Any error that c.UnmarshalError or c.Doer returns will not have its cause masked. If the request returns a response with a status code signifying success, but the response could not be unmarshaled, a *DecodeResponseError will be returned holding the response. Note that if the request returns an error status code, the Client.UnmarshalError function is responsible for doing this if desired (the default error unmarshal functions do). #### func (*Client) CallURL ```go func (c *Client) CallURL(ctx context.Context, url string, params, resp interface{}) error ``` CallURL is like Call except that the given URL is used instead of c.BaseURL. #### func (*Client) Do ```go func (c *Client) Do(ctx context.Context, req *http.Request, resp interface{}) error ``` Do sends the given request and unmarshals its JSON result into resp, which should be a pointer to the response value. If an error status is returned, the error will be unmarshaled as in Client.Call. If resp is nil, the response will be ignored if the response was successful. If resp is of type **http.Response, instead of unmarshaling into it, its element will be set to the returned HTTP response directly and the caller is responsible for closing its Body field. Any error that c.UnmarshalError or c.Doer returns will not have its cause masked. If req.URL does not have a host part it will be treated as relative to c.BaseURL. req.URL will be updated to the actual URL used. If the response cannot by unmarshaled, a *DecodeResponseError will be returned holding the response from the request. the entire response body. #### func (*Client) Get ```go func (c *Client) Get(ctx context.Context, url string, resp interface{}) error ``` Get is a convenience method that uses c.Do to issue a GET request to the given URL. If the given URL does not have a host part then it will be treated as relative to c.BaseURL. #### type CustomHeader ```go type CustomHeader struct { // Body holds the JSON-marshaled body of the response. Body interface{} // SetHeaderFunc holds a function that will be called // to set any custom headers on the response. SetHeaderFunc func(http.Header) } ``` CustomHeader is a type that allows a JSON value to set custom HTTP headers associated with the HTTP response. #### func (CustomHeader) MarshalJSON ```go func (h CustomHeader) MarshalJSON() ([]byte, error) ``` MarshalJSON implements json.Marshaler by marshaling h.Body. #### func (CustomHeader) SetHeader ```go func (h CustomHeader) SetHeader(header http.Header) ``` SetHeader implements HeaderSetter by calling h.SetHeaderFunc. #### type DecodeRequestError ```go type DecodeRequestError struct { // Request holds the problematic HTTP request. // The body of this does not need to be closed // and may be truncated if the response is large. Request *http.Request // DecodeError holds the error that was encountered // when decoding. DecodeError error } ``` DecodeRequestError represents an error when an HTTP request could not be decoded. #### func (*DecodeRequestError) Error ```go func (e *DecodeRequestError) Error() string ``` #### type DecodeResponseError ```go type DecodeResponseError struct { // Response holds the problematic HTTP response. // The body of this does not need to be closed // and may be truncated if the response is large. Response *http.Response // DecodeError holds the error that was encountered // when decoding. DecodeError error } ``` DecodeResponseError represents an error when an HTTP response could not be decoded. #### func (*DecodeResponseError) Error ```go func (e *DecodeResponseError) Error() string ``` #### type Doer ```go type Doer interface { Do(req *http.Request) (*http.Response, error) } ``` Doer is implemented by HTTP client packages to make an HTTP request. It is notably implemented by http.Client and httpbakery.Client. #### type DoerWithContext ```go type DoerWithContext interface { DoWithContext(ctx context.Context, req *http.Request) (*http.Response, error) } ``` DoerWithContext is implemented by HTTP clients that can use a context with the HTTP request. #### type ErrorCoder ```go type ErrorCoder interface { ErrorCode() string } ``` ErrorCoder may be implemented by an error to cause it to return a particular RemoteError code when DefaultErrorMapper is used. #### type ErrorHandler ```go type ErrorHandler func(Params) error ``` ErrorHandler is like httprouter.Handle except it returns an error which may be returned as the error body of the response. An ErrorHandler function should not itself write to the ResponseWriter if it returns an error. #### type Handler ```go type Handler struct { Method string Path string Handle httprouter.Handle } ``` Handler defines a HTTP handler that will handle the given HTTP method at the given httprouter path #### type HeaderSetter ```go type HeaderSetter interface { SetHeader(http.Header) } ``` HeaderSetter is the interface checked for by WriteJSON. If implemented on a value passed to WriteJSON, the SetHeader method will be called to allow it to set custom headers on the response. #### type IsZeroer ```go type IsZeroer interface { IsZero() bool } ``` IsZeroer is used when marshaling to determine if a value is zero (see Marshal). #### type JSONHandler ```go type JSONHandler func(Params) (interface{}, error) ``` JSONHandler is like httprouter.Handle except that it returns a body (to be converted to JSON) and an error. The Header parameter can be used to set custom headers on the response. #### type Params ```go type Params struct { Response http.ResponseWriter Request *http.Request PathVar httprouter.Params // PathPattern holds the path pattern matched by httprouter. // It is only set where httprequest has the information; // that is where the call was made by Server.Handler // or Server.Handlers. PathPattern string // Context holds a context for the request. In Go 1.7 and later, // this should be used in preference to Request.Context. Context context.Context } ``` Params holds the parameters provided to an HTTP request. #### type RemoteError ```go type RemoteError struct { // Message holds the error message. Message string // Code may hold a code that classifies the error. Code string `json:",omitempty"` // Info holds any other information associated with the error. Info *json.RawMessage `json:",omitempty"` } ``` RemoteError holds the default type of a remote error used by Client when no custom error unmarshaler is set. This type is also used by DefaultErrorMapper to marshal errors in Server. #### func Errorf ```go func Errorf(code string, f string, a ...interface{}) *RemoteError ``` Errorf returns a new RemoteError instance that uses the given code and formats the message with fmt.Sprintf(f, a...). If f is empty and there are no other arguments, code will also be used for the message. #### func (*RemoteError) Error ```go func (e *RemoteError) Error() string ``` Error implements the error interface. #### func (*RemoteError) ErrorCode ```go func (e *RemoteError) ErrorCode() string ``` ErrorCode implements ErrorCoder by returning e.Code. #### type Route ```go type Route struct{} ``` Route is the type of a field that specifies a routing path and HTTP method. See Marshal and Unmarshal for details. #### type Server ```go type Server struct { // ErrorMapper holds a function that can convert a Go error // into a form that can be returned as a JSON body from an HTTP request. // // The httpStatus value reports the desired HTTP status. // // If the returned errorBody implements HeaderSetter, then // that method will be called to add custom headers to the request. // // If this both this and ErrorWriter are nil, DefaultErrorMapper will be used. ErrorMapper func(ctxt context.Context, err error) (httpStatus int, errorBody interface{}) // ErrorWriter is a more general form of ErrorMapper. If this // field is set, ErrorMapper will be ignored and any returned // errors will be passed to ErrorWriter, which should use // w to set the HTTP status and write an appropriate // error response. ErrorWriter func(ctx context.Context, w http.ResponseWriter, err error) } ``` Server represents the server side of an HTTP servers, and can be used to create HTTP handlers although it is not an HTTP handler itself. #### func (*Server) Handle ```go func (srv *Server) Handle(f interface{}) Handler ``` Handle converts a function into a Handler. The argument f must be a function of one of the following six forms, where ArgT must be a struct type acceptable to Unmarshal and ResultT is a type that can be marshaled as JSON: func(p Params, arg *ArgT) func(p Params, arg *ArgT) error func(p Params, arg *ArgT) (ResultT, error) func(arg *ArgT) func(arg *ArgT) error func(arg *ArgT) (ResultT, error) When processing a call to the returned handler, the provided parameters are unmarshaled into a new ArgT value using Unmarshal, then f is called with this value. If the unmarshaling fails, f will not be called and the unmarshal error will be written as a JSON response. As an additional special case to the rules defined in Unmarshal, the tag on an anonymous field of type Route specifies the method and path to use in the HTTP request. It should hold two space-separated fields; the first specifies the HTTP method, the second the URL path to use for the request. If this is given, the returned handler will hold that method and path, otherwise they will be empty. If an error is returned from f, it is passed through the error mapper before writing as a JSON response. In the third form, when no error is returned, the result is written as a JSON response with status http.StatusOK. Also in this case, any calls to Params.Response.Write or Params.Response.WriteHeader will be ignored, as the response code and data should be defined entirely by the returned result and error. Handle will panic if the provided function is not in one of the above forms. #### func (*Server) HandleErrors ```go func (srv *Server) HandleErrors(handle ErrorHandler) httprouter.Handle ``` HandleErrors returns a handler that passes any non-nil error returned by handle through the error mapper and writes it as a JSON response. Note that the Params argument passed to handle will not have its PathPattern set as that information is not available. #### func (*Server) HandleJSON ```go func (srv *Server) HandleJSON(handle JSONHandler) httprouter.Handle ``` HandleJSON returns a handler that writes the return value of handle as a JSON response. If handle returns an error, it is passed through the error mapper. Note that the Params argument passed to handle will not have its PathPattern set as that information is not available. #### func (*Server) Handlers ```go func (srv *Server) Handlers(f interface{}) []Handler ``` Handlers returns a list of handlers that will be handled by the value returned by the given argument, which must be a function in one of the following forms: func(p httprequest.Params) (T, context.Context, error) func(p httprequest.Params, handlerArg I) (T, context.Context, error) for some type T and some interface type I. Each exported method defined on T defines a handler, and should be in one of the forms accepted by Server.Handle with the additional constraint that the argument to each of the handlers must be compatible with the type I when the second form is used above. The returned context will be used as the value of Params.Context when Params is passed to any method. It will also be used when writing an error if the function returns an error. Handlers will panic if f is not of the required form, no methods are defined on T or any method defined on T is not suitable for Handle. When any of the returned handlers is invoked, f will be called and then the appropriate method will be called on the value it returns. If specified, the handlerArg parameter to f will hold the ArgT argument that will be passed to the handler method. If T implements io.Closer, its Close method will be called after the request is completed. #### func (*Server) WriteError ```go func (srv *Server) WriteError(ctx context.Context, w http.ResponseWriter, err error) ``` WriteError writes an error to a ResponseWriter and sets the HTTP status code, using srv.ErrorMapper to determine the actually written response. It uses WriteJSON to write the error body returned from the ErrorMapper so it is possible to add custom headers to the HTTP error response by implementing HeaderSetter. httprequest-1.2.1/bench_test.go000066400000000000000000000302271365452616600165720ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package httprequest_test import ( "fmt" "net/http" "net/http/httptest" "net/url" "reflect" "strconv" "testing" "time" "github.com/julienschmidt/httprouter" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" ) const dateFormat = "2006-01-02" type testResult struct { Key string `json:",omitempty"` Date string `json:",omitempty"` Count int64 } type testParams2Fields struct { Id string `httprequest:"id,path"` Limit int `httprequest:"limit,form"` } type testParams4Fields struct { Id string `httprequest:"id,path"` Limit int `httprequest:"limit,form"` From dateTime `httprequest:"from,form"` To dateTime `httprequest:"to,form"` } type dateTime struct { time.Time } func (dt *dateTime) UnmarshalText(b []byte) (err error) { dt.Time, err = time.Parse(dateFormat, string(b)) return } type testParams2StringFields struct { Field0 string `httprequest:",form"` Field1 string `httprequest:",form"` } type testParams4StringFields struct { Field0 string `httprequest:",form"` Field1 string `httprequest:",form"` Field2 string `httprequest:",form"` Field3 string `httprequest:",form"` } type testParams8StringFields struct { Field0 string `httprequest:",form"` Field1 string `httprequest:",form"` Field2 string `httprequest:",form"` Field3 string `httprequest:",form"` Field4 string `httprequest:",form"` Field5 string `httprequest:",form"` Field6 string `httprequest:",form"` Field7 string `httprequest:",form"` } type testParams16StringFields struct { Field0 string `httprequest:",form"` Field1 string `httprequest:",form"` Field2 string `httprequest:",form"` Field3 string `httprequest:",form"` Field4 string `httprequest:",form"` Field5 string `httprequest:",form"` Field6 string `httprequest:",form"` Field7 string `httprequest:",form"` Field8 string `httprequest:",form"` Field9 string `httprequest:",form"` Field10 string `httprequest:",form"` Field11 string `httprequest:",form"` Field12 string `httprequest:",form"` Field13 string `httprequest:",form"` Field14 string `httprequest:",form"` Field15 string `httprequest:",form"` } func BenchmarkUnmarshal2Fields(b *testing.B) { params := httprequest.Params{ Request: &http.Request{ Form: url.Values{ "limit": {"2000"}, }, }, PathVar: httprouter.Params{{ Key: "id", Value: "someid", }}, } var arg testParams2Fields b.ResetTimer() for i := 0; i < b.N; i++ { arg = testParams2Fields{} err := httprequest.Unmarshal(params, &arg) if err != nil { b.Fatalf("unmarshal failed: %v", err) } } b.StopTimer() if !reflect.DeepEqual(arg, testParams2Fields{ Id: "someid", Limit: 2000, }) { b.Errorf("unexpected result: got %#v", arg) } } func BenchmarkHandle2FieldsTrad(b *testing.B) { results := []testResult{} benchmarkHandle2Fields(b, testServer.HandleJSON(func(p httprequest.Params) (interface{}, error) { limit := -1 if limitStr := p.Request.Form.Get("limit"); limitStr != "" { var err error limit, err = strconv.Atoi(limitStr) if err != nil || limit <= 0 { panic("unreachable") } } if id := p.PathVar.ByName("id"); id == "" { panic("unreachable") } return results, nil })) } func BenchmarkHandle2Fields(b *testing.B) { results := []testResult{} benchmarkHandle2Fields(b, testServer.Handle(func(p httprequest.Params, arg *testParams2Fields) ([]testResult, error) { if arg.Limit <= 0 { panic("unreachable") } return results, nil }).Handle) } func BenchmarkHandle2FieldsUnmarshalOnly(b *testing.B) { results := []testResult{} benchmarkHandle2Fields(b, testServer.HandleJSON(func(p httprequest.Params) (interface{}, error) { var arg testParams2Fields if err := httprequest.Unmarshal(p, &arg); err != nil { return nil, err } if arg.Limit <= 0 { panic("unreachable") } return results, nil })) } func benchmarkHandle2Fields(b *testing.B, handle func(w http.ResponseWriter, req *http.Request, pvar httprouter.Params)) { rec := httptest.NewRecorder() params := httprequest.Params{ Request: &http.Request{ Form: url.Values{ "limit": {"2000"}, }, }, PathVar: httprouter.Params{{ Key: "id", Value: "someid", }}, } b.ResetTimer() for i := 0; i < b.N; i++ { rec.Body.Reset() handle(rec, params.Request, params.PathVar) } } func BenchmarkUnmarshal4Fields(b *testing.B) { fromDate, err1 := time.Parse(dateFormat, "2010-10-10") toDate, err2 := time.Parse(dateFormat, "2011-11-11") if err1 != nil || err2 != nil { b.Fatalf("bad times") } type P testParams4Fields params := httprequest.Params{ Request: &http.Request{ Form: url.Values{ "limit": {"2000"}, "from": {fromDate.Format(dateFormat)}, "to": {toDate.Format(dateFormat)}, }, }, PathVar: httprouter.Params{{ Key: "id", Value: "someid", }}, } var args P b.ResetTimer() for i := 0; i < b.N; i++ { args = P{} err := httprequest.Unmarshal(params, &args) if err != nil { b.Fatalf("unmarshal failed: %v", err) } } b.StopTimer() if !reflect.DeepEqual(args, P{ Id: "someid", Limit: 2000, From: dateTime{fromDate}, To: dateTime{toDate}, }) { b.Errorf("unexpected result: got %#v", args) } } func BenchmarkHandle4FieldsTrad(b *testing.B) { results := []testResult{} benchmarkHandle4Fields(b, testServer.HandleJSON(func(p httprequest.Params) (interface{}, error) { start, stop, err := parseDateRange(p.Request.Form) if err != nil { panic("unreachable") } _ = start _ = stop limit := -1 if limitStr := p.Request.Form.Get("limit"); limitStr != "" { limit, err = strconv.Atoi(limitStr) if err != nil || limit <= 0 { panic("unreachable") } } if id := p.PathVar.ByName("id"); id == "" { panic("unreachable") } return results, nil })) } // parseDateRange parses a date range as specified in an http // request. The returned times will be zero if not specified. func parseDateRange(form url.Values) (start, stop time.Time, err error) { if v := form.Get("start"); v != "" { var err error start, err = time.Parse(dateFormat, v) if err != nil { return time.Time{}, time.Time{}, errgo.Newf("invalid 'start' value %q", v) } } if v := form.Get("stop"); v != "" { var err error stop, err = time.Parse(dateFormat, v) if err != nil { return time.Time{}, time.Time{}, errgo.Newf("invalid 'stop' value %q", v) } // Cover all timestamps within the stop day. stop = stop.Add(24*time.Hour - 1*time.Second) } return } func BenchmarkHandle4Fields(b *testing.B) { results := []testResult{} benchmarkHandle4Fields(b, testServer.Handle(func(p httprequest.Params, arg *testParams4Fields) ([]testResult, error) { if arg.To.Before(arg.From.Time) { panic("unreachable") } if arg.Limit <= 0 { panic("unreachable") } return results, nil }).Handle) } func BenchmarkHandle4FieldsUnmarshalOnly(b *testing.B) { results := []testResult{} benchmarkHandle4Fields(b, testServer.HandleJSON(func(p httprequest.Params) (interface{}, error) { var arg testParams4Fields if err := httprequest.Unmarshal(p, &arg); err != nil { return nil, err } if arg.To.Before(arg.From.Time) { panic("unreachable") } if arg.Limit <= 0 { panic("unreachable") } return results, nil })) } func benchmarkHandle4Fields(b *testing.B, handle func(w http.ResponseWriter, req *http.Request, pvar httprouter.Params)) { // example taken from charmstore changes/published endpoint fromDate, err1 := time.Parse(dateFormat, "2010-10-10") toDate, err2 := time.Parse(dateFormat, "2011-11-11") if err1 != nil || err2 != nil { b.Fatalf("bad times") } rec := httptest.NewRecorder() params := httprequest.Params{ Request: &http.Request{ Form: url.Values{ "limit": {"2000"}, "from": {fromDate.Format(dateFormat)}, "to": {toDate.Format(dateFormat)}, }, }, PathVar: httprouter.Params{{ Key: "id", Value: "someid", }}, } b.ResetTimer() for i := 0; i < b.N; i++ { rec.Body.Reset() handle(rec, params.Request, params.PathVar) } } func BenchmarkHandle2StringFields(b *testing.B) { benchmarkHandleNFields(b, 2, testServer.Handle(func(p httprequest.Params, arg *testParams2StringFields) error { return nil }).Handle) } func BenchmarkHandle2StringFieldsUnmarshalOnly(b *testing.B) { benchmarkHandleNFields(b, 2, testServer.HandleErrors(func(p httprequest.Params) error { var arg testParams2StringFields return httprequest.Unmarshal(p, &arg) })) } func BenchmarkHandle2StringFieldsTrad(b *testing.B) { benchmarkHandleNFields(b, 2, testServer.HandleErrors(func(p httprequest.Params) error { var arg testParams2StringFields arg.Field0 = p.Request.Form.Get("Field0") arg.Field1 = p.Request.Form.Get("Field1") return nil })) } func BenchmarkHandle4StringFields(b *testing.B) { benchmarkHandleNFields(b, 4, testServer.Handle(func(p httprequest.Params, arg *testParams4StringFields) error { return nil }).Handle) } func BenchmarkHandle4StringFieldsUnmarshalOnly(b *testing.B) { benchmarkHandleNFields(b, 4, testServer.HandleErrors(func(p httprequest.Params) error { var arg testParams4StringFields return httprequest.Unmarshal(p, &arg) })) } func BenchmarkHandle4StringFieldsTrad(b *testing.B) { benchmarkHandleNFields(b, 4, testServer.HandleErrors(func(p httprequest.Params) error { var arg testParams4StringFields arg.Field0 = p.Request.Form.Get("Field0") arg.Field1 = p.Request.Form.Get("Field1") arg.Field2 = p.Request.Form.Get("Field2") arg.Field3 = p.Request.Form.Get("Field3") return nil })) } func BenchmarkHandle8StringFields(b *testing.B) { benchmarkHandleNFields(b, 8, testServer.Handle(func(p httprequest.Params, arg *testParams8StringFields) error { return nil }).Handle) } func BenchmarkHandle8StringFieldsUnmarshalOnly(b *testing.B) { benchmarkHandleNFields(b, 8, testServer.HandleErrors(func(p httprequest.Params) error { var arg testParams8StringFields return httprequest.Unmarshal(p, &arg) })) } func BenchmarkHandle8StringFieldsTrad(b *testing.B) { benchmarkHandleNFields(b, 8, testServer.HandleErrors(func(p httprequest.Params) error { var arg testParams8StringFields arg.Field0 = p.Request.Form.Get("Field0") arg.Field1 = p.Request.Form.Get("Field1") arg.Field2 = p.Request.Form.Get("Field2") arg.Field3 = p.Request.Form.Get("Field3") arg.Field4 = p.Request.Form.Get("Field4") arg.Field5 = p.Request.Form.Get("Field5") arg.Field6 = p.Request.Form.Get("Field6") arg.Field7 = p.Request.Form.Get("Field7") return nil })) } func BenchmarkHandle16StringFields(b *testing.B) { benchmarkHandleNFields(b, 16, testServer.Handle(func(p httprequest.Params, arg *testParams16StringFields) error { return nil }).Handle) } func BenchmarkHandle16StringFieldsUnmarshalOnly(b *testing.B) { benchmarkHandleNFields(b, 16, testServer.HandleErrors(func(p httprequest.Params) error { var arg testParams16StringFields return httprequest.Unmarshal(p, &arg) })) } func BenchmarkHandle16StringFieldsTrad(b *testing.B) { benchmarkHandleNFields(b, 16, testServer.HandleErrors(func(p httprequest.Params) error { var arg testParams16StringFields arg.Field0 = p.Request.Form.Get("Field0") arg.Field1 = p.Request.Form.Get("Field1") arg.Field2 = p.Request.Form.Get("Field2") arg.Field3 = p.Request.Form.Get("Field3") arg.Field4 = p.Request.Form.Get("Field4") arg.Field5 = p.Request.Form.Get("Field5") arg.Field6 = p.Request.Form.Get("Field6") arg.Field7 = p.Request.Form.Get("Field7") arg.Field8 = p.Request.Form.Get("Field8") arg.Field9 = p.Request.Form.Get("Field9") arg.Field10 = p.Request.Form.Get("Field10") arg.Field11 = p.Request.Form.Get("Field11") arg.Field12 = p.Request.Form.Get("Field12") arg.Field13 = p.Request.Form.Get("Field13") arg.Field14 = p.Request.Form.Get("Field14") arg.Field15 = p.Request.Form.Get("Field15") return nil })) } func benchmarkHandleNFields(b *testing.B, n int, handle func(w http.ResponseWriter, req *http.Request, pvar httprouter.Params)) { form := make(url.Values) for i := 0; i < n; i++ { form[fmt.Sprint("Field", i)] = []string{fmt.Sprintf("field %d", i)} } rec := httptest.NewRecorder() params := httprequest.Params{ Request: &http.Request{ Form: form, }, } b.ResetTimer() for i := 0; i < b.N; i++ { rec.Body.Reset() handle(rec, params.Request, params.PathVar) } } httprequest-1.2.1/client.go000066400000000000000000000240541365452616600157330ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package httprequest import ( "bytes" "context" "encoding/json" "fmt" "io" "io/ioutil" "net/http" "net/url" "reflect" "strings" "gopkg.in/errgo.v1" ) // Doer is implemented by HTTP client packages // to make an HTTP request. It is notably implemented // by http.Client and httpbakery.Client. type Doer interface { Do(req *http.Request) (*http.Response, error) } // DoerWithContext is implemented by HTTP clients that can use a context // with the HTTP request. type DoerWithContext interface { DoWithContext(ctx context.Context, req *http.Request) (*http.Response, error) } // Client represents a client that can invoke httprequest endpoints. type Client struct { // BaseURL holds the base URL to use when making // HTTP requests. BaseURL string // Doer holds a value that will be used to actually // make the HTTP request. If it is nil, http.DefaultClient // will be used instead. If Doer implements DoerWithContext, // DoWithContext will be used instead. Doer Doer // If a request returns an HTTP response that signifies an // error, UnmarshalError is used to unmarshal the response into // an appropriate error. See ErrorUnmarshaler for a convenient // way to create an UnmarshalError function for a given type. If // this is nil, DefaultErrorUnmarshaler will be used. UnmarshalError func(resp *http.Response) error } // Call invokes the endpoint implied by the given params, // which should be of the form accepted by the ArgT // argument to a function passed to Handle, and // unmarshals the response into the given response parameter, // which should be a pointer to the response value. // // If params implements the HeaderSetter interface, its SetHeader method // will be called to add additional headers to the HTTP request. // // If resp is nil, the response will be ignored if the // request was successful. // // If resp is of type **http.Response, instead of unmarshaling // into it, its element will be set to the returned HTTP // response directly and the caller is responsible for // closing its Body field. // // Any error that c.UnmarshalError or c.Doer returns will not // have its cause masked. // // If the request returns a response with a status code signifying // success, but the response could not be unmarshaled, a // *DecodeResponseError will be returned holding the response. Note that if // the request returns an error status code, the Client.UnmarshalError // function is responsible for doing this if desired (the default error // unmarshal functions do). func (c *Client) Call(ctx context.Context, params, resp interface{}) error { return c.CallURL(ctx, c.BaseURL, params, resp) } // CallURL is like Call except that the given URL is used instead of // c.BaseURL. func (c *Client) CallURL(ctx context.Context, url string, params, resp interface{}) error { rt, err := getRequestType(reflect.TypeOf(params)) if err != nil { return errgo.Mask(err) } if rt.method == "" { return errgo.Newf("type %T has no httprequest.Route field", params) } reqURL, err := appendURL(url, rt.path) if err != nil { return errgo.Mask(err) } req, err := Marshal(reqURL.String(), rt.method, params) if err != nil { return errgo.Mask(err) } return c.Do(ctx, req, resp) } // Do sends the given request and unmarshals its JSON // result into resp, which should be a pointer to the response value. // If an error status is returned, the error will be unmarshaled // as in Client.Call. // // If resp is nil, the response will be ignored if the response was // successful. // // If resp is of type **http.Response, instead of unmarshaling // into it, its element will be set to the returned HTTP // response directly and the caller is responsible for // closing its Body field. // // Any error that c.UnmarshalError or c.Doer returns will not // have its cause masked. // // If req.URL does not have a host part it will be treated as relative to // c.BaseURL. req.URL will be updated to the actual URL used. // // If the response cannot by unmarshaled, a *DecodeResponseError // will be returned holding the response from the request. // the entire response body. func (c *Client) Do(ctx context.Context, req *http.Request, resp interface{}) error { if req.URL.Host == "" { var err error req.URL, err = appendURL(c.BaseURL, req.URL.String()) if err != nil { return errgo.Mask(err) } } doer := c.Doer if doer == nil { doer = http.DefaultClient } var httpResp *http.Response var err error if ctxDoer, ok := doer.(DoerWithContext); ok { httpResp, err = ctxDoer.DoWithContext(ctx, req) } else { httpResp, err = doer.Do(req.WithContext(ctx)) } if err != nil { return errgo.Mask(urlError(err, req), errgo.Any) } return c.unmarshalResponse(httpResp, resp) } // Get is a convenience method that uses c.Do to issue a GET request to // the given URL. If the given URL does not have a host part then it will // be treated as relative to c.BaseURL. func (c *Client) Get(ctx context.Context, url string, resp interface{}) error { req, err := http.NewRequest("GET", url, nil) if err != nil { return errgo.Notef(err, "cannot make request") } return c.Do(ctx, req, resp) } // unmarshalResponse unmarshals an HTTP response into the given value. func (c *Client) unmarshalResponse(httpResp *http.Response, resp interface{}) error { if 200 <= httpResp.StatusCode && httpResp.StatusCode < 300 { if respPt, ok := resp.(**http.Response); ok { *respPt = httpResp return nil } defer httpResp.Body.Close() if err := UnmarshalJSONResponse(httpResp, resp); err != nil { return errgo.Mask(urlError(err, httpResp.Request), isDecodeResponseError) } return nil } defer httpResp.Body.Close() errUnmarshaler := c.UnmarshalError if errUnmarshaler == nil { errUnmarshaler = DefaultErrorUnmarshaler } err := errUnmarshaler(httpResp) if err == nil { err = errgo.Newf("unexpected HTTP response status: %s", httpResp.Status) } return errgo.Mask(urlError(err, httpResp.Request), errgo.Any) } // ErrorUnmarshaler returns a function which will unmarshal error // responses into new values of the same type as template. The argument // must be a pointer. A new instance of it is created every time the // returned function is called. // // If the error cannot by unmarshaled, the function will return an // *HTTPResponseError holding the response from the request. func ErrorUnmarshaler(template error) func(*http.Response) error { t := reflect.TypeOf(template) if t.Kind() != reflect.Ptr { panic(errgo.Newf("cannot unmarshal errors into value of type %T", template)) } t = t.Elem() return func(resp *http.Response) error { if 300 <= resp.StatusCode && resp.StatusCode < 400 { // It's a redirection error. loc, _ := resp.Location() return newDecodeResponseError(resp, nil, fmt.Errorf("unexpected redirect (status %s) from %q to %q", resp.Status, resp.Request.URL, loc)) } errv := reflect.New(t) if err := UnmarshalJSONResponse(resp, errv.Interface()); err != nil { return errgo.NoteMask(err, fmt.Sprintf("cannot unmarshal error response (status %s)", resp.Status), isDecodeResponseError) } return errv.Interface().(error) } } // UnmarshalJSONResponse unmarshals the given HTTP response // into x, which should be a pointer to the result to be // unmarshaled into. // // If the response cannot be unmarshaled, an error of type // *DecodeResponseError will be returned. func UnmarshalJSONResponse(resp *http.Response, x interface{}) error { if x == nil { return nil } if !isJSONMediaType(resp.Header) { fancyErr := newFancyDecodeError(resp.Header, resp.Body) return newDecodeResponseError(resp, fancyErr.body, fancyErr) } // Read enough data that we can produce a plausible-looking // possibly-truncated response body in the error. var buf bytes.Buffer n, err := io.Copy(&buf, io.LimitReader(resp.Body, int64(maxErrorBodySize))) bodyData := buf.Bytes() if err != nil { return newDecodeResponseError(resp, bodyData, errgo.Notef(err, "error reading response body")) } if n < int64(maxErrorBodySize) { // We've read all the data; unmarshal it. if err := json.Unmarshal(bodyData, x); err != nil { return newDecodeResponseError(resp, bodyData, err) } return nil } // The response is longer than maxErrorBodySize; stitch the read // bytes together with the body so that we can still read // bodies larger than maxErrorBodySize. dec := json.NewDecoder(io.MultiReader(&buf, resp.Body)) // Try to read all the body so that we can reuse the // connection, but don't try *too* hard. Note that the // usual number of additional bytes is 1 (a single newline // after the JSON). defer io.Copy(ioutil.Discard, io.LimitReader(resp.Body, 8*1024)) if err := dec.Decode(x); err != nil { return newDecodeResponseError(resp, bodyData, err) } return nil } // appendURL returns the result of combining the // given base URL and relative URL. // // The path of the relative URL will be appended // to the base URL, separated by a slash (/) if // needed. // // Any query parameters will be concatenated together. // // appendURL will return an error if relURLStr contains // a host name. func appendURL(baseURLStr, relURLStr string) (*url.URL, error) { b, err := url.Parse(baseURLStr) if err != nil { return nil, errgo.Notef(err, "cannot parse %q", baseURLStr) } r, err := url.Parse(relURLStr) if err != nil { return nil, errgo.Notef(err, "cannot parse %q", relURLStr) } if r.Host != "" { return nil, errgo.Newf("relative URL specifies a host") } if r.Path != "" { b.Path = strings.TrimSuffix(b.Path, "/") + "/" + strings.TrimPrefix(r.Path, "/") } if r.RawQuery != "" { if b.RawQuery != "" { b.RawQuery += "&" + r.RawQuery } else { b.RawQuery = r.RawQuery } } return b, nil } func urlError(err error, req *http.Request) error { _, ok := errgo.Cause(err).(*url.Error) if ok { // The error is already sufficiently annotated. return err } // Convert the method to mostly lower case to match net/http's behaviour // so we don't get silly divergence of messages. method := req.Method[:1] + strings.ToLower(req.Method[1:]) return errgo.NoteMask(err, fmt.Sprintf("%s %s", method, req.URL), errgo.Any) } httprequest-1.2.1/client_test.go000066400000000000000000000540141365452616600167710ustar00rootroot00000000000000package httprequest_test import ( "context" "fmt" "io" "io/ioutil" "net/http" "net/http/httptest" "reflect" "regexp" "strings" "testing" qt "github.com/frankban/quicktest" "github.com/julienschmidt/httprouter" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" ) var callTests = []struct { about string client httprequest.Client req interface{} requestUUID string expectError string assertError func(c *qt.C, err error) expectResp interface{} }{{ about: "GET success", req: &chM1Req{ P: "hello", }, expectResp: &chM1Resp{"hello"}, }, { about: "GET with nil response", req: &chM1Req{ P: "hello", }, }, { about: "POST success", req: &chM2Req{ P: "hello", Body: struct{ I int }{999}, }, expectResp: &chM2Resp{"hello", 999}, }, { about: "GET marshal error", req: 123, expectError: `type is not pointer to struct`, }, { about: "error response", req: &chInvalidM2Req{ P: "hello", Body: struct{ I bool }{true}, }, expectError: `Post http:.*: cannot unmarshal parameters: cannot unmarshal into field Body: cannot unmarshal request body: json: cannot unmarshal .*`, assertError: func(c *qt.C, err error) { c.Assert(errgo.Cause(err), qt.Satisfies, isRemoteError) err1 := errgo.Cause(err).(*httprequest.RemoteError) c.Assert(err1.Code, qt.Equals, "bad request") c.Assert(err1.Message, qt.Matches, `cannot unmarshal parameters: cannot unmarshal into field Body: cannot unmarshal request body: json: cannot unmarshal .*`) }, }, { about: "error unmarshaler returns nil", client: httprequest.Client{ UnmarshalError: func(*http.Response) error { return nil }, }, req: &chM3Req{}, expectError: `Get http://.*/m3: unexpected HTTP response status: 500 Internal Server Error`, }, { about: "unexpected redirect", req: &chM2RedirectM2Req{ Body: struct{ I int }{999}, }, expectResp: &chM2Resp{"foo", 999}, }, { about: "bad content in successful response", req: &chM4Req{}, expectResp: new(int), expectError: `Get http://.*/m4: unexpected content type text/plain; want application/json; content: bad response`, assertError: func(c *qt.C, err error) { err1, ok := errgo.Cause(err).(*httprequest.DecodeResponseError) c.Assert(ok, qt.Equals, true, qt.Commentf("error not of type *httprequest.DecodeResponseError (%T)", errgo.Cause(err))) c.Assert(err1.Response, qt.Not(qt.IsNil)) data, err := ioutil.ReadAll(err1.Response.Body) c.Assert(err, qt.Equals, nil) c.Assert(string(data), qt.Equals, "bad response") }, }, { about: "bad content in error response", req: &chM5Req{}, expectResp: new(int), expectError: `Get http://.*/m5: cannot unmarshal error response \(status 418 I'm a teapot\): unexpected content type text/plain; want application/json; content: bad error value`, assertError: func(c *qt.C, err error) { err1, ok := errgo.Cause(err).(*httprequest.DecodeResponseError) c.Assert(ok, qt.Equals, true, qt.Commentf("error not of type *httprequest.DecodeResponseError (%T)", errgo.Cause(err))) c.Assert(err1.Response, qt.Not(qt.IsNil)) data, err := ioutil.ReadAll(err1.Response.Body) c.Assert(err, qt.Equals, nil) c.Assert(string(data), qt.Equals, "bad error value") c.Assert(err1.Response.StatusCode, qt.Equals, http.StatusTeapot) }, }, { about: "doer with context", client: httprequest.Client{ Doer: doerWithContextFunc(func(ctx context.Context, req *http.Request) (*http.Response, error) { if ctx == nil { panic("Do called when DoWithContext expected") } return http.DefaultClient.Do(req.WithContext(ctx)) }), }, req: &chM2Req{ P: "hello", Body: struct{ I int }{999}, }, expectResp: &chM2Resp{"hello", 999}, }, { about: "doer with context and body", client: httprequest.Client{ Doer: doerWithContextFunc(func(ctx context.Context, req *http.Request) (*http.Response, error) { if ctx == nil { panic("Do called when DoWithContext expected") } return http.DefaultClient.Do(req.WithContext(ctx)) }), }, req: &chM2Req{ P: "hello", Body: struct{ I int }{999}, }, expectResp: &chM2Resp{"hello", 999}, }, { about: "doer with context and body but no body", client: httprequest.Client{ Doer: doerWithContextFunc(func(ctx context.Context, req *http.Request) (*http.Response, error) { if ctx == nil { panic("Do called when DoWithContext expected") } return http.DefaultClient.Do(req.WithContext(ctx)) }), }, req: &chM1Req{ P: "hello", }, expectResp: &chM1Resp{"hello"}, }} func TestCall(t *testing.T) { c := qt.New(t) defer c.Done() srv := newServer() c.Defer(srv.Close) for _, test := range callTests { c.Run(test.about, func(c *qt.C) { var resp interface{} if test.expectResp != nil { resp = reflect.New(reflect.TypeOf(test.expectResp).Elem()).Interface() } client := test.client client.BaseURL = srv.URL ctx := context.Background() err := client.Call(ctx, test.req, resp) if test.expectError != "" { c.Logf("err %v", errgo.Details(err)) c.Check(err, qt.ErrorMatches, test.expectError) if test.assertError != nil { test.assertError(c, err) } return } c.Assert(err, qt.Equals, nil) c.Assert(resp, qt.DeepEquals, test.expectResp) }) } } func TestCallURLNoRequestPath(t *testing.T) { c := qt.New(t) defer c.Done() srv := newServer() c.Defer(srv.Close) var client httprequest.Client req := struct { httprequest.Route `httprequest:"GET"` chM1Req }{ chM1Req: chM1Req{ P: "hello", }, } var resp chM1Resp err := client.CallURL(context.Background(), srv.URL+"/m1/:P", &req, &resp) c.Assert(err, qt.Equals, nil) c.Assert(resp, qt.DeepEquals, chM1Resp{"hello"}) } func mustNewRequest(url string, method string, body io.Reader) *http.Request { return mustNewRequestWithHeader(url, method, body, http.Header{ "Content-Type": []string{"application/json"}, }) } func mustNewRequestWithHeader(url string, method string, body io.Reader, hdr http.Header) *http.Request { req, err := http.NewRequest(method, url, body) if err != nil { panic(err) } for k, v := range hdr { req.Header[k] = append(req.Header[k], v...) } return req } var doTests = []struct { about string client httprequest.Client request *http.Request requestUUID string expectError string expectCause interface{} expectResp interface{} }{{ about: "GET success", request: mustNewRequest("/m1/hello", "GET", nil), expectResp: &chM1Resp{"hello"}, }, { about: "appendURL error", request: mustNewRequest("/m1/hello", "GET", nil), client: httprequest.Client{ BaseURL: ":::", }, expectError: `cannot parse ":::": parse "?:::"?: missing protocol scheme`, }, { about: "Do returns error", client: httprequest.Client{ Doer: doerFunc(func(req *http.Request) (*http.Response, error) { return nil, errgo.Newf("an error") }), }, request: mustNewRequest("/m2/foo", "POST", strings.NewReader(`{"I": 999}`)), expectError: "Post http://.*/m2/foo: an error", }, { about: "doer with context", client: httprequest.Client{ Doer: doerWithContextFunc(func(ctx context.Context, req *http.Request) (*http.Response, error) { if ctx == nil { panic("Do called when DoWithContext expected") } return http.DefaultClient.Do(req.WithContext(ctx)) }), }, request: mustNewRequest("/m2/foo", "POST", strings.NewReader(`{"I": 999}`)), expectResp: &chM2Resp{"foo", 999}, }} func newInt64(i int64) *int64 { return &i } func TestDo(t *testing.T) { c := qt.New(t) defer c.Done() srv := newServer() c.Defer(srv.Close) for _, test := range doTests { test := test c.Run(test.about, func(c *qt.C) { var resp interface{} if test.expectResp != nil { resp = reflect.New(reflect.TypeOf(test.expectResp).Elem()).Interface() } client := test.client if client.BaseURL == "" { client.BaseURL = srv.URL } ctx := context.Background() err := client.Do(ctx, test.request, resp) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) if test.expectCause != nil { c.Assert(errgo.Cause(err), qt.DeepEquals, test.expectCause) } return } c.Assert(err, qt.Equals, nil) c.Assert(resp, qt.DeepEquals, test.expectResp) }) } } func TestDoWithHTTPReponse(t *testing.T) { c := qt.New(t) defer c.Done() srv := newServer() c.Defer(srv.Close) client := &httprequest.Client{ BaseURL: srv.URL, } var resp *http.Response err := client.Get(context.Background(), "/m1/foo", &resp) c.Assert(err, qt.Equals, nil) defer resp.Body.Close() data, err := ioutil.ReadAll(resp.Body) c.Assert(err, qt.Equals, nil) c.Assert(string(data), qt.Equals, `{"P":"foo"}`) } func TestDoWithHTTPReponseAndError(t *testing.T) { c := qt.New(t) defer c.Done() srv := newServer() c.Defer(srv.Close) var doer closeCountingDoer // Also check the body is closed. client := &httprequest.Client{ BaseURL: srv.URL, Doer: &doer, } var resp *http.Response err := client.Get(context.Background(), "/m3", &resp) c.Assert(resp, qt.IsNil) c.Assert(err, qt.ErrorMatches, `Get http:.*/m3: m3 error`) c.Assert(doer.openedBodies, qt.Equals, 1) c.Assert(doer.closedBodies, qt.Equals, 1) } func TestCallWithHTTPResponse(t *testing.T) { c := qt.New(t) defer c.Done() srv := newServer() c.Defer(srv.Close) client := &httprequest.Client{ BaseURL: srv.URL, } var resp *http.Response err := client.Call(context.Background(), &chM1Req{ P: "foo", }, &resp) defer resp.Body.Close() data, err := ioutil.ReadAll(resp.Body) c.Assert(err, qt.Equals, nil) c.Assert(string(data), qt.Equals, `{"P":"foo"}`) } func TestCallClosesResponseBodyOnSuccess(t *testing.T) { c := qt.New(t) defer c.Done() srv := newServer() c.Defer(srv.Close) var doer closeCountingDoer client := &httprequest.Client{ BaseURL: srv.URL, Doer: &doer, } var resp chM1Resp err := client.Call(context.Background(), &chM1Req{ P: "foo", }, &resp) c.Assert(err, qt.Equals, nil) c.Assert(resp, qt.DeepEquals, chM1Resp{"foo"}) c.Assert(doer.openedBodies, qt.Equals, 1) c.Assert(doer.closedBodies, qt.Equals, 1) } func TestCallClosesResponseBodyOnError(t *testing.T) { c := qt.New(t) defer c.Done() srv := newServer() c.Defer(srv.Close) var doer closeCountingDoer client := &httprequest.Client{ BaseURL: srv.URL, Doer: &doer, } err := client.Call(context.Background(), &chM3Req{}, nil) c.Assert(err, qt.ErrorMatches, ".*m3 error") c.Assert(doer.openedBodies, qt.Equals, 1) c.Assert(doer.closedBodies, qt.Equals, 1) } func TestDoClosesResponseBodyOnSuccess(t *testing.T) { c := qt.New(t) defer c.Done() srv := newServer() c.Defer(srv.Close) var doer closeCountingDoer client := &httprequest.Client{ BaseURL: srv.URL, Doer: &doer, } req, err := http.NewRequest("GET", "/m1/foo", nil) c.Assert(err, qt.Equals, nil) var resp chM1Resp err = client.Do(context.Background(), req, &resp) c.Assert(err, qt.Equals, nil) c.Assert(resp, qt.DeepEquals, chM1Resp{"foo"}) c.Assert(doer.openedBodies, qt.Equals, 1) c.Assert(doer.closedBodies, qt.Equals, 1) } func TestDoClosesResponseBodyOnError(t *testing.T) { c := qt.New(t) defer c.Done() srv := newServer() c.Defer(srv.Close) var doer closeCountingDoer client := &httprequest.Client{ BaseURL: srv.URL, Doer: &doer, } req, err := http.NewRequest("GET", "/m3", nil) c.Assert(err, qt.Equals, nil) err = client.Do(context.Background(), req, nil) c.Assert(err, qt.ErrorMatches, ".*m3 error") c.Assert(doer.openedBodies, qt.Equals, 1) c.Assert(doer.closedBodies, qt.Equals, 1) } func TestGet(t *testing.T) { c := qt.New(t) defer c.Done() srv := newServer() c.Defer(srv.Close) client := httprequest.Client{ BaseURL: srv.URL, } var resp chM1Resp err := client.Get(context.Background(), "/m1/foo", &resp) c.Assert(err, qt.Equals, nil) c.Assert(resp, qt.DeepEquals, chM1Resp{"foo"}) } func TestGetNoBaseURL(t *testing.T) { c := qt.New(t) defer c.Done() srv := newServer() c.Defer(srv.Close) client := httprequest.Client{} var resp chM1Resp err := client.Get(context.Background(), srv.URL+"/m1/foo", &resp) c.Assert(err, qt.Equals, nil) c.Assert(resp, qt.DeepEquals, chM1Resp{"foo"}) } func TestUnmarshalJSONResponseWithBodyReadError(t *testing.T) { c := qt.New(t) resp := &http.Response{ Header: http.Header{ "Content-Type": {"application/json"}, }, StatusCode: http.StatusOK, Body: ioutil.NopCloser(io.MultiReader( strings.NewReader(`{"one": "two"}`), errorReader("some bad read"), )), } var val map[string]string err := httprequest.UnmarshalJSONResponse(resp, &val) c.Assert(err, qt.ErrorMatches, `error reading response body: some bad read`) c.Assert(val, qt.IsNil) assertDecodeResponseError(c, err, http.StatusOK, `{"one": "two"}`) } var unmarshalJSONResponseWithVariedJSONContentTypesTests = []struct { contentType string expectError bool }{{ contentType: "application/json", }, { contentType: "application/json+other", }, { contentType: "application/vnd.schemaregistry.v1+json", }, { contentType: "other/json", expectError: true, }, { contentType: "other/jsonx", expectError: true, }, { contentType: "other/xjson", expectError: true, }, { contentType: "other/other+xjson", expectError: true, }, { contentType: "other/other+jsonx", expectError: true, }, { contentType: "application/other+json+foo", }, { contentType: "application/other+json", }, { contentType: "application/other+json+", }, { contentType: "application/+json+", }} func TestUnmarshalJSONResponseWithVariedJSONContentTypes(t *testing.T) { c := qt.New(t) for _, test := range unmarshalJSONResponseWithVariedJSONContentTypesTests { c.Run(test.contentType, func(c *qt.C) { resp := &http.Response{ Header: http.Header{ "Content-Type": {test.contentType}, }, StatusCode: http.StatusTeapot, Body: ioutil.NopCloser(strings.NewReader(`{}`)), } var val map[string]string err := httprequest.UnmarshalJSONResponse(resp, &val) if !test.expectError { c.Assert(err, qt.IsNil) return } c.Assert(err, qt.ErrorMatches, `unexpected content type `+regexp.QuoteMeta(test.contentType)+`; want application/json; content: "{}"`) c.Assert(val, qt.IsNil) assertDecodeResponseError(c, err, http.StatusTeapot, `{}`) }) } } func TestUnmarshalJSONResponseWithErrorAndLargeBody(t *testing.T) { c := qt.New(t) defer c.Done() c.Patch(httprequest.MaxErrorBodySize, 11) resp := &http.Response{ Header: http.Header{ "Content-Type": {"foo/bar"}, }, StatusCode: http.StatusOK, Body: ioutil.NopCloser(strings.NewReader(`123456789 123456789`)), } var val map[string]string err := httprequest.UnmarshalJSONResponse(resp, &val) c.Assert(err, qt.ErrorMatches, `unexpected content type foo/bar; want application/json; content: "123456789 1"`) c.Assert(val, qt.IsNil) assertDecodeResponseError(c, err, http.StatusOK, `123456789 1`) } func TestUnmarshalJSONResponseWithLargeBody(t *testing.T) { c := qt.New(t) defer c.Done() c.Patch(httprequest.MaxErrorBodySize, 11) resp := &http.Response{ Header: http.Header{ "Content-Type": {"application/json"}, }, StatusCode: http.StatusOK, Body: ioutil.NopCloser(strings.NewReader(`"23456789 123456789"`)), } var val string err := httprequest.UnmarshalJSONResponse(resp, &val) c.Assert(err, qt.Equals, nil) c.Assert(val, qt.Equals, "23456789 123456789") } func TestUnmarshalJSONWithDecodeError(t *testing.T) { c := qt.New(t) resp := &http.Response{ Header: http.Header{ "Content-Type": {"application/json"}, }, StatusCode: http.StatusOK, Body: ioutil.NopCloser(strings.NewReader(`{"one": "two"}`)), } var val chan string err := httprequest.UnmarshalJSONResponse(resp, &val) c.Assert(err, qt.ErrorMatches, `json: cannot unmarshal object into Go value of type chan string`) c.Assert(val, qt.IsNil) assertDecodeResponseError(c, err, http.StatusOK, `{"one": "two"}`) } func TestUnmarshalJSONWithDecodeErrorAndLargeBody(t *testing.T) { c := qt.New(t) defer c.Done() c.Patch(httprequest.MaxErrorBodySize, 11) resp := &http.Response{ Header: http.Header{ "Content-Type": {"application/json"}, }, StatusCode: http.StatusOK, Body: ioutil.NopCloser(strings.NewReader(`"23456789 123456789"`)), } var val chan string err := httprequest.UnmarshalJSONResponse(resp, &val) c.Assert(err, qt.ErrorMatches, `json: cannot unmarshal string into Go value of type chan string`) c.Assert(val, qt.IsNil) assertDecodeResponseError(c, err, http.StatusOK, `"23456789 1`) } func assertDecodeResponseError(c *qt.C, err error, status int, body string) { err1, ok := errgo.Cause(err).(*httprequest.DecodeResponseError) c.Assert(ok, qt.Equals, true, qt.Commentf("error not of type *httprequest.DecodeResponseError (%T)", errgo.Cause(err))) data, err := ioutil.ReadAll(err1.Response.Body) c.Assert(err, qt.Equals, nil) c.Assert(err1.Response.StatusCode, qt.Equals, status) c.Assert(string(data), qt.Equals, body) } func newServer() *httptest.Server { f := func(p httprequest.Params) (clientHandlers, context.Context, error) { return clientHandlers{}, p.Context, nil } handlers := testServer.Handlers(f) router := httprouter.New() for _, h := range handlers { router.Handle(h.Method, h.Path, h.Handle) } return httptest.NewServer(router) } var appendURLTests = []struct { u string p string expect string expectError string }{{ u: "http://foo", p: "bar", expect: "http://foo/bar", }, { u: "http://foo", p: "/bar", expect: "http://foo/bar", }, { u: "http://foo/", p: "bar", expect: "http://foo/bar", }, { u: "http://foo/", p: "/bar", expect: "http://foo/bar", }, { u: "", p: "bar", expect: "/bar", }, { u: "http://xxx", p: "", expect: "http://xxx", }, { u: "http://xxx.com", p: "http://foo.com", expectError: "relative URL specifies a host", }, { u: "http://xxx.com/a/b", p: "foo?a=45&b=c", expect: "http://xxx.com/a/b/foo?a=45&b=c", }, { u: "http://xxx.com", p: "?a=45&b=c", expect: "http://xxx.com?a=45&b=c", }, { u: "http://xxx.com/a?z=w", p: "foo?a=45&b=c", expect: "http://xxx.com/a/foo?z=w&a=45&b=c", }, { u: "http://xxx.com?z=w", p: "/a/b/c", expect: "http://xxx.com/a/b/c?z=w", }} func TestAppendURL(t *testing.T) { c := qt.New(t) for _, test := range appendURLTests { test := test c.Run(fmt.Sprintf("%s_%s", test.u, test.p), func(c *qt.C) { u, err := httprequest.AppendURL(test.u, test.p) if test.expectError != "" { c.Assert(u, qt.IsNil) c.Assert(err, qt.ErrorMatches, test.expectError) } else { c.Assert(err, qt.Equals, nil) c.Assert(u.String(), qt.Equals, test.expect) } }) } } type clientHandlers struct{} type chM1Req struct { httprequest.Route `httprequest:"GET /m1/:P"` P string `httprequest:",path"` } type chM1Resp struct { P string } func (clientHandlers) M1(p *chM1Req) (*chM1Resp, error) { return &chM1Resp{p.P}, nil } type chM2Req struct { httprequest.Route `httprequest:"POST /m2/:P"` P string `httprequest:",path"` Body struct { I int } `httprequest:",body"` } type chInvalidM2Req struct { httprequest.Route `httprequest:"POST /m2/:P"` P string `httprequest:",path"` Body struct { I bool } `httprequest:",body"` } type chM2RedirectM2Req struct { httprequest.Route `httprequest:"POST /m2/foo//"` Body struct { I int } `httprequest:",body"` } type chM2Resp struct { P string Arg int } func (clientHandlers) M2(p *chM2Req) (*chM2Resp, error) { return &chM2Resp{p.P, p.Body.I}, nil } type chM3Req struct { httprequest.Route `httprequest:"GET /m3"` } func (clientHandlers) M3(p *chM3Req) error { return errgo.New("m3 error") } type chM4Req struct { httprequest.Route `httprequest:"GET /m4"` } func (clientHandlers) M4(p httprequest.Params, _ *chM4Req) { p.Response.Write([]byte("bad response")) } type chM5Req struct { httprequest.Route `httprequest:"GET /m5"` } func (clientHandlers) M5(p httprequest.Params, _ *chM5Req) { p.Response.WriteHeader(http.StatusTeapot) p.Response.Write([]byte("bad error value")) } type chContentLengthReq struct { httprequest.Route `httprequest:"PUT /content-length"` } func (clientHandlers) ContentLength(rp httprequest.Params, p *chContentLengthReq) (int64, error) { return rp.Request.ContentLength, nil } type doerFunc func(req *http.Request) (*http.Response, error) func (f doerFunc) Do(req *http.Request) (*http.Response, error) { return f(req) } type doerWithContextFunc func(ctx context.Context, req *http.Request) (*http.Response, error) func (f doerWithContextFunc) Do(req *http.Request) (*http.Response, error) { return f(nil, req) } func (f doerWithContextFunc) DoWithContext(ctx context.Context, req *http.Request) (*http.Response, error) { if ctx == nil { panic("unexpected nil context") } return f(ctx, req) } type closeCountingDoer struct { // openBodies records the number of response bodies // that have been returned. openedBodies int // closedBodies records the number of response bodies // that have been closed. closedBodies int } func (doer *closeCountingDoer) Do(req *http.Request) (*http.Response, error) { resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } resp.Body = &closeCountingReader{ doer: doer, ReadCloser: resp.Body, } doer.openedBodies++ return resp, nil } type closeCountingReader struct { doer *closeCountingDoer io.ReadCloser } func (r *closeCountingReader) Close() error { r.doer.closedBodies++ return r.ReadCloser.Close() } // largeReader implements a reader that produces up to total bytes // in 1 byte reads. type largeReader struct { byte byte total int n int } func (r *largeReader) Read(buf []byte) (int, error) { if r.n >= r.total { return 0, io.EOF } r.n++ return copy(buf, []byte{r.byte}), nil } func (r *largeReader) Seek(offset int64, whence int) (int64, error) { if offset != 0 || whence != 0 { panic("unexpected seek") } r.n = 0 return 0, nil } func (r *largeReader) Close() error { // By setting n to zero, we ensure that if there's // a concurrent read, it will also read from n // and so the race detector should pick up the // problem. r.n = 0 return nil } func isRemoteError(err error) bool { _, ok := err.(*httprequest.RemoteError) return ok } httprequest-1.2.1/cmd/000077500000000000000000000000001365452616600146645ustar00rootroot00000000000000httprequest-1.2.1/cmd/httprequest-generate-client/000077500000000000000000000000001365452616600223205ustar00rootroot00000000000000httprequest-1.2.1/cmd/httprequest-generate-client/main.go000066400000000000000000000170341365452616600236000ustar00rootroot00000000000000// +build go1.8 package main import ( "bytes" "flag" "fmt" "go/ast" "go/build" "go/format" "go/parser" "go/token" "go/types" "io/ioutil" "os" "strings" "text/template" "golang.org/x/tools/go/packages" "gopkg.in/errgo.v1" ) // TODO: // - generate exported types if the parameter/response types aren't exported? // - deal with literal interface and struct types. // - copy doc comments from server methods. func main() { flag.Usage = func() { fmt.Fprintf(os.Stderr, "usage: httprequest-generate server-package server-type client-type\n") os.Exit(2) } flag.Parse() if flag.NArg() != 3 { flag.Usage() } serverPkg, serverType, clientType := flag.Arg(0), flag.Arg(1), flag.Arg(2) if err := generate(serverPkg, serverType, clientType); err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) } } type templateArg struct { PkgName string Imports []string Methods []method ClientType string } var code = template.Must(template.New("").Parse(` // The code in this file was automatically generated by running httprequest-generate-client. // DO NOT EDIT package {{.PkgName}} import ( {{range .Imports}}{{printf "%q" .}} {{end}} ) type {{.ClientType}} struct { Client httprequest.Client } {{range .Methods}} {{if .RespType}} {{.Doc}} func (c *{{$.ClientType}}) {{.Name}}(ctx context.Context, p *{{.ParamType}}) ({{.RespType}}, error) { var r {{.RespType}} err := c.Client.Call(ctx, p, &r) return r, err } {{else}} {{.Doc}} func (c *{{$.ClientType}}) {{.Name}}(ctx context.Context, p *{{.ParamType}}) (error) { return c.Client.Call(ctx, p, nil) } {{end}} {{end}} `)) func generate(serverPkgPath, serverType, clientType string) error { currentDir, err := os.Getwd() if err != nil { return err } localPkg, err := build.Import(".", currentDir, 0) if err != nil { return errgo.Notef(err, "cannot open package in current directory") } serverPkg, err := build.Import(serverPkgPath, currentDir, 0) if err != nil { return errgo.Notef(err, "cannot open %q", serverPkgPath) } methods, imports, err := serverMethods(serverPkg.ImportPath, serverType, localPkg.ImportPath) if err != nil { return errgo.Mask(err) } arg := templateArg{ Imports: imports, Methods: methods, PkgName: localPkg.Name, ClientType: clientType, } var buf bytes.Buffer if err := code.Execute(&buf, arg); err != nil { return errgo.Mask(err) } data, err := format.Source(buf.Bytes()) if err != nil { return errgo.Notef(err, "cannot format source") } if err := writeOutput(data, clientType); err != nil { return errgo.Mask(err) } return nil } func writeOutput(data []byte, clientType string) error { filename := strings.ToLower(clientType) + "_generated.go" if err := ioutil.WriteFile(filename, data, 0644); err != nil { return errgo.Mask(err) } return nil } type method struct { Name string Doc string ParamType string RespType string } // serverMethods returns the list of server methods and required import packages // provided by the given server type within the given server package. // // The localPkg package will be the one that the code will be generated in. func serverMethods(serverPkg, serverType, localPkg string) ([]method, []string, error) { cfg := packages.Config{ Mode: packages.LoadAllSyntax, ParseFile: func(fset *token.FileSet, filename string, src []byte) (*ast.File, error) { return parser.ParseFile(fset, filename, src, parser.ParseComments) }, } pkgs, err := packages.Load(&cfg, serverPkg) if err != nil { return nil, nil, errgo.Notef(err, "cannot load %q", serverPkg) } if len(pkgs) != 1 { return nil, nil, errgo.Newf("packages.Load returned %d packages, not 1", len(pkgs)) } pkgInfo := pkgs[0] pkg := pkgInfo.Types obj := pkg.Scope().Lookup(serverType) if obj == nil { return nil, nil, errgo.Newf("type %s not found in %s", serverType, serverPkg) } objTypeName, ok := obj.(*types.TypeName) if !ok { return nil, nil, errgo.Newf("%s is not a type", serverType) } // Use the pointer type to get as many methods as possible. ptrObjType := types.NewPointer(objTypeName.Type()) imports := map[string]string{ "gopkg.in/httprequest.v1": "httprequest", "context": "context", localPkg: "", } var methods []method mset := types.NewMethodSet(ptrObjType) for i := 0; i < mset.Len(); i++ { sel := mset.At(i) if !sel.Obj().Exported() { continue } name := sel.Obj().Name() if name == "Close" { continue } ptype, rtype, err := parseMethodType(sel.Type().(*types.Signature)) if err != nil { fmt.Fprintf(os.Stderr, "ignoring method %s: %v\n", name, err) continue } comment := docComment(pkgInfo, sel) methods = append(methods, method{ Name: name, Doc: comment, ParamType: typeStr(ptype, imports), RespType: typeStr(rtype, imports), }) } delete(imports, localPkg) var allImports []string for path := range imports { allImports = append(allImports, path) } return methods, allImports, nil } // docComment returns the doc comment for the method referred to // by the given selection. func docComment(pkg *packages.Package, sel *types.Selection) string { obj := sel.Obj() tokFile := pkg.Fset.File(obj.Pos()) if tokFile == nil { panic("no file found for method") } filename := tokFile.Name() comment := "" declFound := false packages.Visit([]*packages.Package{pkg}, func(pkg *packages.Package) bool { for _, f := range pkg.Syntax { if tokFile := pkg.Fset.File(f.Pos()); tokFile == nil || tokFile.Name() != filename { continue } // We've found the file we're looking for. Now traverse all // top level declarations looking for the right function declaration. for _, decl := range f.Decls { fdecl, ok := decl.(*ast.FuncDecl) if ok && fdecl.Name.Pos() == obj.Pos() { // Found it! comment = commentStr(fdecl.Doc) declFound = true return false } } } return true }, nil) if !declFound { panic(fmt.Sprintf("method declaration not found")) } return comment } func commentStr(c *ast.CommentGroup) string { if c == nil { return "" } var b []byte for i, cc := range c.List { if i > 0 { b = append(b, '\n') } b = append(b, cc.Text...) } return string(b) } // typeStr returns the type string to be used when using the // given type. It adds any needed import paths to the given // imports map (map from package path to package id). func typeStr(t types.Type, imports map[string]string) string { if t == nil { return "" } qualify := func(pkg *types.Package) string { if name, ok := imports[pkg.Path()]; ok { return name } name := pkg.Name() // Make sure we're not duplicating the name. // TODO if we are, make a new non-duplicated version. for oldPkg, oldName := range imports { if oldName == name { panic(errgo.Newf("duplicate package name %s vs %s", pkg.Path(), oldPkg)) } } imports[pkg.Path()] = name return name } return types.TypeString(t, qualify) } func parseMethodType(t *types.Signature) (ptype, rtype types.Type, err error) { mp := t.Params() if mp.Len() != 1 && mp.Len() != 2 { return nil, nil, errgo.New("wrong argument count") } ptype0 := mp.At(mp.Len() - 1).Type() ptype1, ok := ptype0.(*types.Pointer) if !ok { return nil, nil, errgo.New("parameter is not a pointer") } ptype = ptype1.Elem() if _, ok := ptype.Underlying().(*types.Struct); !ok { return nil, nil, errgo.Newf("parameter is %s, not a pointer to struct", ptype1.Elem()) } rp := t.Results() if rp.Len() > 2 { return nil, nil, errgo.New("wrong result count") } if rp.Len() == 2 { rtype = rp.At(0).Type() } return ptype, rtype, nil } httprequest-1.2.1/context_test.go000066400000000000000000000014601365452616600171740ustar00rootroot00000000000000// Copyright 2016 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package httprequest_test import ( "net/http" "net/http/httptest" "testing" qt "github.com/frankban/quicktest" "github.com/julienschmidt/httprouter" "gopkg.in/httprequest.v1" ) type testRequest struct { httprequest.Route `httprequest:"GET /foo"` } func TestContextCancelledWhenDone(t *testing.T) { c := qt.New(t) var ch <-chan struct{} hnd := testServer.Handle(func(p httprequest.Params, req *testRequest) { ch = p.Context.Done() }) router := httprouter.New() router.Handle(hnd.Method, hnd.Path, hnd.Handle) srv := httptest.NewServer(router) _, err := http.Get(srv.URL + "/foo") c.Assert(err, qt.Equals, nil) select { case <-ch: default: c.Fatal("context not canceled at end of handler.") } } httprequest-1.2.1/error.go000066400000000000000000000064521365452616600156100ustar00rootroot00000000000000package httprequest import ( "context" "encoding/json" "fmt" "net/http" errgo "gopkg.in/errgo.v1" ) // These constants are recognized by DefaultErrorMapper // as mapping to the similarly named HTTP status codes. const ( CodeBadRequest = "bad request" CodeUnauthorized = "unauthorized" CodeForbidden = "forbidden" CodeNotFound = "not found" ) // DefaultErrorUnmarshaler is the default error unmarshaler // used by Client. var DefaultErrorUnmarshaler = ErrorUnmarshaler(new(RemoteError)) // DefaultErrorMapper is used by Server when ErrorMapper is nil. It maps // all errors to RemoteError instances; if an error implements the // ErrorCoder interface, the Code field will be set accordingly; some // codes will map to specific HTTP status codes (for example, if // ErrorCode returns CodeBadRequest, the resulting HTTP status will be // http.StatusBadRequest). var DefaultErrorMapper = defaultErrorMapper func defaultErrorMapper(ctx context.Context, err error) (status int, body interface{}) { errorBody := errorResponseBody(err) switch errorBody.Code { case CodeBadRequest: status = http.StatusBadRequest case CodeUnauthorized: status = http.StatusUnauthorized case CodeForbidden: status = http.StatusForbidden case CodeNotFound: status = http.StatusNotFound default: status = http.StatusInternalServerError } return status, errorBody } // errorResponse returns an appropriate error // response for the provided error. func errorResponseBody(err error) *RemoteError { var errResp RemoteError cause := errgo.Cause(err) if cause, ok := cause.(*RemoteError); ok { // It's a RemoteError already; Preserve the wrapped // error message but copy everything else. errResp = *cause errResp.Message = err.Error() return &errResp } // It's not a RemoteError. Preserve as much info as we can find. errResp.Message = err.Error() if coder, ok := cause.(ErrorCoder); ok { errResp.Code = coder.ErrorCode() } return &errResp } // ErrorCoder may be implemented by an error to cause // it to return a particular RemoteError code when // DefaultErrorMapper is used. type ErrorCoder interface { ErrorCode() string } // RemoteError holds the default type of a remote error // used by Client when no custom error unmarshaler // is set. This type is also used by DefaultErrorMapper // to marshal errors in Server. type RemoteError struct { // Message holds the error message. Message string // Code may hold a code that classifies the error. Code string `json:",omitempty"` // Info holds any other information associated with the error. Info *json.RawMessage `json:",omitempty"` } // Error implements the error interface. func (e *RemoteError) Error() string { if e.Message == "" { return "httprequest: no error message found" } return e.Message } // ErrorCode implements ErrorCoder by returning e.Code. func (e *RemoteError) ErrorCode() string { return e.Code } // Errorf returns a new RemoteError instance that uses the // given code and formats the message with fmt.Sprintf(f, a...). // If f is empty and there are no other arguments, code will also // be used for the message. func Errorf(code string, f string, a ...interface{}) *RemoteError { var msg string if f == "" && len(a) == 0 { msg = code } else { msg = fmt.Sprintf(f, a...) } return &RemoteError{ Code: code, Message: msg, } } httprequest-1.2.1/example_handlers_test.go000066400000000000000000000022071365452616600210230ustar00rootroot00000000000000package httprequest_test import ( "context" "fmt" "io" "net/http" "net/http/httptest" "os" "github.com/julienschmidt/httprouter" "gopkg.in/httprequest.v1" ) type arithHandler struct { } type number struct { N int } func (arithHandler) Add(arg *struct { httprequest.Route `httprequest:"GET /:A/add/:B"` A int `httprequest:",path"` B int `httprequest:",path"` }) (number, error) { return number{ N: arg.A + arg.B, }, nil } func ExampleServer_Handlers() { f := func(p httprequest.Params) (arithHandler, context.Context, error) { fmt.Printf("handle %s %s\n", p.Request.Method, p.Request.URL) return arithHandler{}, p.Context, nil } router := httprouter.New() var reqSrv httprequest.Server for _, h := range reqSrv.Handlers(f) { router.Handle(h.Method, h.Path, h.Handle) } srv := httptest.NewServer(router) resp, err := http.Get(srv.URL + "/123/add/11") if err != nil { panic(err) } defer resp.Body.Close() if resp.StatusCode != 200 { panic("status " + resp.Status) } fmt.Println("result:") io.Copy(os.Stdout, resp.Body) // Output: handle GET /123/add/11 // result: // {"N":134} } httprequest-1.2.1/export_test.go000066400000000000000000000001301365452616600170220ustar00rootroot00000000000000package httprequest var AppendURL = appendURL var MaxErrorBodySize = &maxErrorBodySize httprequest-1.2.1/fancyerror.go000066400000000000000000000176261365452616600166360ustar00rootroot00000000000000package httprequest import ( "bytes" "fmt" "io" "io/ioutil" "mime" "net/http" "strings" "unicode" "golang.org/x/net/html" "golang.org/x/net/html/atom" "gopkg.in/errgo.v1" ) func isDecodeResponseError(err error) bool { _, ok := err.(*DecodeResponseError) return ok } // DecodeResponseError represents an error when an HTTP // response could not be decoded. type DecodeResponseError struct { // Response holds the problematic HTTP response. // The body of this does not need to be closed // and may be truncated if the response is large. Response *http.Response // DecodeError holds the error that was encountered // when decoding. DecodeError error } func (e *DecodeResponseError) Error() string { return e.DecodeError.Error() } // newDecodeResponseError returns a new DecodeResponseError that // uses the given error for its message. The Response field // holds a copy of req. If bodyData is non-nil, it // will be used as the data in the Response.Body field; // otherwise body data will be read from req.Body. func newDecodeResponseError(resp *http.Response, bodyData []byte, err error) *DecodeResponseError { if bodyData == nil { bodyData = readBodyForError(resp.Body) } resp1 := *resp resp1.Body = ioutil.NopCloser(bytes.NewReader(bodyData)) return &DecodeResponseError{ Response: &resp1, DecodeError: errgo.Mask(err, errgo.Any), } } // newDecodeRequestError returns a new DecodeRequestError that // uses the given error for its message. The Request field // holds a copy of req. If bodyData is non-nil, it // will be used as the data in the Request.Body field; // otherwise body data will be read from req.Body. func newDecodeRequestError(req *http.Request, bodyData []byte, err error) *DecodeRequestError { if bodyData == nil { bodyData = readBodyForError(req.Body) } req1 := *req req1.Body = ioutil.NopCloser(bytes.NewReader(bodyData)) return &DecodeRequestError{ Request: &req1, DecodeError: errgo.Mask(err, errgo.Any), } } // DecodeRequestError represents an error when an HTTP // request could not be decoded. type DecodeRequestError struct { // Request holds the problematic HTTP request. // The body of this does not need to be closed // and may be truncated if the response is large. Request *http.Request // DecodeError holds the error that was encountered // when decoding. DecodeError error } func (e *DecodeRequestError) Error() string { return e.DecodeError.Error() } // fancyDecodeError is an error type that tries to // produce a nice error message when the content // type of a request or response is wrong. type fancyDecodeError struct { // contentType holds the contentType of the request or response. contentType string // body holds up to maxErrorBodySize saved bytes of the // request or response body. body []byte } func newFancyDecodeError(h http.Header, body io.Reader) *fancyDecodeError { return &fancyDecodeError{ contentType: h.Get("Content-Type"), body: readBodyForError(body), } } func readBodyForError(r io.Reader) []byte { data, _ := ioutil.ReadAll(io.LimitReader(noErrorReader{r}, int64(maxErrorBodySize))) return data } // maxErrorBodySize holds the maximum amount of body that // we try to read for an error before extracting text from it. // It's reasonably large because: // a) HTML often has large embedded scripts which we want // to skip and // b) it should be an relatively unusual case so the size // shouldn't harm. // // It's defined as a variable so that it can be redefined in tests. var maxErrorBodySize = 200 * 1024 // isJSONMediaType reports whether the content type of the given header implies // that the content is JSON. func isJSONMediaType(header http.Header) bool { contentType := header.Get("Content-Type") mediaType, _, _ := mime.ParseMediaType(contentType) m := strings.TrimPrefix(mediaType, "application/") if len(m) == len(mediaType) { return false } // Look for +json suffix. See https://tools.ietf.org/html/rfc6838#section-4.2.8 // We recognize multiple suffixes too (e.g. application/something+json+other) // as that seems to be a possibility. for { i := strings.Index(m, "+") if i == -1 { return m == "json" } if m[0:i] == "json" { return true } m = m[i+1:] } } // Error implements error.Error by trying to produce a decent // error message derived from the body content. func (e *fancyDecodeError) Error() string { mediaType, _, err := mime.ParseMediaType(e.contentType) if err != nil { // Even if there's no media type, we want to see something useful. mediaType = fmt.Sprintf("%q", e.contentType) } // TODO use charset.NewReader to convert from non-utf8 content? switch mediaType { case "text/html": text, err := htmlToText(bytes.NewReader(e.body)) if err != nil { // Note: it seems that this can never actually // happen - the only way that the HTML parser // can fail is if there's a read error and we've // removed that possibility by using // noErrorReader above. return fmt.Sprintf("unexpected (and invalid) content text/html; want application/json; content: %q", sizeLimit(e.body)) } if len(text) == 0 { return fmt.Sprintf(`unexpected content type text/html; want application/json; content: %q`, sizeLimit(e.body)) } return fmt.Sprintf(`unexpected content type text/html; want application/json; content: %s`, sizeLimit(text)) case "text/plain": return fmt.Sprintf(`unexpected content type text/plain; want application/json; content: %s`, sizeLimit(sanitizeText(string(e.body), true))) default: return fmt.Sprintf(`unexpected content type %s; want application/json; content: %q`, mediaType, sizeLimit(e.body)) } } // noErrorReader wraps a reader, turning any errors into io.EOF // so that we can extract some content even if we get an io error. type noErrorReader struct { r io.Reader } func (r noErrorReader) Read(buf []byte) (int, error) { n, err := r.r.Read(buf) if err != nil { err = io.EOF } return n, err } func sizeLimit(data []byte) []byte { const max = 1024 if len(data) < max { return data } return append(data[0:max], fmt.Sprintf(" ... [%d bytes omitted]", len(data)-max)...) } // htmlToText attempts to return some relevant textual content // from the HTML content in the given reader, formatted // as a single line. func htmlToText(r io.Reader) ([]byte, error) { n, err := html.Parse(r) if err != nil { return nil, err } var buf bytes.Buffer htmlNodeToText(&buf, n) return buf.Bytes(), nil } // htmlNodeToText tries to extract some text from an arbitrary HTML // page. It doesn't try to avoid looking in the header, because the // title is in the header and is often the most succinct description of // the page. func htmlNodeToText(w *bytes.Buffer, n *html.Node) { for ; n != nil; n = n.NextSibling { switch n.Type { case html.TextNode: data := sanitizeText(n.Data, false) if len(data) == 0 { break } if w.Len() > 0 { w.WriteString("; ") } w.Write(data) case html.ElementNode: if n.DataAtom != atom.Script { htmlNodeToText(w, n.FirstChild) } case html.DocumentNode: htmlNodeToText(w, n.FirstChild) } } } // sanitizeText tries to make the given string easier to read when presented // as a single line. It squashes each run of white space into a single // space, trims leading and trailing white space and trailing full // stops. If newlineSemi is true, any newlines will be replaced with a // semicolon. func sanitizeText(s string, newlineSemi bool) []byte { out := make([]byte, 0, len(s)) prevWhite := false for _, r := range s { if newlineSemi && r == '\n' && len(out) > 0 { out = append(out, ';') prevWhite = true continue } if unicode.IsSpace(r) { if len(out) > 0 { prevWhite = true } continue } if prevWhite { out = append(out, ' ') prevWhite = false } out = append(out, string(r)...) } // Remove final space, any full stops and any final semicolon // we might have added. out = bytes.TrimRightFunc(out, func(r rune) bool { return r == '.' || r == ' ' || r == ';' }) return out } httprequest-1.2.1/fancyerror_test.go000066400000000000000000000366441365452616600176760ustar00rootroot00000000000000package httprequest import ( "strings" "testing" qt "github.com/frankban/quicktest" ) var fancyDecodeErrorTests = []struct { about string contentType string body string expectError string }{{ about: "plain text", contentType: "text/plain; charset=UTF-8", body: " some\n text\t\n", expectError: `unexpected content type text/plain; want application/json; content: some; text`, }, { about: "plain text with leading newline", contentType: "text/plain; charset=UTF-8", body: "\nsome text", expectError: `unexpected content type text/plain; want application/json; content: some text`, }, { about: "unknown content type", contentType: "something", body: "some \nstuff", expectError: `unexpected content type something; want application/json; content: "some \\nstuff"`, }, { about: "bad content type", contentType: "/; charset=foo", body: `some stuff`, expectError: `unexpected content type "/; charset=foo"; want application/json; content: "some stuff"`, }, { about: "large text body", contentType: "text/plain", body: strings.Repeat("x", 1024+300), expectError: `unexpected content type text/plain; want application/json; content: ` + strings.Repeat("x", 1024) + ` \.\.\. \[300 bytes omitted]`, }, { about: "html with no text", contentType: "text/html", body: "\n", expectError: `unexpected content type text/html; want application/json; content: "\\n"`, }, { about: "non-utf8 text", contentType: "text/plain; charset=iso8859-1", body: "Pepp\xe9\n", // It would be nice to make this better, but we don't // really want to drag in all the charsets for this. expectError: "unexpected content type text/plain; want application/json; content: Pepp\uFFFD", }, { about: "actual html error message from proxy", contentType: "text/html; charset=UTF-8", body: ` 502 Proxy Error

Proxy Error

The proxy server received an invalid response from an upstream server.
The proxy server could not handle the request GET /identity/v1/wait.

Reason: Error reading from remote server


Apache/2.4.7 (Ubuntu) Server at api.jujucharms.com Port 443
`, expectError: `unexpected content type text/html; want application/json; content: 502 Proxy Error; Proxy Error; The proxy server received an invalid response from an upstream server; The proxy server could not handle the request; GET /identity/v1/wait; Reason:; Error reading from remote server; Apache/2\.4\.7 \(Ubuntu\) Server at api.jujucharms.com Port 443`, }, { about: "actual html error message web page", contentType: "text/html; charset=UTF-8", body: ` Page not found | Juju

404: Sorry, we couldn’t find the page.

Try a different URL, try searching for solutions or learn how to create your own solution.

`, expectError: `unexpected content type text/html; want application/json; content: Page not found \| Juju; Jump to content; Store; Demo; About; Features; Community; Docs; Get started; ☰; Create; \+; 404: Sorry, we couldn’t find the page; Try a different URL, try searching for solutions or learn how to; create your own solution; Browse the store; All bundles; All charms; Submit a bug; Browse the store ›; Back to the top; Demo; About; Features; Docs; Get Started; Juju on Google\+; Ubuntu Cloud on Twitter; Ubuntu Cloud on Facebook; © 2015 Canonical Ltd. Ubuntu and Canonical are registered trademarks of Canonical Ltd; Legal information; Report a bug on this site; Got to the top of the page`, }} func TestFancyDecodeError(t *testing.T) { c := qt.New(t) for _, test := range fancyDecodeErrorTests { test := test c.Run(test.about, func(c *qt.C) { err := &fancyDecodeError{ contentType: test.contentType, body: []byte(test.body), } c.Assert(err, qt.ErrorMatches, test.expectError) }) } } httprequest-1.2.1/fields_test.go000066400000000000000000000103001365452616600167470ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package httprequest import ( "reflect" "testing" qt "github.com/frankban/quicktest" "github.com/google/go-cmp/cmp/cmpopts" ) type structField struct { name string index []int } var fieldsTests = []struct { about string val interface{} expect []structField }{{ about: "simple struct", val: struct { A int B string C bool }{}, expect: []structField{{ name: "A", index: []int{0}, }, { name: "B", index: []int{1}, }, { name: "C", index: []int{2}, }}, }, { about: "non-embedded struct member", val: struct { A struct { X int } }{}, expect: []structField{{ name: "A", index: []int{0}, }}, }, { about: "embedded exported struct", val: struct { SFG }{}, expect: []structField{{ name: "SFG", index: []int{0}, }, { name: "F", index: []int{0, 0}, }, { name: "G", index: []int{0, 1}, }}, }, { about: "embedded unexported struct", val: struct { sFG }{}, expect: []structField{{ name: "sFG", index: []int{0}, }, { name: "F", index: []int{0, 0}, }, { name: "G", index: []int{0, 1}, }}, }, { about: "two embedded structs with cancelling members", val: struct { SFG SF }{}, expect: []structField{{ name: "SFG", index: []int{0}, }, { name: "G", index: []int{0, 1}, }, { name: "SF", index: []int{1}, }}, }, { about: "embedded structs with same fields at different depths", val: struct { SFGH3 SG1 SFG2 SF2 L int }{}, expect: []structField{{ name: "SFGH3", index: []int{0}, }, { name: "SFGH2", index: []int{0, 0}, }, { name: "SFGH1", index: []int{0, 0, 0}, }, { name: "SFGH", index: []int{0, 0, 0, 0}, }, { name: "H", index: []int{0, 0, 0, 0, 2}, }, { name: "SG1", index: []int{1}, }, { name: "SG", index: []int{1, 0}, }, { name: "G", index: []int{1, 0, 0}, }, { name: "SFG2", index: []int{2}, }, { name: "SFG1", index: []int{2, 0}, }, { name: "SFG", index: []int{2, 0, 0}, }, { name: "SF2", index: []int{3}, }, { name: "SF1", index: []int{3, 0}, }, { name: "SF", index: []int{3, 0, 0}, }, { name: "L", index: []int{4}, }}, }, { about: "embedded pointer struct", val: struct { *SF }{}, expect: []structField{{ name: "SF", index: []int{0}, }, { name: "F", index: []int{0, 0}, }}, }, { about: "embedded not a pointer", val: struct { M }{}, expect: []structField{{ name: "M", index: []int{0}, }}, }} type SFG struct { F int `httprequest:",form"` G int `httprequest:",form"` } type SFG1 struct { SFG } type SFG2 struct { SFG1 } type SFGH struct { F int `httprequest:",form"` G int `httprequest:",form"` H int `httprequest:",form"` } type SFGH1 struct { SFGH } type SFGH2 struct { SFGH1 } type SFGH3 struct { SFGH2 } type SF struct { F int `httprequest:",form"` } type SF1 struct { SF } type SF2 struct { SF1 } type SG struct { G int `httprequest:",form"` } type SG1 struct { SG } type sFG struct { F int `httprequest:",form"` G int `httprequest:",form"` } type M map[string]interface{} func TestFields(t *testing.T) { c := qt.New(t) for _, test := range fieldsTests { test := test c.Run(test.about, func(c *qt.C) { t := reflect.TypeOf(test.val) got := fields(t) c.Assert(got, qt.HasLen, len(test.expect)) for j, field := range got { expect := test.expect[j] c.Logf("field %d: %s", j, expect.name) gotField := t.FieldByIndex(field.Index) // Unfortunately, FieldByIndex does not return // a field with the same index that we passed in, // so we set it to the expected value so that // it can be compared later with the result of FieldByName. gotField.Index = field.Index expectField := t.FieldByIndex(expect.index) // ditto. expectField.Index = expect.index c.Assert(gotField, qt.CmpEquals(cmpopts.IgnoreInterfaces(struct{ reflect.Type }{})), expectField) // Sanity check that we can actually access the field by the // expected name. expectField1, ok := t.FieldByName(expect.name) c.Assert(ok, qt.Equals, true) c.Assert(expectField1, qt.CmpEquals(cmpopts.IgnoreInterfaces(struct{ reflect.Type }{})), expectField) } }) } } httprequest-1.2.1/go.mod000066400000000000000000000005221365452616600152260ustar00rootroot00000000000000module gopkg.in/httprequest.v1 go 1.15 require ( github.com/frankban/quicktest v1.10.0 github.com/google/go-cmp v0.4.0 github.com/juju/qthttptest v0.1.1 github.com/julienschmidt/httprouter v1.3.0 golang.org/x/net v0.0.0-20200505041828-1ed23360d12c golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8 gopkg.in/errgo.v1 v1.0.0 ) httprequest-1.2.1/go.sum000066400000000000000000000110211365452616600152470ustar00rootroot00000000000000github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= github.com/frankban/quicktest v1.10.0 h1:Gfh+GAJZOAoKZsIZeZbdn2JF10kN1XHNvjsvQK8gVkE= github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/juju/qthttptest v0.1.1 h1:JPju5P5CDMCy8jmBJV2wGLjDItUsx2KKL514EfOYueM= github.com/juju/qthttptest v0.1.1/go.mod h1:aTlAv8TYaflIiTDIQYzxnl1QdPjAg8Q8qJMErpKy6A4= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200505041828-1ed23360d12c h1:zJ0mtu4jCalhKg6Oaukv6iIkb+cOvDrajDH9DH46Q4M= golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8 h1:BMFHd4OFnFtWX46Xj4DN6vvT1btiBxyq+s0orYBqcQY= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v1 v1.0.0 h1:n+7XfCyygBFb8sEjg6692xjC6Us50TFRO54+xYUEwjE= gopkg.in/errgo.v1 v1.0.0/go.mod h1:CxwszS/Xz1C49Ucd2i6Zil5UToP1EmyrFhKaMVbg1mk= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= httprequest-1.2.1/handler.go000066400000000000000000000513361365452616600160750ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package httprequest import ( "context" "encoding/json" "fmt" "io" "net/http" "reflect" "github.com/julienschmidt/httprouter" errgo "gopkg.in/errgo.v1" ) // Server represents the server side of an HTTP servers, and can be // used to create HTTP handlers although it is not an HTTP handler // itself. type Server struct { // ErrorMapper holds a function that can convert a Go error // into a form that can be returned as a JSON body from an HTTP request. // // The httpStatus value reports the desired HTTP status. // // If the returned errorBody implements HeaderSetter, then // that method will be called to add custom headers to the request. // // If this both this and ErrorWriter are nil, DefaultErrorMapper will be used. ErrorMapper func(ctxt context.Context, err error) (httpStatus int, errorBody interface{}) // ErrorWriter is a more general form of ErrorMapper. If this // field is set, ErrorMapper will be ignored and any returned // errors will be passed to ErrorWriter, which should use // w to set the HTTP status and write an appropriate // error response. ErrorWriter func(ctx context.Context, w http.ResponseWriter, err error) } // Handler defines a HTTP handler that will handle the // given HTTP method at the given httprouter path type Handler struct { Method string Path string Handle httprouter.Handle } // handlerFunc represents a function that can handle an HTTP request. type handlerFunc struct { // unmarshal unmarshals the request parameters into // the argument value required by the method. unmarshal func(p Params) (reflect.Value, error) // call invokes the request on the given function value with the // given argument value (as returned by unmarshal). call func(fv, argv reflect.Value, p Params) // method holds the HTTP method the function will be // registered for. method string // pathPattern holds the path pattern the function will // be registered for. pathPattern string } var ( paramsType = reflect.TypeOf(Params{}) errorType = reflect.TypeOf((*error)(nil)).Elem() contextType = reflect.TypeOf((*context.Context)(nil)).Elem() httpResponseWriterType = reflect.TypeOf((*http.ResponseWriter)(nil)).Elem() httpHeaderType = reflect.TypeOf(http.Header(nil)) httpRequestType = reflect.TypeOf((*http.Request)(nil)) ioCloserType = reflect.TypeOf((*io.Closer)(nil)).Elem() ) // AddHandlers adds all the handlers in the given slice to r. func AddHandlers(r *httprouter.Router, hs []Handler) { for _, h := range hs { r.Handle(h.Method, h.Path, h.Handle) } } // Handle converts a function into a Handler. The argument f // must be a function of one of the following six forms, where ArgT // must be a struct type acceptable to Unmarshal and ResultT is a type // that can be marshaled as JSON: // // func(p Params, arg *ArgT) // func(p Params, arg *ArgT) error // func(p Params, arg *ArgT) (ResultT, error) // // func(arg *ArgT) // func(arg *ArgT) error // func(arg *ArgT) (ResultT, error) // // When processing a call to the returned handler, the provided // parameters are unmarshaled into a new ArgT value using Unmarshal, // then f is called with this value. If the unmarshaling fails, f will // not be called and the unmarshal error will be written as a JSON // response. // // As an additional special case to the rules defined in Unmarshal, the // tag on an anonymous field of type Route specifies the method and path // to use in the HTTP request. It should hold two space-separated // fields; the first specifies the HTTP method, the second the URL path // to use for the request. If this is given, the returned handler will // hold that method and path, otherwise they will be empty. // // If an error is returned from f, it is passed through the error mapper // before writing as a JSON response. // // In the third form, when no error is returned, the result is written // as a JSON response with status http.StatusOK. Also in this case, any // calls to Params.Response.Write or Params.Response.WriteHeader will be // ignored, as the response code and data should be defined entirely by // the returned result and error. // // Handle will panic if the provided function is not in one of the above // forms. func (srv *Server) Handle(f interface{}) Handler { fv := reflect.ValueOf(f) hf, err := srv.handlerFunc(fv.Type(), nil) if err != nil { panic(errgo.Notef(err, "bad handler function")) } return Handler{ Method: hf.method, Path: hf.pathPattern, Handle: func(w http.ResponseWriter, req *http.Request, p httprouter.Params) { ctx := req.Context() p1 := Params{ Response: w, Request: req, PathVar: p, PathPattern: hf.pathPattern, Context: ctx, } argv, err := hf.unmarshal(p1) if err != nil { srv.WriteError(ctx, w, err) return } hf.call(fv, argv, p1) }, } } // Handlers returns a list of handlers that will be handled by the value // returned by the given argument, which must be a function in one of the // following forms: // // func(p httprequest.Params) (T, context.Context, error) // func(p httprequest.Params, handlerArg I) (T, context.Context, error) // // for some type T and some interface type I. Each exported method defined on T defines a handler, // and should be in one of the forms accepted by Server.Handle // with the additional constraint that the argument to each // of the handlers must be compatible with the type I when the // second form is used above. // // The returned context will be used as the value of Params.Context // when Params is passed to any method. It will also be used // when writing an error if the function returns an error. // // Handlers will panic if f is not of the required form, no methods are // defined on T or any method defined on T is not suitable for Handle. // // When any of the returned handlers is invoked, f will be called and // then the appropriate method will be called on the value it returns. // If specified, the handlerArg parameter to f will hold the ArgT argument that // will be passed to the handler method. // // If T implements io.Closer, its Close method will be called // after the request is completed. func (srv *Server) Handlers(f interface{}) []Handler { rootv := reflect.ValueOf(f) wt, argInterfacet, err := checkHandlersWrapperFunc(rootv) if err != nil { panic(errgo.Notef(err, "bad handler function")) } hasClose := wt.Implements(ioCloserType) hs := make([]Handler, 0, wt.NumMethod()) for i := 0; i < wt.NumMethod(); i++ { i := i m := wt.Method(i) if m.PkgPath != "" { continue } if m.Name == "Close" { if !hasClose { panic(errgo.Newf("bad type for Close method (got %v want func(%v) error", m.Type, wt)) } continue } if wt.Kind() != reflect.Interface { // The type in the Method struct includes the receiver type, // which we don't want to look at (and we won't see when // we get the method from the actual value at dispatch time), // so we hide it. m.Type = withoutReceiver(m.Type) } h, err := srv.methodHandler(m, rootv, argInterfacet, hasClose) if err != nil { panic(err) } hs = append(hs, h) } if len(hs) == 0 { panic(errgo.Newf("no exported methods defined on %s", wt)) } return hs } func (srv *Server) methodHandler(m reflect.Method, rootv reflect.Value, argInterfacet reflect.Type, hasClose bool) (Handler, error) { hf, err := srv.handlerFunc(m.Type, argInterfacet) if err != nil { return Handler{}, errgo.Notef(err, "bad type for method %s", m.Name) } if hf.method == "" || hf.pathPattern == "" { return Handler{}, errgo.Notef(err, "method %s does not specify route method and path", m.Name) } handler := func(w http.ResponseWriter, req *http.Request, p httprouter.Params) { ctx := req.Context() p1 := Params{ Response: w, Request: req, PathVar: p, PathPattern: hf.pathPattern, Context: ctx, } inv, err := hf.unmarshal(p1) if err != nil { srv.WriteError(ctx, w, err) return } var outv []reflect.Value if argInterfacet != nil { outv = rootv.Call([]reflect.Value{ reflect.ValueOf(p1), // Pass the value to the root function so it can do wrappy things with it. // Note that because of the checks we've applied earlier, we can be // sure that the value will implement the interface type of this argument. inv, }) } else { outv = rootv.Call([]reflect.Value{ reflect.ValueOf(p1), }) } tv, ctxv, errv := outv[0], outv[1], outv[2] // Get the context value robustly even if the // handler stupidly decides to return nil, and fall // back to the original context if it does. ctx1, _ := ctxv.Interface().(context.Context) if ctx1 != nil { ctx = ctx1 } if !errv.IsNil() { srv.WriteError(ctx, w, errv.Interface().(error)) return } if hasClose { defer tv.Interface().(io.Closer).Close() } hf.call(tv.Method(m.Index), inv, Params{ Response: w, Request: req, PathVar: p, PathPattern: hf.pathPattern, Context: ctx, }) } return Handler{ Method: hf.method, Path: hf.pathPattern, Handle: handler, }, nil } func checkHandlersWrapperFunc(fv reflect.Value) (returnt, argInterfacet reflect.Type, err error) { ft := fv.Type() if ft.Kind() != reflect.Func { return nil, nil, errgo.Newf("expected function, got %v", ft) } if fv.IsNil() { return nil, nil, errgo.Newf("function is nil") } if n := ft.NumIn(); n != 1 && n != 2 { return nil, nil, errgo.Newf("got %d arguments, want 1 or 2", n) } if n := ft.NumOut(); n != 3 { return nil, nil, errgo.Newf("function returns %d values, want (, context.Context, error)", n) } if t := ft.In(0); t != paramsType { return nil, nil, errgo.Newf("invalid first argument, want httprequest.Params, got %v", t) } if ft.NumIn() > 1 { if t := ft.In(1); t.Kind() != reflect.Interface { return nil, nil, errgo.Newf("invalid second argument, want interface type, got %v", t) } argInterfacet = ft.In(1) } if t := ft.Out(1); !t.Implements(contextType) { return nil, nil, errgo.Newf("second return parameter of type %v does not implement context.Context", t) } if t := ft.Out(2); t != errorType { return nil, nil, errgo.Newf("invalid third return parameter, want error, got %v", t) } return ft.Out(0), argInterfacet, nil } func checkHandleType(t, argInterfacet reflect.Type) (*requestType, error) { if t.Kind() != reflect.Func { return nil, errgo.New("not a function") } if n := t.NumIn(); n != 1 && n != 2 { return nil, errgo.Newf("has %d parameters, need 1 or 2", t.NumIn()) } if t.NumOut() > 2 { return nil, errgo.Newf("has %d result parameters, need 0, 1 or 2", t.NumOut()) } if t.NumIn() == 2 { if t.In(0) != paramsType { return nil, errgo.Newf("first argument is %v, need httprequest.Params", t.In(0)) } } else { if t.In(0) == paramsType { return nil, errgo.Newf("no argument parameter after Params argument") } } argt := t.In(t.NumIn() - 1) pt, err := getRequestType(argt) if err != nil { return nil, errgo.Notef(err, "last argument cannot be used for Unmarshal") } if argInterfacet != nil && !argt.Implements(argInterfacet) { return nil, errgo.Notef(err, "argument of type %v does not implement interface required by root handler %v", argt, argInterfacet) } if t.NumOut() > 0 { // func(p Params, arg *ArgT) error // func(p Params, arg *ArgT) (ResultT, error) if et := t.Out(t.NumOut() - 1); et != errorType { return nil, errgo.Newf("final result parameter is %s, need error", et) } } return pt, nil } // handlerFunc returns a function that will call a function of the given type, // unmarshaling request parameters and marshaling the response as // appropriate. func (srv *Server) handlerFunc(ft, argInterfacet reflect.Type) (handlerFunc, error) { rt, err := checkHandleType(ft, argInterfacet) if err != nil { return handlerFunc{}, errgo.Mask(err) } return handlerFunc{ unmarshal: handlerUnmarshaler(ft, rt), call: srv.handlerCaller(ft, rt), method: rt.method, pathPattern: rt.path, }, nil } func handlerUnmarshaler( ft reflect.Type, rt *requestType, ) func(p Params) (reflect.Value, error) { argStructType := ft.In(ft.NumIn() - 1).Elem() return func(p Params) (reflect.Value, error) { if err := p.Request.ParseForm(); err != nil { return reflect.Value{}, errgo.WithCausef(err, ErrUnmarshal, "cannot parse HTTP request form") } argv := reflect.New(argStructType) if err := unmarshal(p, argv, rt); err != nil { return reflect.Value{}, errgo.NoteMask(err, "cannot unmarshal parameters", errgo.Is(ErrUnmarshal)) } return argv, nil } } func (srv *Server) handlerCaller( ft reflect.Type, rt *requestType, ) func(fv, argv reflect.Value, p Params) { returnJSON := ft.NumOut() > 1 needsParams := ft.In(0) == paramsType respond := srv.handlerResponder(ft) return func(fv, argv reflect.Value, p Params) { var rv []reflect.Value if needsParams { p := p if returnJSON { p.Response = headerOnlyResponseWriter{p.Response.Header()} } rv = fv.Call([]reflect.Value{ reflect.ValueOf(p), argv, }) } else { rv = fv.Call([]reflect.Value{ argv, }) } respond(p, rv) } } // handlerResponder handles the marshaling of the result values from the call to a function // of type ft. The returned function accepts the values returned by the handler. func (srv *Server) handlerResponder(ft reflect.Type) func(p Params, outv []reflect.Value) { switch ft.NumOut() { case 0: // func(...) return func(Params, []reflect.Value) {} case 1: // func(...) error return func(p Params, outv []reflect.Value) { if err := outv[0].Interface(); err != nil { srv.WriteError(p.Context, p.Response, err.(error)) } } case 2: // func(...) (ResultT, error) return func(p Params, outv []reflect.Value) { if err := outv[1].Interface(); err != nil { srv.WriteError(p.Context, p.Response, err.(error)) return } if err := WriteJSON(p.Response, http.StatusOK, outv[0].Interface()); err != nil { srv.WriteError(p.Context, p.Response, err) } } default: panic("unreachable") } } // ToHTTP converts an httprouter.Handle into an http.Handler. // It will pass any path variables found in the request context // through to h. func ToHTTP(h httprouter.Handle) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { h(w, req, httprouter.ParamsFromContext(req.Context())) }) } // JSONHandler is like httprouter.Handle except that it returns a // body (to be converted to JSON) and an error. // The Header parameter can be used to set // custom headers on the response. type JSONHandler func(Params) (interface{}, error) // ErrorHandler is like httprouter.Handle except it returns an error // which may be returned as the error body of the response. // An ErrorHandler function should not itself write to the ResponseWriter // if it returns an error. type ErrorHandler func(Params) error // HandleJSON returns a handler that writes the return value of handle // as a JSON response. If handle returns an error, it is passed through // the error mapper. // // Note that the Params argument passed to handle will not // have its PathPattern set as that information is not available. func (srv *Server) HandleJSON(handle JSONHandler) httprouter.Handle { return func(w http.ResponseWriter, req *http.Request, p httprouter.Params) { ctx := req.Context() val, err := handle(Params{ Response: headerOnlyResponseWriter{w.Header()}, Request: req, PathVar: p, Context: ctx, }) if err == nil { if err = WriteJSON(w, http.StatusOK, val); err == nil { return } } srv.WriteError(ctx, w, err) } } // HandleErrors returns a handler that passes any non-nil error returned // by handle through the error mapper and writes it as a JSON response. // // Note that the Params argument passed to handle will not // have its PathPattern set as that information is not available. func (srv *Server) HandleErrors(handle ErrorHandler) httprouter.Handle { return func(w http.ResponseWriter, req *http.Request, p httprouter.Params) { w1 := responseWriter{ ResponseWriter: w, } ctx := req.Context() if err := handle(Params{ Response: &w1, Request: req, PathVar: p, Context: ctx, }); err != nil { if w1.headerWritten { // The header has already been written, // so we can't set the appropriate error // response code and there's a danger // that we may be corrupting the // response by appending a JSON error // message to it. // TODO log an error in this case. return } srv.WriteError(ctx, w, err) } } } // WriteError writes an error to a ResponseWriter and sets the HTTP // status code, using srv.ErrorMapper to determine the actually written // response. // // It uses WriteJSON to write the error body returned from the // ErrorMapper so it is possible to add custom headers to the HTTP error // response by implementing HeaderSetter. func (srv *Server) WriteError(ctx context.Context, w http.ResponseWriter, err error) { if srv.ErrorWriter != nil { srv.ErrorWriter(ctx, w, err) return } errorMapper := srv.ErrorMapper if errorMapper == nil { errorMapper = DefaultErrorMapper } status, resp := errorMapper(ctx, err) err1 := WriteJSON(w, status, resp) if err1 == nil { return } // TODO log an error ? // JSON-marshaling the original error failed, so try to send that // error instead; if that fails, give up and go home. status1, resp1 := errorMapper(ctx, errgo.Notef(err1, "cannot marshal error response %q", err)) err2 := WriteJSON(w, status1, resp1) if err2 == nil { return } w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(fmt.Sprintf("really cannot marshal error response %q: %v", err, err1))) } // WriteJSON writes the given value to the ResponseWriter // and sets the HTTP status to the given code. // // If val implements the HeaderSetter interface, the SetHeader // method will be called to add additional headers to the // HTTP response. It is called after the Content-Type header // has been added, so can be used to override the content type // if required. func WriteJSON(w http.ResponseWriter, code int, val interface{}) error { // TODO consider marshalling directly to w using json.NewEncoder. // pro: this will not require a full buffer allocation. // con: if there's an error after the first write, it will be lost. data, err := json.Marshal(val) if err != nil { return errgo.Mask(err) } w.Header().Set("content-type", "application/json") if headerSetter, ok := val.(HeaderSetter); ok { headerSetter.SetHeader(w.Header()) } w.WriteHeader(code) w.Write(data) return nil } // HeaderSetter is the interface checked for by WriteJSON. // If implemented on a value passed to WriteJSON, the SetHeader // method will be called to allow it to set custom headers // on the response. type HeaderSetter interface { SetHeader(http.Header) } // CustomHeader is a type that allows a JSON value to // set custom HTTP headers associated with the // HTTP response. type CustomHeader struct { // Body holds the JSON-marshaled body of the response. Body interface{} // SetHeaderFunc holds a function that will be called // to set any custom headers on the response. SetHeaderFunc func(http.Header) } // MarshalJSON implements json.Marshaler by marshaling // h.Body. func (h CustomHeader) MarshalJSON() ([]byte, error) { return json.Marshal(h.Body) } // SetHeader implements HeaderSetter by calling // h.SetHeaderFunc. func (h CustomHeader) SetHeader(header http.Header) { h.SetHeaderFunc(header) } // Ensure statically that responseWriter does implement http.Flusher. var _ http.Flusher = (*responseWriter)(nil) // responseWriter wraps http.ResponseWriter but allows us // to find out whether any body has already been written. type responseWriter struct { headerWritten bool http.ResponseWriter } func (w *responseWriter) Write(data []byte) (int, error) { w.headerWritten = true return w.ResponseWriter.Write(data) } func (w *responseWriter) WriteHeader(code int) { w.headerWritten = true w.ResponseWriter.WriteHeader(code) } // Flush implements http.Flusher.Flush. func (w *responseWriter) Flush() { w.headerWritten = true if f, ok := w.ResponseWriter.(http.Flusher); ok { f.Flush() } } type headerOnlyResponseWriter struct { h http.Header } func (w headerOnlyResponseWriter) Header() http.Header { return w.h } func (w headerOnlyResponseWriter) Write([]byte) (int, error) { // TODO log or panic when this happens? return 0, errgo.New("inappropriate call to ResponseWriter.Write in JSON-returning handler") } func (w headerOnlyResponseWriter) WriteHeader(code int) { // TODO log or panic when this happens? } func withoutReceiver(t reflect.Type) reflect.Type { return withoutReceiverType{t} } type withoutReceiverType struct { reflect.Type } func (t withoutReceiverType) NumIn() int { return t.Type.NumIn() - 1 } func (t withoutReceiverType) In(i int) reflect.Type { return t.Type.In(i + 1) } httprequest-1.2.1/handler_test.go000066400000000000000000001135331365452616600171320ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package httprequest_test import ( "context" "encoding/json" "errors" "fmt" "net/http" "net/http/httptest" "net/url" "reflect" "testing" qt "github.com/frankban/quicktest" "github.com/google/go-cmp/cmp/cmpopts" "github.com/juju/qthttptest" "github.com/julienschmidt/httprouter" errgo "gopkg.in/errgo.v1" httprequest "gopkg.in/httprequest.v1" ) type customError struct { httprequest.RemoteError } var handleTests = []struct { about string f func(c *qt.C) interface{} req *http.Request pathVar httprouter.Params expectMethod string expectPath string expectBody interface{} expectStatus int }{{ about: "function with no return", f: func(c *qt.C) interface{} { type testStruct struct { A string `httprequest:"a,path"` B map[string]int `httprequest:",body"` C int `httprequest:"c,form"` } return func(p httprequest.Params, s *testStruct) { c.Assert(s, qt.DeepEquals, &testStruct{ A: "A", B: map[string]int{"hello": 99}, C: 43, }) c.Assert(p.PathVar, qt.DeepEquals, httprouter.Params{{ Key: "a", Value: "A", }}) c.Assert(p.Request.Form, qt.DeepEquals, url.Values{ "c": {"43"}, }) c.Assert(p.PathPattern, qt.Equals, "") p.Response.Header().Set("Content-Type", "application/json") p.Response.Write([]byte("true")) } }, req: &http.Request{ Header: http.Header{"Content-Type": {"application/json"}}, Form: url.Values{ "c": {"43"}, }, Body: body(`{"hello": 99}`), }, pathVar: httprouter.Params{{ Key: "a", Value: "A", }}, expectBody: true, }, { about: "function with error return that returns no error", f: func(c *qt.C) interface{} { type testStruct struct { A int `httprequest:"a,path"` } return func(p httprequest.Params, s *testStruct) error { c.Assert(s, qt.DeepEquals, &testStruct{123}) c.Assert(p.PathPattern, qt.Equals, "") p.Response.Header().Set("Content-Type", "application/json") p.Response.Write([]byte("true")) return nil } }, req: &http.Request{}, pathVar: httprouter.Params{{ Key: "a", Value: "123", }}, expectBody: true, }, { about: "function with error return that returns an error", f: func(c *qt.C) interface{} { type testStruct struct { A int `httprequest:"a,path"` } return func(p httprequest.Params, s *testStruct) error { c.Assert(p.PathPattern, qt.Equals, "") c.Assert(s, qt.DeepEquals, &testStruct{123}) return errUnauth } }, req: &http.Request{}, pathVar: httprouter.Params{{ Key: "a", Value: "123", }}, expectBody: httprequest.RemoteError{ Message: errUnauth.Error(), Code: "unauthorized", }, expectStatus: http.StatusUnauthorized, }, { about: "function with value return that returns a value", f: func(c *qt.C) interface{} { type testStruct struct { A int `httprequest:"a,path"` } return func(p httprequest.Params, s *testStruct) (int, error) { c.Assert(p.PathPattern, qt.Equals, "") c.Assert(s, qt.DeepEquals, &testStruct{123}) return 1234, nil } }, req: &http.Request{}, pathVar: httprouter.Params{{ Key: "a", Value: "123", }}, expectBody: 1234, }, { about: "function with value return that returns an error", f: func(c *qt.C) interface{} { type testStruct struct { A int `httprequest:"a,path"` } return func(p httprequest.Params, s *testStruct) (int, error) { c.Assert(p.PathPattern, qt.Equals, "") c.Assert(s, qt.DeepEquals, &testStruct{123}) return 0, errUnauth } }, req: &http.Request{}, pathVar: httprouter.Params{{ Key: "a", Value: "123", }}, expectBody: httprequest.RemoteError{ Message: errUnauth.Error(), Code: "unauthorized", }, expectStatus: http.StatusUnauthorized, }, { about: "function with value return that writes to p.Response", f: func(c *qt.C) interface{} { type testStruct struct { A int `httprequest:"a,path"` } return func(p httprequest.Params, s *testStruct) (int, error) { c.Assert(p.PathPattern, qt.Equals, "") _, err := p.Response.Write(nil) c.Assert(err, qt.ErrorMatches, "inappropriate call to ResponseWriter.Write in JSON-returning handler") p.Response.WriteHeader(http.StatusTeapot) c.Assert(s, qt.DeepEquals, &testStruct{123}) return 1234, nil } }, req: &http.Request{}, pathVar: httprouter.Params{{ Key: "a", Value: "123", }}, expectBody: 1234, }, { about: "function with no Params and no return", f: func(c *qt.C) interface{} { type testStruct struct { A string `httprequest:"a,path"` B map[string]int `httprequest:",body"` C int `httprequest:"c,form"` } return func(s *testStruct) { c.Assert(s, qt.DeepEquals, &testStruct{ A: "A", B: map[string]int{"hello": 99}, C: 43, }) } }, req: &http.Request{ Header: http.Header{"Content-Type": {"application/json"}}, Form: url.Values{ "c": {"43"}, }, Body: body(`{"hello": 99}`), }, pathVar: httprouter.Params{{ Key: "a", Value: "A", }}, }, { about: "function with no Params with error return that returns no error", f: func(c *qt.C) interface{} { type testStruct struct { A int `httprequest:"a,path"` } return func(s *testStruct) error { c.Assert(s, qt.DeepEquals, &testStruct{123}) return nil } }, req: &http.Request{}, pathVar: httprouter.Params{{ Key: "a", Value: "123", }}, }, { about: "function with no Params with error return that returns an error", f: func(c *qt.C) interface{} { type testStruct struct { A int `httprequest:"a,path"` } return func(s *testStruct) error { c.Assert(s, qt.DeepEquals, &testStruct{123}) return errUnauth } }, req: &http.Request{}, pathVar: httprouter.Params{{ Key: "a", Value: "123", }}, expectBody: httprequest.RemoteError{ Message: errUnauth.Error(), Code: "unauthorized", }, expectStatus: http.StatusUnauthorized, }, { about: "function with no Params with value return that returns a value", f: func(c *qt.C) interface{} { type testStruct struct { A int `httprequest:"a,path"` } return func(s *testStruct) (int, error) { c.Assert(s, qt.DeepEquals, &testStruct{123}) return 1234, nil } }, req: &http.Request{}, pathVar: httprouter.Params{{ Key: "a", Value: "123", }}, expectBody: 1234, }, { about: "function with no Params with value return that returns an error", f: func(c *qt.C) interface{} { type testStruct struct { A int `httprequest:"a,path"` } return func(s *testStruct) (int, error) { c.Assert(s, qt.DeepEquals, &testStruct{123}) return 0, errUnauth } }, req: &http.Request{}, pathVar: httprouter.Params{{ Key: "a", Value: "123", }}, expectBody: httprequest.RemoteError{ Message: errUnauth.Error(), Code: "unauthorized", }, expectStatus: http.StatusUnauthorized, }, { about: "error when unmarshaling", f: func(c *qt.C) interface{} { type testStruct struct { A int `httprequest:"a,path"` } return func(p httprequest.Params, s *testStruct) (int, error) { c.Errorf("function should not have been called") return 0, nil } }, req: &http.Request{}, pathVar: httprouter.Params{{ Key: "a", Value: "not a number", }}, expectBody: httprequest.RemoteError{ Message: `cannot unmarshal parameters: cannot unmarshal into field A: cannot parse "not a number" into int: expected integer`, Code: "bad request", }, expectStatus: http.StatusBadRequest, }, { about: "error when unmarshaling, no Params", f: func(c *qt.C) interface{} { type testStruct struct { A int `httprequest:"a,path"` } return func(s *testStruct) (int, error) { c.Errorf("function should not have been called") return 0, nil } }, req: &http.Request{}, pathVar: httprouter.Params{{ Key: "a", Value: "not a number", }}, expectBody: httprequest.RemoteError{ Message: `cannot unmarshal parameters: cannot unmarshal into field A: cannot parse "not a number" into int: expected integer`, Code: "bad request", }, expectStatus: http.StatusBadRequest, }, { about: "error when unmarshaling single value return", f: func(c *qt.C) interface{} { type testStruct struct { A int `httprequest:"a,path"` } return func(p httprequest.Params, s *testStruct) error { c.Errorf("function should not have been called") return nil } }, req: &http.Request{}, pathVar: httprouter.Params{{ Key: "a", Value: "not a number", }}, expectBody: httprequest.RemoteError{ Message: `cannot unmarshal parameters: cannot unmarshal into field A: cannot parse "not a number" into int: expected integer`, Code: "bad request", }, expectStatus: http.StatusBadRequest, }, { about: "return type that can't be marshaled as JSON", f: func(c *qt.C) interface{} { return func(p httprequest.Params, s *struct{}) (chan int, error) { return make(chan int), nil } }, req: &http.Request{}, pathVar: httprouter.Params{}, expectBody: httprequest.RemoteError{ Message: "json: unsupported type: chan int", }, expectStatus: http.StatusInternalServerError, }, { about: "argument with route", f: func(c *qt.C) interface{} { type testStruct struct { httprequest.Route `httprequest:"GET /foo/:bar"` A string `httprequest:"bar,path"` } return func(p httprequest.Params, s *testStruct) { c.Check(s.A, qt.Equals, "val") c.Assert(p.PathPattern, qt.Equals, "/foo/:bar") } }, req: &http.Request{}, pathVar: httprouter.Params{{ Key: "bar", Value: "val", }}, expectMethod: "GET", expectPath: "/foo/:bar", }} func TestHandle(t *testing.T) { c := qt.New(t) for _, test := range handleTests { test := test c.Run(test.about, func(c *qt.C) { h := testServer.Handle(test.f(c)) c.Assert(h.Method, qt.Equals, test.expectMethod) c.Assert(h.Path, qt.Equals, test.expectPath) rec := httptest.NewRecorder() h.Handle(rec, test.req, test.pathVar) if test.expectStatus == 0 { test.expectStatus = http.StatusOK } qthttptest.AssertJSONResponse(c, rec, test.expectStatus, test.expectBody) }) } } var handlePanicTests = []struct { name string f interface{} expect string }{{ name: "not-a-function", f: 42, expect: "bad handler function: not a function", }, { name: "no-argument", f: func(httprequest.Params) {}, expect: "bad handler function: no argument parameter after Params argument", }, { name: "too-many-parameters", f: func(httprequest.Params, *struct{}, struct{}) {}, expect: "bad handler function: has 3 parameters, need 1 or 2", }, { name: "bad-return-type", f: func(httprequest.Params, *struct{}) struct{} { return struct{}{} }, expect: "bad handler function: final result parameter is struct {}, need error", }, { name: "bad-first-parameter", f: func(http.ResponseWriter, httprequest.Params) (struct{}, error) { return struct{}{}, nil }, expect: "bad handler function: first argument is http.ResponseWriter, need httprequest.Params", }, { name: "bad-final-return-type", f: func(httprequest.Params, *struct{}) (struct{}, struct{}) { return struct{}{}, struct{}{} }, expect: "bad handler function: final result parameter is struct {}, need error", }, { name: "bad-first-parameter2", f: func(*http.Request, *struct{}) {}, expect: `bad handler function: first argument is \*http.Request, need httprequest.Params`, }, { name: "parameter-not-pointer", f: func(httprequest.Params, struct{}) {}, expect: "bad handler function: last argument cannot be used for Unmarshal: type is not pointer to struct", }, { name: "invalid-tag", f: func(httprequest.Params, *struct { A int `httprequest:"a,the-ether"` }) { }, expect: `bad handler function: last argument cannot be used for Unmarshal: bad tag "httprequest:\\"a,the-ether\\"" in field A: unknown tag flag "the-ether"`, }, { name: "too-many-results", f: func(httprequest.Params, *struct{}) (a, b, c struct{}) { return }, expect: `bad handler function: has 3 result parameters, need 0, 1 or 2`, }, { name: "no-route-tag", f: func(*struct { httprequest.Route }) { }, expect: `bad handler function: last argument cannot be used for Unmarshal: bad route tag "": no httprequest tag`, }, { name: "invalid-route-tag", f: func(*struct { httprequest.Route `othertag:"foo"` }) { }, expect: `bad handler function: last argument cannot be used for Unmarshal: bad route tag "othertag:\\"foo\\"": no httprequest tag`, }, { name: "invalid-route-tag-value", f: func(*struct { httprequest.Route `httprequest:""` }) { }, expect: `bad handler function: last argument cannot be used for Unmarshal: bad route tag "httprequest:\\"\\"": no httprequest tag`, }, { name: "invalid-route-tag-too-many-fields", f: func(*struct { httprequest.Route `httprequest:"GET /foo /bar"` }) { }, expect: `bad handler function: last argument cannot be used for Unmarshal: bad route tag "httprequest:\\"GET /foo /bar\\"": wrong field count`, }, { name: "invalid-route-tag-invalid-method", f: func(*struct { httprequest.Route `httprequest:"BAD /foo"` }) { }, expect: `bad handler function: last argument cannot be used for Unmarshal: bad route tag "httprequest:\\"BAD /foo\\"": invalid method`, }} func TestHandlePanicsWithBadFunctions(t *testing.T) { c := qt.New(t) for _, test := range handlePanicTests { c.Run(test.name, func(c *qt.C) { c.Check(func() { testServer.Handle(test.f) }, qt.PanicMatches, test.expect) }) } } var handlersTests = []struct { calledMethod string callParams qthttptest.JSONCallParams expectPathPattern string }{{ calledMethod: "M1", callParams: qthttptest.JSONCallParams{ URL: "/m1/99", }, expectPathPattern: "/m1/:p", }, { calledMethod: "M2", callParams: qthttptest.JSONCallParams{ URL: "/m2/99", ExpectBody: 999, }, expectPathPattern: "/m2/:p", }, { calledMethod: "M3", callParams: qthttptest.JSONCallParams{ URL: "/m3/99", ExpectBody: &httprequest.RemoteError{ Message: "m3 error", }, ExpectStatus: http.StatusInternalServerError, }, expectPathPattern: "/m3/:p", }, { calledMethod: "M3Post", callParams: qthttptest.JSONCallParams{ Method: "POST", URL: "/m3/99", JSONBody: make(map[string]interface{}), }, expectPathPattern: "/m3/:p", }} func TestHandlers(t *testing.T) { c := qt.New(t) handleVal := testHandlers{ c: c, } f := func(p httprequest.Params) (*testHandlers, context.Context, error) { handleVal.p = p return &handleVal, p.Context, nil } handlers := testServer.Handlers(f) handlers1 := make([]httprequest.Handler, len(handlers)) copy(handlers1, handlers) for i := range handlers1 { handlers1[i].Handle = nil } expectHandlers := []httprequest.Handler{{ Method: "GET", Path: "/m1/:p", }, { Method: "GET", Path: "/m2/:p", }, { Method: "GET", Path: "/m3/:p", }, { Method: "POST", Path: "/m3/:p", }} c.Assert(handlers1, qt.DeepEquals, expectHandlers) c.Assert(handlersTests, qt.HasLen, len(expectHandlers)) router := httprouter.New() for _, h := range handlers { c.Logf("adding %s %s", h.Method, h.Path) router.Handle(h.Method, h.Path, h.Handle) } for _, test := range handlersTests { test := test c.Run(test.calledMethod, func(c *qt.C) { handleVal = testHandlers{ c: c, } test.callParams.Handler = router qthttptest.AssertJSONCall(c, test.callParams) c.Assert(handleVal.calledMethod, qt.Equals, test.calledMethod) c.Assert(handleVal.p.PathPattern, qt.Equals, test.expectPathPattern) }) } } type testHandlers struct { calledMethod string calledContext context.Context c *qt.C p httprequest.Params } func (h *testHandlers) M1(p httprequest.Params, arg *struct { httprequest.Route `httprequest:"GET /m1/:p"` P int `httprequest:"p,path"` }) { h.calledMethod = "M1" h.calledContext = p.Context h.c.Check(arg.P, qt.Equals, 99) h.c.Check(p.Response, qt.Equals, h.p.Response) h.c.Check(p.Request, qt.Equals, h.p.Request) h.c.Check(p.PathVar, qt.DeepEquals, h.p.PathVar) h.c.Check(p.PathPattern, qt.Equals, "/m1/:p") h.c.Check(p.Context, qt.Not(qt.IsNil)) } type m2Request struct { httprequest.Route `httprequest:"GET /m2/:p"` P int `httprequest:"p,path"` } func (h *testHandlers) M2(arg *m2Request) (int, error) { h.calledMethod = "M2" h.c.Check(arg.P, qt.Equals, 99) return 999, nil } func (h *testHandlers) unexported() { } func (h *testHandlers) M3(arg *struct { httprequest.Route `httprequest:"GET /m3/:p"` P int `httprequest:"p,path"` }) (int, error) { h.calledMethod = "M3" h.c.Check(arg.P, qt.Equals, 99) return 0, errgo.New("m3 error") } func (h *testHandlers) M3Post(arg *struct { httprequest.Route `httprequest:"POST /m3/:p"` P int `httprequest:"p,path"` }) { h.calledMethod = "M3Post" h.c.Check(arg.P, qt.Equals, 99) } func TestHandlersRootFuncWithRequestArg(t *testing.T) { c := qt.New(t) handleVal := testHandlers{ c: c, } var gotArg interface{} f := func(p httprequest.Params, arg interface{}) (*testHandlers, context.Context, error) { gotArg = arg return &handleVal, p.Context, nil } router := httprouter.New() for _, h := range testServer.Handlers(f) { router.Handle(h.Method, h.Path, h.Handle) } qthttptest.AssertJSONCall(c, qthttptest.JSONCallParams{ Handler: router, URL: "/m2/99", ExpectBody: 999, }) c.Assert(gotArg, qt.DeepEquals, &m2Request{ P: 99, }) } func TestHandlersRootFuncReturningInterface(t *testing.T) { c := qt.New(t) handleVal := testHandlers{ c: c, } type testHandlersI interface { M2(arg *m2Request) (int, error) } f := func(p httprequest.Params) (testHandlersI, context.Context, error) { return &handleVal, p.Context, nil } router := httprouter.New() for _, h := range testServer.Handlers(f) { router.Handle(h.Method, h.Path, h.Handle) } qthttptest.AssertJSONCall(c, qthttptest.JSONCallParams{ Handler: router, URL: "/m2/99", ExpectBody: 999, }) } func TestHandlersRootFuncWithIncompatibleRequestArg(t *testing.T) { c := qt.New(t) handleVal := testHandlers{ c: c, } f := func(p httprequest.Params, arg interface { Foo() }) (*testHandlers, context.Context, error) { return &handleVal, p.Context, nil } c.Assert(func() { testServer.Handlers(f) }, qt.PanicMatches, `bad type for method M1: argument of type \*struct {.*} does not implement interface required by root handler interface \{ Foo\(\) \}`) } func TestHandlersRootFuncWithNonEmptyInterfaceRequestArg(t *testing.T) { c := qt.New(t) type tester interface { Test() string } var argResult string f := func(p httprequest.Params, arg tester) (*handlersWithRequestMethod, context.Context, error) { argResult = arg.Test() return &handlersWithRequestMethod{}, p.Context, nil } router := httprouter.New() for _, h := range testServer.Handlers(f) { router.Handle(h.Method, h.Path, h.Handle) } qthttptest.AssertJSONCall(c, qthttptest.JSONCallParams{ Handler: router, URL: "/x1/something", ExpectBody: "something", }) c.Assert(argResult, qt.DeepEquals, "test something") } var badHandlersFuncTests = []struct { about string f interface{} expectPanic string }{{ about: "not a function", f: 123, expectPanic: "bad handler function: expected function, got int", }, { about: "nil function", f: (func())(nil), expectPanic: "bad handler function: function is nil", }, { about: "no arguments", f: func() {}, expectPanic: "bad handler function: got 0 arguments, want 1 or 2", }, { about: "more than two argument", f: func(http.ResponseWriter, *http.Request, int) {}, expectPanic: "bad handler function: got 3 arguments, want 1 or 2", }, { about: "no return values", f: func(httprequest.Params) {}, expectPanic: `bad handler function: function returns 0 values, want \(, context.Context, error\)`, }, { about: "only one return value", f: func(httprequest.Params) string { return "" }, expectPanic: `bad handler function: function returns 1 values, want \(, context.Context, error\)`, }, { about: "only two return values", f: func(httprequest.Params) (_ arithHandler, _ error) { return }, expectPanic: `bad handler function: function returns 2 values, want \(, context.Context, error\)`, }, { about: "too many return values", f: func(httprequest.Params) (_ string, _ error, _ error, _ error) { return }, expectPanic: `bad handler function: function returns 4 values, want \(, context.Context, error\)`, }, { about: "invalid first argument", f: func(string) (_ string, _ context.Context, _ error) { return }, expectPanic: `bad handler function: invalid first argument, want httprequest.Params, got string`, }, { about: "second argument not an interface", f: func(httprequest.Params, *http.Request) (_ string, _ context.Context, _ error) { return }, expectPanic: `bad handler function: invalid second argument, want interface type, got \*http.Request`, }, { about: "non-error return", f: func(httprequest.Params) (_ string, _ context.Context, _ string) { return }, expectPanic: `bad handler function: invalid third return parameter, want error, got string`, }, { about: "non-context return", f: func(httprequest.Params) (_ arithHandler, _ string, _ error) { return }, expectPanic: `bad handler function: second return parameter of type string does not implement context.Context`, }, { about: "no methods on return type", f: func(httprequest.Params) (_ string, _ context.Context, _ error) { return }, expectPanic: `no exported methods defined on string`, }, { about: "method with invalid parameter count", f: func(httprequest.Params) (_ badHandlersType1, _ context.Context, _ error) { return }, expectPanic: `bad type for method M: has 3 parameters, need 1 or 2`, }, { about: "method with invalid route", f: func(httprequest.Params) (_ badHandlersType2, _ context.Context, _ error) { return }, expectPanic: `method M does not specify route method and path`, }, { about: "bad type for close method", f: func(httprequest.Params) (_ badHandlersType3, _ context.Context, _ error) { return }, expectPanic: `bad type for Close method \(got func\(httprequest_test\.badHandlersType3\) want func\(httprequest_test.badHandlersType3\) error`, }} type badHandlersType1 struct{} func (badHandlersType1) M(a, b, c int) { } type badHandlersType2 struct{} func (badHandlersType2) M(*struct { P int `httprequest:",path"` }) { } type badHandlersType3 struct{} func (badHandlersType3) M(arg *struct { httprequest.Route `httprequest:"GET /m1/:P"` P int `httprequest:",path"` }) { } func (badHandlersType3) Close() { } func TestBadHandlersFunc(t *testing.T) { c := qt.New(t) for _, test := range badHandlersFuncTests { test := test c.Run(test.about, func(c *qt.C) { c.Check(func() { testServer.Handlers(test.f) }, qt.PanicMatches, test.expectPanic) }) } } func TestHandlersFuncReturningError(t *testing.T) { c := qt.New(t) handlers := testServer.Handlers(func(p httprequest.Params) (*testHandlers, context.Context, error) { return nil, p.Context, errgo.WithCausef(errgo.New("failure"), errUnauth, "something") }) router := httprouter.New() for _, h := range handlers { router.Handle(h.Method, h.Path, h.Handle) } qthttptest.AssertJSONCall(c, qthttptest.JSONCallParams{ URL: "/m1/99", Handler: router, ExpectStatus: http.StatusUnauthorized, ExpectBody: &httprequest.RemoteError{ Message: "something: failure", Code: "unauthorized", }, }) } func TestHandlersFuncReturningCustomContext(t *testing.T) { c := qt.New(t) handleVal := testHandlers{ c: c, } handlers := testServer.Handlers(func(p httprequest.Params) (*testHandlers, context.Context, error) { handleVal.p = p ctx := context.WithValue(p.Context, "some key", "some value") return &handleVal, ctx, nil }) router := httprouter.New() for _, h := range handlers { router.Handle(h.Method, h.Path, h.Handle) } qthttptest.AssertJSONCall(c, qthttptest.JSONCallParams{ URL: "/m1/99", Handler: router, }) c.Assert(handleVal.calledContext, qt.Not(qt.IsNil)) c.Assert(handleVal.calledContext.Value("some key"), qt.Equals, "some value") } type closeHandlersType struct { p int closed bool } func (h *closeHandlersType) M(arg *struct { httprequest.Route `httprequest:"GET /m1/:P"` P int `httprequest:",path"` }) { h.p = arg.P } func (h *closeHandlersType) Close() error { h.closed = true return nil } func TestHandlersWithTypeThatImplementsIOCloser(t *testing.T) { c := qt.New(t) var v closeHandlersType handlers := testServer.Handlers(func(p httprequest.Params) (*closeHandlersType, context.Context, error) { return &v, p.Context, nil }) router := httprouter.New() for _, h := range handlers { router.Handle(h.Method, h.Path, h.Handle) } qthttptest.AssertJSONCall(c, qthttptest.JSONCallParams{ URL: "/m1/99", Handler: router, }) c.Assert(v.closed, qt.Equals, true) c.Assert(v.p, qt.Equals, 99) } func TestBadForm(t *testing.T) { c := qt.New(t) h := testServer.Handle(func(p httprequest.Params, _ *struct{}) { c.Fatalf("shouldn't be called") }) testBadForm(c, h.Handle) } func TestBadFormNoParams(t *testing.T) { c := qt.New(t) h := testServer.Handle(func(_ *struct{}) { c.Fatalf("shouldn't be called") }) testBadForm(c, h.Handle) } func testBadForm(c *qt.C, h httprouter.Handle) { rec := httptest.NewRecorder() req := &http.Request{ Method: "POST", Header: http.Header{ "Content-Type": {"application/x-www-form-urlencoded"}, }, Body: body("%6"), } h(rec, req, httprouter.Params{}) qthttptest.AssertJSONResponse(c, rec, http.StatusBadRequest, httprequest.RemoteError{ Message: `cannot parse HTTP request form: invalid URL escape "%6"`, Code: "bad request", }) } func TestToHTTP(t *testing.T) { c := qt.New(t) h := httprequest.ToHTTP(testServer.Handle(func(p httprequest.Params, s *struct{}) { c.Check(p.PathVar, qt.IsNil) p.Response.WriteHeader(http.StatusOK) }).Handle) rec := httptest.NewRecorder() req := &http.Request{ Body: body(""), } h.ServeHTTP(rec, req) c.Assert(rec.Code, qt.Equals, http.StatusOK) } func TestToHTTPPreservesParams(t *testing.T) { c := qt.New(t) rh := testServer.Handle(func(p httprequest.Params, s *struct { httprequest.Route `httprequest:"GET /x/:p1/:p2"` P1 int `httprequest:"p1,path"` P2 string `httprequest:"p2,path"` }) { c.Check(s.P1, qt.Equals, 99) c.Check(s.P2, qt.Equals, "foo") p.Response.WriteHeader(http.StatusTeapot) }) router := httprouter.New() router.Handle(rh.Method, rh.Path, rh.Handle) rec := httptest.NewRecorder() req, _ := http.NewRequest("GET", "http://0.1.2.3/x/99/foo", nil) router.ServeHTTP(rec, req) c.Assert(rec.Code, qt.Equals, http.StatusTeapot) } func TestWriteJSON(t *testing.T) { c := qt.New(t) rec := httptest.NewRecorder() type Number struct { N int } err := httprequest.WriteJSON(rec, http.StatusTeapot, Number{1234}) c.Assert(err, qt.IsNil) c.Assert(rec.Code, qt.Equals, http.StatusTeapot) c.Assert(rec.Body.String(), qt.Equals, `{"N":1234}`) c.Assert(rec.Header().Get("content-type"), qt.Equals, "application/json") } var ( errUnauth = errors.New("unauth") errBadReq = errors.New("bad request") errOther = errors.New("other") errCustomHeaders = errors.New("custom headers") errUnmarshalableError = errors.New("unmarshalable error") errNil = errors.New("nil result") ) type HeaderNumber struct { N int } func (HeaderNumber) SetHeader(h http.Header) { h.Add("some-custom-header", "yes") } func TestSetHeader(t *testing.T) { c := qt.New(t) rec := httptest.NewRecorder() err := httprequest.WriteJSON(rec, http.StatusTeapot, HeaderNumber{1234}) c.Assert(err, qt.Equals, nil) c.Assert(rec.Code, qt.Equals, http.StatusTeapot) c.Assert(rec.Body.String(), qt.Equals, `{"N":1234}`) c.Assert(rec.Header().Get("content-type"), qt.Equals, "application/json") c.Assert(rec.Header().Get("some-custom-header"), qt.Equals, "yes") } var testServer = httprequest.Server{ ErrorMapper: testErrorMapper, } func testErrorMapper(_ context.Context, err error) (int, interface{}) { resp := &httprequest.RemoteError{ Message: err.Error(), } status := http.StatusInternalServerError switch errgo.Cause(err) { case errUnauth: status = http.StatusUnauthorized resp.Code = "unauthorized" case errBadReq, httprequest.ErrUnmarshal: status = http.StatusBadRequest resp.Code = "bad request" case errCustomHeaders: return http.StatusNotAcceptable, httprequest.CustomHeader{ Body: resp, SetHeaderFunc: func(h http.Header) { h.Set("Acceptability", "not at all") }, } case errUnmarshalableError: return http.StatusTeapot, make(chan int) case errNil: return status, nil } return status, &resp } var writeErrorTests = []struct { about string err error srv httprequest.Server assertResponse func(c *qt.C, rec *httptest.ResponseRecorder) expectStatus int expectResp *httprequest.RemoteError expectHeader http.Header }{{ about: "unauthorized error", err: errUnauth, srv: testServer, assertResponse: assertErrorResponse( http.StatusUnauthorized, &httprequest.RemoteError{ Message: errUnauth.Error(), Code: "unauthorized", }, nil, ), }, { about: "bad request error", err: errBadReq, srv: testServer, assertResponse: assertErrorResponse( http.StatusBadRequest, &httprequest.RemoteError{ Message: errBadReq.Error(), Code: "bad request", }, nil, ), }, { about: "unclassified error", err: errOther, srv: testServer, assertResponse: assertErrorResponse( http.StatusInternalServerError, &httprequest.RemoteError{ Message: errOther.Error(), }, nil, ), }, { about: "nil body", err: errNil, srv: testServer, assertResponse: assertErrorResponse( http.StatusInternalServerError, (*httprequest.RemoteError)(nil), nil, ), }, { about: "custom headers", err: errCustomHeaders, srv: testServer, assertResponse: assertErrorResponse( http.StatusNotAcceptable, &httprequest.RemoteError{ Message: errCustomHeaders.Error(), }, http.Header{ "Acceptability": {"not at all"}, }, ), }, { about: "unmarshalable error", err: errUnmarshalableError, srv: testServer, assertResponse: assertErrorResponse( http.StatusInternalServerError, &httprequest.RemoteError{ Message: `cannot marshal error response "unmarshalable error": json: unsupported type: chan int`, }, nil, ), }, { about: "error with default error mapper", err: errgo.Newf("some error"), srv: httprequest.Server{}, assertResponse: assertErrorResponse( http.StatusInternalServerError, &httprequest.RemoteError{ Message: "some error", }, nil, ), }, { about: "default error mapper with specific error code", err: httprequest.Errorf(httprequest.CodeBadRequest, "some bad request %d", 99), srv: httprequest.Server{}, assertResponse: assertErrorResponse( http.StatusBadRequest, &httprequest.RemoteError{ Message: "some bad request 99", Code: httprequest.CodeBadRequest, }, nil, ), }, { about: "edefault error mapper with specific error code with wrapped error", err: errgo.NoteMask(httprequest.Errorf(httprequest.CodeBadRequest, "some bad request %d", 99), "wrap", errgo.Any), srv: httprequest.Server{}, assertResponse: assertErrorResponse( http.StatusBadRequest, &httprequest.RemoteError{ Message: "wrap: some bad request 99", Code: httprequest.CodeBadRequest, }, nil, ), }, { about: "default error mapper with specific error code with wrapped error", err: errgo.NoteMask(httprequest.Errorf(httprequest.CodeBadRequest, "some bad request %d", 99), "wrap", errgo.Any), srv: httprequest.Server{}, assertResponse: assertErrorResponse( http.StatusBadRequest, &httprequest.RemoteError{ Message: "wrap: some bad request 99", Code: httprequest.CodeBadRequest, }, nil, ), }, { about: "default error mapper with custom error with ErrorCode implementation", err: &customError{ RemoteError: httprequest.RemoteError{ Code: httprequest.CodeNotFound, Message: "bar", }, }, srv: httprequest.Server{}, assertResponse: assertErrorResponse( http.StatusNotFound, &httprequest.RemoteError{ Message: "bar", Code: httprequest.CodeNotFound, }, nil, ), }, { about: "error writer", err: errBadReq, srv: httprequest.Server{ ErrorWriter: func(ctx context.Context, w http.ResponseWriter, err error) { fmt.Fprintf(w, "custom error") }, }, assertResponse: func(c *qt.C, rec *httptest.ResponseRecorder) { c.Assert(rec.Body.String(), qt.Equals, "custom error") c.Assert(rec.Code, qt.Equals, http.StatusOK) }, }, { about: "error writer overrides error mapper", err: errBadReq, srv: httprequest.Server{ ErrorWriter: func(ctx context.Context, w http.ResponseWriter, err error) { fmt.Fprintf(w, "custom error") }, ErrorMapper: func(_ context.Context, err error) (int, interface{}) { return http.StatusInternalServerError, nil }, }, assertResponse: func(c *qt.C, rec *httptest.ResponseRecorder) { c.Assert(rec.Body.String(), qt.Equals, "custom error") c.Assert(rec.Code, qt.Equals, http.StatusOK) }, }} func TestErrorfWithEmptyMessage(t *testing.T) { c := qt.New(t) err := httprequest.Errorf(httprequest.CodeNotFound, "") c.Assert(err, qt.DeepEquals, &httprequest.RemoteError{ Message: httprequest.CodeNotFound, Code: httprequest.CodeNotFound, }) } func TestWriteError(t *testing.T) { c := qt.New(t) for _, test := range writeErrorTests { test := test c.Run(test.err.Error(), func(c *qt.C) { rec := httptest.NewRecorder() test.srv.WriteError(context.TODO(), rec, test.err) test.assertResponse(c, rec) }) } } func TestHandleErrors(t *testing.T) { c := qt.New(t) req := new(http.Request) params := httprouter.Params{} // Test when handler returns an error. handler := testServer.HandleErrors(func(p httprequest.Params) error { c.Assert(p.Request, requestEquals, req) c.Assert(p.PathVar, qt.DeepEquals, params) c.Assert(p.PathPattern, qt.Equals, "") ctx := p.Context c.Assert(ctx, qt.Not(qt.IsNil)) return errUnauth }) rec := httptest.NewRecorder() handler(rec, req, params) c.Assert(rec.Code, qt.Equals, http.StatusUnauthorized) resp := parseErrorResponse(c, rec.Body.Bytes()) c.Assert(resp, qt.DeepEquals, &httprequest.RemoteError{ Message: errUnauth.Error(), Code: "unauthorized", }) // Test when handler returns nil. handler = testServer.HandleErrors(func(p httprequest.Params) error { c.Assert(p.Request, requestEquals, req) c.Assert(p.PathVar, qt.DeepEquals, params) c.Assert(p.PathPattern, qt.Equals, "") ctx := p.Context c.Assert(ctx, qt.Not(qt.IsNil)) p.Response.WriteHeader(http.StatusCreated) p.Response.Write([]byte("something")) return nil }) rec = httptest.NewRecorder() handler(rec, req, params) c.Assert(rec.Code, qt.Equals, http.StatusCreated) c.Assert(rec.Body.String(), qt.Equals, "something") } var handleErrorsWithErrorAfterWriteHeaderTests = []struct { about string causeWriteHeader func(w http.ResponseWriter) }{{ about: "write", causeWriteHeader: func(w http.ResponseWriter) { w.Write([]byte("")) }, }, { about: "write header", causeWriteHeader: func(w http.ResponseWriter) { w.WriteHeader(http.StatusOK) }, }, { about: "flush", causeWriteHeader: func(w http.ResponseWriter) { w.(http.Flusher).Flush() }, }} func TestHandleErrorsWithErrorAfterWriteHeader(t *testing.T) { c := qt.New(t) for i, test := range handleErrorsWithErrorAfterWriteHeaderTests { c.Logf("test %d: %s", i, test.about) handler := testServer.HandleErrors(func(p httprequest.Params) error { test.causeWriteHeader(p.Response) return errgo.New("unexpected") }) rec := httptest.NewRecorder() handler(rec, new(http.Request), nil) c.Assert(rec.Code, qt.Equals, http.StatusOK) c.Assert(rec.Body.String(), qt.Equals, "") } } func TestHandleJSON(t *testing.T) { c := qt.New(t) req := new(http.Request) params := httprouter.Params{} // Test when handler returns an error. handler := testServer.HandleJSON(func(p httprequest.Params) (interface{}, error) { c.Assert(p.Request, requestEquals, req) c.Assert(p.PathVar, qt.DeepEquals, params) c.Assert(p.PathPattern, qt.Equals, "") return nil, errUnauth }) rec := httptest.NewRecorder() handler(rec, new(http.Request), params) resp := parseErrorResponse(c, rec.Body.Bytes()) c.Assert(resp, qt.DeepEquals, &httprequest.RemoteError{ Message: errUnauth.Error(), Code: "unauthorized", }) c.Assert(rec.Code, qt.Equals, http.StatusUnauthorized) // Test when handler returns a body. handler = testServer.HandleJSON(func(p httprequest.Params) (interface{}, error) { c.Assert(p.Request, requestEquals, req) c.Assert(p.PathVar, qt.DeepEquals, params) c.Assert(p.PathPattern, qt.Equals, "") p.Response.Header().Set("Some-Header", "value") return "something", nil }) rec = httptest.NewRecorder() handler(rec, req, params) c.Assert(rec.Code, qt.Equals, http.StatusOK) c.Assert(rec.Body.String(), qt.Equals, `"something"`) c.Assert(rec.Header().Get("Some-Header"), qt.Equals, "value") } var requestEquals = qt.CmpEquals(cmpopts.IgnoreUnexported(http.Request{})) type handlersWithRequestMethod struct{} type x1Request struct { httprequest.Route `httprequest:"GET /x1/:p"` P string `httprequest:"p,path"` } func (r *x1Request) Test() string { return "test " + r.P } func (h *handlersWithRequestMethod) X1(arg *x1Request) (string, error) { return arg.P, nil } func assertErrorResponse(code int, body interface{}, header http.Header) func(c *qt.C, rec *httptest.ResponseRecorder) { return func(c *qt.C, rec *httptest.ResponseRecorder) { resp := reflect.New(reflect.ValueOf(body).Type()) err := json.Unmarshal(rec.Body.Bytes(), resp.Interface()) c.Assert(err, qt.Equals, nil) c.Assert(resp.Elem().Interface(), qt.DeepEquals, body) c.Assert(rec.Code, qt.Equals, code) for name, vals := range header { c.Assert(rec.HeaderMap[name], qt.DeepEquals, vals) } } } func parseErrorResponse(c *qt.C, body []byte) *httprequest.RemoteError { var errResp *httprequest.RemoteError err := json.Unmarshal(body, &errResp) c.Assert(err, qt.Equals, nil) return errResp } httprequest-1.2.1/marshal.go000066400000000000000000000311471365452616600161050ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package httprequest import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "reflect" "strings" "github.com/julienschmidt/httprouter" "gopkg.in/errgo.v1" ) // Marshal is the counterpart of Unmarshal. It takes information from // x, which must be a pointer to a struct, and returns an HTTP request // using the given method that holds all of the information. // // The Body field in the returned request will always be of type // BytesReaderCloser. // // If x implements the HeaderSetter interface, its SetHeader method will // be called to add additional headers to the HTTP request after it has // been marshaled. If x is pointer to a CustomHeader object then Marshal will use // its Body member to create the HTTP request. // // The HTTP request will use the given method. Named fields in the given // baseURL will be filled out from "path"-tagged fields in x to form the // URL path in the returned request. These are specified as for httprouter. // // If a field in baseURL is a suffix of the form "*var" (a trailing wildcard element // that holds the rest of the path), the marshaled string must begin with a "/". // This matches the httprouter convention that it always returns such fields // with a "/" prefix. // // If a field is of type string or []string, the value of the field will // be used directly; otherwise if implements encoding.TextMarshaler, that // will be used to marshal the field, otherwise fmt.Sprint will be used. // // An "omitempty" attribute on a form or header field specifies that // if the form or header value is zero, the form or header entry // will be omitted. If the field is a nil pointer, it will be omitted; // otherwise if the field type implements IsZeroer, that method // will be used to determine whether the value is zero, otherwise // if the value is comparable, it will be compared with the zero // value for its type, otherwise the value will never be omitted. // One notable implementation of IsZeroer is time.Time. // // An "inbody" attribute on a form field specifies that the field will // be marshaled as part of an application/x-www-form-urlencoded body. // Note that the field may still be unmarshaled from either a URL query // parameter or a form-encoded body. // // For example, this code: // // type UserDetails struct { // Age int // } // // type Test struct { // Username string `httprequest:"user,path"` // ContextId int64 `httprequest:"context,form"` // Extra string `httprequest:"context,form,omitempty"` // Details UserDetails `httprequest:",body"` // } // req, err := Marshal("http://example.com/users/:user/details", "GET", &Test{ // Username: "bob", // ContextId: 1234, // Details: UserDetails{ // Age: 36, // } // }) // if err != nil { // ... // } // // will produce an HTTP request req with a URL of // http://example.com/users/bob/details?context=1234 and a JSON-encoded // body holding `{"Age":36}`. // // It is an error if there is a field specified in the URL that is not // found in x. func Marshal(baseURL, method string, x interface{}) (*http.Request, error) { var xv reflect.Value if ch, ok := x.(*CustomHeader); ok { xv = reflect.ValueOf(ch.Body) } else { xv = reflect.ValueOf(x) } pt, err := getRequestType(xv.Type()) if err != nil { return nil, errgo.WithCausef(err, ErrBadUnmarshalType, "bad type %s", xv.Type()) } req, err := http.NewRequest(method, baseURL, BytesReaderCloser{bytes.NewReader(nil)}) if err != nil { return nil, errgo.Mask(err) } req.GetBody = func() (io.ReadCloser, error) { return BytesReaderCloser{bytes.NewReader(nil)}, nil } req.Form = url.Values{} if pt.formBody { // Use req.PostForm as a place to put the values that // will be marshaled as part of the form body. // It's ignored by http.Client, but that's OK because // we'll make the body ourselves later. req.PostForm = url.Values{} } p := &Params{ Request: req, } if err := marshal(p, xv, pt); err != nil { return nil, errgo.Mask(err, errgo.Is(ErrUnmarshal)) } if pt.formBody { data := []byte(req.PostForm.Encode()) p.Request.Body = BytesReaderCloser{bytes.NewReader(data)} p.Request.GetBody = func() (io.ReadCloser, error) { return BytesReaderCloser{bytes.NewReader(data)}, nil } p.Request.ContentLength = int64(len(data)) p.Request.Header.Set("Content-Type", "application/x-www-form-urlencoded") p.Request.PostForm = nil } if headerSetter, ok := x.(HeaderSetter); ok { headerSetter.SetHeader(p.Request.Header) } return p.Request, nil } // marshal is the internal version of Marshal. func marshal(p *Params, xv reflect.Value, pt *requestType) error { xv = xv.Elem() for _, f := range pt.fields { fv := xv.FieldByIndex(f.index) if f.isPointer { if fv.IsNil() { continue } fv = fv.Elem() } // TODO store the field name in the field so // that we can produce a nice error message. if err := f.marshal(fv, p); err != nil { return errgo.WithCausef(err, ErrUnmarshal, "cannot marshal field") } } path, err := buildPath(p.Request.URL.Path, p.PathVar) if err != nil { return errgo.Mask(err) } p.Request.URL.Path = path if q := p.Request.Form.Encode(); q != "" && p.Request.URL.RawQuery != "" { p.Request.URL.RawQuery += "&" + q } else { p.Request.URL.RawQuery += q } return nil } func buildPath(path string, p httprouter.Params) (string, error) { pathBytes := make([]byte, 0, len(path)*2) for { s, rest := nextPathSegment(path) if s == "" { break } if s[0] != ':' && s[0] != '*' { pathBytes = append(pathBytes, s...) path = rest continue } if s[0] == '*' && rest != "" { return "", errgo.New("star path parameter is not at end of path") } if len(s) == 1 { return "", errgo.New("empty path parameter") } val := p.ByName(s[1:]) if val == "" { return "", errgo.Newf("missing value for path parameter %q", s[1:]) } if s[0] == '*' { if !strings.HasPrefix(val, "/") { return "", errgo.Newf("value %q for path parameter %q does not start with required /", val, s) } val = val[1:] } pathBytes = append(pathBytes, val...) path = rest } return string(pathBytes), nil } // nextPathSegment returns the next wildcard or constant // segment of the given path and everything after that // segment. func nextPathSegment(s string) (string, string) { if s == "" { return "", "" } if s[0] == ':' || s[0] == '*' { if i := strings.Index(s, "/"); i != -1 { return s[0:i], s[i:] } return s, "" } if i := strings.IndexAny(s, ":*"); i != -1 { return s[0:i], s[i:] } return s, "" } // getMarshaler returns a marshaler function suitable for marshaling // a field with the given tag into an HTTP request. func getMarshaler(tag tag, t reflect.Type) (marshaler, error) { switch { case tag.source == sourceNone: return marshalNop, nil case tag.source == sourceBody: return marshalBody, nil case t == reflect.TypeOf([]string(nil)): switch tag.source { default: return nil, errgo.New("invalid target type []string for path parameter") case sourceForm: return marshalAllForm(tag.name), nil case sourceFormBody: return marshalAllFormBody(tag.name), nil case sourceHeader: return marshalAllHeader(tag.name), nil } case t == reflect.TypeOf(""): return marshalString(tag), nil case implementsTextMarshaler(t): return marshalWithMarshalText(t, tag), nil default: return marshalWithSprint(t, tag), nil } } // marshalNop does nothing with the value. func marshalNop(v reflect.Value, p *Params) error { return nil } // marshalBody marshals the specified value into the body of the http request. func marshalBody(v reflect.Value, p *Params) error { // TODO allow body types that aren't necessarily JSON. data, err := json.Marshal(v.Addr().Interface()) if err != nil { return errgo.Notef(err, "cannot marshal request body") } p.Request.Body = BytesReaderCloser{bytes.NewReader(data)} p.Request.GetBody = func() (io.ReadCloser, error) { return BytesReaderCloser{bytes.NewReader(data)}, nil } p.Request.ContentLength = int64(len(data)) p.Request.Header.Set("Content-Type", "application/json") return nil } // marshalAllForm marshals a []string slice into form fields. func marshalAllForm(name string) marshaler { return func(v reflect.Value, p *Params) error { if ss := v.Interface().([]string); len(ss) > 0 { p.Request.Form[name] = ss } return nil } } // marshalAllFormBody marshals a []string slice into form body fields. func marshalAllFormBody(name string) marshaler { return func(v reflect.Value, p *Params) error { if ss := v.Interface().([]string); len(ss) > 0 { p.Request.PostForm[name] = ss } return nil } } // marshalAllHeader marshals a []string slice into a header. func marshalAllHeader(name string) marshaler { return func(v reflect.Value, p *Params) error { if ss := v.Interface().([]string); len(ss) > 0 { p.Request.Header[name] = ss } return nil } } // marshalString marshals s string field. func marshalString(tag tag) marshaler { formSet := formSetter(tag) return func(v reflect.Value, p *Params) error { s := v.String() if tag.omitempty && s == "" { return nil } formSet(tag.name, v.String(), p) return nil } } // encodingTextMarshaler is the same as encoding.TextUnmarshaler // but avoids us importing the encoding package, which some // broken gccgo installations do not allow. // TODO remove this and use encoding.TextMarshaler instead. type encodingTextMarshaler interface { MarshalText() (text []byte, err error) } var textMarshalerType = reflect.TypeOf((*encodingTextMarshaler)(nil)).Elem() func implementsTextMarshaler(t reflect.Type) bool { // Use the pointer type, because a pointer // type will implement a superset of the methods // of a non-pointer type. return reflect.PtrTo(t).Implements(textMarshalerType) } // marshalWithMarshalText returns a marshaler // that marshals the given type from the given tag // using its MarshalText method. func marshalWithMarshalText(t reflect.Type, tag tag) marshaler { formSet := formSetter(tag) omit := omitter(t, tag) return func(v reflect.Value, p *Params) error { if omit(v) { return nil } m := v.Addr().Interface().(encodingTextMarshaler) data, err := m.MarshalText() if err != nil { return errgo.Mask(err) } formSet(tag.name, string(data), p) return nil } } // IsZeroer is used when marshaling to determine if a value // is zero (see Marshal). type IsZeroer interface { IsZero() bool } var isZeroerType = reflect.TypeOf((*IsZeroer)(nil)).Elem() // omitter returns a function that determins if a value // with the given type and tag should be omitted from // marshal output. The value passed to the function // will be the underlying value, not its address. // // It returns nil if the value should never be omitted. func omitter(t reflect.Type, tag tag) func(reflect.Value) bool { never := func(reflect.Value) bool { return false } if !tag.omitempty { return never } if reflect.PtrTo(t).Implements(isZeroerType) { return func(v reflect.Value) bool { return v.Addr().Interface().(IsZeroer).IsZero() } } if t.Comparable() { zeroVal := reflect.Zero(t).Interface() return func(v reflect.Value) bool { return v.Interface() == zeroVal } } return never } // marshalWithSprint returns an marshaler // that unmarshals the given tag using fmt.Sprint. func marshalWithSprint(t reflect.Type, tag tag) marshaler { formSet := formSetter(tag) omit := omitter(t, tag) return func(v reflect.Value, p *Params) error { if omit(v) { return nil } formSet(tag.name, fmt.Sprint(v.Interface()), p) return nil } } // formSetter returns a function that can set the value // for a given tag. func formSetter(t tag) func(name, value string, p *Params) { formSet := formSetters[t.source] if formSet == nil { panic("unexpected source") } if !t.omitempty { return formSet } return func(name, value string, p *Params) { if value != "" { formSet(name, value, p) } } } // formSetters maps from source to a function that // sets the value for a given key. var formSetters = []func(string, string, *Params){ sourceForm: func(name, value string, p *Params) { p.Request.Form.Set(name, value) }, sourceFormBody: func(name, value string, p *Params) { p.Request.PostForm.Set(name, value) }, sourcePath: func(name, value string, p *Params) { p.PathVar = append(p.PathVar, httprouter.Param{Key: name, Value: value}) }, sourceBody: nil, sourceHeader: func(name, value string, p *Params) { p.Request.Header.Set(name, value) }, } // BytesReaderCloser is a bytes.Reader which // implements io.Closer with a no-op Close method. type BytesReaderCloser struct { *bytes.Reader } // Close implements io.Closer.Close. func (BytesReaderCloser) Close() error { return nil } httprequest-1.2.1/marshal_test.go000066400000000000000000000355401365452616600171450ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package httprequest_test import ( "fmt" "io/ioutil" "net/http" "testing" "time" qt "github.com/frankban/quicktest" "gopkg.in/errgo.v1" "gopkg.in/httprequest.v1" ) type embedded struct { F1 string `json:"name"` F2 int `json:"age"` F3 *string `json:"address"` } var marshalTests = []struct { about string urlString string method string val interface{} expectURLString string expectBody *string expectHeader http.Header expectError string }{{ about: "struct with simple fields", urlString: "http://localhost:8081/:F01", val: &struct { F01 int `httprequest:",path"` F02 string `httprequest:",form"` F03 string `httprequest:",form,omitempty"` F04 string `httprequest:",form,omitempty"` F06 time.Time `httprequest:",form,omitempty"` F07 time.Time `httprequest:",form,omitempty"` F08 *time.Time `httprequest:",form,omitempty"` F09 *time.Time `httprequest:",form,omitempty"` F10 stringer `httprequest:",form,omitempty"` F11 stringer `httprequest:",form,omitempty"` F12 *stringer `httprequest:",form,omitempty"` F13 *stringer `httprequest:",form,omitempty"` F14 *stringer `httprequest:",form,omitempty"` F15 time.Time `httprequest:",form"` // Note that this gets omitted anyway because it's nil. F16 *time.Time `httprequest:",form"` }{ F01: 99, F02: "some text", F03: "", F04: "something", F07: time.Date(2001, 2, 3, 4, 5, 6, 0, time.UTC), F09: func() *time.Time { t := time.Date(2011, 2, 3, 4, 5, 6, 0, time.UTC) return &t }(), F11: stringer(99), F13: func() *stringer { s := stringer(99) return &s }(), F14: new(stringer), }, expectURLString: "http://localhost:8081/99" + "?F02=some+text" + "&F04=something" + "&F07=2001-02-03T04%3A05%3A06Z" + "&F09=2011-02-03T04%3A05%3A06Z" + "&F11=str99" + "&F13=str99" + "&F15=0001-01-01T00%3A00%3A00Z", }, { about: "struct with renamed fields", urlString: "http://localhost:8081/:name", val: &struct { F1 string `httprequest:"name,path"` F2 int `httprequest:"age,form"` }{ F1: "some random user", F2: 42, }, expectURLString: "http://localhost:8081/some%20random%20user?age=42", }, { about: "fields without httprequest tags are ignored", urlString: "http://localhost:8081/:name", val: &struct { F1 string `httprequest:"name,path"` F2 int `httprequest:"age,form"` F3 string }{ F1: "some random user", F2: 42, F3: "some more random text", }, expectURLString: "http://localhost:8081/some%20random%20user?age=42", }, { about: "pointer fields are correctly handled", urlString: "http://localhost:8081/:name", val: &struct { F1 *string `httprequest:"name,path"` F2 *string `httprequest:"age,form"` F3 *string `httprequest:"address,form"` }{ F1: newString("some random user"), F2: newString("42"), }, expectURLString: "http://localhost:8081/some%20random%20user?age=42", }, { about: "MarshalText called on TextMarshalers", urlString: "http://localhost:8081/:param1/:param2", val: &struct { F1 testMarshaler `httprequest:"param1,path"` F2 *testMarshaler `httprequest:"param2,path"` F3 testMarshaler `httprequest:"param3,form"` F4 *testMarshaler `httprequest:"param4,form"` }{ F1: "test1", F2: (*testMarshaler)(newString("test2")), F3: "test3", F4: (*testMarshaler)(newString("test4")), }, expectURLString: "http://localhost:8081/test_test1/test_test2?param3=test_test3¶m4=test_test4", }, { about: "MarshalText not called on values that do not implement TextMarshaler", urlString: "http://localhost:8081/user/:name/:surname", val: &struct { F1 notTextMarshaler `httprequest:"name,path"` F2 *notTextMarshaler `httprequest:"surname,path"` }{ F1: "name", F2: (*notTextMarshaler)(newString("surname")), }, expectURLString: "http://localhost:8081/user/name/surname", }, { about: "MarshalText returns an error", urlString: "http://localhost:8081/user/:name/:surname", val: &struct { F1 testMarshaler `httprequest:"name,path"` F2 *testMarshaler `httprequest:"surname,path"` }{ F1: "", F2: (*testMarshaler)(newString("surname")), }, expectError: "cannot marshal field: empty string", }, { about: "[]string field form value", urlString: "http://localhost:8081/user", val: &struct { F1 []string `httprequest:"users,form"` }{ F1: []string{"user1", "user2", "user3"}, }, expectURLString: "http://localhost:8081/user?users=user1&users=user2&users=user3", }, { about: "nil []string field form value", urlString: "http://localhost:8081/user", val: &struct { F1 *[]string `httprequest:"users,form"` }{ F1: nil, }, expectURLString: "http://localhost:8081/user", }, { about: "form values in body", urlString: "http://localhost:8081", val: &struct { F1 string `httprequest:"f1,form,inbody"` F2 []string `httprequest:"f2,form,inbody"` F3 string `httprequest:"f3,form"` }{ F1: "f1", F2: []string{"f2.1", "f2.2"}, F3: "f3", }, expectURLString: "http://localhost:8081?f3=f3", expectHeader: http.Header{ "Content-Type": {"application/x-www-form-urlencoded"}, }, expectBody: newString(`f1=f1&f2=f2.1&f2=f2.2`), }, { about: "form inbody values with explicit body", urlString: "http://localhost:8081", val: &struct { F1 string `httprequest:"f1,form,inbody"` F2 string `httprequest:"f3,body"` }{}, expectError: `bad type .*: cannot specify inbody field with a body field`, }, { about: "cannot marshal []string field to path", urlString: "http://localhost:8081/:users", val: &struct { F1 []string `httprequest:"users,path"` }{ F1: []string{"user1", "user2"}, }, expectError: `bad type \*struct { F1 \[\]string "httprequest:\\"users,path\\"" }: invalid target type \[\]string for path parameter`, }, { about: "[]string field fails to marshal to path", urlString: "http://localhost:8081/user/:users", val: &struct { F1 []string `httprequest:"users,path"` }{ F1: []string{"user1", "user2", "user3"}, }, expectError: "bad type .*: invalid target type.*", }, { about: "omitempty on body", urlString: "http://localhost:8081/:users", val: &struct { Body string `httprequest:",body,omitempty"` }{}, expectError: `bad type \*struct { Body string "httprequest:\\",body,omitempty\\"" }: bad tag "httprequest:\\",body,omitempty\\"" in field Body: can only use omitempty with form or header fields`, }, { about: "omitempty on path", urlString: "http://localhost:8081/:Users", val: &struct { Users string `httprequest:",path,omitempty"` }{}, expectError: `bad type \*struct { Users string "httprequest:\\",path,omitempty\\"" }: bad tag "httprequest:\\",path,omitempty\\"" in field Users: can only use omitempty with form or header fields`, }, { about: "more than one field with body tag", urlString: "http://localhost:8081/user", method: "POST", val: &struct { F1 string `httprequest:"user,body"` F2 int `httprequest:"age,body"` }{ F1: "test user", F2: 42, }, expectError: "bad type .*: more than one body field specified", }, { about: "required path parameter, but not specified", urlString: "http://localhost:8081/u/:username", method: "POST", val: &struct { F1 string `httprequest:"user,body"` }{ F1: "test user", }, expectError: `missing value for path parameter "username"`, }, { about: "marshal to body", urlString: "http://localhost:8081/u", method: "POST", val: &struct { F1 embedded `httprequest:"info,body"` }{ F1: embedded{ F1: "test user", F2: 42, F3: newString("test address"), }, }, expectBody: newString(`{"name":"test user","age":42,"address":"test address"}`), }, { about: "empty path wildcard", urlString: "http://localhost:8081/u/:", method: "POST", val: &struct { F1 string `httprequest:"user,body"` }{ F1: "test user", }, expectError: "empty path parameter", }, { about: "nil field to form", urlString: "http://localhost:8081/u", val: &struct { F1 *string `httprequest:"user,form"` }{}, expectURLString: "http://localhost:8081/u", }, { about: "nil field to path", urlString: "http://localhost:8081/u", val: &struct { F1 *string `httprequest:"user,path"` }{}, expectURLString: "http://localhost:8081/u", }, { about: "marshal to body of a GET request", urlString: "http://localhost:8081/u", val: &struct { F1 string `httprequest:",body"` }{ F1: "hello test", }, // Providing a body to a GET request is unusual but // some people do it anyway. expectBody: newString(`"hello test"`), }, { about: "marshal to nil value to body", urlString: "http://localhost:8081/u", val: &struct { F1 *string `httprequest:",body"` }{ F1: nil, }, expectBody: newString(""), }, { about: "nil TextMarshaler", urlString: "http://localhost:8081/u", val: &struct { F1 *testMarshaler `httprequest:"surname,form"` }{ F1: (*testMarshaler)(nil), }, expectURLString: "http://localhost:8081/u", }, { about: "marshal nil with Sprint", urlString: "http://localhost:8081/u", val: &struct { F1 *int `httprequest:"surname,form"` }{ F1: (*int)(nil), }, expectURLString: "http://localhost:8081/u", }, { about: "marshal to path with * placeholder", urlString: "http://localhost:8081/u/*name", val: &struct { F1 string `httprequest:"name,path"` }{ F1: "/test", }, expectURLString: "http://localhost:8081/u/test", }, { about: "marshal to path with * placeholder, but the marshaled value does not start with /", urlString: "http://localhost:8081/u/*name", val: &struct { F1 string `httprequest:"name,path"` }{ F1: "test", }, expectError: `value \"test\" for path parameter \"\*name\" does not start with required /`, }, { about: "* placeholder allowed only at the end", urlString: "http://localhost:8081/u/*name/document", val: &struct { F1 string `httprequest:"name,path"` }{ F1: "test", }, expectError: "star path parameter is not at end of path", }, { about: "unparsable base url string", urlString: "%%", val: &struct { F1 string `httprequest:"name,form"` }{ F1: "test", }, expectError: `parse \"?%%\"?: invalid URL escape \"%%\"`, }, { about: "value cannot be marshaled to json", urlString: "http://localhost", method: "POST", val: &struct { F1 failJSONMarshaler `httprequest:"field,body"` }{ F1: "test", }, expectError: `cannot marshal field: cannot marshal request body: json: error calling MarshalJSON for type \*httprequest_test.failJSONMarshaler: marshal error`, }, { about: "url with query parameters", urlString: "http://localhost?a=b", method: "POST", val: &struct { F1 failJSONMarshaler `httprequest:"f1,form"` }{ F1: "test", }, expectURLString: "http://localhost?a=b&f1=test", }, { about: "url with query parameters no form", urlString: "http://localhost?a=b", method: "POST", val: &struct{}{}, expectURLString: "http://localhost?a=b", }, { about: "struct with headers", urlString: "http://localhost:8081/", val: &struct { F01 string `httprequest:",header"` F02 int `httprequest:",header"` F03 bool `httprequest:",header"` F04 string `httprequest:",header,omitempty"` F05 string `httprequest:",header,omitempty"` F06 time.Time `httprequest:",header,omitempty"` F07 time.Time `httprequest:",header,omitempty"` F08 *time.Time `httprequest:",header,omitempty"` F09 *time.Time `httprequest:",header,omitempty"` F10 stringer `httprequest:",header,omitempty"` F11 stringer `httprequest:",header,omitempty"` F12 *stringer `httprequest:",header,omitempty"` F13 *stringer `httprequest:",header,omitempty"` F14 *stringer `httprequest:",header,omitempty"` F15 time.Time `httprequest:",header"` // Note that this gets omitted anyway because it's nil. F16 *time.Time `httprequest:",header"` }{ F01: "some text", F02: 99, F03: true, F04: "", F05: "something", F07: time.Date(2001, 2, 3, 4, 5, 6, 0, time.UTC), F09: func() *time.Time { t := time.Date(2011, 2, 3, 4, 5, 6, 0, time.UTC) return &t }(), F11: stringer(99), F13: func() *stringer { s := stringer(99) return &s }(), F14: new(stringer), }, expectURLString: "http://localhost:8081/", expectHeader: http.Header{ "F01": {"some text"}, "F02": {"99"}, "F03": {"true"}, "F05": {"something"}, "F07": {"2001-02-03T04:05:06Z"}, "F09": {"2011-02-03T04:05:06Z"}, "F11": {"str99"}, "F13": {"str99"}, "F15": {"0001-01-01T00:00:00Z"}, }, }, { about: "struct with header slice", urlString: "http://localhost:8081/:F1", val: &struct { F1 int `httprequest:",path"` F2 string `httprequest:",form"` F3 []string `httprequest:",header"` }{ F1: 99, F2: "some text", F3: []string{"A", "B", "C"}, }, expectURLString: "http://localhost:8081/99?F2=some+text", expectHeader: http.Header{"F3": []string{"A", "B", "C"}}, }, { about: "SetHeader called after marshaling", urlString: "http://localhost:8081/", val: &httprequest.CustomHeader{ Body: &struct { F1 string `httprequest:",header"` F2 int `httprequest:",header"` F3 bool `httprequest:",header"` }{ F1: "some text", F2: 99, F3: false, }, SetHeaderFunc: func(h http.Header) { h.Set("F2", "some other text") }, }, expectURLString: "http://localhost:8081/", expectHeader: http.Header{ "F1": {"some text"}, "F2": {"some other text"}, "F3": {"false"}, }, }} func getStruct() interface{} { return &struct { F1 string }{ F1: "hello", } } func TestMarshal(t *testing.T) { c := qt.New(t) for _, test := range marshalTests { test := test c.Run(test.about, func(c *qt.C) { method := "GET" if test.method != "" { method = test.method } req, err := httprequest.Marshal(test.urlString, method, test.val) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) return } c.Assert(err, qt.Equals, nil) if test.expectURLString != "" { c.Assert(req.URL.String(), qt.Equals, test.expectURLString) } if test.expectBody != nil { data, err := ioutil.ReadAll(req.Body) c.Assert(err, qt.Equals, nil) if *test.expectBody != "" && test.expectHeader["Content-Type"] == nil { c.Assert(req.Header.Get("Content-Type"), qt.Equals, "application/json") } c.Assert(string(data), qt.Equals, *test.expectBody) } for k, v := range test.expectHeader { c.Assert(req.Header[k], qt.DeepEquals, v, qt.Commentf("key %q", k)) } }) } } type testMarshaler string func (t *testMarshaler) MarshalText() ([]byte, error) { if len(*t) == 0 { return nil, errgo.New("empty string") } return []byte("test_" + *t), nil } type notTextMarshaler string // MarshalText does *not* implement encoding.TextMarshaler func (t *notTextMarshaler) MarshalText() { panic("unexpected call") } type failJSONMarshaler string func (*failJSONMarshaler) MarshalJSON() ([]byte, error) { return nil, errgo.New("marshal error") } type stringer int func (s stringer) String() string { return fmt.Sprintf("str%d", int(s)) } httprequest-1.2.1/type.go000066400000000000000000000244461365452616600154430ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. // Package httprequest provides functionality for marshaling // unmarshaling HTTP request parameters into a struct type. // It also provides a way to define methods as HTTP routes // using the same approach. // // It requires at least Go 1.7, and Go 1.9 is required if the importing // program also uses golang.org/x/net/context. package httprequest import ( "context" "fmt" "net/http" "reflect" "sort" "strings" "sync" "github.com/julienschmidt/httprouter" "gopkg.in/errgo.v1" ) // TODO include field name and source in error messages. var ( typeMutex sync.RWMutex typeMap = make(map[reflect.Type]*requestType) ) // Route is the type of a field that specifies a routing // path and HTTP method. See Marshal and Unmarshal // for details. type Route struct{} // Params holds the parameters provided to an HTTP request. type Params struct { Response http.ResponseWriter Request *http.Request PathVar httprouter.Params // PathPattern holds the path pattern matched by httprouter. // It is only set where httprequest has the information; // that is where the call was made by Server.Handler // or Server.Handlers. PathPattern string // Context holds a context for the request. In Go 1.7 and later, // this should be used in preference to Request.Context. Context context.Context } // resultMaker is provided to the unmarshal functions. // When called with the value passed to the unmarshaler, // it returns the field value to be assigned to, // creating it if necessary. type resultMaker func(reflect.Value) reflect.Value // unmarshaler unmarshals some value from params into // the given value. The value should not be assigned to directly, // but passed to makeResult and then updated. type unmarshaler func(v reflect.Value, p Params, makeResult resultMaker) error // marshaler marshals the specified value into params. // The value is always the value type, even if the field type // is a pointer. type marshaler func(reflect.Value, *Params) error // requestType holds information derived from a request // type, preprocessed so that it's quick to marshal or unmarshal. type requestType struct { method string path string formBody bool fields []field } // field holds preprocessed information on an individual field // in the request. type field struct { name string // index holds the index slice of the field. index []int // unmarshal is used to unmarshal the value into // the given field. The value passed as its first // argument is not a pointer type, but is addressable. unmarshal unmarshaler // marshal is used to marshal the value into the // given field. The value passed as its first argument is not // a pointer type, but it is addressable. marshal marshaler // makeResult is the resultMaker that will be // passed into the unmarshaler. makeResult resultMaker // isPointer is true if the field is pointer to the underlying type. isPointer bool } // getRequestType is like parseRequestType except that // it returns the cached requestType when possible, // adding the type to the cache otherwise. func getRequestType(t reflect.Type) (*requestType, error) { typeMutex.RLock() pt := typeMap[t] typeMutex.RUnlock() if pt != nil { return pt, nil } typeMutex.Lock() defer typeMutex.Unlock() if pt = typeMap[t]; pt != nil { // The type has been parsed after we dropped // the read lock, so use it. return pt, nil } pt, err := parseRequestType(t) if err != nil { return nil, errgo.Mask(err) } typeMap[t] = pt return pt, nil } // parseRequestType preprocesses the given type // into a form that can be efficiently interpreted // by Unmarshal. func parseRequestType(t reflect.Type) (*requestType, error) { if t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Struct { return nil, fmt.Errorf("type is not pointer to struct") } hasBody := false var pt requestType foundRoute := false // taggedFieldIndex holds the index of most recent anonymous // tagged field - we will skip any fields inside that. // It is nil when we're not inside an anonymous tagged field. var taggedFieldIndex []int for _, f := range fields(t.Elem()) { if f.PkgPath != "" && !f.Anonymous { // Ignore non-anonymous unexported fields. continue } if taggedFieldIndex != nil && withinIndex(f.Index, taggedFieldIndex) { // Ignore fields within tagged anonymous fields. continue } taggedFieldIndex = nil if !foundRoute && f.Anonymous && f.Type == reflect.TypeOf(Route{}) { var err error pt.method, pt.path, err = parseRouteTag(f.Tag) if err != nil { return nil, errgo.Notef(err, "bad route tag %q", f.Tag) } foundRoute = true continue } tag, err := parseTag(f.Tag, f.Name) if err != nil { return nil, errgo.Notef(err, "bad tag %q in field %s", f.Tag, f.Name) } switch tag.source { case sourceFormBody: pt.formBody = true case sourceBody: if hasBody { return nil, errgo.New("more than one body field specified") } hasBody = true } if hasBody && pt.formBody { return nil, errgo.New("cannot specify inbody field with a body field") } field := field{ index: f.Index, name: f.Name, } if f.Type.Kind() == reflect.Ptr { // The field is a pointer, so when the value is set, // we need to create a new pointer to put // it into. field.makeResult = makePointerResult field.isPointer = true f.Type = f.Type.Elem() } else { field.makeResult = makeValueResult field.isPointer = false } field.unmarshal, err = getUnmarshaler(tag, f.Type) if err != nil { return nil, errgo.Mask(err) } field.marshal, err = getMarshaler(tag, f.Type) if err != nil { return nil, errgo.Mask(err) } if f.Anonymous && tag.source != sourceNone { taggedFieldIndex = f.Index } pt.fields = append(pt.fields, field) } return &pt, nil } // withinIndex reports whether the field with index i0 should be // considered to be within the field with index i1. func withinIndex(i0, i1 []int) bool { // The index of a field within an anonymous field is formed by // appending its field offset to the anonymous field's index, so // it is sufficient that we check that i0 is prefixed by i1. if len(i0) < len(i1) { return false } for i := range i1 { if i0[i] != i1[i] { return false } } return true } // Note: we deliberately omit HEAD and OPTIONS // from this list. HEAD will be routed through GET handlers // and OPTIONS is handled separately. var validMethod = map[string]bool{ "PUT": true, "POST": true, "DELETE": true, "GET": true, "PATCH": true, } func parseRouteTag(tag reflect.StructTag) (method, path string, err error) { tagStr := tag.Get("httprequest") if tagStr == "" { return "", "", errgo.New("no httprequest tag") } f := strings.Fields(tagStr) switch len(f) { case 2: path = f[1] fallthrough case 1: method = f[0] default: return "", "", errgo.New("wrong field count") } if !validMethod[method] { return "", "", errgo.Newf("invalid method") } // TODO check that path looks valid return method, path, nil } func makePointerResult(v reflect.Value) reflect.Value { if v.IsNil() { v.Set(reflect.New(v.Type().Elem())) } return v.Elem() } func makeValueResult(v reflect.Value) reflect.Value { return v } type tagSource uint8 const ( sourceNone = iota sourcePath sourceForm sourceFormBody sourceBody sourceHeader ) type tag struct { name string source tagSource omitempty bool } // parseTag parses the given struct tag attached to the given // field name into a tag structure. func parseTag(rtag reflect.StructTag, fieldName string) (tag, error) { t := tag{ name: fieldName, } tagStr := rtag.Get("httprequest") if tagStr == "" { return t, nil } fields := strings.Split(tagStr, ",") if fields[0] != "" { t.name = fields[0] } inBody := false for _, f := range fields[1:] { switch f { case "path": t.source = sourcePath case "form": t.source = sourceForm case "inbody": inBody = true case "body": t.source = sourceBody case "header": t.source = sourceHeader case "omitempty": t.omitempty = true default: return tag{}, fmt.Errorf("unknown tag flag %q", f) } } if t.omitempty && t.source != sourceForm && t.source != sourceHeader { return tag{}, fmt.Errorf("can only use omitempty with form or header fields") } if inBody { if t.source != sourceForm { return tag{}, fmt.Errorf("can only use inbody with form field") } t.source = sourceFormBody } return t, nil } // fields returns all the fields in the given struct type // including fields inside anonymous struct members. // The fields are ordered with top level fields first // followed by the members of those fields // for anonymous fields. func fields(t reflect.Type) []reflect.StructField { byName := make(map[string]reflect.StructField) addFields(t, byName, nil) fields := make(fieldsByIndex, 0, len(byName)) for _, f := range byName { if f.Name != "" { fields = append(fields, f) } } sort.Sort(fields) return fields } func addFields(t reflect.Type, byName map[string]reflect.StructField, index []int) { for i := 0; i < t.NumField(); i++ { f := t.Field(i) index := append(index, i) var add bool old, ok := byName[f.Name] switch { case ok && len(old.Index) == len(index): // Fields with the same name at the same depth // cancel one another out. Set the field name // to empty to signify that has happened. old.Name = "" byName[f.Name] = old add = false case ok: // Fields at less depth win. add = len(index) < len(old.Index) default: // The field did not previously exist. add = true } if add { // copy the index so that it's not overwritten // by the other appends. f.Index = append([]int(nil), index...) byName[f.Name] = f } if f.Anonymous { if f.Type.Kind() == reflect.Ptr { f.Type = f.Type.Elem() } if f.Type.Kind() == reflect.Struct { addFields(f.Type, byName, index) } } } } type fieldsByIndex []reflect.StructField func (f fieldsByIndex) Len() int { return len(f) } func (f fieldsByIndex) Swap(i, j int) { f[i], f[j] = f[j], f[i] } func (f fieldsByIndex) Less(i, j int) bool { indexi, indexj := f[i].Index, f[j].Index for len(indexi) != 0 && len(indexj) != 0 { ii, ij := indexi[0], indexj[0] if ii != ij { return ii < ij } indexi, indexj = indexi[1:], indexj[1:] } return len(indexi) < len(indexj) } httprequest-1.2.1/unmarshal.go000066400000000000000000000174111365452616600164460ustar00rootroot00000000000000package httprequest import ( "encoding/json" "fmt" "io/ioutil" "reflect" "gopkg.in/errgo.v1" ) var ( ErrUnmarshal = errgo.New("httprequest unmarshal error") ErrBadUnmarshalType = errgo.New("httprequest bad unmarshal type") ) // Unmarshal takes values from given parameters and fills // out fields in x, which must be a pointer to a struct. // // Tags on the struct's fields determine where each field is filled in // from. Similar to encoding/json and other encoding packages, the tag // holds a comma-separated list. The first item in the list is an // alternative name for the field (the field name itself will be used if // this is empty). The next item specifies where the field is filled in // from. It may be: // // "path" - the field is taken from a parameter in p.PathVar // with a matching field name. // // "form" - the field is taken from the given name in p.Request.Form // (note that this covers both URL query parameters and // POST form parameters). // // "header" - the field is taken from the given name in // p.Request.Header. // // "body" - the field is filled in by parsing the request body // as JSON. // // For path and form parameters, the field will be filled out from // the field in p.PathVar or p.Form using one of the following // methods (in descending order of preference): // // - if the type is string, it will be set from the first value. // // - if the type is []string, it will be filled out using all values for that field // (allowed only for form) // // - if the type implements encoding.TextUnmarshaler, its // UnmarshalText method will be used // // - otherwise fmt.Sscan will be used to set the value. // // When the unmarshaling fails, Unmarshal returns an error with an // ErrUnmarshal cause. If the type of x is inappropriate, // it returns an error with an ErrBadUnmarshalType cause. func Unmarshal(p Params, x interface{}) error { xv := reflect.ValueOf(x) pt, err := getRequestType(xv.Type()) if err != nil { return errgo.WithCausef(err, ErrBadUnmarshalType, "bad type %s", xv.Type()) } if err := unmarshal(p, xv, pt); err != nil { return errgo.Mask(err, errgo.Is(ErrUnmarshal)) } return nil } // unmarshal is the internal version of Unmarshal. func unmarshal(p Params, xv reflect.Value, pt *requestType) error { xv = xv.Elem() for _, f := range pt.fields { fv := xv.FieldByIndex(f.index) if err := f.unmarshal(fv, p, f.makeResult); err != nil { return errgo.WithCausef(err, ErrUnmarshal, "cannot unmarshal into field %s", f.name) } } return nil } // getUnmarshaler returns an unmarshaler function // suitable for unmarshaling a field with the given tag // into a value of the given type. func getUnmarshaler(tag tag, t reflect.Type) (unmarshaler, error) { switch { case tag.source == sourceNone: return unmarshalNop, nil case tag.source == sourceBody: return unmarshalBody, nil case t == reflect.TypeOf([]string(nil)): switch tag.source { default: return nil, errgo.New("invalid target type []string for path parameter") case sourceForm, sourceFormBody: return unmarshalAllForm(tag.name), nil case sourceHeader: return unmarshalAllHeader(tag.name), nil } case t == reflect.TypeOf(""): return unmarshalString(tag), nil case implementsTextUnmarshaler(t): return unmarshalWithUnmarshalText(t, tag), nil default: return unmarshalWithScan(tag), nil } } // unmarshalNop just creates the result value but does not // fill it out with anything. This is used to create pointers // to new anonymous field members. func unmarshalNop(v reflect.Value, p Params, makeResult resultMaker) error { makeResult(v) return nil } // unmarshalAllForm unmarshals all the form fields for a given // attribute into a []string slice. func unmarshalAllForm(name string) unmarshaler { return func(v reflect.Value, p Params, makeResult resultMaker) error { vals := p.Request.Form[name] if len(vals) > 0 { makeResult(v).Set(reflect.ValueOf(vals)) } return nil } } // unmarshalAllHeader unmarshals all the header fields for a given // attribute into a []string slice. func unmarshalAllHeader(name string) unmarshaler { return func(v reflect.Value, p Params, makeResult resultMaker) error { vals := p.Request.Header[name] if len(vals) > 0 { makeResult(v).Set(reflect.ValueOf(vals)) } return nil } } // unmarshalString unmarshals into a string field. func unmarshalString(tag tag) unmarshaler { getVal := formGetters[tag.source] if getVal == nil { panic("unexpected source") } return func(v reflect.Value, p Params, makeResult resultMaker) error { val, ok := getVal(tag.name, p) if ok { makeResult(v).SetString(val) } return nil } } // unmarshalBody unmarshals the http request body // into the given value. func unmarshalBody(v reflect.Value, p Params, makeResult resultMaker) error { if !isJSONMediaType(p.Request.Header) { fancyErr := newFancyDecodeError(p.Request.Header, p.Request.Body) return newDecodeRequestError(p.Request, fancyErr.body, fancyErr) } data, err := ioutil.ReadAll(p.Request.Body) if err != nil { return errgo.Notef(err, "cannot read request body") } // TODO allow body types that aren't necessarily JSON. result := makeResult(v) if err := json.Unmarshal(data, result.Addr().Interface()); err != nil { return errgo.Notef(err, "cannot unmarshal request body") } return nil } // formGetters maps from source to a function that // returns the value for a given key and reports // whether the value was found. var formGetters = []func(name string, p Params) (string, bool){ sourceForm: getFromForm, sourceFormBody: getFromForm, sourcePath: func(name string, p Params) (string, bool) { for _, pv := range p.PathVar { if pv.Key == name { return pv.Value, true } } return "", false }, sourceBody: nil, sourceHeader: func(name string, p Params) (string, bool) { vs := p.Request.Header[name] if len(vs) == 0 { return "", false } return vs[0], true }, } func getFromForm(name string, p Params) (string, bool) { vs := p.Request.Form[name] if len(vs) == 0 { return "", false } return vs[0], true } // encodingTextUnmarshaler is the same as encoding.TextUnmarshaler // but avoids us importing the encoding package, which some // broken gccgo installations do not allow. // TODO remove this and use encoding.TextUnmarshaler instead. type encodingTextUnmarshaler interface { UnmarshalText(text []byte) error } var textUnmarshalerType = reflect.TypeOf((*encodingTextUnmarshaler)(nil)).Elem() func implementsTextUnmarshaler(t reflect.Type) bool { // Use the pointer type, because a pointer // type will implement a superset of the methods // of a non-pointer type. return reflect.PtrTo(t).Implements(textUnmarshalerType) } // unmarshalWithUnmarshalText returns an unmarshaler // that unmarshals the given type from the given tag // using its UnmarshalText method. func unmarshalWithUnmarshalText(t reflect.Type, tag tag) unmarshaler { getVal := formGetters[tag.source] if getVal == nil { panic("unexpected source") } return func(v reflect.Value, p Params, makeResult resultMaker) error { val, ok := getVal(tag.name, p) if !ok { // TODO allow specifying that a field is mandatory? return nil } uv := makeResult(v).Addr().Interface().(encodingTextUnmarshaler) return uv.UnmarshalText([]byte(val)) } } // unmarshalWithScan returns an unmarshaler // that unmarshals the given tag using fmt.Scan. func unmarshalWithScan(tag tag) unmarshaler { formGet := formGetters[tag.source] if formGet == nil { panic("unexpected source") } return func(v reflect.Value, p Params, makeResult resultMaker) error { val, ok := formGet(tag.name, p) if !ok { // TODO allow specifying that a field is mandatory? return nil } _, err := fmt.Sscan(val, makeResult(v).Addr().Interface()) if err != nil { return errgo.Notef(err, "cannot parse %q into %s", val, v.Type()) } return nil } } httprequest-1.2.1/unmarshal_test.go000066400000000000000000000261331365452616600175060ustar00rootroot00000000000000// Copyright 2015 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package httprequest_test import ( "fmt" "io" "io/ioutil" "net/http" "net/url" "reflect" "strings" "testing" "time" qt "github.com/frankban/quicktest" "github.com/google/go-cmp/cmp" "github.com/julienschmidt/httprouter" "gopkg.in/httprequest.v1" ) var unmarshalTests = []struct { about string val interface{} expect interface{} params httprequest.Params expectError string // TODO expectErrorCause func(error) bool }{{ about: "struct with simple fields", val: struct { F1 int `httprequest:",form"` F2 int `httprequest:",form"` G1 string `httprequest:",path"` G2 string `httprequest:",path"` H string `httprequest:",body"` T time.Time `httprequest:",form"` TZ time.Time `httprequest:",form"` TP *time.Time `httprequest:",form"` TPZ *time.Time `httprequest:",form"` UnknownForm string `httprequest:",form"` UnknownPath string `httprequest:",path"` }{ F1: 99, F2: -35, G1: "g1 val", G2: "g2 val", H: "h val", T: time.Date(2001, 2, 3, 4, 5, 6, 0, time.UTC), TP: func() *time.Time { t := time.Date(2011, 2, 3, 4, 5, 6, 0, time.UTC) return &t }(), }, params: httprequest.Params{ Request: &http.Request{ Header: http.Header{"Content-Type": {"application/json"}}, Form: url.Values{ "F1": {"99"}, "F2": {"-35", "not a number"}, "T": {"2001-02-03T04:05:06Z"}, "TP": {"2011-02-03T04:05:06Z"}, }, Body: body(`"h val"`), }, PathVar: httprouter.Params{{ Key: "G2", Value: "g2 val", }, { Key: "G1", Value: "g1 val", }, { Key: "G1", Value: "g1 wrong val", }}, }, }, { about: "struct with renamed fields", val: struct { F1 int `httprequest:"x1,form"` F2 int `httprequest:"x2,form"` G1 string `httprequest:"g1,path"` G2 string `httprequest:"g2,path"` }{ F1: 99, F2: -35, G1: "g1 val", G2: "g2 val", }, params: httprequest.Params{ Request: &http.Request{ Form: url.Values{ "x1": {"99"}, "x2": {"-35", "not a number"}, }, }, PathVar: httprouter.Params{{ Key: "g2", Value: "g2 val", }, { Key: "g1", Value: "g1 val", }, { Key: "g1", Value: "g1 wrong val", }}, }, }, { about: "unexported fields are ignored", val: sfG{ G: 99, }, params: httprequest.Params{ Request: &http.Request{ Form: url.Values{ "G": {"99"}, "f": {"100"}, }, }, }, }, { about: "unexported embedded type works ok", val: esFG{ sFG: sFG{ F: 99, G: 100, }, }, params: httprequest.Params{ Request: &http.Request{ Form: url.Values{ "F": {"99"}, "G": {"100"}, }, }, }, }, { about: "unexported type for body is ignored", val: bsFG{}, params: httprequest.Params{ Request: &http.Request{ Header: http.Header{"Content-Type": {"application/json"}}, Body: body(`{"F": 99, "G": 100}`), }, }, }, { about: "fields without httprequest tags are ignored", val: struct { F int }{}, params: httprequest.Params{ Request: &http.Request{ Form: url.Values{ "F": {"foo"}, }, }, PathVar: httprouter.Params{{ Key: "F", Value: "foo", }}, }, }, { about: "pointer fields are filled out", val: struct { F *int `httprequest:",form"` *SFG S *string `httprequest:",form"` T *string `httprequest:",form"` }{ F: newInt(99), SFG: &SFG{ F: 0, G: 534, }, S: newString("s val"), }, params: httprequest.Params{ Request: &http.Request{ Form: url.Values{ "F": {"99"}, "G": {"534"}, "S": {"s val"}, }, }, }, }, { about: "UnmarshalText called on TextUnmarshalers", val: struct { F exclamationUnmarshaler `httprequest:",form"` G exclamationUnmarshaler `httprequest:",path"` FP *exclamationUnmarshaler `httprequest:",form"` }{ F: "yes!", G: "no!", FP: (*exclamationUnmarshaler)(newString("maybe!")), }, params: httprequest.Params{ Request: &http.Request{ Form: url.Values{ "F": {"yes"}, "FP": {"maybe"}, }, }, PathVar: httprouter.Params{{ Key: "G", Value: "no", }}, }, }, { about: "UnmarshalText not called on values with a non-TextUnmarshaler UnmarshalText method", val: struct { F notTextUnmarshaler `httprequest:",form"` }{ F: "hello", }, params: httprequest.Params{ Request: &http.Request{ Form: url.Values{ "F": {"hello"}, }, }, }, }, { about: "UnmarshalText returning an error", val: struct { F exclamationUnmarshaler `httprequest:",form"` }{}, params: httprequest.Params{ Request: &http.Request{ Form: url.Values{ "F": {""}, }, }, }, expectError: "cannot unmarshal into field F: empty string!", }, { about: "all field form values", val: struct { A []string `httprequest:",form"` B *[]string `httprequest:",form"` C []string `httprequest:",form"` D *[]string `httprequest:",form"` }{ A: []string{"a1", "a2"}, B: func() *[]string { x := []string{"b1", "b2", "b3"} return &x }(), }, params: httprequest.Params{ Request: &http.Request{ Form: url.Values{ "A": {"a1", "a2"}, "B": {"b1", "b2", "b3"}, }, }, }, }, { about: "invalid scan field", val: struct { A int `httprequest:",form"` }{}, params: httprequest.Params{ Request: &http.Request{ Form: url.Values{ "A": {"not an int"}, }, }, }, expectError: `cannot unmarshal into field A: cannot parse "not an int" into int: expected integer`, }, { about: "scan field not present", val: struct { A int `httprequest:",form"` }{}, params: httprequest.Params{ Request: &http.Request{}, }, }, { about: "invalid JSON body", val: struct { A string `httprequest:",body"` }{}, params: httprequest.Params{ Request: &http.Request{ Header: http.Header{"Content-Type": {"application/json"}}, Body: body("invalid JSON"), }, }, expectError: "cannot unmarshal into field A: cannot unmarshal request body: invalid character 'i' looking for beginning of value", }, { about: "body with read error", val: struct { A string `httprequest:",body"` }{}, params: httprequest.Params{ Request: &http.Request{ Header: http.Header{"Content-Type": {"application/json"}}, Body: errorReader("some error"), }, }, expectError: "cannot unmarshal into field A: cannot read request body: some error", }, { about: "[]string not allowed for URL source", val: struct { A []string `httprequest:",path"` }{}, expectError: `bad type .*: invalid target type \[]string for path parameter`, }, { about: "duplicated body", val: struct { B1 int `httprequest:",body"` B2 string `httprequest:",body"` }{}, expectError: "bad type .*: more than one body field specified", }, { about: "body tag name is ignored", val: struct { B string `httprequest:"foo,body"` }{ B: "hello", }, params: httprequest.Params{ Request: &http.Request{ Header: http.Header{"Content-Type": {"application/json"}}, Body: body(`"hello"`), }, }, }, { about: "tag with invalid source", val: struct { B1 int `httprequest:",xxx"` }{}, expectError: `bad type .*: bad tag "httprequest:\\",xxx\\"" in field B1: unknown tag flag "xxx"`, }, { about: "non-struct pointer", val: 0, expectError: `bad type \*int: type is not pointer to struct`, }, { about: "unmarshaling with wrong request content type", val: struct { A string `httprequest:",body"` }{}, params: httprequest.Params{ Request: &http.Request{ Header: http.Header{"Content-Type": {"text/html"}}, Body: body("invalid JSON"), }, }, expectError: `cannot unmarshal into field A: unexpected content type text/html; want application/json; content: invalid JSON`, }, { about: "struct with header fields", val: struct { A int `httprequest:"a,header"` B string `httprequest:"b,header"` C []string `httprequest:"c,header"` }{ A: 99, B: "b val", C: []string{"c val1", "c val2"}, }, params: httprequest.Params{ Request: &http.Request{ Header: http.Header{ "a": {"99"}, "b": {"b val"}, "c": {"c val1", "c val2"}, }, }, }, }, { about: "all field header values", val: struct { A []string `httprequest:",header"` B *[]string `httprequest:",header"` C []string `httprequest:",header"` D *[]string `httprequest:",header"` }{ A: []string{"a1", "a2"}, B: func() *[]string { x := []string{"b1", "b2", "b3"} return &x }(), }, params: httprequest.Params{ Request: &http.Request{ Header: http.Header{ "A": {"a1", "a2"}, "B": {"b1", "b2", "b3"}, }, }, }, }, { about: "anonymous body field with pointer field", val: struct { BodyWithPointer `httprequest:",body"` }{}, params: httprequest.Params{ Request: &http.Request{ Header: http.Header{"Content-Type": {"application/json"}}, Body: body("{}"), }, }, }, { about: "anonymous field with tag", val: struct { StructTextUnmarshaler `httprequest:"x,form"` }{ StructTextUnmarshaler{"something"}, }, params: httprequest.Params{ Request: &http.Request{ Form: url.Values{ "x": {"something"}, }, Body: body(`"h val"`), }, PathVar: httprouter.Params{{ // Note that this is the tagged field in the StructTextUnmarshaler type. Key: "foo", Value: "ignored", }}, }, }} // User represents a user in the system. type BodyWithPointer struct { N *int } type sfG struct { f int `httprequest:",form"` G int `httprequest:",form"` } type SFG struct { F int `httprequest:",form"` G int `httprequest:",form"` } type sFG struct { F int `httprequest:",form"` G int `httprequest:",form"` } type esFG struct { sFG } type bsFG struct { foo sFG `httprequest:",body"` } func TestUnmarshal(t *testing.T) { c := qt.New(t) for _, test := range unmarshalTests { test := test c.Run(test.about, func(c *qt.C) { t := reflect.TypeOf(test.val) fillv := reflect.New(t) err := httprequest.Unmarshal(test.params, fillv.Interface()) if test.expectError != "" { c.Assert(err, qt.ErrorMatches, test.expectError) return } c.Assert(err, qt.Equals, nil) c.Assert(fillv.Elem().Interface(), qt.CmpEquals(cmp.AllowUnexported(sfG{}, esFG{}, bsFG{})), test.val) }) } } // TODO non-pointer struct type notTextUnmarshaler string // UnmarshalText does *not* implement encoding.TextUnmarshaler // (it has no arguments or error return value) func (t *notTextUnmarshaler) UnmarshalText() { panic("unexpected call") } type exclamationUnmarshaler string func (t *exclamationUnmarshaler) UnmarshalText(b []byte) error { if len(b) == 0 { return fmt.Errorf("empty string!") } *t = exclamationUnmarshaler(b) + "!" return nil } type StructTextUnmarshaler struct { // The tag on this field should be ignored because // the struct is embedded as an anonymous field. Foo string `httprequest:"foo,path"` } func (t *StructTextUnmarshaler) UnmarshalText(data []byte) error { t.Foo = string(data) return nil } func newInt(i int) *int { return &i } func newString(s string) *string { return &s } type errorReader string func (r errorReader) Read([]byte) (int, error) { return 0, fmt.Errorf("%s", r) } func (r errorReader) Close() error { return nil } func body(s string) io.ReadCloser { return ioutil.NopCloser(strings.NewReader(s)) }