├── .gitignore ├── docs_example.png ├── .travis.yml ├── go.mod ├── aviary.yaml ├── rest ├── middleware │ ├── path_trim.go │ ├── path_trim_test.go │ ├── cors.go │ └── cors_test.go ├── rest.go ├── example_response_serializer_test.go ├── errors_test.go ├── example_hello_world_test.go ├── errors.go ├── handler_test.go ├── index_tpl.go ├── example_ids_only_test.go ├── serialize.go ├── example_middleware_test.go ├── base_handler_test.go ├── example_rules_test.go ├── example_simple_crud_test.go ├── handler_tpl.go ├── base_handler.go ├── context_test.go ├── client.go ├── types.go ├── payload.go ├── client_test.go ├── handler.go ├── context.go ├── payload_test.go ├── doc.go └── api.go ├── .github └── workflows │ └── ci.yml ├── README.md ├── go.sum ├── documentation.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor/ 3 | glide.lock 4 | -------------------------------------------------------------------------------- /docs_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Workiva/go-rest/HEAD/docs_example.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.15" 5 | - tip 6 | 7 | before_install: go get golang.org/x/tools/cmd/cover 8 | script: 9 | - go test -cover ./... 10 | 11 | notifications: 12 | email: false 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Workiva/go-rest 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/gorilla/mux v1.8.0 7 | github.com/hoisie/mustache v0.0.0-20160804235033-6375acf62c69 8 | github.com/stretchr/testify v1.6.1 9 | gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 10 | ) 11 | -------------------------------------------------------------------------------- /aviary.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | 3 | exclude: 4 | - tests?/ 5 | 6 | raven_monitored_classes: null 7 | 8 | raven_monitored_files: 9 | ^rest/middleware/cors.go$: 10 | reason: Contains CORS logic 11 | 12 | raven_monitored_functions: 13 | ^rest/api.go$: 14 | StartTLS: 15 | reason: Logic for serving HTTPS connections 16 | 17 | raven_monitored_keywords: null 18 | 19 | -------------------------------------------------------------------------------- /rest/middleware/path_trim.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/Workiva/go-rest/rest" 8 | ) 9 | 10 | // NewPathTrimMiddleware returns a Middleware which inspects request paths 11 | // and removes the content of trimPortion from the prefix. With a path of /foo/bar 12 | // and a trimpPortion of /foo you would be left with a path of /bar. 13 | func NewPathTrimMiddleware(trimPortion string) rest.Middleware { 14 | return func(w http.ResponseWriter, r *http.Request) *rest.MiddlewareError { 15 | r.URL.Path = strings.TrimPrefix(r.URL.Path, trimPortion) 16 | return nil 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /rest/middleware/path_trim_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | // Ensure that PathTrimMiddleware is correctly trimming the request path 12 | func TestPathTrimMiddlewareTrimPrefix(t *testing.T) { 13 | assert := assert.New(t) 14 | req, _ := http.NewRequest("GET", "http://example.com/foo/bar", nil) 15 | w := httptest.NewRecorder() 16 | 17 | assert.Nil(NewPathTrimMiddleware("/foo")(w, req)) 18 | assert.Equal(req.URL.Path, "/bar") 19 | } 20 | 21 | // Ensure that PathTrimMiddleware correclty passes a path that doesn't need 22 | // to be trimmed 23 | func TestPathTrimMiddlewareNoTrimPrefix(t *testing.T) { 24 | assert := assert.New(t) 25 | req, _ := http.NewRequest("GET", "http://example.com/foo/bar", nil) 26 | w := httptest.NewRecorder() 27 | 28 | assert.Nil(NewPathTrimMiddleware("/baz")(w, req)) 29 | assert.Equal(req.URL.Path, "/foo/bar") 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - 'master' 8 | tags: 9 | - '*' 10 | 11 | permissions: 12 | pull-requests: write 13 | contents: write 14 | id-token: write 15 | 16 | jobs: 17 | CI: 18 | runs-on: ubuntu-latest 19 | name: Tests on Go 20 | steps: 21 | - name: Checkout Repo 22 | uses: actions/checkout@v4 23 | 24 | - name: Setup Go 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: "1.15" 28 | 29 | - name: Format 30 | run: | 31 | test -z $(go fmt ./...) 32 | 33 | - name: Tidy 34 | run: | 35 | go mod download 36 | test -z $(go mod tidy -v) 37 | 38 | - name: Verify 39 | run: | 40 | go mod verify 41 | 42 | - name: Build 43 | run: | 44 | go build ./... 45 | 46 | - name: Test 47 | run: | 48 | go test ./... 49 | 50 | - uses: anchore/sbom-action@v0 51 | with: 52 | path: ./ 53 | format: cyclonedx-json 54 | 55 | -------------------------------------------------------------------------------- /rest/rest.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 - 2015 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | Package rest provides a framework that makes it easy to build a flexible and 19 | (mostly) unopinionated REST API with little ceremony. It offers tooling for 20 | creating stable, resource-oriented endpoints with fine-grained control over 21 | input and output fields. The go-rest framework is platform-agnostic, meaning it 22 | works both on- and off- App Engine, and pluggable in that it supports custom 23 | response serializers, middleware and authentication. It also includes a utility 24 | for generating API documentation. 25 | */ 26 | package rest 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-rest 2 | 3 | The goal of go-rest is to provide a framework that makes it easy to build a flexible and (mostly) unopinionated REST API with little ceremony. It offers tooling for creating stable, resource-oriented endpoints with fine-grained control over input and output fields. The go-rest framework is platform-agnostic, meaning it works both on- and off- App Engine, and pluggable in that it supports custom response serializers, middleware and authentication. It also includes a utility for generating API documentation. Refer to [documentation.md](documentation.md) for more of the motivation behind go-rest. 4 | 5 | See the examples to get started. Additionally, the `rest` package contains a simple client implementation for consuming go-rest APIs. 6 | 7 | For documentation, see [godoc](http://godoc.org/github.com/Workiva/go-rest/rest). 8 | 9 | ## Installation 10 | 11 | ```bash 12 | go get github.com/Workiva/go-rest/rest 13 | ``` 14 | 15 | ## Contributing 16 | 17 | Requirements to commit here: 18 | 19 | - Branch off master, PR back to master. 20 | - [gofmt](http://golang.org/cmd/go/#hdr-Run_gofmt_on_package_sources) your code. Unformatted code will be rejected. 21 | - Follow the style suggestions found [here](https://code.google.com/p/go-wiki/wiki/CodeReviewComments). 22 | - Unit test coverage is required. 23 | - Good docstrs are required for at least exported names, and preferably for all functions. 24 | - Good [commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) are required. 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 4 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 5 | github.com/hoisie/mustache v0.0.0-20160804235033-6375acf62c69 h1:umaj0TCQ9lWUUKy2DxAhEzPbwd0jnxiw1EI2z3FiILM= 6 | github.com/hoisie/mustache v0.0.0-20160804235033-6375acf62c69/go.mod h1:zdLK9ilQRSMjSeLKoZ4BqUfBT7jswTGF8zRlKEsiRXA= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 10 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 11 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 12 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU= 16 | gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= 17 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 18 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 19 | -------------------------------------------------------------------------------- /rest/example_response_serializer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 - 2015 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package rest 18 | 19 | import "gopkg.in/yaml.v1" 20 | 21 | // YAMLSerializer implements the ResponseSerializer interface. 22 | type YAMLSerializer struct{} 23 | 24 | // Serialize marshals a response payload into a byte slice to be sent over the 25 | // wire. 26 | func (y YAMLSerializer) Serialize(p Payload) ([]byte, error) { 27 | return yaml.Marshal(p) 28 | } 29 | 30 | // ContentType returns the MIME type of the response. 31 | func (y YAMLSerializer) ContentType() string { 32 | return "text/yaml" 33 | } 34 | 35 | // This example shows how to implement a custom ResponseSerializer. The format 36 | // responses are sent in is specified by the "format" query string parameter. By 37 | // default, json is the only available format, but the ResponseSerializer 38 | // interface allows different formats to be implemented. 39 | func Example_responseSerializer() { 40 | api := NewAPI(NewConfiguration()) 41 | 42 | // Call RegisterResponseSerializer to wire up YAMLSerializer. 43 | api.RegisterResponseSerializer("yaml", YAMLSerializer{}) 44 | 45 | // Call RegisterResourceHandler to wire up HelloWorldHandler. 46 | api.RegisterResourceHandler(HelloWorldHandler{}) 47 | 48 | // We're ready to hit our CRUD endpoints. Use ?format=yaml to get responses as YAML. 49 | api.Start(":8080") 50 | } 51 | -------------------------------------------------------------------------------- /rest/errors_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 - 2015 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package rest 18 | 19 | import ( 20 | "net/http" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | // Ensures that the Error factories produce errors with expected values. 27 | func TestErrors(t *testing.T) { 28 | assert := assert.New(t) 29 | 30 | err := ResourceNotFound("foo") 31 | assert.Equal("foo", err.Error()) 32 | assert.Equal(http.StatusNotFound, err.Status()) 33 | 34 | err = ResourceNotPermitted("foo") 35 | assert.Equal("foo", err.Error()) 36 | assert.Equal(http.StatusForbidden, err.Status()) 37 | 38 | err = ResourceConflict("foo") 39 | assert.Equal("foo", err.Error()) 40 | assert.Equal(http.StatusConflict, err.Status()) 41 | 42 | err = BadRequest("foo") 43 | assert.Equal("foo", err.Error()) 44 | assert.Equal(http.StatusBadRequest, err.Status()) 45 | 46 | err = UnprocessableRequest("foo") 47 | assert.Equal("foo", err.Error()) 48 | assert.Equal(statusUnprocessableEntity, err.Status()) 49 | 50 | err = UnauthorizedRequest("foo") 51 | assert.Equal("foo", err.Error()) 52 | assert.Equal(http.StatusUnauthorized, err.Status()) 53 | 54 | err = MethodNotAllowed("foo") 55 | assert.Equal("foo", err.Error()) 56 | assert.Equal(http.StatusMethodNotAllowed, err.Status()) 57 | 58 | err = ResourceNotPermitted("foo") 59 | assert.Equal("foo", err.Error()) 60 | assert.Equal(http.StatusForbidden, err.Status()) 61 | 62 | err = InternalServerError("foo") 63 | assert.Equal("foo", err.Error()) 64 | assert.Equal(http.StatusInternalServerError, err.Status()) 65 | } 66 | -------------------------------------------------------------------------------- /rest/middleware/cors.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "strings" 7 | 8 | "github.com/Workiva/go-rest/rest" 9 | ) 10 | 11 | // NewCORSMiddleware returns a Middleware which enables cross-origin requests. 12 | // Origin must match the supplied whitelist (which supports wildcards). Returns 13 | // a MiddlewareError if the request should be terminated. 14 | func NewCORSMiddleware(originWhitelist []string) rest.Middleware { 15 | return func(w http.ResponseWriter, r *http.Request) *rest.MiddlewareError { 16 | origin := r.Header.Get("Origin") 17 | if origin == "" && r.Method != "OPTIONS" { 18 | return nil 19 | } 20 | 21 | originMatch := false 22 | if checkOrigin(origin, originWhitelist) { 23 | w.Header().Set("Access-Control-Allow-Origin", origin) 24 | w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") 25 | w.Header()["Access-Control-Allow-Headers"] = r.Header["Access-Control-Request-Headers"] 26 | w.Header().Set("Access-Control-Allow-Credentials", "true") 27 | originMatch = true 28 | } 29 | 30 | if r.Method == "OPTIONS" { 31 | return &rest.MiddlewareError{Code: http.StatusOK} 32 | } 33 | 34 | if !originMatch { 35 | return &rest.MiddlewareError{ 36 | Code: http.StatusBadRequest, 37 | Response: []byte("Origin does not match whitelist"), 38 | } 39 | } 40 | 41 | return nil 42 | } 43 | } 44 | 45 | // checkOrigin checks if the given origin is contained in the origin whitelist. 46 | // Returns true if the origin is in the whitelist, false if not. 47 | func checkOrigin(origin string, whitelist []string) bool { 48 | url, err := url.Parse(origin) 49 | if err != nil { 50 | return false 51 | } 52 | originComponents := strings.Split(url.Host, ".") 53 | 54 | checkWhitelist: 55 | for _, whitelisted := range whitelist { 56 | if whitelisted == "*" { 57 | return true 58 | } 59 | 60 | whitelistedComponents := strings.Split(whitelisted, ".") 61 | 62 | if len(originComponents) != len(whitelistedComponents) { 63 | // Do not match, try next host in whitelist. 64 | continue 65 | } 66 | 67 | for i, originComponent := range originComponents { 68 | whitelistedComponent := whitelistedComponents[i] 69 | if whitelistedComponent == "*" { 70 | // Wildcard, check next component. 71 | continue 72 | } 73 | 74 | if originComponent != whitelistedComponent { 75 | // Mismatch, try next host in whitelist. 76 | continue checkWhitelist 77 | } 78 | } 79 | 80 | // Origin matches whitelisted domain. 81 | return true 82 | } 83 | 84 | // No matches. 85 | return false 86 | } 87 | -------------------------------------------------------------------------------- /rest/example_hello_world_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 - 2015 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package rest 18 | 19 | import "fmt" 20 | 21 | // HelloWorldResource represents a domain model for which we want to perform 22 | // CRUD operations with. Endpoints can operate on any type of entity -- 23 | // primitive, struct, or composite -- so long as it is serializable (by default, 24 | // this means JSON-serializable via either MarshalJSON or JSON struct tags). 25 | type HelloWorldResource struct { 26 | ID int `json:"id"` 27 | Foobar string `json:"foobar"` 28 | } 29 | 30 | // HelloWorldHandler implements the ResourceHandler interface. It specifies the 31 | // business logic for performing CRUD operations. BaseResourceHandler provides 32 | // stubs for each method if you only need to implement certain operations (as 33 | // this example illustrates). 34 | type HelloWorldHandler struct { 35 | BaseResourceHandler 36 | } 37 | 38 | // ResourceName is used to identify what resource a handler corresponds to and 39 | // is used in the endpoint URLs, i.e. /api/:version/helloworld. 40 | func (h HelloWorldHandler) ResourceName() string { 41 | return "helloworld" 42 | } 43 | 44 | // ReadResource is the logic that corresponds to reading a single resource by 45 | // its ID at GET /api/:version/helloworld/{id}. Typically, this would make some 46 | // sort of database query to load the resource. If the resource doesn't exist, 47 | // nil should be returned along with an appropriate error. 48 | func (h HelloWorldHandler) ReadResource(ctx RequestContext, id string, 49 | version string) (Resource, error) { 50 | // Make a database call here. 51 | if id == "42" { 52 | return &HelloWorldResource{ID: 42, Foobar: "hello world"}, nil 53 | } 54 | return nil, ResourceNotFound(fmt.Sprintf("No resource with id %s", id)) 55 | } 56 | 57 | // This example shows a minimal implementation of a ResourceHandler by using the 58 | // BaseResourceHandler. It only implements an endpoint for fetching a resource. 59 | func Example_helloWorld() { 60 | api := NewAPI(NewConfiguration()) 61 | 62 | // Call RegisterResourceHandler to wire up HelloWorldHandler. 63 | api.RegisterResourceHandler(HelloWorldHandler{}) 64 | 65 | // We're ready to hit our CRUD endpoints. 66 | api.Start(":8080") 67 | } 68 | -------------------------------------------------------------------------------- /rest/errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 - 2015 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package rest 18 | 19 | import "net/http" 20 | 21 | // statusUnprocessableEntity indicates the request was well-formed but was 22 | // unable to be followed due to semantic errors. 23 | const statusUnprocessableEntity = 422 24 | 25 | // Error is an implementation of the error interface representing an HTTP error. 26 | type Error struct { 27 | reason string 28 | status int 29 | } 30 | 31 | // Error returns the Error message. 32 | func (r Error) Error() string { return r.reason } 33 | 34 | // Status returns the HTTP status code. 35 | func (r Error) Status() int { return r.status } 36 | 37 | // ResourceNotFound returns a Error for a 404 Not Found error. 38 | func ResourceNotFound(reason string) Error { 39 | return Error{reason, http.StatusNotFound} 40 | } 41 | 42 | // ResourceNotPermitted returns a Error for a 403 Forbidden error. 43 | func ResourceNotPermitted(reason string) Error { 44 | return Error{reason, http.StatusForbidden} 45 | } 46 | 47 | // ResourceConflict returns a Error for a 409 Conflict error. 48 | func ResourceConflict(reason string) Error { 49 | return Error{reason, http.StatusConflict} 50 | } 51 | 52 | // BadRequest returns a Error for a 400 Bad Request error. 53 | func BadRequest(reason string) Error { 54 | return Error{reason, http.StatusBadRequest} 55 | } 56 | 57 | // UnprocessableRequest returns a Error for a 422 Unprocessable Entity error. 58 | func UnprocessableRequest(reason string) Error { 59 | return Error{reason, statusUnprocessableEntity} 60 | } 61 | 62 | // UnauthorizedRequest returns a Error for a 401 Unauthorized error. 63 | func UnauthorizedRequest(reason string) Error { 64 | return Error{reason, http.StatusUnauthorized} 65 | } 66 | 67 | // MethodNotAllowed returns a Error for a 405 Method Not Allowed error. 68 | func MethodNotAllowed(reason string) Error { 69 | return Error{reason, http.StatusMethodNotAllowed} 70 | } 71 | 72 | // InternalServerError returns a Error for a 500 Internal Server error. 73 | func InternalServerError(reason string) Error { 74 | return Error{reason, http.StatusInternalServerError} 75 | } 76 | 77 | // CustomError returns an Error for the given HTTP status code. 78 | func CustomError(reason string, status int) Error { 79 | return Error{reason, status} 80 | } 81 | -------------------------------------------------------------------------------- /rest/handler_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 - 2015 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package rest 18 | 19 | import ( 20 | "bytes" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | // Ensures that decodePayload returns an empty map for empty payloads. 27 | func TestDecodePayloadEmpty(t *testing.T) { 28 | assert := assert.New(t) 29 | payload := bytes.NewBufferString("") 30 | 31 | decoded, err := decodePayload(payload.Bytes()) 32 | 33 | assert.Equal(Payload{}, decoded) 34 | assert.Nil(err) 35 | } 36 | 37 | // Ensures that decodePayload returns a nil and an error for invalid JSON payloads. 38 | func TestDecodePayloadBadJSON(t *testing.T) { 39 | assert := assert.New(t) 40 | body := `{"foo": "bar", "baz": 1` 41 | payload := bytes.NewBufferString(body) 42 | 43 | decoded, err := decodePayload(payload.Bytes()) 44 | 45 | assert.Nil(decoded) 46 | assert.NotNil(err) 47 | } 48 | 49 | // Ensures that decodePayload returns a decoded map for JSON payloads. 50 | func TestDecodePayloadHappyPath(t *testing.T) { 51 | assert := assert.New(t) 52 | body := `{"foo": "bar", "baz": 1}` 53 | payload := bytes.NewBufferString(body) 54 | 55 | decoded, err := decodePayload(payload.Bytes()) 56 | 57 | assert.Equal(Payload{"foo": "bar", "baz": float64(1)}, decoded) 58 | assert.Nil(err) 59 | } 60 | 61 | // Ensures that decodePayloadSlice returns an empty slice for empty payloads. 62 | func TestDecodePayloadSliceEmpty(t *testing.T) { 63 | assert := assert.New(t) 64 | payload := bytes.NewBufferString("") 65 | 66 | decoded, err := decodePayloadSlice(payload.Bytes()) 67 | 68 | assert.Equal([]Payload{}, decoded) 69 | assert.Nil(err) 70 | } 71 | 72 | // Ensures that decodePayloadSlice returns a nil and an error for invalid JSON payloads. 73 | func TestDecodePayloadSliceBadJSON(t *testing.T) { 74 | assert := assert.New(t) 75 | body := `[{"foo": "bar", "baz": 1` 76 | payload := bytes.NewBufferString(body) 77 | 78 | decoded, err := decodePayloadSlice(payload.Bytes()) 79 | 80 | assert.Nil(decoded) 81 | assert.NotNil(err) 82 | } 83 | 84 | // Ensures that decodePayloadSlice returns a decoded map for JSON payloads. 85 | func TestDecodePayloadSliceHappyPath(t *testing.T) { 86 | assert := assert.New(t) 87 | body := `[{"foo": "bar", "baz": 1}]` 88 | payload := bytes.NewBufferString(body) 89 | 90 | decoded, err := decodePayloadSlice(payload.Bytes()) 91 | 92 | assert.Equal([]Payload{Payload{"foo": "bar", "baz": float64(1)}}, decoded) 93 | assert.Nil(err) 94 | } 95 | -------------------------------------------------------------------------------- /rest/index_tpl.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 - 2015 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package rest 18 | 19 | // indexTemplate is the mustache template for the documentation index. 20 | const indexTemplate = ` 21 | 22 | 23 | 24 | REST API v{{version}} Documentation 25 | 27 | 29 | 30 | 32 | 33 | 34 | 35 |
36 | 37 | 64 | 65 | 70 |
71 | 72 | 73 | 74 | ` 75 | -------------------------------------------------------------------------------- /rest/middleware/cors_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | // Ensures that CORSMiddleware applies the headers needed for CORS when * is 12 | // present in the whitelist. 13 | func TestCORSMiddlewareAll(t *testing.T) { 14 | assert := assert.New(t) 15 | req, _ := http.NewRequest("GET", "http://example.com/foo", nil) 16 | req.Header.Set("Origin", "http://foo.com") 17 | req.Header.Set("Access-Control-Request-Headers", "def") 18 | w := httptest.NewRecorder() 19 | assert.Nil(NewCORSMiddleware([]string{"*"})(w, req)) 20 | 21 | assert.Equal("http://foo.com", w.Header().Get("Access-Control-Allow-Origin")) 22 | assert.Equal("POST, GET, OPTIONS, PUT, DELETE", w.Header().Get("Access-Control-Allow-Methods")) 23 | assert.Equal([]string{"def"}, w.Header()["Access-Control-Allow-Headers"]) 24 | assert.Equal([]string{"true"}, w.Header()["Access-Control-Allow-Credentials"]) 25 | } 26 | 27 | // Ensures that CORSMiddleware applies the headers needed for CORS and respects 28 | // the origin whitelist. 29 | func TestCORSMiddlewareWhitelist(t *testing.T) { 30 | assert := assert.New(t) 31 | req, _ := http.NewRequest("GET", "http://example.com/foo", nil) 32 | req.Header.Set("Origin", "http://foo.wdesk.com") 33 | req.Header.Set("Access-Control-Request-Headers", "def") 34 | w := httptest.NewRecorder() 35 | middleware := NewCORSMiddleware([]string{"blah.wdesk.org", "*.wdesk.com"}) 36 | assert.Nil(middleware(w, req)) 37 | 38 | assert.Equal("http://foo.wdesk.com", w.Header().Get("Access-Control-Allow-Origin")) 39 | assert.Equal("POST, GET, OPTIONS, PUT, DELETE", w.Header().Get("Access-Control-Allow-Methods")) 40 | assert.Equal([]string{"def"}, w.Header()["Access-Control-Allow-Headers"]) 41 | assert.Equal([]string{"true"}, w.Header()["Access-Control-Allow-Credentials"]) 42 | 43 | // Mismatched origin 44 | req, _ = http.NewRequest("GET", "http://example.com/foo", nil) 45 | req.Header.Set("Origin", "http://baz.wdesk.org") 46 | req.Header.Set("Access-Control-Request-Headers", "def") 47 | w = httptest.NewRecorder() 48 | err := middleware(w, req) 49 | assert.NotNil(err) 50 | assert.Equal(http.StatusBadRequest, err.Code) 51 | 52 | assert.Equal("", w.Header().Get("Access-Control-Allow-Origin")) 53 | assert.Equal("", w.Header().Get("Access-Control-Allow-Methods")) 54 | assert.Nil(w.Header()["Access-Control-Allow-Headers"]) 55 | assert.Nil(w.Header()["Access-Control-Allow-Credentials"]) 56 | } 57 | 58 | // Ensures that CORSMiddleware returns a MiddlewareError with a 200 response 59 | // code. 60 | func TestCORSMiddlewareOptionsRequest(t *testing.T) { 61 | req, _ := http.NewRequest("OPTIONS", "http://example.com/foo", nil) 62 | req.Header.Set("Origin", "http://foo.com") 63 | w := httptest.NewRecorder() 64 | err := NewCORSMiddleware([]string{"foo.com"})(w, req) 65 | assert.Equal(t, http.StatusOK, err.Code) 66 | } 67 | 68 | // Ensures that CORSMiddleware doesn't return an error when a request is given 69 | // without an Origin header. 70 | func TestCORSMiddlewareNoOrigin(t *testing.T) { 71 | req, _ := http.NewRequest("GET", "http://example.com/foo", nil) 72 | w := httptest.NewRecorder() 73 | err := NewCORSMiddleware([]string{"blah.wdesk.org", "*.wdesk.com"})(w, req) 74 | assert.Nil(t, err) 75 | } 76 | -------------------------------------------------------------------------------- /rest/example_ids_only_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 - 2015 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package rest 18 | 19 | import "strconv" 20 | 21 | // MyResource represents a domain model for which we want to perform CRUD 22 | // operations with. Endpoints can operate on any type of entity -- primitive, 23 | // struct, or composite -- so long as it is serializable (by default, this means 24 | // JSON-serializable via either MarshalJSON or JSON struct tags). 25 | type MyResource struct { 26 | ID int `json:"id"` 27 | Foobar string `json:"foobar"` 28 | } 29 | 30 | // MyResourceHandler implements the ResourceHandler interface. It specifies the 31 | // business logic for performing CRUD operations. BaseResourceHandler provides 32 | // stubs for each method if you only need to implement certain operations (as 33 | // this example illustrates). 34 | type MyResourceHandler struct { 35 | BaseResourceHandler 36 | } 37 | 38 | // ResourceName is used to identify what resource a handler corresponds to and 39 | // is used in the endpoint URLs, i.e. /api/:version/myresource. 40 | func (m MyResourceHandler) ResourceName() string { 41 | return "myresource" 42 | } 43 | 44 | // ReadResourceList is the logic that corresponds to reading multiple resources, 45 | // perhaps with specified query parameters accessed through the RequestContext. 46 | // This is mapped to GET /api/:version/myresource. Typically, this would make 47 | // some sort of database query to fetch the resources. It returns the slice of 48 | // results, a cursor (or empty) string, and error (or nil). In this example, we 49 | // illustrate how to use a query parameter to return only the IDs of our 50 | // resources. 51 | func (m MyResourceHandler) ReadResourceList(ctx RequestContext, limit int, 52 | cursor string, version string) ([]Resource, string, error) { 53 | // Make a database call here. 54 | resources := make([]Resource, 0, limit) 55 | resources = append(resources, &FooResource{ID: 1, Foobar: "hello"}) 56 | resources = append(resources, &FooResource{ID: 2, Foobar: "world"}) 57 | 58 | // ids_only is a query string parameter (i.e. /api/v1/myresource?ids_only=true). 59 | keysOnly, _ := strconv.ParseBool(ctx.ValueWithDefault("ids_only", "0").(string)) 60 | 61 | if keysOnly { 62 | keys := make([]Resource, 0, len(resources)) 63 | for _, resource := range resources { 64 | keys = append(keys, resource.(*FooResource).ID) 65 | } 66 | return keys, "", nil 67 | } 68 | 69 | return resources, "", nil 70 | } 71 | 72 | // This example shows how to implement a ResourceHandler which has an endpoint 73 | // for fetching multiple resources and, optionally, retrieving only their IDs. 74 | func Example_idsOnly() { 75 | api := NewAPI(NewConfiguration()) 76 | 77 | // Call RegisterResourceHandler to wire up HelloWorldHandler. 78 | api.RegisterResourceHandler(MyResourceHandler{}) 79 | 80 | // We're ready to hit our CRUD endpoints. 81 | api.Start(":8080") 82 | } 83 | -------------------------------------------------------------------------------- /rest/serialize.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 - 2015 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package rest 18 | 19 | import ( 20 | "encoding/json" 21 | "net/http" 22 | "reflect" 23 | ) 24 | 25 | const ( 26 | status = "status" 27 | reason = "reason" 28 | messages = "messages" 29 | result = "result" 30 | results = "results" 31 | next = "next" 32 | ) 33 | 34 | // response is a data structure holding the serializable response body for a request and 35 | // HTTP status code. It should be created using NewResponse. 36 | type response struct { 37 | Payload Payload 38 | Status int 39 | } 40 | 41 | // ResponseSerializer is responsible for serializing REST responses and sending 42 | // them back to the client. 43 | type ResponseSerializer interface { 44 | 45 | // Serialize marshals a response payload into a byte slice to be sent over the wire. 46 | Serialize(Payload) ([]byte, error) 47 | 48 | // ContentType returns the MIME type of the response. 49 | ContentType() string 50 | } 51 | 52 | // jsonSerializer is an implementation of ResponseSerializer which serializes responses 53 | // as JSON. 54 | type jsonSerializer struct{} 55 | 56 | // Serialize marshals a response payload into a JSON byte slice to be sent over the wire. 57 | func (j jsonSerializer) Serialize(p Payload) ([]byte, error) { 58 | return json.Marshal(p) 59 | } 60 | 61 | // ContentType returns the JSON MIME type of the response. 62 | func (j jsonSerializer) ContentType() string { 63 | return "application/json" 64 | } 65 | 66 | // NewResponse constructs a new response struct containing the payload to send back. 67 | // It will either be a success or error response depending on the RequestContext. 68 | func NewResponse(ctx RequestContext) response { 69 | err := ctx.Error() 70 | if err != nil { 71 | return newErrorResponse(ctx) 72 | } 73 | 74 | return newSuccessResponse(ctx) 75 | } 76 | 77 | // newSuccessResponse constructs a new response struct containing a resource response. 78 | func newSuccessResponse(ctx RequestContext) response { 79 | r := ctx.Result() 80 | resultKey := result 81 | if r != nil && reflect.TypeOf(r).Kind() == reflect.Slice { 82 | resultKey = results 83 | } 84 | 85 | s := ctx.Status() 86 | response := response{Status: s} 87 | 88 | if s != http.StatusNoContent { 89 | payload := Payload{ 90 | status: s, 91 | reason: http.StatusText(s), 92 | messages: ctx.Messages(), 93 | resultKey: r, 94 | } 95 | 96 | if nextURL, err := ctx.NextURL(); err == nil && nextURL != "" { 97 | payload[next] = nextURL 98 | } 99 | 100 | response.Payload = payload 101 | } 102 | 103 | return response 104 | } 105 | 106 | // newErrorResponse constructs a new response struct containing an error message. 107 | func newErrorResponse(ctx RequestContext) response { 108 | err := ctx.Error() 109 | s := http.StatusInternalServerError 110 | if restError, ok := err.(Error); ok { 111 | s = restError.Status() 112 | } 113 | 114 | payload := Payload{ 115 | status: s, 116 | reason: http.StatusText(s), 117 | messages: ctx.Messages(), 118 | } 119 | 120 | response := response{ 121 | Payload: payload, 122 | Status: s, 123 | } 124 | 125 | return response 126 | } 127 | -------------------------------------------------------------------------------- /rest/example_middleware_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 - 2015 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package rest 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "log" 23 | "net/http" 24 | ) 25 | 26 | // MiddlewareResource represents a domain model for which we want to perform 27 | // CRUD operations with. Endpoints can operate on any type of entity -- 28 | // primitive, struct, or composite -- so long as it is serializable (by default, 29 | // this means JSON-serializable via either MarshalJSON or JSON struct tags). 30 | type MiddlewareResource struct { 31 | ID int `json:"id"` 32 | Foobar string `json:"foobar"` 33 | } 34 | 35 | // MiddlewareHandler implements the ResourceHandler interface. It specifies the 36 | // business logic for performing CRUD operations. BaseResourceHandler provides 37 | // stubs for each method if you only need to implement certain operations (as 38 | // this example illustrates). 39 | type MiddlewareHandler struct { 40 | BaseResourceHandler 41 | } 42 | 43 | // ResourceName is used to identify what resource a handler corresponds to and 44 | // is used in the endpoint URLs, i.e. /api/:version/example. 45 | func (e MiddlewareHandler) ResourceName() string { 46 | return "example" 47 | } 48 | 49 | // Authenticate is logic that is used to authenticate requests to this 50 | // ResourceHandler. Returns nil if the request is authenticated or an error if 51 | // it is not. 52 | func (e MiddlewareHandler) Authenticate(r *http.Request) error { 53 | if r.Header.Get("Answer") != "42" { 54 | return errors.New("what is the answer?") 55 | } 56 | return nil 57 | } 58 | 59 | // ReadResource is the logic that corresponds to reading a single resource by 60 | // its ID at GET /api/:version/example/{id}. Typically, this would make some 61 | // sort of database query to load the resource. If the resource doesn't exist, 62 | // nil should be returned along with an appropriate error. 63 | func (e MiddlewareHandler) ReadResource(ctx RequestContext, id string, version string) (Resource, error) { 64 | // Make a database call here. 65 | if id == "42" { 66 | return &MiddlewareResource{ID: 42, Foobar: "hello world"}, nil 67 | } 68 | return nil, ResourceNotFound(fmt.Sprintf("No resource with id %s", id)) 69 | } 70 | 71 | // ResourceHandler middleware is implemented as a closure which takes an 72 | // http.Handler and returns one. 73 | func HandlerMiddleware(next http.Handler) http.Handler { 74 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 75 | log.Printf("Request: %s", r.URL.String()) 76 | next.ServeHTTP(w, r) 77 | }) 78 | } 79 | 80 | // Global API middleware is implemented as a function which takes an 81 | // http.ResponseWriter and http.Request and returns a MiddlewareError if the 82 | // request should terminate. 83 | func GlobalMiddleware(w http.ResponseWriter, r *http.Request) *MiddlewareError { 84 | log.Println(r) 85 | return nil 86 | } 87 | 88 | // This example shows how to implement request middleware. ResourceHandlers 89 | // provide the Authenticate method which is used to authenticate requests, but 90 | // middleware allows you to insert additional authorization, logging, or other 91 | // AOP-style operations. 92 | func Example_middleware() { 93 | api := NewAPI(NewConfiguration()) 94 | 95 | // Call RegisterResourceHandler to wire up MiddlewareHandler and apply middleware. 96 | api.RegisterResourceHandler(MiddlewareHandler{}, HandlerMiddleware) 97 | 98 | // Middleware provided to Start and StartTLS are invoked for every request handled 99 | // by the API. 100 | api.Start(":8080", GlobalMiddleware) 101 | } 102 | -------------------------------------------------------------------------------- /rest/base_handler_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 - 2015 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package rest 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | type TestDefaultHandler struct { 26 | BaseResourceHandler 27 | } 28 | 29 | func (t TestDefaultHandler) ResourceName() string { 30 | return "foo" 31 | } 32 | 33 | // Ensures that CreateURI falls back to the correct default. 34 | func TestCreateURIDefault(t *testing.T) { 35 | assert := assert.New(t) 36 | proxy := resourceHandlerProxy{TestDefaultHandler{}} 37 | 38 | assert.Equal("/api/v{version:[^/]+}/foo", proxy.CreateURI()) 39 | } 40 | 41 | // Ensures that ReadURI falls back to the correct default. 42 | func TestReadURIDefault(t *testing.T) { 43 | assert := assert.New(t) 44 | proxy := resourceHandlerProxy{TestDefaultHandler{}} 45 | 46 | assert.Equal("/api/v{version:[^/]+}/foo/{resource_id}", proxy.ReadURI()) 47 | } 48 | 49 | // Ensures that ReadListURI falls back to the correct default. 50 | func TestReadListURIDefault(t *testing.T) { 51 | assert := assert.New(t) 52 | proxy := resourceHandlerProxy{TestDefaultHandler{}} 53 | 54 | assert.Equal("/api/v{version:[^/]+}/foo", proxy.ReadListURI()) 55 | } 56 | 57 | // Ensures that UpdateURI falls back to the correct default. 58 | func TestUpdateURIDefault(t *testing.T) { 59 | assert := assert.New(t) 60 | proxy := resourceHandlerProxy{TestDefaultHandler{}} 61 | 62 | assert.Equal("/api/v{version:[^/]+}/foo/{resource_id}", proxy.UpdateURI()) 63 | } 64 | 65 | // Ensures that DeleteURI falls back to the correct default. 66 | func TestDeleteURIDefault(t *testing.T) { 67 | assert := assert.New(t) 68 | proxy := resourceHandlerProxy{TestDefaultHandler{}} 69 | 70 | assert.Equal("/api/v{version:[^/]+}/foo/{resource_id}", proxy.DeleteURI()) 71 | } 72 | 73 | type TestHandler struct { 74 | BaseResourceHandler 75 | } 76 | 77 | func (t TestHandler) ResourceName() string { 78 | return "foo" 79 | } 80 | 81 | func (t TestHandler) CreateURI() string { 82 | return "/api/{version}/create_foo" 83 | } 84 | 85 | func (t TestHandler) ReadURI() string { 86 | return "/api/{version}/read_foo/{resource_id}" 87 | } 88 | 89 | func (t TestHandler) ReadListURI() string { 90 | return "/api/{version}/read_foo" 91 | } 92 | 93 | func (t TestHandler) UpdateURI() string { 94 | return "/api/{version}/update_foo/{resource_id}" 95 | } 96 | 97 | func (t TestHandler) DeleteURI() string { 98 | return "/api/{version}/delete_foo/{resource_id}" 99 | } 100 | 101 | // Ensures that CreateURI returns the custom URI. 102 | func TestCreateURICustom(t *testing.T) { 103 | assert := assert.New(t) 104 | proxy := resourceHandlerProxy{TestHandler{}} 105 | 106 | assert.Equal("/api/{version}/create_foo", proxy.CreateURI()) 107 | } 108 | 109 | // Ensures that ReadURI returns the custom URI. 110 | func TestReadURICustom(t *testing.T) { 111 | assert := assert.New(t) 112 | proxy := resourceHandlerProxy{TestHandler{}} 113 | 114 | assert.Equal("/api/{version}/read_foo/{resource_id}", proxy.ReadURI()) 115 | } 116 | 117 | // Ensures that ReadListURI returns the custom URI. 118 | func TestReadListURICustom(t *testing.T) { 119 | assert := assert.New(t) 120 | proxy := resourceHandlerProxy{TestHandler{}} 121 | 122 | assert.Equal("/api/{version}/read_foo", proxy.ReadListURI()) 123 | } 124 | 125 | // Ensures that UpdateURI returns the custom URI. 126 | func TestUpdateURICustom(t *testing.T) { 127 | assert := assert.New(t) 128 | proxy := resourceHandlerProxy{TestHandler{}} 129 | 130 | assert.Equal("/api/{version}/update_foo/{resource_id}", proxy.UpdateURI()) 131 | } 132 | 133 | // Ensures that DeleteURI returns the custom URI. 134 | func TestDeleteURICustom(t *testing.T) { 135 | assert := assert.New(t) 136 | proxy := resourceHandlerProxy{TestHandler{}} 137 | 138 | assert.Equal("/api/{version}/delete_foo/{resource_id}", proxy.DeleteURI()) 139 | } 140 | -------------------------------------------------------------------------------- /rest/example_rules_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 - 2015 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package rest 18 | 19 | import ( 20 | "fmt" 21 | "math/rand" 22 | ) 23 | 24 | // ResourceWithSecret represents a domain model for which we want to perform 25 | // CRUD operations with. Endpoints can operate on any type of entity -- 26 | // primitive, struct, or composite -- so long as it is serializable (by default, 27 | // this means JSON-serializable via either MarshalJSON or JSON struct tags). The 28 | // resource in this example has a field, "Secret", which we don't want to 29 | // include in REST responses. 30 | type ResourceWithSecret struct { 31 | ID int 32 | Foo string 33 | Nested FooResource 34 | Secret string 35 | } 36 | 37 | // ResourceWithSecretHandler implements the ResourceHandler interface. It 38 | // specifies the business logic for performing CRUD operations. 39 | // BaseResourceHandler provides stubs for each method if you only need to 40 | // implement certain operations (as this example illustrates). 41 | type ResourceWithSecretHandler struct { 42 | BaseResourceHandler 43 | } 44 | 45 | // ResourceName is used to identify what resource a handler corresponds to and 46 | // is used in the endpoint URLs, i.e. /api/:version/resource. 47 | func (r ResourceWithSecretHandler) ResourceName() string { 48 | return "resource" 49 | } 50 | 51 | // CreateResource is the logic that corresponds to creating a new resource at 52 | // POST /api/:version/resource. Typically, this would insert a record into a 53 | // database. It returns the newly created resource or an error if the create 54 | // failed. Because our Rules specify types, we can access the Payload data in a 55 | // type-safe way. 56 | func (r ResourceWithSecretHandler) CreateResource(ctx RequestContext, data Payload, 57 | version string) (Resource, error) { 58 | // Make a database call here. 59 | id := rand.Int() 60 | foo, _ := data.GetString("foo") 61 | created := &ResourceWithSecret{ID: id, Foo: foo, Secret: "secret"} 62 | return created, nil 63 | } 64 | 65 | // ReadResource is the logic that corresponds to reading a single resource by 66 | // its ID at GET /api/:version/resource/{id}. Typically, this would make some 67 | // sort of database query to load the resource. If the resource doesn't exist, 68 | // nil should be returned along with an appropriate error. 69 | func (r ResourceWithSecretHandler) ReadResource(ctx RequestContext, id string, 70 | version string) (Resource, error) { 71 | // Make a database call here. 72 | if id == "42" { 73 | return &ResourceWithSecret{ 74 | ID: 42, 75 | Foo: "hello world", 76 | Secret: "keep it secret, keep it safe", 77 | }, nil 78 | } 79 | return nil, ResourceNotFound(fmt.Sprintf("No resource with id %s", id)) 80 | } 81 | 82 | // Rules returns the resource rules to apply to incoming requests and outgoing 83 | // responses. The default behavior, seen in BaseResourceHandler, is to apply no 84 | // rules. Note that a Rule is not specified for the "Secret" field. This means 85 | // that field will not be included in the response. The "Type" field on a Rule 86 | // indicates the type the incoming data should be coerced to. If coercion fails, 87 | // an error indicating this will be sent back in the response. If no type is 88 | // specified, no coercion will be performed. Rules may also be nested. NewRules 89 | // is used to initialize Rules and associate them with the resource type using 90 | // the nil pointer. This allows Rule validation to occur at startup. 91 | func (r ResourceWithSecretHandler) Rules() Rules { 92 | return NewRules((*ResourceWithSecret)(nil), 93 | &Rule{ 94 | Field: "ID", 95 | FieldAlias: "id", 96 | Type: Int, 97 | Versions: []string{"1, 2"}, 98 | OutputOnly: true, 99 | }, 100 | &Rule{ 101 | Field: "Foo", 102 | FieldAlias: "f", 103 | Type: String, 104 | Versions: []string{"1"}, 105 | Required: true, 106 | }, 107 | &Rule{ 108 | Field: "Foo", 109 | FieldAlias: "foo", 110 | Type: String, 111 | Versions: []string{"2"}, 112 | Required: true, 113 | }, 114 | &Rule{ 115 | Field: "Nested", 116 | FieldAlias: "nested", 117 | Versions: []string{"2"}, 118 | Rules: NewRules((*FooResource)(nil), 119 | &Rule{ 120 | Field: "ID", 121 | FieldAlias: "id", 122 | Type: Int, 123 | OutputOnly: true, 124 | }, 125 | &Rule{ 126 | Field: "Foobar", 127 | FieldAlias: "foobar", 128 | Type: String, 129 | }, 130 | ), 131 | }, 132 | ) 133 | } 134 | 135 | // This example shows how Rules are used to provide fine-grained control over 136 | // response input and output. 137 | func Example_rules() { 138 | api := NewAPI(NewConfiguration()) 139 | 140 | // Call RegisterResourceHandler to wire up ResourceWithSecretHandler. 141 | api.RegisterResourceHandler(ResourceWithSecretHandler{}) 142 | 143 | // We're ready to hit our CRUD endpoints. 144 | api.Start(":8080") 145 | } 146 | -------------------------------------------------------------------------------- /rest/example_simple_crud_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 - 2015 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package rest 18 | 19 | import ( 20 | "fmt" 21 | "math/rand" 22 | "net/http" 23 | "strconv" 24 | ) 25 | 26 | // FooResource represents a domain model for which we want to perform CRUD 27 | // operations with. Endpoints can operate on any type of entity -- primitive, 28 | // struct, or composite -- so long as it is serializable (by default, this means 29 | // JSON-serializable via either MarshalJSON or JSON struct tags). 30 | type FooResource struct { 31 | ID int `json:"id"` 32 | Foobar string `json:"foobar"` 33 | } 34 | 35 | // FooHandler implements the ResourceHandler interface. It specifies the 36 | // business logic for performing CRUD operations. 37 | type FooHandler struct { 38 | BaseResourceHandler 39 | } 40 | 41 | // ResourceName is used to identify what resource a handler corresponds to and 42 | // is used in the endpoint URLs, i.e. /api/:version/foo. 43 | func (f FooHandler) ResourceName() string { 44 | return "foo" 45 | } 46 | 47 | // CreateResource is the logic that corresponds to creating a new resource at 48 | // POST /api/:version/foo. Typically, this would insert a record into a 49 | // database. It returns the newly created resource or an error if the create 50 | // failed. 51 | func (f FooHandler) CreateResource(ctx RequestContext, data Payload, 52 | version string) (Resource, error) { 53 | // Make a database call here. 54 | id := rand.Int() 55 | foobar, _ := data.GetString("foobar") 56 | created := &FooResource{ID: id, Foobar: foobar} 57 | return created, nil 58 | } 59 | 60 | // ReadResource is the logic that corresponds to reading a single resource by 61 | // its ID at GET /api/:version/foo/{id}. Typically, this would make some sort of 62 | // database query to load the resource. If the resource doesn't exist, nil 63 | // should be returned along with an appropriate error. 64 | func (f FooHandler) ReadResource(ctx RequestContext, id string, 65 | version string) (Resource, error) { 66 | // Make a database call here. 67 | if id == "42" { 68 | return &FooResource{ID: 42, Foobar: "hello world"}, nil 69 | } 70 | return nil, ResourceNotFound(fmt.Sprintf("No resource with id %s", id)) 71 | } 72 | 73 | // ReadResourceList is the logic that corresponds to reading multiple resources, 74 | // perhaps with specified query parameters accessed through the RequestContext. 75 | // This is mapped to GET /api/:version/foo. Typically, this would make some sort 76 | // of database query to fetch the resources. It returns the slice of results, a 77 | // cursor (or empty) string, and error (or nil). 78 | func (f FooHandler) ReadResourceList(ctx RequestContext, limit int, 79 | cursor string, version string) ([]Resource, string, error) { 80 | // Make a database call here. 81 | resources := make([]Resource, 0, limit) 82 | resources = append(resources, &FooResource{ID: 1, Foobar: "hello"}) 83 | resources = append(resources, &FooResource{ID: 2, Foobar: "world"}) 84 | return resources, "", nil 85 | } 86 | 87 | // UpdateResource is the logic that corresponds to updating an existing resource 88 | // at PUT /api/:version/foo/{id}. Typically, this would make some sort of 89 | // database update call. It returns the updated resource or an error if the 90 | // update failed. 91 | func (f FooHandler) UpdateResource(ctx RequestContext, id string, data Payload, 92 | version string) (Resource, error) { 93 | // Make a database call here. 94 | updateID, _ := strconv.Atoi(id) 95 | foobar, _ := data.GetString("foobar") 96 | foo := &FooResource{ID: updateID, Foobar: foobar} 97 | return foo, nil 98 | } 99 | 100 | // DeleteResource is the logic that corresponds to deleting an existing resource 101 | // at DELETE /api/:version/foo/{id}. Typically, this would make some sort of 102 | // database delete call. It returns the deleted resource or an error if the 103 | // delete failed. 104 | func (f FooHandler) DeleteResource(ctx RequestContext, id string, 105 | version string) (Resource, error) { 106 | // Make a database call here. 107 | deleteID, _ := strconv.Atoi(id) 108 | foo := &FooResource{ID: deleteID, Foobar: "Goodbye world"} 109 | return foo, nil 110 | } 111 | 112 | // Authenticate is logic that is used to authenticate requests. The default 113 | // behavior of Authenticate, seen in BaseResourceHandler, always returns nil, 114 | // meaning all requests are authenticated. Returning an error means that the 115 | // request is unauthorized and any error message will be sent back with the 116 | // response. 117 | func (f FooHandler) Authenticate(r *http.Request) error { 118 | if secrets, ok := r.Header["Authorization"]; ok { 119 | if secrets[0] == "secret" { 120 | return nil 121 | } 122 | } 123 | 124 | return UnauthorizedRequest("You shall not pass") 125 | } 126 | 127 | // This example shows how to fully implement a basic ResourceHandler for 128 | // performing CRUD operations. 129 | func Example_simpleCrud() { 130 | api := NewAPI(NewConfiguration()) 131 | 132 | // Call RegisterResourceHandler to wire up FooHandler. 133 | api.RegisterResourceHandler(FooHandler{}) 134 | 135 | // We're ready to hit our CRUD endpoints. 136 | api.Start(":8080") 137 | } 138 | -------------------------------------------------------------------------------- /documentation.md: -------------------------------------------------------------------------------- 1 | # Introducing go-rest 2 | 3 | The goal of go-rest is to provide a framework that makes it easy to build a flexible and (mostly) unopinionated REST API with little ceremony. More important, it offers a way to build stable, resource-oriented endpoints which are production-ready. 4 | 5 | At Workiva, we require standardized REST endpoints across all our services. This makes development more fluent and consumption easier and predictable, not just through syntax but also semantics. Many of our internal web services are written in Python, often running within Google App Engine. Within the past year, however, we've begun to build more services using Go—both on and off App Engine—because it lends itself well to many of the problems we're solving. As a result, we needed a way to build RESTful web services in a fashion similar to our Python ones. 6 | 7 | go-rest provides the tooling to build RESTful services rapidly and in a way that meets our internal API specification. Because it takes care of a lot of the boilerplate involved with building these types of services and offers some useful utilities, we're opening go-rest up to the open-source community. The remainder of this post explains go-rest in more detail, how it can be used to build REST APIs, and how we use it at Workiva. 8 | 9 | ## Composition and Extension 10 | 11 | go-rest is designed to be composable and pluggable. It makes no assumptions about your stack and doesn't care what libraries you're using or what your data model looks like. This is important to us at Workiva because it means we don't dictate down to developers what they need to use to solve their problems or force any artificial design constraints. Some services are running on App Engine and use its datastore. Others run off App Engine and don't necessarily use the datastore for persistence or entity modeling. go-rest is suited to either situation. 12 | 13 | Several pieces of go-rest are pluggable, meaning custom implementations can be provided. JSON is arguably the most common serialization format for REST APIs and it's what we use at Workiva, so the framework ships with JSON response serialization out of the box. Other response serializers, e.g. YAML or XML, can be provided. 14 | 15 | go-rest supports a notion of middleware, which are essentially just functions invoked on API requests. This has a wide range of application, ranging from authentication to logging and stats. For example, we use OAuth2 middleware to ensure authorized resource access. 16 | 17 | ## Building Stable APIs 18 | 19 | In order to provide fine-grained control over API input and output, go-rest uses a concept of REST rules. Rules provide schema validation and type coercion for request input and fine-grained control over response output. They can specify types in which input and output values should be coerced. If coercion fails, a meaningful error will be returned in the response. Rule validation is also built in to ensure the names and types specified on rules match up with their corresponding struct properties. 20 | 21 | Rules provide further control in that they can be used to discard values. For example, input fields which don't have corresponding rules can be ignored. Likewise, output fields with no respective rules will be dropped from the response. 22 | 23 | A useful property of stable REST APIs is endpoint versioning. go-rest provides support for this idea, facilitating different codepath execution based on API versions. Furthermore, rules can be specified with versions, allowing for control over input and output of versioned endpoints. This prevents new fields from leaking into old API versions. 24 | 25 | We use a [SemVer](http://semver.org/)-like approach to managing endpoint versions. REST rules allow us to accomplish this quite easily. If endpoint stability isn't a concern, you can neglect to specify rule versions or simply return full resource representations. This can be useful for rapid prototyping. 26 | 27 | ## Generating API Documentation 28 | 29 | Using the REST rules described above has a beneficial side effect: they provide a means of self-documentation. go-rest can use rules to generate documentation for your REST API. This documentation describes what endpoints are available and the payloads they expect, including what fields there are, which of these are required, and the expected types as well as what the response looks like. The beautiful thing is you get this all for *free*. 30 | 31 | We make extensive use of this documentation for several reasons. First, it acts as a contract for services. We write integration tests which verify our endpoints behave as they say they do. Second, they minimize unneeded communication between teams. We publish API documentation internally and people can look at this to understand how a service is used rather than going to the team directly. Third, and related to the first point, they promote building stable, high-quality APIs. By making this information visible, we expose the surface area of the API and document how it changes. This also forces teams to be held accountable for the services they provide. 32 | 33 | ![Generated Documentation](docs_example.png) 34 | 35 | ## Going Forward 36 | 37 | go-rest serves as a foundational piece for building production-quality REST APIs and is intended to fit into existing applications with ease. It lays much of the groundwork needed and allows for more frictionless development. 38 | 39 | REST rules provide a convenient way to maintain endpoints. While they offer a great deal of value already, there is more work to be done in order to make them even more useful. This includes pluggable type coercion, which would allow for conversion and validation of custom types. Another item is making the data validation more robust. Whether a custom-written or pre-existing library is used, this would enable semantic validation and custom validation logic. Currently, rules only provide type validation. 40 | 41 | Further work might include building a test-harness system to speed up unit and integration testing and improving documentation generation. 42 | -------------------------------------------------------------------------------- /rest/handler_tpl.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 - 2015 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package rest 18 | 19 | // handlerTemplate is the mustache template for the handler documentation. 20 | const handlerTemplate = ` 21 | 22 | 23 | 24 | Documentation - {{resource}} 25 | 27 | 29 | 30 | 32 | 43 | 44 | 45 | 46 |
47 | 48 | 75 | 76 | 79 | 80 | {{#endpoints}} 81 |
82 |

{{method}} {{uri}}

83 |

{{{description}}}

84 | 85 |
86 | {{#hasInput}} 87 |
88 |

Request Payload

89 | 95 |
96 |
97 |
98 | {{#inputFields}} 99 |
100 | 101 | {{name}} 102 | 103 | {{required}} 104 | 105 | 106 |

107 | ({{type}}) {{description}} 108 |

109 |
110 | {{/inputFields}} 111 |
112 |
113 |
114 |
115 |
117 |
{{exampleRequest}}
118 |
119 |
120 |
121 |
122 |
123 | {{/hasInput}} 124 | 125 |
126 |

Response Payload

127 | 133 |
134 |
135 |
136 | {{#outputFields}} 137 |
138 | 139 | {{name}} 140 | 141 |

142 | ({{type}}) {{description}} 143 |

144 |
145 | {{/outputFields}} 146 |
147 |
148 |
149 |
150 |
152 |
{{exampleResponse}}
153 |
154 |
155 |
156 |
157 |
158 | 159 |
160 | {{/endpoints}} 161 | 162 |
163 |
164 | 165 | 170 | 171 | 172 | 173 | 174 | ` 175 | -------------------------------------------------------------------------------- /rest/base_handler.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 - 2015 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package rest 18 | 19 | import ( 20 | "fmt" 21 | "net/http" 22 | ) 23 | 24 | // BaseResourceHandler is a base implementation of ResourceHandler with stubs for the 25 | // CRUD operations. This allows ResourceHandler implementations to only implement 26 | // what they need. 27 | type BaseResourceHandler struct{} 28 | 29 | // ResourceName is a stub. It must be implemented. 30 | func (b BaseResourceHandler) ResourceName() string { 31 | return "" 32 | } 33 | 34 | // CreateURI is a stub. Implement if necessary. The default create URI is 35 | // /api/v{version:[^/]+}/resourceName. 36 | func (b BaseResourceHandler) CreateURI() string { 37 | return "" 38 | } 39 | 40 | // CreateDocumentation is a stub. Implement if necessary. 41 | func (b BaseResourceHandler) CreateDocumentation() string { 42 | return "" 43 | } 44 | 45 | // ReadURI is a stub. Implement if necessary. The default read URI is 46 | // /api/v{version:[^/]+}/resourceName/{resource_id}. 47 | func (b BaseResourceHandler) ReadURI() string { 48 | return "" 49 | } 50 | 51 | // ReadDocumentation is a stub. Implement if necessary. 52 | func (b BaseResourceHandler) ReadDocumentation() string { 53 | return "" 54 | } 55 | 56 | // ReadListURI is a stub. Implement if necessary. The default read list URI is 57 | // /api/v{version:[^/]+}/resourceName. 58 | func (b BaseResourceHandler) ReadListURI() string { 59 | return "" 60 | } 61 | 62 | // ReadListDocumentation is a stub. Implement if necessary. 63 | func (b BaseResourceHandler) ReadListDocumentation() string { 64 | return "" 65 | } 66 | 67 | // UpdateURI is a stub. Implement if necessary. The default update URI is 68 | // /api/v{version:[^/]+}/resourceName/{resource_id}. 69 | func (b BaseResourceHandler) UpdateURI() string { 70 | return "" 71 | } 72 | 73 | // UpdateDocumentation is a stub. Implement if necessary. 74 | func (b BaseResourceHandler) UpdateDocumentation() string { 75 | return "" 76 | } 77 | 78 | // UpdateListURI is a stub. Implement if necessary. The default update list URI is 79 | // /api/v{version:[^/]+}/resourceName. 80 | func (b BaseResourceHandler) UpdateListURI() string { 81 | return "" 82 | } 83 | 84 | // UpdateListDocumentation is a stub. Implement if necessary. 85 | func (b BaseResourceHandler) UpdateListDocumentation() string { 86 | return "" 87 | } 88 | 89 | // DeleteURI is a stub. Implement if necessary. The default delete URI is 90 | // /api/v{version:[^/]+}/resourceName/{resource_id}. 91 | func (b BaseResourceHandler) DeleteURI() string { 92 | return "" 93 | } 94 | 95 | // DeleteDocumentation is a stub. Implement if necessary. 96 | func (b BaseResourceHandler) DeleteDocumentation() string { 97 | return "" 98 | } 99 | 100 | // CreateResource is a stub. Implement if necessary. 101 | func (b BaseResourceHandler) CreateResource(ctx RequestContext, data Payload, 102 | version string) (Resource, error) { 103 | return nil, MethodNotAllowed("CreateResource is not implemented") 104 | } 105 | 106 | // ReadResourceList is a stub. Implement if necessary. 107 | func (b BaseResourceHandler) ReadResourceList(ctx RequestContext, limit int, 108 | cursor string, version string) ([]Resource, string, error) { 109 | return nil, "", MethodNotAllowed("ReadResourceList not implemented") 110 | } 111 | 112 | // ReadResource is a stub. Implement if necessary. 113 | func (b BaseResourceHandler) ReadResource(ctx RequestContext, id string, 114 | version string) (Resource, error) { 115 | return nil, MethodNotAllowed("ReadResource not implemented") 116 | } 117 | 118 | // UpdateResourceList is a stub. Implement if necessary. 119 | func (b BaseResourceHandler) UpdateResourceList(ctx RequestContext, data []Payload, 120 | version string) ([]Resource, error) { 121 | return nil, MethodNotAllowed("UpdateResourceList not implemented") 122 | } 123 | 124 | // UpdateResource is a stub. Implement if necessary. 125 | func (b BaseResourceHandler) UpdateResource(ctx RequestContext, id string, 126 | data Payload, version string) (Resource, error) { 127 | return nil, MethodNotAllowed("UpdateResource not implemented") 128 | } 129 | 130 | // DeleteResource is a stub. Implement if necessary. 131 | func (b BaseResourceHandler) DeleteResource(ctx RequestContext, id string, 132 | version string) (Resource, error) { 133 | return nil, MethodNotAllowed("DeleteResource not implemented") 134 | } 135 | 136 | // Authenticate is the default authentication logic. All requests are authorized. 137 | // Implement custom authentication logic if necessary. 138 | func (b BaseResourceHandler) Authenticate(r *http.Request) error { 139 | return nil 140 | } 141 | 142 | func (b BaseResourceHandler) ValidVersions() []string { 143 | return nil 144 | } 145 | 146 | // Rules returns the resource rules to apply to incoming requests and outgoing 147 | // responses. No rules are applied by default. Implement if necessary. 148 | func (b BaseResourceHandler) Rules() Rules { 149 | return &rules{} 150 | } 151 | 152 | // resourceHandlerProxy wraps a ResourceHandler and allows the framework to provide 153 | // additional logic around the proxied ResourceHandler, including default logic such 154 | // as REST URIs. 155 | type resourceHandlerProxy struct { 156 | ResourceHandler 157 | } 158 | 159 | // ResourceName returns the wrapped ResourceHandler's resource name. If the proxied 160 | // handler doesn't have ResourceName implemented, it panics. 161 | func (r resourceHandlerProxy) ResourceName() string { 162 | name := r.ResourceHandler.ResourceName() 163 | if name == "" { 164 | panic("ResourceHandler must implement ResourceName()") 165 | } 166 | return name 167 | } 168 | 169 | // CreateURI returns the URI for creating a resource using the handler-specified 170 | // URI while falling back to a sensible default if not provided. 171 | func (r resourceHandlerProxy) CreateURI() string { 172 | uri := r.ResourceHandler.CreateURI() 173 | if uri == "" { 174 | uri = fmt.Sprintf("/api/v{%s:[^/]+}/%s", versionKey, r.ResourceName()) 175 | } 176 | return uri 177 | } 178 | 179 | // ReadURI returns the URI for reading a specific resource using the handler-specified 180 | // URI while falling back to a sensible default if not provided. 181 | func (r resourceHandlerProxy) ReadURI() string { 182 | uri := r.ResourceHandler.ReadURI() 183 | if uri == "" { 184 | uri = fmt.Sprintf("/api/v{%s:[^/]+}/%s/{%s}", versionKey, r.ResourceName(), 185 | resourceIDKey) 186 | } 187 | return uri 188 | } 189 | 190 | // ReadListURI returns the URI for reading a list of resources using the handler- 191 | // specified URI while falling back to a sensible default if not provided. 192 | func (r resourceHandlerProxy) ReadListURI() string { 193 | uri := r.ResourceHandler.ReadListURI() 194 | if uri == "" { 195 | uri = fmt.Sprintf("/api/v{%s:[^/]+}/%s", versionKey, r.ResourceName()) 196 | } 197 | return uri 198 | } 199 | 200 | // UpdateURI returns the URI for updating a specific resource using the handler- 201 | // specified URI while falling back to a sensible default if not provided. 202 | func (r resourceHandlerProxy) UpdateURI() string { 203 | uri := r.ResourceHandler.UpdateURI() 204 | if uri == "" { 205 | uri = fmt.Sprintf("/api/v{%s:[^/]+}/%s/{%s}", versionKey, r.ResourceName(), 206 | resourceIDKey) 207 | } 208 | return uri 209 | } 210 | 211 | // UpdateListURI returns the URI for updating a list of resources using the handler- 212 | // specified URI while falling back to a sensible default if not provided. 213 | func (r resourceHandlerProxy) UpdateListURI() string { 214 | uri := r.ResourceHandler.UpdateListURI() 215 | if uri == "" { 216 | uri = fmt.Sprintf("/api/v{%s:[^/]+}/%s", versionKey, r.ResourceName()) 217 | } 218 | return uri 219 | } 220 | 221 | // DeleteURI returns the URI for deleting a specific resource using the handler- 222 | // specified URI while falling back to a sensible default if not provided. 223 | func (r resourceHandlerProxy) DeleteURI() string { 224 | uri := r.ResourceHandler.DeleteURI() 225 | if uri == "" { 226 | uri = fmt.Sprintf("/api/v{%s:[^/]+}/%s/{%s}", versionKey, 227 | r.ResourceHandler.ResourceName(), resourceIDKey) 228 | } 229 | return uri 230 | } 231 | -------------------------------------------------------------------------------- /rest/context_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 - 2015 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package rest 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "crypto/tls" 23 | "fmt" 24 | "net/http" 25 | "net/http/httptest" 26 | "testing" 27 | 28 | "github.com/stretchr/testify/assert" 29 | "github.com/stretchr/testify/require" 30 | ) 31 | 32 | // Test Handlers 33 | type TestResourceHandler struct { 34 | BaseResourceHandler 35 | } 36 | 37 | func (t TestResourceHandler) ResourceName() string { 38 | return "widgets" 39 | } 40 | 41 | func (t TestResourceHandler) CreateResource(r RequestContext, data Payload, 42 | version string) (Resource, error) { 43 | 44 | resource := map[string]string{"test": "resource"} 45 | return resource, nil 46 | } 47 | 48 | func (t TestResourceHandler) ReadResource(r RequestContext, id string, 49 | version string) (Resource, error) { 50 | 51 | resource := map[string]string{"test": "resource"} 52 | return resource, nil 53 | } 54 | 55 | type ComplexTestResourceHandler struct { 56 | BaseResourceHandler 57 | } 58 | 59 | type contextKey string 60 | 61 | func (t ComplexTestResourceHandler) ResourceName() string { 62 | return "resources" 63 | } 64 | 65 | func (t ComplexTestResourceHandler) CreateURI() string { 66 | return "/api/v{version:[^/]+}/{company}/{category}/resources" 67 | } 68 | func (t ComplexTestResourceHandler) CreateResource(r RequestContext, data Payload, 69 | version string) (Resource, error) { 70 | 71 | resource := map[string]string{"test": "resource"} 72 | return resource, nil 73 | } 74 | 75 | // Ensures that we're correctly setting a value on the http.Request Context 76 | func TestSetValueOnRequestContext(t *testing.T) { 77 | req, err := http.NewRequestWithContext(context.Background(), "GET", "http://example.com/foo", nil) 78 | require.NoError(t, err) 79 | 80 | k := contextKey("mykey") 81 | val := req.Context().Value(k) 82 | assert.Nil(t, val) 83 | 84 | expected := "myval" 85 | req = setValueOnRequestContext(req, k, expected) 86 | actual := req.Context().Value(k) 87 | assert.Equal(t, expected, actual) 88 | } 89 | 90 | func TestValueReturnsRequest(t *testing.T) { 91 | assert := assert.New(t) 92 | req, err := http.NewRequest("GET", "http://example.com/foo", nil) 93 | require.NoError(t, err) 94 | 95 | ctx := NewContext(req, nil) 96 | val := ctx.Value(requestKey) 97 | assert.Equal(req, val, "Value called with the requestKey should return the request") 98 | } 99 | 100 | func TestValueReturnsPreviouslySetValue(t *testing.T) { 101 | assert := assert.New(t) 102 | req, err := http.NewRequest("GET", "http://example.com/foo", nil) 103 | require.NoError(t, err) 104 | 105 | ctx := NewContext(req, nil) 106 | k := contextKey("mykey") 107 | expected := "myval" 108 | ctx = ctx.WithValue(k, expected) 109 | 110 | actual := ctx.Value(k) 111 | assert.Equal(expected, actual, "Value called with a key should find the value on the request's context if it exists") 112 | } 113 | 114 | func TestValueReturnsNilIfNoKey(t *testing.T) { 115 | assert := assert.New(t) 116 | req, err := http.NewRequest("GET", "http://example.com/foo", nil) 117 | require.NoError(t, err) 118 | 119 | ctx := NewContext(req, nil).(*requestContext) 120 | k := contextKey("mykey") 121 | 122 | actual := ctx.Value(k) 123 | assert.Nil(actual, "Value called with a non-existent key should return nil") 124 | } 125 | 126 | // Ensures that if a limit doesn't exist on the context, the default is returned. 127 | func TestLimitDefault(t *testing.T) { 128 | assert := assert.New(t) 129 | req, err := http.NewRequest("GET", "http://example.com/foo", nil) 130 | require.NoError(t, err) 131 | 132 | writer := httptest.NewRecorder() 133 | ctx := NewContext(req, writer) 134 | assert.Equal(100, ctx.Limit()) 135 | } 136 | 137 | // Ensures that if an invalid limit value is on the context, the default is returned. 138 | func TestLimitBadValue(t *testing.T) { 139 | assert := assert.New(t) 140 | req, err := http.NewRequest("GET", "http://example.com/foo", nil) 141 | require.NoError(t, err) 142 | 143 | writer := httptest.NewRecorder() 144 | ctx := NewContext(req, writer) 145 | ctx = ctx.WithValue(limitKey, "blah") 146 | assert.Equal(100, ctx.Limit()) 147 | } 148 | 149 | // Ensures that the correct limit is returned from the context. 150 | func TestLimit(t *testing.T) { 151 | assert := assert.New(t) 152 | req, err := http.NewRequest("GET", "http://example.com/foo", nil) 153 | require.NoError(t, err) 154 | 155 | writer := httptest.NewRecorder() 156 | ctx := NewContext(req, writer) 157 | ctx = ctx.WithValue(limitKey, "5") 158 | assert.Equal(5, ctx.Limit()) 159 | } 160 | 161 | // Ensures that Messages returns the messages set on the context. 162 | func TestMessagesNoError(t *testing.T) { 163 | assert := assert.New(t) 164 | req, err := http.NewRequest("GET", "http://example.com/foo", nil) 165 | require.NoError(t, err) 166 | 167 | writer := httptest.NewRecorder() 168 | ctx := NewContext(req, writer) 169 | message := "foo" 170 | 171 | assert.Equal(0, len(ctx.Messages())) 172 | 173 | ctx.AddMessage(message) 174 | 175 | if assert.Equal(1, len(ctx.Messages())) { 176 | assert.Equal(message, ctx.Messages()[0]) 177 | } 178 | } 179 | 180 | // Ensures that Messages returns the messages set on the context and the error message 181 | // when an error is set. 182 | func TestMessagesWithError(t *testing.T) { 183 | assert := assert.New(t) 184 | req, err := http.NewRequest("GET", "http://example.com/foo", nil) 185 | require.NoError(t, err) 186 | 187 | writer := httptest.NewRecorder() 188 | ctx := NewContext(req, writer) 189 | message := "foo" 190 | errMessage := "blah" 191 | err = fmt.Errorf(errMessage) 192 | 193 | ctx = ctx.setError(err) 194 | if assert.Equal(1, len(ctx.Messages())) { 195 | assert.Equal(errMessage, ctx.Messages()[0]) 196 | } 197 | 198 | ctx.AddMessage(message) 199 | 200 | if assert.Equal(2, len(ctx.Messages())) { 201 | assert.Equal(message, ctx.Messages()[0]) 202 | assert.Equal(errMessage, ctx.Messages()[1]) 203 | } 204 | } 205 | 206 | // Ensures that Header returns the request Header. 207 | func TestHeader(t *testing.T) { 208 | assert := assert.New(t) 209 | req, err := http.NewRequest("GET", "http://example.com/foo", nil) 210 | require.NoError(t, err) 211 | 212 | writer := httptest.NewRecorder() 213 | ctx := NewContext(req, writer) 214 | 215 | assert.Equal(req.Header, ctx.Header()) 216 | } 217 | 218 | // Ensures that Body returns a buffer containing the request Body. 219 | func TestBody(t *testing.T) { 220 | assert := assert.New(t) 221 | payload := []byte(`[{"foo": "bar"}]`) 222 | r := bytes.NewReader(payload) 223 | req, err := http.NewRequest("GET", "http://example.com/foo", r) 224 | require.NoError(t, err) 225 | 226 | writer := httptest.NewRecorder() 227 | ctx := NewContext(req, writer) 228 | 229 | assert.Equal(payload, ctx.Body().Bytes()) 230 | } 231 | 232 | func TestBuildURL(t *testing.T) { 233 | assert := assert.New(t) 234 | 235 | api := NewAPI(NewConfiguration()) 236 | api.RegisterResourceHandler(TestResourceHandler{}) 237 | api.RegisterResourceHandler(ComplexTestResourceHandler{}) 238 | 239 | req, err := http.NewRequest("GET", "https://example.com/api/v1/widgets", nil) 240 | require.NoError(t, err) 241 | req = setValueOnRequestContext(req, "version", "1") 242 | 243 | writer := httptest.NewRecorder() 244 | ctx := NewContextWithRouter(req, writer, api.(*muxAPI).router) 245 | 246 | url, err := ctx.BuildURL("widgets", HandleCreate, nil) 247 | require.NoError(t, err) 248 | assert.Equal(url.String(), "http://example.com/api/v1/widgets") 249 | 250 | url, err = ctx.BuildURL("widgets", HandleRead, RouteVars{"resource_id": "111"}) 251 | require.NoError(t, err) 252 | assert.Equal(url.String(), "http://example.com/api/v1/widgets/111") 253 | 254 | url, err = ctx.BuildURL("widgets", HandleRead, RouteVars{"resource_id": "111"}) 255 | require.NoError(t, err) 256 | assert.Equal(url.Path, "/api/v1/widgets/111") 257 | 258 | // Secure request should produce https URL 259 | req.TLS = &tls.ConnectionState{} 260 | url, err = ctx.BuildURL("widgets", HandleRead, RouteVars{"resource_id": "222"}) 261 | require.NoError(t, err) 262 | assert.Equal(url.String(), "https://example.com/api/v1/widgets/222") 263 | 264 | // Make sure this works with another version number 265 | ctx = ctx.WithValue("version", "2") 266 | url, err = ctx.BuildURL("resources", HandleCreate, RouteVars{ 267 | "company": "acme", 268 | "category": "anvils"}) 269 | require.NoError(t, err) 270 | assert.Equal(url.String(), "https://example.com/api/v2/acme/anvils/resources") 271 | } 272 | -------------------------------------------------------------------------------- /rest/client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 - 2015 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package rest 18 | 19 | import ( 20 | "bytes" 21 | "encoding/json" 22 | "fmt" 23 | "io" 24 | "io/ioutil" 25 | "net/http" 26 | "net/url" 27 | ) 28 | 29 | // Supported HTTP Methods 30 | const ( 31 | httpDelete = "DELETE" 32 | httpGet = "GET" 33 | httpPost = "POST" 34 | httpPut = "PUT" 35 | ) 36 | 37 | // InvocationHandler is a function that is to be wrapped by the ClientMiddleware 38 | type InvocationHandler func(c *http.Client, method, url string, body interface{}, header http.Header) (*Response, error) 39 | 40 | // ClientMiddleware is a function that wraps another function, and returns the wrapped function 41 | type ClientMiddleware func(InvocationHandler) InvocationHandler 42 | 43 | // HttpClient is the type that is used to perform HTTP Methods 44 | type HttpClient interface { 45 | Do(req *http.Request) (resp *http.Response, err error) 46 | Get(url string) (resp *http.Response, err error) 47 | Post(url string, bodyType string, body io.Reader) (resp *http.Response, err error) 48 | PostForm(url string, data url.Values) (resp *http.Response, err error) 49 | Head(url string) (resp *http.Response, err error) 50 | } 51 | 52 | // RestClient performs HTTP methods including Get, Post, Put, and Delete 53 | type RestClient interface { 54 | // Get will perform an HTTP GET on the specified URL and return the response. 55 | Get(url string, header http.Header) (*Response, error) 56 | 57 | // Post will perform an HTTP POST on the specified URL and return the response. 58 | Post(url string, body interface{}, header http.Header) (*Response, error) 59 | 60 | // Put will perform an HTTP PUT on the specified URL and return the response. 61 | Put(url string, body interface{}, header http.Header) (*Response, error) 62 | 63 | // Delete will perform an HTTP DELETE on the specified URL and return the response. 64 | Delete(url string, header http.Header) (*Response, error) 65 | } 66 | 67 | func NewRestClient(c HttpClient, middleware ...ClientMiddleware) RestClient { 68 | return &client{c, middleware} 69 | } 70 | 71 | // Client is the type that encapsulates and uses the Authorizer to sign any REST 72 | // requests that are performed. 73 | type Client struct { 74 | HttpClient 75 | } 76 | 77 | // client is the type that encapsulates and uses the Authorizer to sign any REST 78 | // requests that are performed and has a list of middlewares to be applied to 79 | // it's GET, POST, PUT, and DELETE functions. 80 | type client struct { 81 | HttpClient 82 | middleware []ClientMiddleware 83 | } 84 | 85 | // Response is unmarshaled struct returned from an HTTP request. 86 | type Response struct { 87 | Status int // HTTP status code. 88 | Reason string // Reason message for the status code. 89 | Messages []string // Any server messages attached to the Response. 90 | Next string // A cursor to the next result set. 91 | Result interface{} // The decoded result of the REST request. 92 | Raw *http.Response // The raw HTTP response. 93 | } 94 | 95 | // Wraps response decoding error in a helpful way 96 | type ResponseDecodeError struct { 97 | StatusCode int // Response status code 98 | Status string // Response status message 99 | Response []byte // Payload of the response that could not be decoded 100 | DecodeError error // Error that occurred while decoding the response 101 | } 102 | 103 | func (rde *ResponseDecodeError) Error() string { 104 | return fmt.Sprintf("(Error, Status Code, Status Message, Payload) = (%s, %d, %s, %s)", 105 | rde.DecodeError.Error(), rde.StatusCode, rde.Status, string(rde.Response)) 106 | } 107 | 108 | // applyMiddleware wraps a given InvocationHandler with all of the middleware in the client 109 | func (c *client) applyMiddleware(method InvocationHandler) InvocationHandler { 110 | for _, middleware := range c.middleware { 111 | method = middleware(method) 112 | } 113 | return method 114 | } 115 | 116 | func (c *client) process(method, url string, body interface{}, header http.Header) (*Response, error) { 117 | m := c.applyMiddleware(do) 118 | return m(c.HttpClient.(*http.Client), method, url, body, header) 119 | } 120 | 121 | // Get will perform an HTTP GET on the specified URL and return the response. 122 | func (c *client) Get(url string, header http.Header) (*Response, error) { 123 | return c.process(httpGet, url, nil, header) 124 | } 125 | 126 | // Post will perform an HTTP POST on the specified URL and return the response. 127 | func (c *client) Post(url string, body interface{}, header http.Header) (*Response, error) { 128 | return c.process(httpPost, url, body, header) 129 | } 130 | 131 | // Put will perform an HTTP PUT on the specified URL and return the response. 132 | func (c *client) Put(url string, body interface{}, header http.Header) (*Response, error) { 133 | return c.process(httpPut, url, body, header) 134 | } 135 | 136 | // Delete will perform an HTTP DELETE on the specified URL and return the response. 137 | func (c *client) Delete(url string, header http.Header) (*Response, error) { 138 | return c.process(httpDelete, url, nil, header) 139 | } 140 | 141 | /* Internal client calls. Would like to move up to the top level with a major 142 | version change. Keeping this around for now to maintain backwards compat and allow 143 | consumers to still construct the Client struct directly vs using the new constructor 144 | method that returns the internal implementation that maps to the interface. 145 | */ 146 | 147 | // Get will perform an HTTP GET on the specified URL and return the response. 148 | func (c *Client) Get(url string, header http.Header) (*Response, error) { 149 | return do(c.HttpClient.(*http.Client), httpGet, url, nil, header) 150 | } 151 | 152 | // Post will perform an HTTP POST on the specified URL and return the response. 153 | func (c *Client) Post(url string, body interface{}, header http.Header) (*Response, error) { 154 | return do(c.HttpClient.(*http.Client), httpPost, url, body, header) 155 | } 156 | 157 | // Put will perform an HTTP PUT on the specified URL and return the response. 158 | func (c *Client) Put(url string, body interface{}, header http.Header) (*Response, error) { 159 | return do(c.HttpClient.(*http.Client), httpPut, url, body, header) 160 | } 161 | 162 | // Delete will perform an HTTP DELETE on the specified URL and return the response. 163 | func (c *Client) Delete(url string, header http.Header) (*Response, error) { 164 | return do(c.HttpClient.(*http.Client), httpDelete, url, nil, header) 165 | } 166 | 167 | var do = func(c *http.Client, method, url string, body interface{}, header http.Header) (*Response, error) { 168 | if header == nil { 169 | header = http.Header{} 170 | } 171 | 172 | var reqBody io.Reader 173 | switch method { 174 | case httpPost, httpPut: 175 | body, err := json.Marshal(body) 176 | if err != nil { 177 | return nil, err 178 | } 179 | reqBody = bytes.NewReader(body) 180 | header.Set("Content-Type", "application/json") 181 | } 182 | 183 | req, err := http.NewRequest(method, url, reqBody) 184 | if err != nil { 185 | return nil, err 186 | } 187 | 188 | req.Header = header 189 | 190 | resp, err := c.Do(req) 191 | if err != nil { 192 | return nil, err 193 | } 194 | defer resp.Body.Close() 195 | 196 | // Don't try to decode the response on 404. 197 | if resp.StatusCode == http.StatusNotFound { 198 | return &Response{ 199 | Status: resp.StatusCode, 200 | Reason: http.StatusText(resp.StatusCode), 201 | Messages: []string{}, 202 | Next: "", 203 | Raw: resp, 204 | }, nil 205 | } 206 | 207 | rawResp, err := ioutil.ReadAll(resp.Body) 208 | if err != nil { 209 | return nil, err 210 | } 211 | 212 | return decodeResponse(rawResp, resp) 213 | } 214 | 215 | func decodeResponse(response []byte, r *http.Response) (*Response, error) { 216 | var payload map[string]interface{} 217 | if err := json.Unmarshal(response, &payload); err != nil { 218 | return nil, &ResponseDecodeError{ 219 | StatusCode: r.StatusCode, 220 | Status: r.Status, 221 | DecodeError: err, 222 | Response: response, 223 | } 224 | } 225 | 226 | messages := []string{} 227 | for _, message := range payload["messages"].([]interface{}) { 228 | messages = append(messages, message.(string)) 229 | } 230 | 231 | next := "" 232 | if n, ok := payload["next"]; ok { 233 | next = n.(string) 234 | } 235 | 236 | result, ok := payload["result"] 237 | if !ok { 238 | result = payload["results"] 239 | } 240 | 241 | resp := &Response{ 242 | Status: int(payload["status"].(float64)), 243 | Reason: payload["reason"].(string), 244 | Messages: messages, 245 | Next: next, 246 | Result: result, 247 | Raw: r, 248 | } 249 | 250 | return resp, nil 251 | } 252 | -------------------------------------------------------------------------------- /rest/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 - 2015 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package rest 18 | 19 | import ( 20 | "fmt" 21 | "reflect" 22 | "strconv" 23 | "time" 24 | ) 25 | 26 | // Type is a data type to coerce a value to specified with a Rule. 27 | type Type uint 28 | 29 | // Type constants define the data types that Rules can specify for coercion. 30 | const ( 31 | Interface Type = iota 32 | Int 33 | Int8 34 | Int16 35 | Int32 36 | Int64 37 | Uint 38 | Uint8 39 | Uint16 40 | Uint32 41 | Uint64 42 | Float32 43 | Float64 44 | String 45 | Bool 46 | Slice 47 | Map 48 | Duration 49 | Time 50 | Byte = Uint8 51 | Unspecified = Interface 52 | ) 53 | 54 | // typeToName maps Types to their human-readable names. 55 | var typeToName = map[Type]string{ 56 | Interface: "interface{}", 57 | Int: "int", 58 | Int8: "int8", 59 | Int16: "int16", 60 | Int32: "int32", 61 | Int64: "int64", 62 | Uint: "uint", 63 | Uint8: "uint8", 64 | Uint16: "uint16", 65 | Uint32: "uint32", 66 | Uint64: "uint64", 67 | Float32: "float32", 68 | Float64: "float64", 69 | String: "string", 70 | Bool: "bool", 71 | Slice: "[]interface{}", 72 | Map: "map[string]interface{}", 73 | Duration: "time.Duration", 74 | Time: "time.Time", 75 | } 76 | 77 | // typeToKind maps Types to their reflect Kind. 78 | var typeToKind = map[Type]reflect.Kind{ 79 | Interface: reflect.Interface, 80 | Int: reflect.Int, 81 | Int8: reflect.Int8, 82 | Int16: reflect.Int16, 83 | Int32: reflect.Int32, 84 | Int64: reflect.Int64, 85 | Uint: reflect.Uint, 86 | Uint8: reflect.Uint8, 87 | Uint16: reflect.Uint16, 88 | Uint32: reflect.Uint32, 89 | Uint64: reflect.Uint64, 90 | Float32: reflect.Float32, 91 | Float64: reflect.Float64, 92 | String: reflect.String, 93 | Bool: reflect.Bool, 94 | Slice: reflect.Slice, 95 | Map: reflect.Map, 96 | Duration: reflect.Int64, 97 | Time: reflect.Struct, 98 | } 99 | 100 | // timeLayout is the format in which strings are parsed as time.Time (ISO 8601). 101 | const timeLayout = "2006-01-02T15:04:05Z" 102 | 103 | // coerceType attempts to convert the given value to the specified Type. If it cannot 104 | // be coerced, nil will be returned along with an error. 105 | func coerceType(value interface{}, coerceTo Type) (interface{}, error) { 106 | if coerceTo == Interface { 107 | return value, nil 108 | } 109 | 110 | // json.Unmarshal converts values to bool, float64, string, nil, slice, and map. 111 | switch value.(type) { 112 | case bool: 113 | return coerceFromBool(value.(bool), coerceTo) 114 | case float64: 115 | return coerceFromFloat(value.(float64), coerceTo) 116 | case string: 117 | return coerceFromString(value.(string), coerceTo) 118 | case nil: 119 | return value, nil 120 | case []interface{}: 121 | return coerceFromSlice(value.([]interface{}), coerceTo) 122 | case map[string]interface{}: 123 | return coerceFromMap(value.(map[string]interface{}), coerceTo) 124 | default: 125 | return nil, fmt.Errorf("Unable to coerce %s to %s", 126 | reflect.TypeOf(value), typeToName[coerceTo]) 127 | } 128 | } 129 | 130 | // coerceFromBool attempts to convert the given bool to the specified Type. If it 131 | // cannot be coerced, nil will be returned along with an error. 132 | func coerceFromBool(value bool, coerceTo Type) (interface{}, error) { 133 | if coerceTo == Bool { 134 | return value, nil 135 | } 136 | if coerceTo == String { 137 | if value { 138 | return "true", nil 139 | } 140 | return "false", nil 141 | } 142 | 143 | return nil, fmt.Errorf("Unable to coerce bool to %s", typeToName[coerceTo]) 144 | } 145 | 146 | // coerceFromFloat attempts to convert the given float64 to the specified Type. 147 | // If it cannot be coerced, nil will be returned along with an error. 148 | func coerceFromFloat(value float64, coerceTo Type) (interface{}, error) { 149 | switch coerceTo { 150 | // To int. 151 | case Int: 152 | return int(value), nil 153 | case Int8: 154 | return int8(value), nil 155 | case Int16: 156 | return int16(value), nil 157 | case Int32: 158 | return int32(value), nil 159 | case Int64: 160 | return int64(value), nil 161 | 162 | // To unsigned int. 163 | case Uint: 164 | return uint(value), nil 165 | case Uint8: 166 | return uint8(value), nil 167 | case Uint16: 168 | return uint16(value), nil 169 | case Uint32: 170 | return uint32(value), nil 171 | case Uint64: 172 | return uint64(value), nil 173 | 174 | // To float. 175 | case Float32: 176 | return float32(value), nil 177 | case Float64: 178 | return value, nil 179 | 180 | // To string. 181 | case String: 182 | return strconv.FormatFloat(value, 'f', -1, 64), nil 183 | 184 | // To Duration. 185 | case Duration: 186 | return time.Duration(int64(value)), nil 187 | 188 | // Bool and Time cases left off intentionally. 189 | default: 190 | return nil, fmt.Errorf("Unable to coerce float to %s", typeToName[coerceTo]) 191 | } 192 | } 193 | 194 | // coerceFromString attempts to convert the given string to the specified Type. If 195 | // it cannot be coerced, nil will be returned along with an error. 196 | func coerceFromString(value string, coerceTo Type) (interface{}, error) { 197 | switch coerceTo { 198 | // To int. 199 | case Int: 200 | val, err := strconv.ParseInt(value, 0, 0) 201 | if err != nil { 202 | return nil, err 203 | } 204 | return int(val), nil 205 | case Int8: 206 | val, err := strconv.ParseInt(value, 0, 8) 207 | if err != nil { 208 | return nil, err 209 | } 210 | return int8(val), nil 211 | case Int16: 212 | val, err := strconv.ParseInt(value, 0, 16) 213 | if err != nil { 214 | return nil, err 215 | } 216 | return int16(val), nil 217 | case Int32: 218 | val, err := strconv.ParseInt(value, 0, 32) 219 | if err != nil { 220 | return nil, err 221 | } 222 | return int32(val), nil 223 | case Int64: 224 | val, err := strconv.ParseInt(value, 0, 64) 225 | if err != nil { 226 | return nil, err 227 | } 228 | return int64(val), nil 229 | 230 | // To unsigned int. 231 | case Uint: 232 | val, err := strconv.ParseUint(value, 0, 0) 233 | if err != nil { 234 | return nil, err 235 | } 236 | return uint(val), nil 237 | case Uint8: 238 | val, err := strconv.ParseUint(value, 0, 8) 239 | if err != nil { 240 | return nil, err 241 | } 242 | return uint8(val), nil 243 | case Uint16: 244 | val, err := strconv.ParseUint(value, 0, 16) 245 | if err != nil { 246 | return nil, err 247 | } 248 | return uint16(val), nil 249 | case Uint32: 250 | val, err := strconv.ParseUint(value, 0, 32) 251 | if err != nil { 252 | return nil, err 253 | } 254 | return uint32(val), nil 255 | case Uint64: 256 | val, err := strconv.ParseUint(value, 0, 64) 257 | if err != nil { 258 | return nil, err 259 | } 260 | return uint64(val), nil 261 | 262 | // To float. 263 | case Float32: 264 | val, err := strconv.ParseFloat(value, 32) 265 | if err != nil { 266 | return nil, err 267 | } 268 | return float32(val), nil 269 | case Float64: 270 | val, err := strconv.ParseFloat(value, 64) 271 | if err != nil { 272 | return nil, err 273 | } 274 | return float64(val), nil 275 | 276 | // To string. 277 | case String: 278 | return value, nil 279 | 280 | // To bool. 281 | case Bool: 282 | val, err := strconv.ParseBool(value) 283 | if err != nil { 284 | return nil, err 285 | } 286 | return val, nil 287 | 288 | // To Duration. 289 | case Duration: 290 | val, err := time.ParseDuration(value) 291 | if err != nil { 292 | return nil, err 293 | } 294 | return val, nil 295 | 296 | // To Time. 297 | case Time: 298 | val, err := time.Parse(timeLayout, value) 299 | if err != nil { 300 | return nil, err 301 | } 302 | return val, nil 303 | 304 | default: 305 | return nil, fmt.Errorf("Unable to coerce string to %s", typeToName[coerceTo]) 306 | } 307 | } 308 | 309 | // coerceFromSlice attempts to convert the given slice to the specified Type. Currently, 310 | // slices can only be coerced to slices (identity). If it cannot be coerced, nil will be 311 | // returned along with an error. 312 | func coerceFromSlice(value []interface{}, coerceTo Type) (interface{}, error) { 313 | if coerceTo == Slice { 314 | return value, nil 315 | } 316 | 317 | return nil, fmt.Errorf("Unable to coerce slice to %s", typeToName[coerceTo]) 318 | } 319 | 320 | // coerceFromMap attempts to convert the given map to the specified Type. Currently, 321 | // maps can only be coerced to maps (identity). If it cannot be coerced, nil will be 322 | // returned along with an error. 323 | func coerceFromMap(value map[string]interface{}, coerceTo Type) (interface{}, error) { 324 | if coerceTo == Map { 325 | return value, nil 326 | } 327 | 328 | return nil, fmt.Errorf("Unable to coerce map to %s", typeToName[coerceTo]) 329 | } 330 | -------------------------------------------------------------------------------- /rest/payload.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 - 2015 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package rest 18 | 19 | import ( 20 | "fmt" 21 | "time" 22 | ) 23 | 24 | // Payload is the unmarshalled request body. 25 | type Payload map[string]interface{} 26 | 27 | // Get returns the value with the given key as an interface{}. If the key doesn't 28 | // exist, nil is returned with an error. 29 | func (p Payload) Get(key string) (interface{}, error) { 30 | if value, ok := p[key]; ok { 31 | return value, nil 32 | } 33 | return nil, fmt.Errorf("No value with key '%s'", key) 34 | } 35 | 36 | // GetInt returns the value with the given key as an int. If the key doesn't 37 | // exist or is not an int, the zero value is returned with an error. 38 | func (p Payload) GetInt(key string) (int, error) { 39 | value, err := p.Get(key) 40 | if err != nil { 41 | return 0, err 42 | } 43 | if value, ok := value.(int); ok { 44 | return value, nil 45 | } 46 | return 0, fmt.Errorf("Value with key '%s' not an int", key) 47 | } 48 | 49 | // GetInt8 returns the value with the given key as an int8. If the key doesn't 50 | // exist or is not an int8, the zero value is returned with an error. 51 | func (p Payload) GetInt8(key string) (int8, error) { 52 | value, err := p.Get(key) 53 | if err != nil { 54 | return 0, err 55 | } 56 | if value, ok := value.(int8); ok { 57 | return value, nil 58 | } 59 | return 0, fmt.Errorf("Value with key '%s' not an int8", key) 60 | } 61 | 62 | // GetInt16 returns the value with the given key as an int16. If the key doesn't 63 | // exist or is not an int16, the zero value is returned with an error. 64 | func (p Payload) GetInt16(key string) (int16, error) { 65 | value, err := p.Get(key) 66 | if err != nil { 67 | return 0, err 68 | } 69 | if value, ok := value.(int16); ok { 70 | return value, nil 71 | } 72 | return 0, fmt.Errorf("Value with key '%s' not an int16", key) 73 | } 74 | 75 | // GetInt32 returns the value with the given key as an int32. If the key doesn't 76 | // exist or is not an int32, the zero value is returned with an error. 77 | func (p Payload) GetInt32(key string) (int32, error) { 78 | value, err := p.Get(key) 79 | if err != nil { 80 | return 0, err 81 | } 82 | if value, ok := value.(int32); ok { 83 | return value, nil 84 | } 85 | return 0, fmt.Errorf("Value with key '%s' not an int32", key) 86 | } 87 | 88 | // GetInt64 returns the value with the given key as an int64. If the key doesn't 89 | // exist or is not an int64, the zero value is returned with an error. 90 | func (p Payload) GetInt64(key string) (int64, error) { 91 | value, err := p.Get(key) 92 | if err != nil { 93 | return 0, err 94 | } 95 | if value, ok := value.(int64); ok { 96 | return value, nil 97 | } 98 | return 0, fmt.Errorf("Value with key '%s' not an int64", key) 99 | } 100 | 101 | // GetUint returns the value with the given key as a uint. If the key doesn't 102 | // exist or is not a uint, the zero value is returned with an error. 103 | func (p Payload) GetUint(key string) (uint, error) { 104 | value, err := p.Get(key) 105 | if err != nil { 106 | return 0, err 107 | } 108 | if value, ok := value.(uint); ok { 109 | return value, nil 110 | } 111 | return 0, fmt.Errorf("Value with key '%s' not a uint", key) 112 | } 113 | 114 | // GetUint8 returns the value with the given key as a uint8. If the key doesn't 115 | // exist or is not a uint8, the zero value is returned with an error. 116 | func (p Payload) GetUint8(key string) (uint8, error) { 117 | value, err := p.Get(key) 118 | if err != nil { 119 | return 0, err 120 | } 121 | if value, ok := value.(uint8); ok { 122 | return value, nil 123 | } 124 | return 0, fmt.Errorf("Value with key '%s' not a uint8", key) 125 | } 126 | 127 | // GetUint16 returns the value with the given key as a uint16. If the key doesn't 128 | // exist or is not a uint16, the zero value is returned with an error. 129 | func (p Payload) GetUint16(key string) (uint16, error) { 130 | value, err := p.Get(key) 131 | if err != nil { 132 | return 0, err 133 | } 134 | if value, ok := value.(uint16); ok { 135 | return value, nil 136 | } 137 | return 0, fmt.Errorf("Value with key '%s' not a uint16", key) 138 | } 139 | 140 | // GetUint32 returns the value with the given key as a uint32. If the key doesn't 141 | // exist or is not a uint32, the zero value is returned with an error. 142 | func (p Payload) GetUint32(key string) (uint32, error) { 143 | value, err := p.Get(key) 144 | if err != nil { 145 | return 0, err 146 | } 147 | if value, ok := value.(uint32); ok { 148 | return value, nil 149 | } 150 | return 0, fmt.Errorf("Value with key '%s' not a uint32", key) 151 | } 152 | 153 | // GetUint64 returns the value with the given key as a uint64. If the key doesn't 154 | // exist or is not a uint64, the zero value is returned with an error. 155 | func (p Payload) GetUint64(key string) (uint64, error) { 156 | value, err := p.Get(key) 157 | if err != nil { 158 | return 0, err 159 | } 160 | if value, ok := value.(uint64); ok { 161 | return value, nil 162 | } 163 | return 0, fmt.Errorf("Value with key '%s' not a uint64", key) 164 | } 165 | 166 | // GetFloat32 returns the value with the given key as a Float32. If the key doesn't 167 | // exist or is not a Float32, the zero value is returned with an error. 168 | func (p Payload) GetFloat32(key string) (float32, error) { 169 | value, err := p.Get(key) 170 | if err != nil { 171 | return 0, err 172 | } 173 | if value, ok := value.(float32); ok { 174 | return value, nil 175 | } 176 | return 0, fmt.Errorf("Value with key '%s' not a float32", key) 177 | } 178 | 179 | // GetFloat64 returns the value with the given key as a Float64. If the key doesn't 180 | // exist or is not a Float32, the zero value is returned with an error. 181 | func (p Payload) GetFloat64(key string) (float64, error) { 182 | value, err := p.Get(key) 183 | if err != nil { 184 | return 0, err 185 | } 186 | if value, ok := value.(float64); ok { 187 | return value, nil 188 | } 189 | return 0, fmt.Errorf("Value with key '%s' not a float64", key) 190 | } 191 | 192 | // GetByte returns the value with the given key as a byte. If the key doesn't 193 | // exist or is not a byte, the zero value is returned with an error. 194 | func (p Payload) GetByte(key string) (byte, error) { 195 | value, err := p.Get(key) 196 | if err != nil { 197 | return 0, err 198 | } 199 | if value, ok := value.(byte); ok { 200 | return value, nil 201 | } 202 | return 0, fmt.Errorf("Value with key '%s' not a byte", key) 203 | } 204 | 205 | // GetString returns the value with the given key as a string. If the key doesn't 206 | // exist or is not a string, the zero value is returned with an error. 207 | func (p Payload) GetString(key string) (string, error) { 208 | value, err := p.Get(key) 209 | if err != nil { 210 | return "", err 211 | } 212 | if value, ok := value.(string); ok { 213 | return value, nil 214 | } 215 | return "", fmt.Errorf("Value with key '%s' not a string", key) 216 | } 217 | 218 | // GetBool returns the value with the given key as a bool. If the key doesn't 219 | // exist or is not a bool, false is returned with an error. 220 | func (p Payload) GetBool(key string) (bool, error) { 221 | value, err := p.Get(key) 222 | if err != nil { 223 | return false, err 224 | } 225 | if value, ok := value.(bool); ok { 226 | return value, nil 227 | } 228 | return false, fmt.Errorf("Value with key '%s' not a bool", key) 229 | } 230 | 231 | // GetSlice returns the value with the given key as an []interface{}. If the value 232 | // doesn't exist or is not an []interface{}, nil is returned with an error. 233 | func (p Payload) GetSlice(key string) ([]interface{}, error) { 234 | value, err := p.Get(key) 235 | if err != nil { 236 | return nil, err 237 | } 238 | if value, ok := value.([]interface{}); ok { 239 | return value, nil 240 | } 241 | return nil, fmt.Errorf("Value with key '%s' not a slice", key) 242 | } 243 | 244 | // GetMap returns the value with the given key as a map[string]interface{}. If the 245 | // key doesn't exist or is not a map[string]interface{}, nil is returned with an 246 | // error. 247 | func (p Payload) GetMap(key string) (map[string]interface{}, error) { 248 | value, err := p.Get(key) 249 | if err != nil { 250 | return nil, err 251 | } 252 | if value, ok := value.(map[string]interface{}); ok { 253 | return value, nil 254 | } 255 | return nil, fmt.Errorf("Value with key '%s' not a map", key) 256 | } 257 | 258 | // GetDuration returns the value with the given key as a time.Duration. If the key 259 | // doesn't exist or is not a time.Duration, the zero value is returned with an 260 | // error. 261 | func (p Payload) GetDuration(key string) (time.Duration, error) { 262 | value, err := p.Get(key) 263 | if err != nil { 264 | return 0, err 265 | } 266 | if value, ok := value.(time.Duration); ok { 267 | return value, nil 268 | } 269 | return 0, fmt.Errorf("Value with key '%s' not a time.Duration", key) 270 | } 271 | 272 | // GetTime returns the value with the given key as a time.Time. If the key doesn't 273 | // exist or is not a time.Time, the zero value is returned with an error. 274 | func (p Payload) GetTime(key string) (time.Time, error) { 275 | value, err := p.Get(key) 276 | if err != nil { 277 | return time.Time{}, err 278 | } 279 | if value, ok := value.(time.Time); ok { 280 | return value, nil 281 | } 282 | return time.Time{}, fmt.Errorf("Value with key '%s' not a time.Time", key) 283 | } 284 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /rest/client_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 - 2015 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package rest 18 | 19 | import ( 20 | "encoding/json" 21 | "net/http" 22 | "net/http/httptest" 23 | "testing" 24 | 25 | "github.com/stretchr/testify/assert" 26 | ) 27 | 28 | func newMockDo(response *Response, err error) func(*http.Client, string, interface{}, http.Header) (*Response, error) { 29 | return func(*http.Client, string, interface{}, http.Header) (*Response, error) { 30 | return response, err 31 | } 32 | } 33 | 34 | // Ensures that Get invokes do with the correct HTTP client, method, url and 35 | // header. 36 | func TestClientGet(t *testing.T) { 37 | assert := assert.New(t) 38 | httpClient := http.DefaultClient 39 | client := &Client{httpClient} 40 | header := http.Header{} 41 | url := "http://localhost" 42 | mockResponse := &Response{} 43 | mockDo := func(c *http.Client, m, u string, b interface{}, h http.Header) (*Response, error) { 44 | assert.Equal(httpClient, c) 45 | assert.Equal(httpGet, m) 46 | assert.Nil(b) 47 | assert.Equal(header, h) 48 | assert.Equal(url, u) 49 | return mockResponse, nil 50 | } 51 | 52 | before := do 53 | do = mockDo 54 | 55 | resp, err := client.Get(url, header) 56 | 57 | assert.Equal(mockResponse, resp) 58 | assert.Nil(err) 59 | 60 | do = before 61 | } 62 | 63 | // Ensures that Post invokes do with the correct HTTP client, method, url and 64 | // header. 65 | func TestClientPost(t *testing.T) { 66 | assert := assert.New(t) 67 | httpClient := http.DefaultClient 68 | client := &Client{httpClient} 69 | header := http.Header{} 70 | url := "http://localhost" 71 | body := "foo" 72 | mockResponse := &Response{} 73 | mockDo := func(c *http.Client, m, u string, b interface{}, h http.Header) (*Response, error) { 74 | assert.Equal(httpClient, c) 75 | assert.Equal(httpPost, m) 76 | assert.Equal(body, b) 77 | assert.Equal(header, h) 78 | assert.Equal(url, u) 79 | return mockResponse, nil 80 | } 81 | 82 | before := do 83 | do = mockDo 84 | 85 | resp, err := client.Post(url, body, header) 86 | 87 | assert.Equal(mockResponse, resp) 88 | assert.Nil(err) 89 | 90 | do = before 91 | } 92 | 93 | // Ensures that Put invokes do with the correct HTTP client, method, url and 94 | // header. 95 | func TestClientPut(t *testing.T) { 96 | assert := assert.New(t) 97 | httpClient := http.DefaultClient 98 | client := &Client{httpClient} 99 | header := http.Header{} 100 | url := "http://localhost" 101 | body := "foo" 102 | mockResponse := &Response{} 103 | mockDo := func(c *http.Client, m, u string, b interface{}, h http.Header) (*Response, error) { 104 | assert.Equal(httpClient, c) 105 | assert.Equal(httpPut, m) 106 | assert.Equal(body, b) 107 | assert.Equal(header, h) 108 | assert.Equal(url, u) 109 | return mockResponse, nil 110 | } 111 | 112 | before := do 113 | do = mockDo 114 | 115 | resp, err := client.Put(url, body, header) 116 | 117 | assert.Equal(mockResponse, resp) 118 | assert.Nil(err) 119 | 120 | do = before 121 | } 122 | 123 | // Ensures that Delete invokes do with the correct HTTP client, method, url and 124 | // header. 125 | func TestClientDelete(t *testing.T) { 126 | assert := assert.New(t) 127 | httpClient := http.DefaultClient 128 | client := &Client{httpClient} 129 | header := http.Header{} 130 | url := "http://localhost" 131 | mockResponse := &Response{} 132 | mockDo := func(c *http.Client, m, u string, b interface{}, h http.Header) (*Response, error) { 133 | assert.Equal(httpClient, c) 134 | assert.Equal(httpDelete, m) 135 | assert.Nil(b) 136 | assert.Equal(header, h) 137 | assert.Equal(url, u) 138 | return mockResponse, nil 139 | } 140 | 141 | before := do 142 | do = mockDo 143 | 144 | resp, err := client.Delete(url, header) 145 | 146 | assert.Equal(mockResponse, resp) 147 | assert.Nil(err) 148 | 149 | do = before 150 | } 151 | 152 | // Ensures that do returns an error if the POST/PUT body is not JSON- 153 | // marshalable. 154 | func TestDoInvalidBody(t *testing.T) { 155 | var body interface{} 156 | _, err := do(http.DefaultClient, httpPost, "http://localhost", body, nil) 157 | assert.Error(t, err) 158 | } 159 | 160 | // Ensures that do returns an error if the request fails. 161 | func TestDoBadRequest(t *testing.T) { 162 | _, err := do(http.DefaultClient, httpGet, "blah", nil, nil) 163 | assert.Error(t, err) 164 | } 165 | 166 | // Ensures that do returns a Response with a 404 code and the error is nil 167 | // when a 404 response is received. 168 | func TestDoNotFound(t *testing.T) { 169 | assert := assert.New(t) 170 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 171 | w.WriteHeader(http.StatusNotFound) 172 | })) 173 | defer ts.Close() 174 | 175 | resp, err := do(http.DefaultClient, httpGet, ts.URL, nil, nil) 176 | 177 | if assert.NotNil(resp) { 178 | assert.Equal(http.StatusNotFound, resp.Status) 179 | assert.Equal(http.StatusText(http.StatusNotFound), resp.Reason) 180 | assert.Equal([]string{}, resp.Messages) 181 | assert.Equal("", resp.Next) 182 | assert.NotNil(resp.Raw) 183 | assert.Nil(resp.Result) 184 | } 185 | 186 | assert.Nil(err) 187 | } 188 | 189 | // Ensures that do returns an error when the response is not valid JSON. 190 | func TestDoBadResponse(t *testing.T) { 191 | assert := assert.New(t) 192 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 193 | w.WriteHeader(http.StatusOK) 194 | w.Write([]byte(`{"foo":`)) 195 | })) 196 | defer ts.Close() 197 | 198 | _, err := do(http.DefaultClient, httpGet, ts.URL, nil, nil) 199 | 200 | assert.NotNil(err) 201 | } 202 | 203 | // Ensures that do returns a Response with the result decoded correctly for 204 | // "list" endpoints ("results" key). 205 | func TestDoDecodeResults(t *testing.T) { 206 | assert := assert.New(t) 207 | entity := map[string]interface{}{"foo": 1, "bar": "baz"} 208 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 209 | response := Payload{ 210 | status: http.StatusOK, 211 | reason: http.StatusText(http.StatusOK), 212 | messages: []string{}, 213 | results: []interface{}{entity}, 214 | } 215 | responseJSON, _ := json.Marshal(response) 216 | w.WriteHeader(http.StatusOK) 217 | w.Write(responseJSON) 218 | })) 219 | defer ts.Close() 220 | 221 | resp, err := do(http.DefaultClient, httpGet, ts.URL, nil, nil) 222 | 223 | if assert.NotNil(resp) { 224 | assert.Equal(http.StatusOK, resp.Status) 225 | assert.Equal(http.StatusText(http.StatusOK), resp.Reason) 226 | assert.Equal([]string{}, resp.Messages) 227 | assert.Equal("", resp.Next) 228 | assert.NotNil(resp.Raw) 229 | if assert.NotNil(resp.Result) { 230 | m := resp.Result.([]interface{})[0].(map[string]interface{}) 231 | assert.Equal(float64(1.0), m["foo"]) 232 | assert.Equal("baz", m["bar"]) 233 | } 234 | } 235 | 236 | assert.Nil(err) 237 | } 238 | 239 | // Ensures that do returns a Response with the result decoded correctly for 240 | // "non-list" endpoints ("result" key). 241 | func TestDoDecodeResult(t *testing.T) { 242 | assert := assert.New(t) 243 | entity := map[string]interface{}{"foo": 1, "bar": "baz"} 244 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 245 | response := Payload{ 246 | status: http.StatusOK, 247 | reason: http.StatusText(http.StatusOK), 248 | messages: []string{}, 249 | result: entity, 250 | } 251 | responseJSON, _ := json.Marshal(response) 252 | w.WriteHeader(http.StatusOK) 253 | w.Write(responseJSON) 254 | })) 255 | defer ts.Close() 256 | 257 | resp, err := do(http.DefaultClient, httpGet, ts.URL, nil, nil) 258 | 259 | if assert.NotNil(resp) { 260 | assert.Equal(http.StatusOK, resp.Status) 261 | assert.Equal(http.StatusText(http.StatusOK), resp.Reason) 262 | assert.Equal([]string{}, resp.Messages) 263 | assert.Equal("", resp.Next) 264 | assert.NotNil(resp.Raw) 265 | if assert.NotNil(resp.Result) { 266 | m := resp.Result.(map[string]interface{}) 267 | assert.Equal(float64(1), m["foo"]) 268 | assert.Equal("baz", m["bar"]) 269 | } 270 | } 271 | 272 | assert.Nil(err) 273 | } 274 | 275 | // Ensures that Get invokes do with the correct HTTP client, method, url and 276 | // header, and that middlewares are applied. 277 | func TestNewClientGet(t *testing.T) { 278 | assert := assert.New(t) 279 | middlewaresApplied := 0 280 | middleware := func(next InvocationHandler) InvocationHandler { 281 | return InvocationHandler(func(c *http.Client, method string, url string, body interface{}, header http.Header) (*Response, error) { 282 | middlewaresApplied++ 283 | return next(c, method, url, body, header) 284 | }) 285 | } 286 | middlewares := []ClientMiddleware{middleware, middleware} 287 | 288 | httpClient := http.DefaultClient 289 | client := &client{httpClient, middlewares} 290 | header := http.Header{} 291 | url := "http://localhost" 292 | mockResponse := &Response{} 293 | mockDo := func(c *http.Client, m, u string, b interface{}, h http.Header) (*Response, error) { 294 | assert.Equal(httpClient, c) 295 | assert.Equal(httpGet, m) 296 | assert.Nil(b) 297 | assert.Equal(header, h) 298 | assert.Equal(url, u) 299 | return mockResponse, nil 300 | } 301 | 302 | before := do 303 | do = mockDo 304 | 305 | resp, err := client.Get(url, header) 306 | 307 | assert.Equal(mockResponse, resp) 308 | assert.Equal(middlewaresApplied, 2) 309 | assert.Nil(err) 310 | 311 | do = before 312 | } 313 | 314 | // Ensures that Post invokes do with the correct HTTP client, method, url and 315 | // header, and that middlewares are applied. 316 | func TestNewClientPost(t *testing.T) { 317 | assert := assert.New(t) 318 | middlewaresApplied := 0 319 | middleware := func(next InvocationHandler) InvocationHandler { 320 | return InvocationHandler(func(c *http.Client, method string, url string, body interface{}, header http.Header) (*Response, error) { 321 | middlewaresApplied++ 322 | return next(c, method, url, body, header) 323 | }) 324 | } 325 | middlewares := []ClientMiddleware{middleware, middleware} 326 | 327 | httpClient := http.DefaultClient 328 | client := &client{httpClient, middlewares} 329 | header := http.Header{} 330 | url := "http://localhost" 331 | body := "foo" 332 | mockResponse := &Response{} 333 | mockDo := func(c *http.Client, m, u string, b interface{}, h http.Header) (*Response, error) { 334 | assert.Equal(httpClient, c) 335 | assert.Equal(httpPost, m) 336 | assert.Equal(body, b) 337 | assert.Equal(header, h) 338 | assert.Equal(url, u) 339 | return mockResponse, nil 340 | } 341 | 342 | before := do 343 | do = mockDo 344 | 345 | resp, err := client.Post(url, body, header) 346 | 347 | assert.Equal(mockResponse, resp) 348 | assert.Equal(middlewaresApplied, 2) 349 | assert.Nil(err) 350 | 351 | do = before 352 | } 353 | 354 | // Ensures that Put invokes do with the correct HTTP client, method, url and 355 | // header, and that middlewares are applied. 356 | func TestNewClientPut(t *testing.T) { 357 | assert := assert.New(t) 358 | middlewaresApplied := 0 359 | middleware := func(next InvocationHandler) InvocationHandler { 360 | return InvocationHandler(func(c *http.Client, method string, url string, body interface{}, header http.Header) (*Response, error) { 361 | middlewaresApplied++ 362 | return next(c, method, url, body, header) 363 | }) 364 | } 365 | middlewares := []ClientMiddleware{middleware, middleware} 366 | 367 | httpClient := http.DefaultClient 368 | client := &client{httpClient, middlewares} 369 | header := http.Header{} 370 | url := "http://localhost" 371 | body := "foo" 372 | mockResponse := &Response{} 373 | mockDo := func(c *http.Client, m, u string, b interface{}, h http.Header) (*Response, error) { 374 | assert.Equal(httpClient, c) 375 | assert.Equal(httpPut, m) 376 | assert.Equal(body, b) 377 | assert.Equal(header, h) 378 | assert.Equal(url, u) 379 | return mockResponse, nil 380 | } 381 | 382 | before := do 383 | do = mockDo 384 | 385 | resp, err := client.Put(url, body, header) 386 | 387 | assert.Equal(mockResponse, resp) 388 | assert.Equal(middlewaresApplied, 2) 389 | assert.Nil(err) 390 | 391 | do = before 392 | } 393 | 394 | // Ensures that Delete invokes do with the correct HTTP client, method, url and 395 | // header, and that middlewares are applied. 396 | func TestNewClientDelete(t *testing.T) { 397 | assert := assert.New(t) 398 | middlewaresApplied := 0 399 | middleware := func(next InvocationHandler) InvocationHandler { 400 | return InvocationHandler(func(c *http.Client, method string, url string, body interface{}, header http.Header) (*Response, error) { 401 | middlewaresApplied++ 402 | return next(c, method, url, body, header) 403 | }) 404 | } 405 | middlewares := []ClientMiddleware{middleware, middleware} 406 | 407 | httpClient := http.DefaultClient 408 | client := &client{httpClient, middlewares} 409 | header := http.Header{} 410 | url := "http://localhost" 411 | mockResponse := &Response{} 412 | mockDo := func(c *http.Client, m, u string, b interface{}, h http.Header) (*Response, error) { 413 | assert.Equal(httpClient, c) 414 | assert.Equal(httpDelete, m) 415 | assert.Nil(b) 416 | assert.Equal(header, h) 417 | assert.Equal(url, u) 418 | return mockResponse, nil 419 | } 420 | 421 | before := do 422 | do = mockDo 423 | 424 | resp, err := client.Delete(url, header) 425 | 426 | assert.Equal(mockResponse, resp) 427 | assert.Equal(middlewaresApplied, 2) 428 | assert.Nil(err) 429 | 430 | do = before 431 | } 432 | -------------------------------------------------------------------------------- /rest/handler.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 - 2015 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package rest 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "log" 23 | "net/http" 24 | 25 | "github.com/gorilla/mux" 26 | ) 27 | 28 | // Resource represents a domain model. 29 | type Resource interface{} 30 | 31 | // ResourceHandler specifies the endpoint handlers for working with a resource. This 32 | // consists of the business logic for performing CRUD operations. 33 | type ResourceHandler interface { 34 | // ResourceName is used to identify what resource a handler corresponds to and is 35 | // used in the endpoint URLs, i.e. /api/:version/resourceName. This should be 36 | // unique across all ResourceHandlers. 37 | ResourceName() string 38 | 39 | // CreateURI returns the URI for creating a resource. 40 | CreateURI() string 41 | 42 | // CreateDocumentation returns a string describing the handler's create endpoint. 43 | CreateDocumentation() string 44 | 45 | // ReadURI returns the URI for reading a specific resource. 46 | ReadURI() string 47 | 48 | // ReadDocumentation returns a string describing the handler's read endpoint. 49 | ReadDocumentation() string 50 | 51 | // ReadListURI returns the URI for reading a list of resources. 52 | ReadListURI() string 53 | 54 | // ReadListDocumentation returns a string describing the handler's read list endpoint. 55 | ReadListDocumentation() string 56 | 57 | // UpdateURI returns the URI for updating a specific resource. 58 | UpdateURI() string 59 | 60 | // UpdateDocumentation returns a string describing the handler's update endpoint. 61 | UpdateDocumentation() string 62 | 63 | // UpdateListURI returns the URI for updating a list of resources. 64 | UpdateListURI() string 65 | 66 | // UpdateListDocumentation returns a string describing the handler's update list 67 | // endpoint. 68 | UpdateListDocumentation() string 69 | 70 | // DeleteURI returns the URI for deleting a specific resource. 71 | DeleteURI() string 72 | 73 | // DeleteDocumentation returns a string describing the handler's delete endpoint. 74 | DeleteDocumentation() string 75 | 76 | // CreateResource is the logic that corresponds to creating a new resource at 77 | // POST /api/:version/resourceName. Typically, this would insert a record into a 78 | // database. It returns the newly created resource or an error if the create failed. 79 | CreateResource(RequestContext, Payload, string) (Resource, error) 80 | 81 | // ReadResourceList is the logic that corresponds to reading multiple resources, 82 | // perhaps with specified query parameters accessed through the RequestContext. This 83 | // is mapped to GET /api/:version/resourceName. Typically, this would make some sort 84 | // of database query to fetch the resources. It returns the slice of results, a 85 | // cursor (or empty) string, and error (or nil). 86 | ReadResourceList(RequestContext, int, string, string) ([]Resource, string, error) 87 | 88 | // ReadResource is the logic that corresponds to reading a single resource by its ID 89 | // at GET /api/:version/resourceName/{id}. Typically, this would make some sort of 90 | // database query to load the resource. If the resource doesn't exist, nil should be 91 | // returned along with an appropriate error. 92 | ReadResource(RequestContext, string, string) (Resource, error) 93 | 94 | // UpdateResourceList is the logic that corresponds to updating a collection of 95 | // resources at PUT /api/:version/resourceName. Typically, this would make some 96 | // sort of database update call. It returns the updated resources or an error if 97 | // the update failed. 98 | UpdateResourceList(RequestContext, []Payload, string) ([]Resource, error) 99 | 100 | // UpdateResource is the logic that corresponds to updating an existing resource at 101 | // PUT /api/:version/resourceName/{id}. Typically, this would make some sort of 102 | // database update call. It returns the updated resource or an error if the update 103 | // failed. 104 | UpdateResource(RequestContext, string, Payload, string) (Resource, error) 105 | 106 | // DeleteResource is the logic that corresponds to deleting an existing resource at 107 | // DELETE /api/:version/resourceName/{id}. Typically, this would make some sort of 108 | // database delete call. It returns the deleted resource or an error if the delete 109 | // failed. 110 | DeleteResource(RequestContext, string, string) (Resource, error) 111 | 112 | // Authenticate is logic that is used to authenticate requests. The default behavior 113 | // of Authenticate, seen in BaseResourceHandler, always returns nil, meaning all 114 | // requests are authenticated. Returning an error means that the request is 115 | // unauthorized and any error message will be sent back with the response. 116 | Authenticate(*http.Request) error 117 | 118 | // ValidVersions returns the list of all versions accepted at this endpoint. 119 | // Invalid versions will result in a BadRequest error. 120 | // If the value is nil, any version will be accepted. 121 | ValidVersions() []string 122 | 123 | // Rules returns the resource rules to apply to incoming requests and outgoing 124 | // responses. The default behavior, seen in BaseResourceHandler, is to apply no 125 | // rules. 126 | Rules() Rules 127 | } 128 | 129 | // requestHandler constructs http.HandlerFuncs responsible for handling HTTP requests. 130 | type requestHandler struct { 131 | API 132 | router *mux.Router 133 | } 134 | 135 | // handleCreate returns a HandlerFunc which will deserialize the request payload, pass 136 | // it to the provided create function, and then serialize and dispatch the response. 137 | // The serialization mechanism used is specified by the "format" query parameter. 138 | func (h requestHandler) handleCreate(handler ResourceHandler) http.Handler { 139 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 140 | ctx := NewContextWithRouter(r, w, h.router) 141 | version := ctx.Version() 142 | rules := handler.Rules() 143 | 144 | data, err := decodePayload(ctx.Body().Bytes()) 145 | if err != nil { 146 | // Payload decoding failed. 147 | ctx = ctx.setError(BadRequest(err.Error())) 148 | } else { 149 | data, err := applyInboundRules(data, rules, version) 150 | if err != nil { 151 | // Type coercion failed. 152 | ctx = ctx.setError(UnprocessableRequest(err.Error())) 153 | } else { 154 | resource, err := handler.CreateResource(ctx, data, ctx.Version()) 155 | if err == nil { 156 | resource = applyOutboundRules(resource, rules, version) 157 | } 158 | 159 | if resource != nil { 160 | ctx = ctx.setResult(resource) 161 | ctx = ctx.setStatus(http.StatusCreated) 162 | } else { 163 | ctx = ctx.setStatus(http.StatusNoContent) 164 | } 165 | 166 | if err != nil { 167 | ctx = ctx.setError(err) 168 | } 169 | } 170 | } 171 | 172 | h.sendResponse(ctx) 173 | }) 174 | } 175 | 176 | // handleReadList returns a Handler which will pass the request context to the 177 | // provided read function and then serialize and dispatch the response. The 178 | // serialization mechanism used is specified by the "format" query parameter. 179 | func (h requestHandler) handleReadList(handler ResourceHandler) http.Handler { 180 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 181 | ctx := NewContextWithRouter(r, w, h.router) 182 | version := ctx.Version() 183 | rules := handler.Rules() 184 | 185 | resources, cursor, err := handler.ReadResourceList( 186 | ctx, ctx.Limit(), ctx.Cursor(), version) 187 | 188 | if err == nil { 189 | // Apply rules to results. 190 | for idx, resource := range resources { 191 | resources[idx] = applyOutboundRules(resource, rules, version) 192 | } 193 | } 194 | 195 | ctx = ctx.setResult(resources) 196 | ctx = ctx.setCursor(cursor) 197 | ctx = ctx.setError(err) 198 | ctx = ctx.setStatus(http.StatusOK) 199 | 200 | h.sendResponse(ctx) 201 | }) 202 | } 203 | 204 | // handleRead returns a Handler which will pass the resource id to the provided 205 | // read function and then serialize and dispatch the response. The serialization 206 | // mechanism used is specified by the "format" query parameter. 207 | func (h requestHandler) handleRead(handler ResourceHandler) http.Handler { 208 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 209 | ctx := NewContextWithRouter(r, w, h.router) 210 | version := ctx.Version() 211 | rules := handler.Rules() 212 | 213 | resource, err := handler.ReadResource(ctx, ctx.ResourceID(), version) 214 | if err == nil { 215 | resource = applyOutboundRules(resource, rules, version) 216 | } 217 | 218 | ctx = ctx.setResult(resource) 219 | ctx = ctx.setError(err) 220 | ctx = ctx.setStatus(http.StatusOK) 221 | 222 | h.sendResponse(ctx) 223 | }) 224 | } 225 | 226 | // handleUpdateList returns a Handler which will deserialize the request payload, 227 | // pass it to the provided update function, and then serialize and dispatch the 228 | // response. The serialization mechanism used is specified by the "format" query 229 | // parameter. 230 | func (h requestHandler) handleUpdateList(handler ResourceHandler) http.Handler { 231 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 232 | ctx := NewContextWithRouter(r, w, h.router) 233 | version := ctx.Version() 234 | rules := handler.Rules() 235 | 236 | payloadStr := ctx.Body().Bytes() 237 | var data []Payload 238 | var err error 239 | data, err = decodePayloadSlice(payloadStr) 240 | if err != nil { 241 | var p Payload 242 | p, err = decodePayload(payloadStr) 243 | data = []Payload{p} 244 | } 245 | 246 | if err != nil { 247 | // Payload decoding failed. 248 | ctx = ctx.setError(BadRequest(err.Error())) 249 | } else { 250 | for i := range data { 251 | data[i], err = applyInboundRules(data[i], rules, version) 252 | } 253 | if err != nil { 254 | // Type coercion failed. 255 | ctx = ctx.setError(UnprocessableRequest(err.Error())) 256 | } else { 257 | resources, err := handler.UpdateResourceList(ctx, data, version) 258 | if err == nil { 259 | // Apply rules to results. 260 | for idx, resource := range resources { 261 | resources[idx] = applyOutboundRules(resource, rules, version) 262 | } 263 | } 264 | 265 | ctx = ctx.setResult(resources) 266 | ctx = ctx.setError(err) 267 | ctx = ctx.setStatus(http.StatusOK) 268 | } 269 | } 270 | 271 | h.sendResponse(ctx) 272 | }) 273 | } 274 | 275 | // handleUpdate returns a Handler which will deserialize the request payload, 276 | // pass it to the provided update function, and then serialize and dispatch the 277 | // response. The serialization mechanism used is specified by the "format" query 278 | // parameter. 279 | func (h requestHandler) handleUpdate(handler ResourceHandler) http.Handler { 280 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 281 | ctx := NewContextWithRouter(r, w, h.router) 282 | version := ctx.Version() 283 | rules := handler.Rules() 284 | 285 | data, err := decodePayload(ctx.Body().Bytes()) 286 | if err != nil { 287 | // Payload decoding failed. 288 | ctx = ctx.setError(BadRequest(err.Error())) 289 | } else { 290 | data, err := applyInboundRules(data, rules, version) 291 | if err != nil { 292 | // Type coercion failed. 293 | ctx = ctx.setError(UnprocessableRequest(err.Error())) 294 | } else { 295 | resource, err := handler.UpdateResource( 296 | ctx, ctx.ResourceID(), data, version) 297 | if err == nil { 298 | resource = applyOutboundRules(resource, rules, version) 299 | } 300 | 301 | ctx = ctx.setResult(resource) 302 | ctx = ctx.setError(err) 303 | ctx = ctx.setStatus(http.StatusOK) 304 | } 305 | } 306 | 307 | h.sendResponse(ctx) 308 | }) 309 | } 310 | 311 | // handleDelete returns a Handler which will pass the resource id to the provided 312 | // delete function and then serialize and dispatch the response. The serialization 313 | // mechanism used is specified by the "format" query parameter. 314 | func (h requestHandler) handleDelete(handler ResourceHandler) http.Handler { 315 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 316 | ctx := NewContextWithRouter(r, w, h.router) 317 | version := ctx.Version() 318 | rules := handler.Rules() 319 | 320 | resource, err := handler.DeleteResource(ctx, ctx.ResourceID(), version) 321 | if err == nil { 322 | resource = applyOutboundRules(resource, rules, version) 323 | } 324 | 325 | ctx = ctx.setResult(resource) 326 | ctx = ctx.setError(err) 327 | ctx = ctx.setStatus(http.StatusOK) 328 | 329 | h.sendResponse(ctx) 330 | }) 331 | } 332 | 333 | // sendResponse writes a success or error response to the provided http.ResponseWriter 334 | // based on the contents of the RequestContext. 335 | func (h requestHandler) sendResponse(ctx RequestContext) { 336 | format := ctx.ResponseFormat() 337 | serializer, err := h.responseSerializer(format) 338 | if err != nil { 339 | // Fall back to json serialization. 340 | serializer = jsonSerializer{} 341 | ctx = ctx.setError(BadRequest(fmt.Sprintf("Format not implemented: %s", format))) 342 | } 343 | 344 | sendResponse(ctx.ResponseWriter(), NewResponse(ctx), serializer) 345 | } 346 | 347 | // sendResponse writes a response to the http.ResponseWriter. 348 | func sendResponse(w http.ResponseWriter, r response, serializer ResponseSerializer) { 349 | status := r.Status 350 | contentType := serializer.ContentType() 351 | 352 | var response []byte 353 | if r.Payload != nil { 354 | var err error 355 | response, err = serializer.Serialize(r.Payload) 356 | if err != nil { 357 | log.Printf("Response serialization failed: %s", err) 358 | status = http.StatusInternalServerError 359 | contentType = "text/plain" 360 | response = []byte(err.Error()) 361 | } 362 | } 363 | 364 | w.Header().Set("Content-Type", contentType) 365 | w.WriteHeader(status) 366 | w.Write(response) 367 | } 368 | 369 | // decodePayload unmarshals the JSON payload and returns the resulting map. If the 370 | // content is empty, an empty map is returned. If decoding fails, nil is returned 371 | // with an error. 372 | func decodePayload(payload []byte) (Payload, error) { 373 | if len(payload) == 0 { 374 | return map[string]interface{}{}, nil 375 | } 376 | 377 | var data Payload 378 | if err := json.Unmarshal(payload, &data); err != nil { 379 | return nil, err 380 | } 381 | 382 | return data, nil 383 | } 384 | 385 | // decodePayloadSlice unmarshals the JSON payload and returns the resulting slice. 386 | // If the content is empty, an empty list is returned. If decoding fails, nil is 387 | // returned with an error. 388 | func decodePayloadSlice(payload []byte) ([]Payload, error) { 389 | if len(payload) == 0 { 390 | return []Payload{}, nil 391 | } 392 | 393 | var data []Payload 394 | if err := json.Unmarshal(payload, &data); err != nil { 395 | return nil, err 396 | } 397 | 398 | return data, nil 399 | } 400 | -------------------------------------------------------------------------------- /rest/context.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 - 2015 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package rest 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "fmt" 23 | "io/ioutil" 24 | "net/http" 25 | "net/url" 26 | "strconv" 27 | 28 | "github.com/gorilla/mux" 29 | ) 30 | 31 | const ( 32 | // resourceIDKey is the name of the URL path variable for a resource ID. 33 | resourceIDKey = "resource_id" 34 | 35 | // formatKey is the name of the query string variable for the response format. 36 | formatKey = "format" 37 | 38 | // versionKey is the name of the URL path variable for the endpoint version. 39 | versionKey = "version" 40 | 41 | // cursorKey is the name of the query string variable for the results cursor. 42 | cursorKey = "next" 43 | 44 | // limitKey is the name of the query string variable for the results limit. 45 | limitKey = "limit" 46 | 47 | requestKey int = iota 48 | statusKey 49 | errorKey 50 | resultKey 51 | ) 52 | 53 | // RequestContext contains the context information for the current HTTP request. Context 54 | // values are stored on the http.Request context. 55 | type RequestContext interface { 56 | // Value returns the value associated with this context for key, or nil 57 | // if no value is associated with key. Successive calls to Value with 58 | // the same key returns the same result. 59 | Value(key interface{}) interface{} 60 | 61 | // WithValue returns a new RequestContext with the provided key-value pair and this context 62 | // as the parent. 63 | WithValue(interface{}, interface{}) RequestContext 64 | 65 | // ValueWithDefault returns the context value for the given key. If there's no such value, 66 | // the provided default is returned. 67 | ValueWithDefault(interface{}, interface{}) interface{} 68 | 69 | // Request returns the *http.Request associated with context using NewContext, if any. 70 | Request() (*http.Request, bool) 71 | 72 | // NextURL returns the URL to use to request the next page of results using the current 73 | // cursor. If there is no cursor for this request or the URL fails to be built, an empty 74 | // string is returned with the error set. 75 | NextURL() (string, error) 76 | 77 | // BuildURL builds a url.URL struct for a resource name & method. 78 | // 79 | // resourceName should have the same value as the handler's ResourceName method. 80 | // 81 | // method is the HandleMethod constant that corresponds with the resource 82 | // method for which to build the URL. E.g. HandleCreate with build a URL that 83 | // corresponds with the CreateResource method. 84 | // 85 | // All URL variables should be named in the vars map. 86 | BuildURL(resourceName string, method HandleMethod, vars RouteVars) (*url.URL, error) 87 | 88 | // ResponseFormat returns the response format for the request, defaulting to "json" if 89 | // one is not specified using the "format" query parameter. 90 | ResponseFormat() string 91 | 92 | // ResourceID returns the resource id for the request, defaulting to an empty string if 93 | // there isn't one. 94 | ResourceID() string 95 | 96 | // Version returns the API version for the request, defaulting to an empty string if 97 | // one is not specified in the request path. 98 | Version() string 99 | 100 | // Status returns the current HTTP status code that will be returned for the request, 101 | // defaulting to 200 if one hasn't been set yet. 102 | Status() int 103 | 104 | // setStatus sets the HTTP status code to be returned for the request. 105 | setStatus(int) RequestContext 106 | 107 | // Error returns the current error for the request or nil if no errors have been set. 108 | Error() error 109 | 110 | // setError sets the current error for the request. 111 | setError(error) RequestContext 112 | 113 | // Result returns the result resource for the request or nil if no result has been set. 114 | Result() interface{} 115 | 116 | // setResult sets the result resource for the request. 117 | setResult(interface{}) RequestContext 118 | 119 | // Cursor returns the current result cursor for the request, defaulting to an empty 120 | // string if one hasn't been set. 121 | Cursor() string 122 | 123 | // setCursor sets the current result cursor for the request. 124 | setCursor(string) RequestContext 125 | 126 | // Limit returns the maximum number of results that should be fetched. 127 | Limit() int 128 | 129 | // Messages returns all of the messages set by the request handler to be included in 130 | // the response. 131 | Messages() []string 132 | 133 | // AddMessage adds a message to the request messages to be included in the response. 134 | AddMessage(string) 135 | 136 | // Header returns the header key-value pairs for the request. 137 | Header() http.Header 138 | 139 | // Body returns a buffer containing the raw body of the request. 140 | Body() *bytes.Buffer 141 | 142 | // ResponseWriter Access to Response Writer Interface to allow for setting Response Header values 143 | ResponseWriter() http.ResponseWriter 144 | } 145 | 146 | // requestContext is an implementation of the RequestContext interface. 147 | type requestContext struct { 148 | req *http.Request 149 | body *bytes.Buffer 150 | writer http.ResponseWriter 151 | router *mux.Router 152 | messages []string 153 | } 154 | 155 | func setValueOnRequestContext(req *http.Request, key, val interface{}) *http.Request { 156 | ctx := context.WithValue(req.Context(), key, val) 157 | return req.WithContext(ctx) 158 | } 159 | 160 | // NewContext returns a RequestContext populated with parameters from the request path and 161 | // query string. 162 | func NewContext(req *http.Request, writer http.ResponseWriter) RequestContext { 163 | for key, value := range req.URL.Query() { 164 | var val interface{} 165 | val = value 166 | 167 | // Query string values are slices (e.g. ?foo=bar,baz,qux yields 168 | // [bar, baz, qux] for foo), but we unbox single values (e.g. ?foo=bar 169 | // yields bar for foo). 170 | if len(value) == 1 { 171 | val = value[0] 172 | } 173 | 174 | req = setValueOnRequestContext(req, key, val) 175 | } 176 | 177 | for key, value := range mux.Vars(req) { 178 | req = setValueOnRequestContext(req, key, value) 179 | } 180 | 181 | var body []byte 182 | if req.Body != nil { 183 | bytes, err := ioutil.ReadAll(req.Body) 184 | if err == nil { 185 | body = bytes 186 | } 187 | } 188 | 189 | // TODO: Keys can potentially be overwritten if the request path has 190 | // parameters with the same name as query string values. Figure out a 191 | // better way to handle this. 192 | 193 | return &requestContext{ 194 | req: req, 195 | body: bytes.NewBuffer(body), 196 | writer: writer, 197 | router: nil, 198 | messages: []string{}, 199 | } 200 | } 201 | 202 | func NewContextWithRouter(req *http.Request, writer http.ResponseWriter, router *mux.Router) RequestContext { 203 | context := NewContext(req, writer) 204 | context.(*requestContext).router = router 205 | return context 206 | } 207 | 208 | // WithValue returns a new RequestContext with the provided key-value pair and this context 209 | // as the parent. 210 | func (ctx *requestContext) WithValue(key, value interface{}) RequestContext { 211 | if r, ok := ctx.Request(); ok { 212 | req := setValueOnRequestContext(r, key, value) 213 | return &requestContext{ 214 | req: req, 215 | body: ctx.body, 216 | writer: ctx.writer, 217 | router: ctx.router, 218 | messages: ctx.messages, 219 | } 220 | } 221 | 222 | // Should not reach this. 223 | panic("Unable to set value on context: no request") 224 | } 225 | 226 | // Value returns this Context's request's value for the given key, or the request if 227 | // passed the request key. It will return nil if no value is associated with the key. 228 | func (ctx *requestContext) Value(key interface{}) interface{} { 229 | if key == requestKey { 230 | return ctx.req 231 | } 232 | val := ctx.req.Context().Value(key) 233 | if val != nil { 234 | return val 235 | } 236 | return nil 237 | } 238 | 239 | // ValueWithDefault returns the context value for the given key. If there's no 240 | // such value, the provided default is returned. 241 | func (ctx *requestContext) ValueWithDefault(key, defaultVal interface{}) interface{} { 242 | value := ctx.req.Context().Value(key) 243 | if value == nil { 244 | value = defaultVal 245 | } 246 | return value 247 | } 248 | 249 | // ResponseFormat returns the response format for the request, defaulting to "json" 250 | // if one is not specified using the "format" query parameter. 251 | func (ctx *requestContext) ResponseFormat() string { 252 | return ctx.ValueWithDefault(formatKey, "json").(string) 253 | } 254 | 255 | // ResourceID returns the resource id for the request, defaulting to an empty string 256 | // if there isn't one. 257 | func (ctx *requestContext) ResourceID() string { 258 | return ctx.ValueWithDefault(resourceIDKey, "").(string) 259 | } 260 | 261 | // Version returns the API version for the request, defaulting to an empty string 262 | // if one is not specified in the request path. 263 | func (ctx *requestContext) Version() string { 264 | return ctx.ValueWithDefault(versionKey, "").(string) 265 | } 266 | 267 | // Status returns the current HTTP status code that will be returned for the request, 268 | // defaulting to 200 if one hasn't been set yet. 269 | func (ctx *requestContext) Status() int { 270 | return ctx.ValueWithDefault(statusKey, http.StatusOK).(int) 271 | } 272 | 273 | // setStatus sets the HTTP status code to be returned for the request. 274 | func (ctx *requestContext) setStatus(status int) RequestContext { 275 | return ctx.WithValue(statusKey, status) 276 | } 277 | 278 | // Error returns the current error for the request or nil if no errors have been set. 279 | func (ctx *requestContext) Error() error { 280 | err := ctx.ValueWithDefault(errorKey, nil) 281 | 282 | if err == nil { 283 | return nil 284 | } 285 | 286 | return err.(error) 287 | } 288 | 289 | // setError sets the current error for the request. 290 | func (ctx *requestContext) setError(err error) RequestContext { 291 | return ctx.WithValue(errorKey, err) 292 | } 293 | 294 | // Result returns the result resource for the request or nil if no result has been set. 295 | func (ctx *requestContext) Result() interface{} { 296 | return ctx.ValueWithDefault(resultKey, nil) 297 | } 298 | 299 | // setResult sets the result resource for the request. 300 | func (ctx *requestContext) setResult(result interface{}) RequestContext { 301 | return ctx.WithValue(resultKey, result) 302 | } 303 | 304 | // Cursor returns the current result cursor for the request, defaulting to an empty 305 | // string if one hasn't been set. 306 | func (ctx *requestContext) Cursor() string { 307 | return ctx.ValueWithDefault(cursorKey, "").(string) 308 | } 309 | 310 | // setCursor sets the current result cursor for the request. 311 | func (ctx *requestContext) setCursor(cursor string) RequestContext { 312 | return ctx.WithValue(cursorKey, cursor) 313 | } 314 | 315 | // Header returns the header key-value pairs for the request. 316 | func (ctx *requestContext) Header() http.Header { 317 | req, ok := ctx.Request() 318 | if !ok { 319 | return http.Header{} 320 | } 321 | 322 | return req.Header 323 | } 324 | 325 | // Body returns a buffer containing the raw body of the request. 326 | func (ctx *requestContext) Body() *bytes.Buffer { 327 | return ctx.body 328 | } 329 | 330 | // Request returns the *http.Request associated with context using NewContext, if any. 331 | func (ctx *requestContext) Request() (*http.Request, bool) { 332 | // We cannot use ctx.(*requestContext).req to get the request because ctx may 333 | // be a Context derived from a *requestContext. Instead, we use Value to 334 | // access the request if it is anywhere up the Context tree. 335 | req, ok := ctx.Value(requestKey).(*http.Request) 336 | return req, ok 337 | } 338 | 339 | // Limit returns the maximum number of results that should be fetched. 340 | func (ctx *requestContext) Limit() int { 341 | limitStr := ctx.ValueWithDefault(limitKey, "100") 342 | limit, err := strconv.Atoi(limitStr.(string)) 343 | if err != nil { 344 | limit = 100 345 | } 346 | return limit 347 | } 348 | 349 | // NextURL returns the URL to use to request the next page of results using the current 350 | // cursor. If there is no cursor for this request or the URL fails to be built, an empty 351 | // string is returned with the error set. 352 | func (ctx *requestContext) NextURL() (string, error) { 353 | cursor := ctx.Cursor() 354 | if cursor == "" { 355 | return "", fmt.Errorf("Unable to build next url: no cursor") 356 | } 357 | 358 | r, ok := ctx.Request() 359 | if !ok { 360 | return "", fmt.Errorf("Unable to build next url: no request") 361 | } 362 | 363 | var scheme string 364 | scheme = r.URL.Scheme 365 | if scheme == "" { 366 | scheme = "http" 367 | } 368 | 369 | urlStr := fmt.Sprintf("%s://%s%s", scheme, r.Host, r.RequestURI) 370 | u, err := url.Parse(urlStr) 371 | if err != nil { 372 | return "", fmt.Errorf("Unable to build next url: %s", urlStr) 373 | } 374 | 375 | q := u.Query() 376 | q.Set("next", cursor) 377 | u.RawQuery = q.Encode() 378 | return u.String(), nil 379 | } 380 | 381 | // RouteVars is a map of URL route variables to values. 382 | // 383 | // vars = RouteVars{"category": "widgets", "resource_id": "42"} 384 | // 385 | // Variables are defined in CreateURI and the other URI methods. 386 | type RouteVars map[string]string 387 | 388 | // BuildURL builds a full URL for a resource name & method. 389 | // 390 | // resourceName should have the same value as the handler's ResourceName method. 391 | // 392 | // method is the HandleMethod constant that corresponds with the resource 393 | // method for which to build the URL. E.g. HandleCreate with build a URL that 394 | // corresponds with the CreateResource method. 395 | // 396 | // All URL variables should be named in the vars map. 397 | func (ctx *requestContext) BuildURL(resourceName string, 398 | method HandleMethod, vars RouteVars) (*url.URL, error) { 399 | r, ok := ctx.Request() 400 | if !ok { 401 | return nil, fmt.Errorf("unable to build URL for resource name %q: no request available", 402 | resourceName) 403 | } 404 | 405 | routeName := resourceName + ":" + string(method) 406 | route := ctx.router.Get(routeName) 407 | 408 | // Transform RouteVars map to list of key, val pairs for Gorilla's API 409 | pairs := make([]string, (len(vars)*2)+2) 410 | for key, val := range vars { 411 | pairs = append(pairs, key, val) 412 | } 413 | pairs = append(pairs, "version", ctx.Version()) 414 | url, err := route.URL(pairs...) 415 | if err != nil { 416 | return nil, err 417 | } 418 | url.Host = r.Host 419 | 420 | url.Scheme = "http" 421 | if r.TLS != nil { 422 | url.Scheme += "s" 423 | } 424 | 425 | return url, nil 426 | } 427 | 428 | // Messages returns all of the messages set by the request handler to be included in 429 | // the response. 430 | func (ctx *requestContext) Messages() []string { 431 | messages := ctx.messages 432 | if err := ctx.Error(); err != nil { 433 | messages = append(messages, err.Error()) 434 | } 435 | return messages 436 | } 437 | 438 | // AddMessage adds a message to the request messages to be included in the response. 439 | func (ctx *requestContext) AddMessage(message string) { 440 | ctx.messages = append(ctx.messages, message) 441 | } 442 | 443 | func (ctx *requestContext) ResponseWriter() http.ResponseWriter { 444 | return ctx.writer 445 | } 446 | -------------------------------------------------------------------------------- /rest/payload_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 - 2015 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package rest 18 | 19 | import ( 20 | "fmt" 21 | "testing" 22 | "time" 23 | 24 | "github.com/stretchr/testify/assert" 25 | ) 26 | 27 | // Ensures that Get returns nil and an error if the key doesn't exist. 28 | func TestGetBadKey(t *testing.T) { 29 | assert := assert.New(t) 30 | payload := Payload{} 31 | 32 | actual, err := payload.Get("foo") 33 | 34 | assert.Nil(actual, "Incorrect return value") 35 | assert.Equal(fmt.Errorf("No value with key 'foo'"), err, "Incorrect error value") 36 | } 37 | 38 | // Ensures that Get returns the correct value. 39 | func TestGet(t *testing.T) { 40 | assert := assert.New(t) 41 | payload := Payload{"foo": 1} 42 | 43 | actual, err := payload.Get("foo") 44 | 45 | assert.Equal(1, actual, "Incorrect return value") 46 | assert.Nil(err, "Error should be nil") 47 | } 48 | 49 | // Ensures that GetInt returns zero value and an error if the value isn't an int. 50 | func TestGetIntBadValue(t *testing.T) { 51 | assert := assert.New(t) 52 | payload := Payload{"foo": "bar"} 53 | 54 | actual, err := payload.GetInt("foo") 55 | 56 | assert.Equal(int(0), actual, "Incorrect return value") 57 | assert.Equal(fmt.Errorf("Value with key 'foo' not an int"), err, "Incorrect error value") 58 | } 59 | 60 | // Ensures that GetInt returns the correct value. 61 | func TestGetInt(t *testing.T) { 62 | assert := assert.New(t) 63 | payload := Payload{"foo": int(1)} 64 | 65 | actual, err := payload.Get("foo") 66 | 67 | assert.Equal(int(1), actual, "Incorrect return value") 68 | assert.Nil(err, "Error should be nil") 69 | } 70 | 71 | // Ensures that GetInt8 returns zero value and an error if the value isn't an int8. 72 | func TestGetInt8BadValue(t *testing.T) { 73 | assert := assert.New(t) 74 | payload := Payload{"foo": 9000} 75 | 76 | actual, err := payload.GetInt8("foo") 77 | 78 | assert.Equal(int8(0), actual, "Incorrect return value") 79 | assert.Equal(fmt.Errorf("Value with key 'foo' not an int8"), err, "Incorrect error value") 80 | } 81 | 82 | // Ensures that GetInt8 returns the correct value. 83 | func TestGetInt8(t *testing.T) { 84 | assert := assert.New(t) 85 | payload := Payload{"foo": int8(1)} 86 | 87 | actual, err := payload.GetInt8("foo") 88 | 89 | assert.Equal(int8(1), actual, "Incorrect return value") 90 | assert.Nil(err, "Error should be nil") 91 | } 92 | 93 | // Ensures that GetInt16 returns zero value and an error if the value isn't an int16. 94 | func TestGetInt16BadValue(t *testing.T) { 95 | assert := assert.New(t) 96 | payload := Payload{"foo": "bar"} 97 | 98 | actual, err := payload.GetInt16("foo") 99 | 100 | assert.Equal(int16(0), actual, "Incorrect return value") 101 | assert.Equal(fmt.Errorf("Value with key 'foo' not an int16"), err, "Incorrect error value") 102 | } 103 | 104 | // Ensures that GetInt16 returns the correct value. 105 | func TestGetInt16(t *testing.T) { 106 | assert := assert.New(t) 107 | payload := Payload{"foo": int16(1)} 108 | 109 | actual, err := payload.GetInt16("foo") 110 | 111 | assert.Equal(int16(1), actual, "Incorrect return value") 112 | assert.Nil(err, "Error should be nil") 113 | } 114 | 115 | // Ensures that GetInt32 returns zero value and an error if the value isn't an int32. 116 | func TestGetInt32BadValue(t *testing.T) { 117 | assert := assert.New(t) 118 | payload := Payload{"foo": "bar"} 119 | 120 | actual, err := payload.GetInt32("foo") 121 | 122 | assert.Equal(int32(0), actual, "Incorrect return value") 123 | assert.Equal(fmt.Errorf("Value with key 'foo' not an int32"), err, "Incorrect error value") 124 | } 125 | 126 | // Ensures that GetInt32 returns the correct value. 127 | func TestGetInt32(t *testing.T) { 128 | assert := assert.New(t) 129 | payload := Payload{"foo": int32(1)} 130 | 131 | actual, err := payload.GetInt32("foo") 132 | 133 | assert.Equal(int32(1), actual, "Incorrect return value") 134 | assert.Nil(err, "Error should be nil") 135 | } 136 | 137 | // Ensures that GetInt64 returns zero value and an error if the value isn't an int64. 138 | func TestGetInt64BadValue(t *testing.T) { 139 | assert := assert.New(t) 140 | payload := Payload{"foo": "bar"} 141 | 142 | actual, err := payload.GetInt64("foo") 143 | 144 | assert.Equal(int64(0), actual, "Incorrect return value") 145 | assert.Equal(fmt.Errorf("Value with key 'foo' not an int64"), err, "Incorrect error value") 146 | } 147 | 148 | // Ensures that GetInt64 returns the correct value. 149 | func TestGetInt64(t *testing.T) { 150 | assert := assert.New(t) 151 | payload := Payload{"foo": int64(1)} 152 | 153 | actual, err := payload.GetInt64("foo") 154 | 155 | assert.Equal(int64(1), actual, "Incorrect return value") 156 | assert.Nil(err, "Error should be nil") 157 | } 158 | 159 | // Ensures that GetUint returns zero value and an error if the value isn't an int. 160 | func TestGetUintBadValue(t *testing.T) { 161 | assert := assert.New(t) 162 | payload := Payload{"foo": "bar"} 163 | 164 | actual, err := payload.GetUint("foo") 165 | 166 | assert.Equal(uint(0), actual, "Incorrect return value") 167 | assert.Equal(fmt.Errorf("Value with key 'foo' not a uint"), err, "Incorrect error value") 168 | } 169 | 170 | // Ensures that GetUint returns the correct value. 171 | func TestGetUint(t *testing.T) { 172 | assert := assert.New(t) 173 | payload := Payload{"foo": uint(1)} 174 | 175 | actual, err := payload.Get("foo") 176 | 177 | assert.Equal(uint(1), actual, "Incorrect return value") 178 | assert.Nil(err, "Error should be nil") 179 | } 180 | 181 | // Ensures that GetUint8 returns zero value and an error if the value isn't an int8. 182 | func TestGetUint8BadValue(t *testing.T) { 183 | assert := assert.New(t) 184 | payload := Payload{"foo": 9000} 185 | 186 | actual, err := payload.GetUint8("foo") 187 | 188 | assert.Equal(uint8(0), actual, "Incorrect return value") 189 | assert.Equal(fmt.Errorf("Value with key 'foo' not a uint8"), err, "Incorrect error value") 190 | } 191 | 192 | // Ensures that GetUint8 returns the correct value. 193 | func TestGetUint8(t *testing.T) { 194 | assert := assert.New(t) 195 | payload := Payload{"foo": uint8(1)} 196 | 197 | actual, err := payload.GetUint8("foo") 198 | 199 | assert.Equal(uint8(1), actual, "Incorrect return value") 200 | assert.Nil(err, "Error should be nil") 201 | } 202 | 203 | // Ensures that GetUint16 returns zero value and an error if the value isn't an int16. 204 | func TestGetUint16BadValue(t *testing.T) { 205 | assert := assert.New(t) 206 | payload := Payload{"foo": "bar"} 207 | 208 | actual, err := payload.GetUint16("foo") 209 | 210 | assert.Equal(uint16(0), actual, "Incorrect return value") 211 | assert.Equal(fmt.Errorf("Value with key 'foo' not a uint16"), err, "Incorrect error value") 212 | } 213 | 214 | // Ensures that GetUint16 returns the correct value. 215 | func TestGetUint16(t *testing.T) { 216 | assert := assert.New(t) 217 | payload := Payload{"foo": uint16(1)} 218 | 219 | actual, err := payload.GetUint16("foo") 220 | 221 | assert.Equal(uint16(1), actual, "Incorrect return value") 222 | assert.Nil(err, "Error should be nil") 223 | } 224 | 225 | // Ensures that GetUint32 returns zero value and an error if the value isn't an int32. 226 | func TestGetUint32BadValue(t *testing.T) { 227 | assert := assert.New(t) 228 | payload := Payload{"foo": "bar"} 229 | 230 | actual, err := payload.GetUint32("foo") 231 | 232 | assert.Equal(uint32(0), actual, "Incorrect return value") 233 | assert.Equal(fmt.Errorf("Value with key 'foo' not a uint32"), err, "Incorrect error value") 234 | } 235 | 236 | // Ensures that GetUint32 returns the correct value. 237 | func TestGetUint32(t *testing.T) { 238 | assert := assert.New(t) 239 | payload := Payload{"foo": uint32(1)} 240 | 241 | actual, err := payload.GetUint32("foo") 242 | 243 | assert.Equal(uint32(1), actual, "Incorrect return value") 244 | assert.Nil(err, "Error should be nil") 245 | } 246 | 247 | // Ensures that GetUint64 returns zero value and an error if the value isn't an int64. 248 | func TestGetUint64BadValue(t *testing.T) { 249 | assert := assert.New(t) 250 | payload := Payload{"foo": "bar"} 251 | 252 | actual, err := payload.GetUint64("foo") 253 | 254 | assert.Equal(uint64(0), actual, "Incorrect return value") 255 | assert.Equal(fmt.Errorf("Value with key 'foo' not a uint64"), err, "Incorrect error value") 256 | } 257 | 258 | // Ensures that GetUint64 returns the correct value. 259 | func TestGetUint64(t *testing.T) { 260 | assert := assert.New(t) 261 | payload := Payload{"foo": uint64(1)} 262 | 263 | actual, err := payload.GetUint64("foo") 264 | 265 | assert.Equal(uint64(1), actual, "Incorrect return value") 266 | assert.Nil(err, "Error should be nil") 267 | } 268 | 269 | // Ensures that GetFloat32 returns zero value and an error if the value isn't a float32. 270 | func TestGetFloat32BadValue(t *testing.T) { 271 | assert := assert.New(t) 272 | payload := Payload{"foo": "bar"} 273 | 274 | actual, err := payload.GetFloat32("foo") 275 | 276 | assert.Equal(float32(0), actual, "Incorrect return value") 277 | assert.Equal(fmt.Errorf("Value with key 'foo' not a float32"), err, "Incorrect error value") 278 | } 279 | 280 | // Ensures that GetFloat32 returns the correct value. 281 | func TestGetFloat32(t *testing.T) { 282 | assert := assert.New(t) 283 | payload := Payload{"foo": float32(1)} 284 | 285 | actual, err := payload.GetFloat32("foo") 286 | 287 | assert.Equal(float32(1), actual, "Incorrect return value") 288 | assert.Nil(err, "Error should be nil") 289 | } 290 | 291 | // Ensures that GetFloat64 returns zero value and an error if the value isn't a float64. 292 | func TestGetFloat64BadValue(t *testing.T) { 293 | assert := assert.New(t) 294 | payload := Payload{"foo": "bar"} 295 | 296 | actual, err := payload.GetFloat64("foo") 297 | 298 | assert.Equal(float64(0), actual, "Incorrect return value") 299 | assert.Equal(fmt.Errorf("Value with key 'foo' not a float64"), err, "Incorrect error value") 300 | } 301 | 302 | // Ensures that GetFloat64 returns the correct value. 303 | func TestGetFloat64(t *testing.T) { 304 | assert := assert.New(t) 305 | payload := Payload{"foo": float64(1)} 306 | 307 | actual, err := payload.GetFloat64("foo") 308 | 309 | assert.Equal(float64(1), actual, "Incorrect return value") 310 | assert.Nil(err, "Error should be nil") 311 | } 312 | 313 | // Ensures that GetByte returns zero value and an error if the value isn't a byte. 314 | func TestGetByteBadValue(t *testing.T) { 315 | assert := assert.New(t) 316 | payload := Payload{"foo": 1.0} 317 | 318 | actual, err := payload.GetByte("foo") 319 | 320 | assert.Equal(byte(0), actual, "Incorrect return value") 321 | assert.Equal(fmt.Errorf("Value with key 'foo' not a byte"), err, "Incorrect error value") 322 | } 323 | 324 | // Ensures that GetByte returns the correct value. 325 | func TestGetByte(t *testing.T) { 326 | assert := assert.New(t) 327 | payload := Payload{"foo": byte(1)} 328 | 329 | actual, err := payload.GetByte("foo") 330 | 331 | assert.Equal(byte(1), actual, "Incorrect return value") 332 | assert.Nil(err, "Error should be nil") 333 | } 334 | 335 | // Ensures that GetString returns zero value and an error if the value isn't a string. 336 | func TestGetStringBadValue(t *testing.T) { 337 | assert := assert.New(t) 338 | payload := Payload{"foo": 1.0} 339 | 340 | actual, err := payload.GetString("foo") 341 | 342 | assert.Equal("", actual, "Incorrect return value") 343 | assert.Equal(fmt.Errorf("Value with key 'foo' not a string"), err, "Incorrect error value") 344 | } 345 | 346 | // Ensures that GetString returns the correct value. 347 | func TestGetString(t *testing.T) { 348 | assert := assert.New(t) 349 | payload := Payload{"foo": "bar"} 350 | 351 | actual, err := payload.GetString("foo") 352 | 353 | assert.Equal("bar", actual, "Incorrect return value") 354 | assert.Nil(err, "Error should be nil") 355 | } 356 | 357 | // Ensures that GetBool returns false and an error if the value isn't a bool. 358 | func TestGetBoolBadValue(t *testing.T) { 359 | assert := assert.New(t) 360 | payload := Payload{"foo": 1.0} 361 | 362 | actual, err := payload.GetBool("foo") 363 | 364 | assert.Equal(false, actual, "Incorrect return value") 365 | assert.Equal(fmt.Errorf("Value with key 'foo' not a bool"), err, "Incorrect error value") 366 | } 367 | 368 | // Ensures that GetBool returns the correct value. 369 | func TestGetBool(t *testing.T) { 370 | assert := assert.New(t) 371 | payload := Payload{"foo": true} 372 | 373 | actual, err := payload.GetBool("foo") 374 | 375 | assert.Equal(true, actual, "Incorrect return value") 376 | assert.Nil(err, "Error should be nil") 377 | } 378 | 379 | // Ensures that GetSlice returns nil and an error if the value isn't a slice. 380 | func TestGetSliceBadValue(t *testing.T) { 381 | assert := assert.New(t) 382 | payload := Payload{"foo": 1.0} 383 | 384 | actual, err := payload.GetSlice("foo") 385 | 386 | assert.Equal([]interface{}(nil), actual, "Incorrect return value") 387 | assert.Equal(fmt.Errorf("Value with key 'foo' not a slice"), err, "Incorrect error value") 388 | } 389 | 390 | // Ensures that GetSlice returns the correct value. 391 | func TestGetSlice(t *testing.T) { 392 | assert := assert.New(t) 393 | payload := Payload{"foo": []interface{}{1, 2, 3}} 394 | 395 | actual, err := payload.GetSlice("foo") 396 | 397 | assert.Equal([]interface{}{1, 2, 3}, actual, "Incorrect return value") 398 | assert.Nil(err, "Error should be nil") 399 | } 400 | 401 | // Ensures that GetMap returns nil and an error if the value isn't a map. 402 | func TestGetMapBadValue(t *testing.T) { 403 | assert := assert.New(t) 404 | payload := Payload{"foo": 1.0} 405 | 406 | actual, err := payload.GetMap("foo") 407 | 408 | assert.Equal(map[string]interface{}(nil), actual, "Incorrect return value") 409 | assert.Equal(fmt.Errorf("Value with key 'foo' not a map"), err, "Incorrect error value") 410 | } 411 | 412 | // Ensures that GetMap returns the correct value. 413 | func TestGetMap(t *testing.T) { 414 | assert := assert.New(t) 415 | payload := Payload{"foo": map[string]interface{}{"a": 1}} 416 | 417 | actual, err := payload.GetMap("foo") 418 | 419 | assert.Equal(map[string]interface{}{"a": 1}, actual, "Incorrect return value") 420 | assert.Nil(err, "Error should be nil") 421 | } 422 | 423 | // Ensures that GetDuration returns zero value and an error if the value isn't a 424 | // time.Duration. 425 | func TestGetDurationBadValue(t *testing.T) { 426 | assert := assert.New(t) 427 | payload := Payload{"foo": "bar"} 428 | 429 | actual, err := payload.GetDuration("foo") 430 | 431 | assert.Equal(time.Duration(0), actual, "Incorrect return value") 432 | assert.Equal(fmt.Errorf("Value with key 'foo' not a time.Duration"), 433 | err, "Incorrect error value") 434 | } 435 | 436 | // Ensures that GetDuration returns the correct value. 437 | func TestGetDuration(t *testing.T) { 438 | assert := assert.New(t) 439 | payload := Payload{"foo": time.Duration(100)} 440 | 441 | actual, err := payload.GetDuration("foo") 442 | 443 | assert.Equal(time.Duration(100), actual, "Incorrect return value") 444 | assert.Nil(err, "Error should be nil") 445 | } 446 | 447 | // Ensures that GetTime returns zero value and an error if the value isn't a time.Time. 448 | func TestGetTimeBadValue(t *testing.T) { 449 | assert := assert.New(t) 450 | payload := Payload{"foo": "bar"} 451 | 452 | actual, err := payload.GetTime("foo") 453 | 454 | assert.Equal(time.Time{}, actual, "Incorrect return value") 455 | assert.Equal(fmt.Errorf("Value with key 'foo' not a time.Time"), 456 | err, "Incorrect error value") 457 | } 458 | 459 | // Ensures that GetTime returns the correct value. 460 | func TestGetTime(t *testing.T) { 461 | assert := assert.New(t) 462 | now := time.Now() 463 | payload := Payload{"foo": now} 464 | 465 | actual, err := payload.GetTime("foo") 466 | 467 | assert.Equal(now, actual, "Incorrect return value") 468 | assert.Nil(err, "Error should be nil") 469 | } 470 | -------------------------------------------------------------------------------- /rest/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 - 2015 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package rest 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "io/ioutil" 23 | "os" 24 | "reflect" 25 | "regexp" 26 | "sort" 27 | "strings" 28 | "time" 29 | 30 | "github.com/hoisie/mustache" 31 | ) 32 | 33 | type endpoint map[string]interface{} 34 | type field map[string]interface{} 35 | type handlerDoc map[string]string 36 | 37 | // templateRenderer is a template which can be rendered as a string. 38 | type templateRenderer interface { 39 | // render will render the template as a string using the provided context values. 40 | render(interface{}) string 41 | } 42 | 43 | // templateParser is used to parse a template string into a templateRenderer struct. 44 | type templateParser interface { 45 | // parse will parse the string into a templateRenderer or return an error if the 46 | // template is malformed. 47 | parse(string) (templateRenderer, error) 48 | } 49 | 50 | // mustacheRenderer is an implementation of the templateRenderer interface which relies on 51 | // mustache templating. 52 | type mustacheRenderer struct { 53 | *mustache.Template 54 | } 55 | 56 | // render will render the template as a string using the provided context values. 57 | func (m *mustacheRenderer) render(context interface{}) string { 58 | return m.Render(context) 59 | } 60 | 61 | // mustacheParser is an implementation of the templateParser interface which relies on 62 | // mustache templating. 63 | type mustacheParser struct{} 64 | 65 | // parse will parse the string into a templateRenderer or return an error if the template 66 | // is malformed. 67 | func (m *mustacheParser) parse(template string) (templateRenderer, error) { 68 | tpl, err := mustache.ParseString(template) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | return &mustacheRenderer{tpl}, nil 74 | } 75 | 76 | // docContextGenerator creates template contexts for rendering ResourceHandler documentation. 77 | type docContextGenerator interface { 78 | // generate creates a template context for the provided ResourceHandler. 79 | generate(ResourceHandler, string) (map[string]interface{}, error) 80 | } 81 | 82 | // docWriter writes rendered documentation to a persistent medium. 83 | type docWriter interface { 84 | // mkdir creates a directory to store documentation in. 85 | mkdir(string, os.FileMode) error 86 | 87 | // write saves the rendered documentation. 88 | write(string, []byte, os.FileMode) error 89 | } 90 | 91 | // fsDocWriter is an implementation of the docWriter interface which writes documentation to 92 | // the local file system. 93 | type fsDocWriter struct{} 94 | 95 | // mkdir creates a directory to store documentation in. 96 | func (f *fsDocWriter) mkdir(dir string, mode os.FileMode) error { 97 | return os.MkdirAll(dir, mode) 98 | } 99 | 100 | // write saves the rendered documentation. 101 | func (f *fsDocWriter) write(file string, data []byte, mode os.FileMode) error { 102 | return ioutil.WriteFile(file, data, mode) 103 | } 104 | 105 | // docGenerator produces documentation files for APIs by introspecting ResourceHandlers and 106 | // their Rules. 107 | type docGenerator struct { 108 | templateParser 109 | docContextGenerator 110 | docWriter 111 | } 112 | 113 | // newDocGenerator creates a new docGenerator instance which relies on mustache templating. 114 | func newDocGenerator() *docGenerator { 115 | return &docGenerator{ 116 | &mustacheParser{}, 117 | &defaultContextGenerator{}, 118 | &fsDocWriter{}, 119 | } 120 | } 121 | 122 | // generateDocs creates the HTML documentation for the provided API. The resulting HTML files 123 | // will be placed in the directory specified by the API Configuration. Returns an error if 124 | // generating the documentation failed, nil otherwise. 125 | func (d *docGenerator) generateDocs(api API) error { 126 | dir := api.Configuration().DocsDirectory 127 | if !strings.HasSuffix(dir, "/") { 128 | dir = dir + "/" 129 | } 130 | 131 | if err := d.mkdir(dir, os.FileMode(0777)); err != nil { 132 | api.Configuration().Logger.Println(err) 133 | return err 134 | } 135 | 136 | handlers := api.ResourceHandlers() 137 | docs := map[string][]handlerDoc{} 138 | versions := versions(handlers) 139 | 140 | for _, version := range versions { 141 | versionDocs := make([]handlerDoc, 0, len(handlers)) 142 | for _, handler := range handlers { 143 | doc, err := d.generateHandlerDoc(handler, version, dir) 144 | if err != nil { 145 | api.Configuration().Logger.Println(err) 146 | return err 147 | } else if doc != nil { 148 | versionDocs = append(versionDocs, doc) 149 | } 150 | } 151 | 152 | docs[version] = versionDocs 153 | } 154 | 155 | if err := d.generateIndexDocs(docs, versions, dir); err != nil { 156 | api.Configuration().Logger.Println(err) 157 | return err 158 | } 159 | 160 | api.Configuration().Debugf("Documentation generated in %s", dir) 161 | return nil 162 | } 163 | 164 | // generateIndexDocs creates index files for each API version with documented endpoints. 165 | func (d *docGenerator) generateIndexDocs(docs map[string][]handlerDoc, versions []string, 166 | dir string) error { 167 | 168 | tpl, err := d.parse(indexTemplate) 169 | if err != nil { 170 | return err 171 | } 172 | 173 | for version, docList := range docs { 174 | rendered := tpl.render(map[string]interface{}{ 175 | "handlers": docList, 176 | "version": version, 177 | "versions": versions, 178 | }) 179 | if err := d.write(fmt.Sprintf("%sindex_v%s.html", dir, version), 180 | []byte(rendered), 0644); err != nil { 181 | return err 182 | } 183 | } 184 | 185 | return nil 186 | } 187 | 188 | // generateHandlerDoc creates a documentation file for the versioned ResourceHandler. 189 | // Returns nil if the handler contains no documented endpoints or has no output fields. 190 | func (d *docGenerator) generateHandlerDoc(handler ResourceHandler, version, 191 | dir string) (handlerDoc, error) { 192 | 193 | tpl, err := d.parse(handlerTemplate) 194 | if err != nil { 195 | return nil, err 196 | } 197 | 198 | context, err := d.generate(handler, version) 199 | if context == nil || err != nil { 200 | return nil, err 201 | } 202 | rendered := tpl.render(context) 203 | 204 | name := handlerTypeName(handler) 205 | file := fileName(name, version) 206 | if err := d.write(fmt.Sprintf("%s%s", dir, file), []byte(rendered), 0644); err != nil { 207 | return nil, err 208 | } 209 | 210 | doc := handlerDoc{"name": name, "file": file} 211 | return doc, nil 212 | } 213 | 214 | // defaultContextGenerator is an implementation of the docContextGenerator interface. 215 | type defaultContextGenerator struct{} 216 | 217 | // generate creates a template context for the provided ResourceHandler. 218 | func (d *defaultContextGenerator) generate(handler ResourceHandler, version string) ( 219 | map[string]interface{}, error) { 220 | 221 | inputFields := getInputFields(handler.Rules().ForVersion(version)) 222 | outputFields := getOutputFields(handler.Rules().ForVersion(version)) 223 | 224 | if len(inputFields) == 0 && len(outputFields) == 0 { 225 | // Handler has no fields for this version. 226 | return nil, nil 227 | } 228 | 229 | index := 0 230 | endpoints := []endpoint{} 231 | if handler.CreateDocumentation() != "" { 232 | endpoints = append(endpoints, endpoint{ 233 | "uri": formatURI(handler.CreateURI(), version), 234 | "method": "POST", 235 | "label": "success", 236 | "description": handler.CreateDocumentation(), 237 | "hasInput": true, 238 | "inputFields": inputFields, 239 | "outputFields": outputFields, 240 | "exampleRequest": buildExampleRequest(handler.Rules(), false, version), 241 | "exampleResponse": buildExampleResponse(handler.Rules(), false, version), 242 | "index": index, 243 | }) 244 | } 245 | index++ 246 | 247 | if handler.ReadListDocumentation() != "" { 248 | endpoints = append(endpoints, endpoint{ 249 | "uri": formatURI(handler.ReadListURI(), version), 250 | "method": "GET", 251 | "label": "info", 252 | "description": handler.ReadListDocumentation(), 253 | "hasInput": false, 254 | "outputFields": outputFields, 255 | "exampleResponse": buildExampleResponse(handler.Rules(), true, version), 256 | "index": index, 257 | }) 258 | } 259 | index++ 260 | 261 | if handler.ReadDocumentation() != "" { 262 | endpoints = append(endpoints, endpoint{ 263 | "uri": formatURI(handler.ReadURI(), version), 264 | "method": "GET", 265 | "label": "info", 266 | "description": handler.ReadDocumentation(), 267 | "hasInput": false, 268 | "outputFields": outputFields, 269 | "exampleResponse": buildExampleResponse(handler.Rules(), false, version), 270 | "index": index, 271 | }) 272 | } 273 | index++ 274 | 275 | if handler.UpdateListDocumentation() != "" { 276 | endpoints = append(endpoints, endpoint{ 277 | "uri": formatURI(handler.UpdateListURI(), version), 278 | "method": "PUT", 279 | "label": "warning", 280 | "description": handler.UpdateListDocumentation(), 281 | "hasInput": true, 282 | "inputFields": inputFields, 283 | "outputFields": outputFields, 284 | "exampleRequest": buildExampleRequest(handler.Rules(), true, version), 285 | "exampleResponse": buildExampleResponse(handler.Rules(), true, version), 286 | "index": index, 287 | }) 288 | } 289 | index++ 290 | 291 | if handler.UpdateDocumentation() != "" { 292 | endpoints = append(endpoints, endpoint{ 293 | "uri": formatURI(handler.UpdateURI(), version), 294 | "method": "PUT", 295 | "label": "warning", 296 | "description": handler.UpdateDocumentation(), 297 | "hasInput": true, 298 | "inputFields": inputFields, 299 | "outputFields": outputFields, 300 | "exampleRequest": buildExampleRequest(handler.Rules(), false, version), 301 | "exampleResponse": buildExampleResponse(handler.Rules(), false, version), 302 | "index": index, 303 | }) 304 | } 305 | index++ 306 | 307 | if handler.DeleteDocumentation() != "" { 308 | endpoints = append(endpoints, endpoint{ 309 | "uri": formatURI(handler.DeleteURI(), version), 310 | "method": "DELETE", 311 | "label": "danger", 312 | "description": handler.DeleteDocumentation(), 313 | "hasInput": false, 314 | "outputFields": outputFields, 315 | "exampleResponse": buildExampleResponse(handler.Rules(), false, version), 316 | "index": index, 317 | }) 318 | } 319 | index++ 320 | 321 | if len(endpoints) == 0 { 322 | // No documented endpoints. 323 | return nil, nil 324 | } 325 | 326 | name := handlerTypeName(handler) 327 | context := map[string]interface{}{ 328 | "resource": name, 329 | "version": version, 330 | "versions": handlerVersions(handler), 331 | "endpoints": endpoints, 332 | "fileNamePrefix": fileNamePrefix(name), 333 | } 334 | 335 | return context, nil 336 | } 337 | 338 | // formatURI returns the specified URI replacing templated variable names with their 339 | // human-readable documentation equivalent. It also replaces the version regex with 340 | // the actual version string. 341 | func formatURI(uri, version string) string { 342 | uri = strings.Replace(uri, "{version:[^/]+}", version, -1) 343 | 344 | re := regexp.MustCompile("{.*?}") 345 | 346 | for _, param := range re.FindAllString(uri, -1) { 347 | uri = replaceURIParam(uri, param) 348 | } 349 | 350 | return uri 351 | } 352 | 353 | // replaceURIParam replaces the templated variable name with the human-readable 354 | // documentation equivalent, e.g. {foo} is replaced with :foo. 355 | func replaceURIParam(uri, param string) string { 356 | paramName := param[1 : len(param)-1] 357 | return strings.Replace(uri, param, ":"+paramName, -1) 358 | } 359 | 360 | // getInputFields returns input field descriptions. 361 | func getInputFields(rules Rules) []field { 362 | rules = rules.Filter(Inbound) 363 | fields := make([]field, 0, rules.Size()) 364 | 365 | for _, rule := range rules.Contents() { 366 | required := "optional" 367 | if rule.Required { 368 | required = "required" 369 | } 370 | 371 | field := field{ 372 | "name": rule.Name(), 373 | "required": required, 374 | "type": ruleTypeName(rule, Inbound), 375 | "description": rule.DocString, 376 | } 377 | 378 | fields = append(fields, field) 379 | } 380 | 381 | return fields 382 | } 383 | 384 | // getInputFields returns output field descriptions. 385 | func getOutputFields(rules Rules) []field { 386 | rules = rules.Filter(Outbound) 387 | fields := make([]field, 0, rules.Size()) 388 | 389 | for _, rule := range rules.Contents() { 390 | field := field{ 391 | "name": rule.Name(), 392 | "type": ruleTypeName(rule, Outbound), 393 | "description": rule.DocString, 394 | } 395 | 396 | fields = append(fields, field) 397 | } 398 | 399 | return fields 400 | } 401 | 402 | // ruleTypeName returns the human-readable type name for a Rule. 403 | func ruleTypeName(r *Rule, filter Filter) string { 404 | name := typeToName[r.Type] 405 | 406 | nested := r.Rules 407 | if nested != nil && nested.Filter(filter).Size() > 0 { 408 | name = resourceTypeName(nested.ResourceType().String()) 409 | if r.Type == Slice { 410 | name = "[]" + name 411 | } 412 | } 413 | 414 | return name 415 | } 416 | 417 | // resourceTypeName returns the human-readable type name for a resource. 418 | func resourceTypeName(qualifiedName string) string { 419 | i := strings.LastIndex(qualifiedName, ".") 420 | if i < 0 { 421 | return qualifiedName 422 | } 423 | 424 | return qualifiedName[i+1 : len(qualifiedName)] 425 | } 426 | 427 | // handlerTypeName returns the human-readable type name for a ResourceHandler resource. 428 | func handlerTypeName(handler ResourceHandler) string { 429 | rulesType := handler.Rules().ResourceType() 430 | if rulesType == nil { 431 | return handler.ResourceName() 432 | } 433 | 434 | return resourceTypeName(rulesType.String()) 435 | } 436 | 437 | // fileName returns the constructed HTML file name for the provided name and version. 438 | func fileName(name, version string) string { 439 | return strings.ToLower(fmt.Sprintf("%s_v%s.html", fileNamePrefix(name), version)) 440 | } 441 | 442 | // fileNamePrefix returns the provided name as lower case with spaces replaced with 443 | // underscores. 444 | func fileNamePrefix(name string) string { 445 | return strings.ToLower(strings.Replace(name, " ", "_", -1)) 446 | } 447 | 448 | // buildExampleRequest returns a JSON string representing an example endpoint request. 449 | func buildExampleRequest(rules Rules, list bool, version string) string { 450 | return buildExamplePayload(rules, Inbound, list, version) 451 | } 452 | 453 | // buildExampleRequest returns a JSON string representing an example endpoint response. 454 | func buildExampleResponse(rules Rules, list bool, version string) string { 455 | return buildExamplePayload(rules, Outbound, list, version) 456 | } 457 | 458 | // buildExamplePayload returns a JSON string representing either an example endpoint request 459 | // or response depending on the Filter provided. 460 | func buildExamplePayload(rules Rules, filter Filter, list bool, version string) string { 461 | rules = rules.ForVersion(version).Filter(filter) 462 | if rules.Size() == 0 { 463 | return "" 464 | } 465 | 466 | data := map[string]interface{}{} 467 | for _, r := range rules.Contents() { 468 | data[r.Name()] = getExampleValue(r, version) 469 | } 470 | 471 | var payload interface{} 472 | payload = data 473 | if list { 474 | payload = []interface{}{data} 475 | } 476 | 477 | serialized, err := json.MarshalIndent(payload, "", " ") 478 | if err != nil { 479 | return "" 480 | } 481 | 482 | return string(serialized) 483 | } 484 | 485 | // getExampleValue returns an example value for the provided Rule. 486 | func getExampleValue(r *Rule, version string) interface{} { 487 | value := r.DocExample 488 | if value != nil { 489 | return value 490 | } 491 | 492 | switch r.Type { 493 | case Int, Int8, Int16, Int32, Int64, Uint, Uint8, Uint16, Uint32, Uint64: 494 | return 0 495 | case Float32, Float64: 496 | return 31.5 497 | case String: 498 | return "foo" 499 | case Bool: 500 | return true 501 | case Duration: 502 | return time.Duration(10000) 503 | case Time: 504 | return time.Date(2014, 9, 5, 15, 45, 36, 0, time.UTC) 505 | default: 506 | return getNestedExampleValue(r, version) 507 | } 508 | } 509 | 510 | // getNestedExampleValue returns an example value for a nested Rule value. 511 | func getNestedExampleValue(r *Rule, version string) interface{} { 512 | if r.Rules == nil { 513 | switch r.Type { 514 | case Map: 515 | return map[string]interface{}{} 516 | case Slice: 517 | return []interface{}{} 518 | default: 519 | return nil 520 | } 521 | } 522 | 523 | ptr := reflect.New(r.Rules.ResourceType()) 524 | value := applyOutboundRules(ptr.Elem().Interface(), r.Rules, version) 525 | if r.Type == Slice { 526 | value = []interface{}{value} 527 | } 528 | return value 529 | } 530 | 531 | // versions returns a slice containing all versions specified by the provided 532 | // ResourceHandlers. 533 | func versions(handlers []ResourceHandler) []string { 534 | versionMap := map[string]bool{} 535 | for _, handler := range handlers { 536 | for _, rule := range handler.Rules().Contents() { 537 | for _, version := range rule.Versions { 538 | versionMap[version] = true 539 | } 540 | } 541 | } 542 | 543 | versions := make([]string, 0, len(versionMap)) 544 | for version := range versionMap { 545 | versions = append(versions, version) 546 | } 547 | 548 | sort.Strings(versions) 549 | return versions 550 | } 551 | 552 | // handlerVersions returns a slice containing all versions specified by the provided 553 | // ResourceHandler. 554 | func handlerVersions(handler ResourceHandler) []string { 555 | return versions([]ResourceHandler{handler}) 556 | } 557 | -------------------------------------------------------------------------------- /rest/api.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 - 2015 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package rest 18 | 19 | import ( 20 | "fmt" 21 | "log" 22 | "net/http" 23 | "os" 24 | "sort" 25 | "sync" 26 | 27 | "github.com/gorilla/mux" 28 | ) 29 | 30 | type HandleMethod string 31 | 32 | const ( 33 | defaultLogPrefix = "rest " 34 | defaultDocsDirectory = "_docs/" 35 | 36 | // Handler names 37 | HandleCreate HandleMethod = "create" 38 | HandleRead = "read" 39 | HandleUpdate = "update" 40 | HandleDelete = "delete" 41 | HandleReadList = "readList" 42 | HandleUpdateList = "updateList" 43 | ) 44 | 45 | // Address is the address and port to bind to (e.g. ":8080"). 46 | type Address string 47 | 48 | // FilePath represents a file path. 49 | type FilePath string 50 | 51 | // An interface satisfied by log.Logger 52 | type StdLogger interface { 53 | Print(...interface{}) 54 | Printf(string, ...interface{}) 55 | Println(...interface{}) 56 | 57 | Fatal(...interface{}) 58 | Fatalf(string, ...interface{}) 59 | Fatalln(...interface{}) 60 | 61 | Panic(...interface{}) 62 | Panicf(string, ...interface{}) 63 | Panicln(...interface{}) 64 | } 65 | 66 | // Configuration contains settings for configuring an API. 67 | type Configuration struct { 68 | Debug bool 69 | Logger StdLogger 70 | GenerateDocs bool 71 | DocsDirectory string 72 | } 73 | 74 | // Debugf prints the formatted string to the Configuration Logger if Debug is enabled. 75 | func (c *Configuration) Debugf(format string, v ...interface{}) { 76 | if c.Debug { 77 | c.Logger.Printf(format, v...) 78 | } 79 | } 80 | 81 | // NewConfiguration returns a default Configuration. 82 | func NewConfiguration() *Configuration { 83 | logger := log.New(os.Stdout, defaultLogPrefix, log.LstdFlags) 84 | return &Configuration{ 85 | Debug: true, 86 | Logger: logger, 87 | GenerateDocs: true, 88 | DocsDirectory: defaultDocsDirectory, 89 | } 90 | } 91 | 92 | // MiddlewareError is returned by Middleware to indicate that a request should 93 | // not be served. 94 | type MiddlewareError struct { 95 | Code int 96 | Response []byte 97 | } 98 | 99 | // Middleware can be passed in to API#Start and API#StartTLS and will be 100 | // invoked on every request to a route handled by the API. Returns a 101 | // MiddlewareError if the request should be terminated. 102 | type Middleware func(w http.ResponseWriter, r *http.Request) *MiddlewareError 103 | 104 | // middlewareProxy proxies an http.Handler by invoking middleware before 105 | // passing the request to the Handler. It implements the http.Handler 106 | // interface. 107 | type middlewareProxy struct { 108 | handler http.Handler 109 | middleware []Middleware 110 | } 111 | 112 | // ServeHTTP invokes middleware on the request and then delegates to the 113 | // proxied http.Handler. 114 | func (m *middlewareProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { 115 | for _, middleware := range m.middleware { 116 | if err := middleware(w, r); err != nil { 117 | w.WriteHeader(err.Code) 118 | w.Write(err.Response) 119 | return 120 | } 121 | } 122 | m.handler.ServeHTTP(w, r) 123 | } 124 | 125 | // wrapMiddleware returns an http.Handler with the Middleware applied. 126 | func wrapMiddleware(handler http.Handler, middleware ...Middleware) http.Handler { 127 | return &middlewareProxy{ 128 | handler: handler, 129 | middleware: middleware, 130 | } 131 | } 132 | 133 | // API is the top-level interface encapsulating an HTTP REST server. It's responsible for 134 | // registering ResourceHandlers and routing requests. Use NewAPI to retrieve an instance. 135 | type API interface { 136 | http.Handler 137 | 138 | // Start begins serving requests. This will block unless it fails, in which case an 139 | // error will be returned. This will validate any defined Rules. If any Rules are 140 | // invalid, it will panic. Any provided Middleware will be invoked for every request 141 | // handled by the API. 142 | Start(Address, ...Middleware) error 143 | 144 | // StartTLS begins serving requests received over HTTPS connections. This will block 145 | // unless it fails, in which case an error will be returned. Files containing a 146 | // certificate and matching private key for the server must be provided. If the 147 | // certificate is signed by a certificate authority, the certFile should be the 148 | // concatenation of the server's certificate followed by the CA's certificate. This 149 | // will validate any defined Rules. If any Rules are invalid, it will panic. Any 150 | // provided Middleware will be invoked for every request handled by the API. 151 | StartTLS(Address, FilePath, FilePath, ...Middleware) error 152 | 153 | // RegisterResourceHandler binds the provided ResourceHandler to the appropriate REST 154 | // endpoints and applies any specified middleware. Endpoints will have the following 155 | // base URL: /api/:version/resourceName. 156 | RegisterResourceHandler(ResourceHandler, ...RequestMiddleware) 157 | 158 | // RegisterHandlerFunc binds the http.HandlerFunc to the provided URI and applies any 159 | // specified middleware. 160 | RegisterHandlerFunc(string, http.HandlerFunc, ...RequestMiddleware) 161 | 162 | // RegisterHandler binds the http.Handler to the provided URI and applies any specified 163 | // middleware. 164 | RegisterHandler(string, http.Handler, ...RequestMiddleware) 165 | 166 | // RegisterPathPrefix binds the http.HandlerFunc to URIs matched by the given path 167 | // prefix and applies any specified middleware. 168 | RegisterPathPrefix(string, http.HandlerFunc, ...RequestMiddleware) 169 | 170 | // RegisterResponseSerializer registers the provided ResponseSerializer with the given 171 | // format. If the format has already been registered, it will be overwritten. 172 | RegisterResponseSerializer(string, ResponseSerializer) 173 | 174 | // UnregisterResponseSerializer unregisters the ResponseSerializer with the provided 175 | // format. If the format hasn't been registered, this is a no-op. 176 | UnregisterResponseSerializer(string) 177 | 178 | // AvailableFormats returns a slice containing all of the available serialization 179 | // formats currently available. 180 | AvailableFormats() []string 181 | 182 | // Configuration returns the API Configuration. 183 | Configuration() *Configuration 184 | 185 | // ResourceHandlers returns a slice containing the registered ResourceHandlers. 186 | ResourceHandlers() []ResourceHandler 187 | 188 | // Validate will validate the Rules configured for this API. It returns nil 189 | // if all Rules are valid, otherwise returns the first encountered 190 | // validation error. 191 | Validate() error 192 | 193 | // responseSerializer returns a ResponseSerializer for the given format type. If the 194 | // format is not implemented, the returned serializer will be nil and the error set. 195 | responseSerializer(string) (ResponseSerializer, error) 196 | } 197 | 198 | // RequestMiddleware is a function that returns a Handler wrapping the provided Handler. 199 | // This allows injecting custom logic to operate on requests (e.g. performing authentication). 200 | type RequestMiddleware func(http.Handler) http.Handler 201 | 202 | // newAuthMiddleware returns a RequestMiddleware used to authenticate requests. 203 | func newAuthMiddleware(authenticate func(*http.Request) error) RequestMiddleware { 204 | return func(next http.Handler) http.Handler { 205 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 206 | if err := authenticate(r); err != nil { 207 | w.WriteHeader(http.StatusUnauthorized) 208 | w.Write([]byte(err.Error())) 209 | return 210 | } 211 | next.ServeHTTP(w, r) 212 | }) 213 | } 214 | } 215 | 216 | // newVersionMiddleware checks the request version against all valid versions. 217 | func newVersionMiddleware(validVersions []string) RequestMiddleware { 218 | return func(next http.Handler) http.Handler { 219 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 220 | requestVersion := mux.Vars(r)["version"] 221 | 222 | for _, v := range validVersions { 223 | if requestVersion == v { 224 | next.ServeHTTP(w, r) 225 | return 226 | } 227 | } 228 | 229 | w.WriteHeader(http.StatusBadRequest) 230 | w.Write([]byte(fmt.Sprintf("Version %q is not available.", requestVersion))) 231 | }) 232 | } 233 | } 234 | 235 | // muxAPI is an implementation of the API interface which relies on the gorilla/mux 236 | // package to handle request dispatching (see http://www.gorillatoolkit.org/pkg/mux). 237 | type muxAPI struct { 238 | config *Configuration 239 | router *mux.Router 240 | mu sync.RWMutex 241 | handler *requestHandler 242 | serializerRegistry map[string]ResponseSerializer 243 | resourceHandlers []ResourceHandler 244 | } 245 | 246 | // NewAPI returns a newly allocated API instance. 247 | func NewAPI(config *Configuration) API { 248 | r := mux.NewRouter() 249 | restAPI := &muxAPI{ 250 | config: config, 251 | router: r, 252 | serializerRegistry: map[string]ResponseSerializer{"json": &jsonSerializer{}}, 253 | resourceHandlers: make([]ResourceHandler, 0), 254 | } 255 | restAPI.handler = &requestHandler{restAPI, r} 256 | return restAPI 257 | } 258 | 259 | // Start begins serving requests. This will block unless it fails, in which case an error will be 260 | // returned. 261 | func (r *muxAPI) Start(addr Address, middleware ...Middleware) error { 262 | r.preprocess() 263 | return http.ListenAndServe(string(addr), wrapMiddleware(r.router, middleware...)) 264 | } 265 | 266 | // StartTLS begins serving requests received over HTTPS connections. This will block unless it 267 | // fails, in which case an error will be returned. Files containing a certificate and matching 268 | // private key for the server must be provided. If the certificate is signed by a certificate 269 | // authority, the certFile should be the concatenation of the server's certificate followed by 270 | // the CA's certificate. 271 | func (r *muxAPI) StartTLS(addr Address, certFile, keyFile FilePath, middleware ...Middleware) error { 272 | r.preprocess() 273 | return http.ListenAndServeTLS(string(addr), string(certFile), string(keyFile), wrapMiddleware(r.router, middleware...)) 274 | } 275 | 276 | // preprocess performs any necessary preprocessing before the server can be started, including 277 | // Rule validation. 278 | func (r *muxAPI) preprocess() { 279 | r.validateRulesOrPanic() 280 | if r.config.GenerateDocs { 281 | if err := newDocGenerator().generateDocs(r); err != nil { 282 | log.Printf("documentation could not be generated: %v", err) 283 | } 284 | } 285 | } 286 | 287 | // Check the route for an error and log the error if it exists. 288 | func (r *muxAPI) checkRoute(handler, method, uri string, route *mux.Route) { 289 | err := route.GetError() 290 | 291 | if err != nil { 292 | log.Printf("Failed to setup route %s with %v", uri, err) 293 | } else { 294 | r.config.Debugf("Registered %s handler at %s %s", handler, method, uri) 295 | } 296 | } 297 | 298 | // RegisterResourceHandler binds the provided ResourceHandler to the appropriate REST endpoints and 299 | // applies any specified middleware. Endpoints will have the following base URL: 300 | // /api/:version/resourceName. 301 | func (r *muxAPI) RegisterResourceHandler(h ResourceHandler, middleware ...RequestMiddleware) { 302 | h = resourceHandlerProxy{h} 303 | resource := h.ResourceName() 304 | middleware = append(middleware, newAuthMiddleware(h.Authenticate)) 305 | if validVersions := h.ValidVersions(); validVersions != nil { 306 | middleware = append(middleware, newVersionMiddleware(validVersions)) 307 | } 308 | 309 | // Some browsers don't support PUT and DELETE, so allow method overriding. 310 | // POST requests with X-HTTP-Method-Override=PUT/DELETE will route to the 311 | // respective handlers. 312 | 313 | route := r.router.Handle( 314 | h.ReadListURI(), applyMiddleware(r.handler.handleReadList(h), middleware), 315 | ).Methods("POST").Headers("X-HTTP-Method-Override", "GET").Name(resource + ":readListOverride") 316 | r.checkRoute("read list override", h.ReadListURI(), "OVERRIDE-GET", route) 317 | 318 | route = r.router.Handle( 319 | h.ReadURI(), applyMiddleware(r.handler.handleRead(h), middleware), 320 | ).Methods("POST").Headers("X-HTTP-Method-Override", "GET").Name(resource + ":readOverride") 321 | r.checkRoute("read override", h.ReadURI(), "OVERRIDE-GET", route) 322 | 323 | route = r.router.Handle( 324 | h.UpdateListURI(), applyMiddleware(r.handler.handleUpdateList(h), middleware), 325 | ).Methods("POST").Headers("X-HTTP-Method-Override", "PUT").Name(resource + ":updateListOverride") 326 | r.checkRoute("update list override", h.UpdateListURI(), "OVERRIDE-PUT", route) 327 | 328 | route = r.router.Handle( 329 | h.UpdateURI(), applyMiddleware(r.handler.handleUpdate(h), middleware), 330 | ).Methods("POST").Headers("X-HTTP-Method-Override", "PUT").Name(resource + ":updateOverride") 331 | r.checkRoute("update override", h.UpdateURI(), "OVERRIDE-PUT", route) 332 | 333 | route = r.router.Handle( 334 | h.DeleteURI(), applyMiddleware(r.handler.handleDelete(h), middleware), 335 | ).Methods("POST").Headers("X-HTTP-Method-Override", "DELETE").Name(resource + ":deleteOverride") 336 | r.checkRoute("delete override", h.DeleteURI(), "OVERRIDE-DELETE", route) 337 | 338 | // These return a Route which has a GetError command. Probably should check 339 | // that and log it if it fails :) 340 | r.router.Handle( 341 | h.CreateURI(), applyMiddleware(r.handler.handleCreate(h), middleware), 342 | ).Methods("POST").Name(resource + ":" + string(HandleCreate)) 343 | r.checkRoute("create", h.CreateURI(), "POST", route) 344 | 345 | r.router.Handle( 346 | h.ReadListURI(), applyMiddleware(r.handler.handleReadList(h), middleware), 347 | ).Methods("GET").Name(resource + ":" + string(HandleReadList)) 348 | r.checkRoute("read list", h.ReadListURI(), "GET", route) 349 | 350 | r.router.Handle( 351 | h.ReadURI(), applyMiddleware(r.handler.handleRead(h), middleware), 352 | ).Methods("GET").Name(resource + ":" + string(HandleRead)) 353 | r.checkRoute("read", h.ReadURI(), "GET", route) 354 | 355 | r.router.Handle( 356 | h.UpdateListURI(), applyMiddleware(r.handler.handleUpdateList(h), middleware), 357 | ).Methods("PUT").Name(resource + ":" + string(HandleUpdateList)) 358 | r.checkRoute("update list", h.UpdateListURI(), "PUT", route) 359 | 360 | r.router.Handle( 361 | h.UpdateURI(), applyMiddleware(r.handler.handleUpdate(h), middleware), 362 | ).Methods("PUT").Name(resource + ":" + string(HandleUpdate)) 363 | r.checkRoute("update", h.UpdateURI(), "PUT", route) 364 | 365 | r.router.Handle( 366 | h.DeleteURI(), applyMiddleware(r.handler.handleDelete(h), middleware), 367 | ).Methods("DELETE").Name(resource + ":" + string(HandleDelete)) 368 | r.checkRoute("delete", h.DeleteURI(), "DELETE", route) 369 | 370 | r.resourceHandlers = append(r.resourceHandlers, h) 371 | } 372 | 373 | // RegisterHandlerFunc binds the http.HandlerFunc to the provided URI and applies any 374 | // specified middleware. 375 | func (r *muxAPI) RegisterHandlerFunc(uri string, handlerfunc http.HandlerFunc, 376 | middleware ...RequestMiddleware) { 377 | r.router.Handle(uri, applyMiddleware(http.HandlerFunc(handlerfunc), middleware)) 378 | } 379 | 380 | // RegisterHandler binds the http.Handler to the provided URI and applies any specified 381 | // middleware. 382 | func (r *muxAPI) RegisterHandler(uri string, handler http.Handler, middleware ...RequestMiddleware) { 383 | r.router.Handle(uri, applyMiddleware(handler, middleware)) 384 | } 385 | 386 | // RegisterPathPrefix binds the http.HandlerFunc to URIs matched by the given path 387 | // prefix and applies any specified middleware. 388 | func (r *muxAPI) RegisterPathPrefix(uri string, handler http.HandlerFunc, 389 | middleware ...RequestMiddleware) { 390 | r.router.PathPrefix(uri).Handler(applyMiddleware(handler, middleware)) 391 | } 392 | 393 | // ServeHTTP handles an HTTP request. 394 | func (r *muxAPI) ServeHTTP(w http.ResponseWriter, req *http.Request) { 395 | r.router.ServeHTTP(w, req) 396 | } 397 | 398 | // RegisterResponseSerializer registers the provided ResponseSerializer with the given format. If the 399 | // format has already been registered, it will be overwritten. 400 | func (r *muxAPI) RegisterResponseSerializer(format string, serializer ResponseSerializer) { 401 | r.mu.Lock() 402 | defer r.mu.Unlock() 403 | r.serializerRegistry[format] = serializer 404 | } 405 | 406 | // UnregisterResponseSerializer unregisters the ResponseSerializer with the provided format. If the 407 | // format hasn't been registered, this is a no-op. 408 | func (r *muxAPI) UnregisterResponseSerializer(format string) { 409 | r.mu.Lock() 410 | defer r.mu.Unlock() 411 | delete(r.serializerRegistry, format) 412 | } 413 | 414 | // AvailableFormats returns a slice containing all of the available serialization formats 415 | // currently available. 416 | func (r *muxAPI) AvailableFormats() []string { 417 | r.mu.RLock() 418 | defer r.mu.RUnlock() 419 | formats := make([]string, 0, len(r.serializerRegistry)) 420 | for format := range r.serializerRegistry { 421 | formats = append(formats, format) 422 | } 423 | sort.Strings(formats) 424 | return formats 425 | } 426 | 427 | // ResourceHandlers returns a slice containing the registered ResourceHandlers. 428 | func (r *muxAPI) ResourceHandlers() []ResourceHandler { 429 | return r.resourceHandlers 430 | } 431 | 432 | // Configuration returns the API Configuration. 433 | func (r *muxAPI) Configuration() *Configuration { 434 | return r.config 435 | } 436 | 437 | // Validate will validate the Rules configured for this API. It returns nil if 438 | // all Rules are valid, otherwise returns the first encountered validation 439 | // error. 440 | func (r *muxAPI) Validate() error { 441 | for _, handler := range r.resourceHandlers { 442 | rules := handler.Rules() 443 | if rules == nil || rules.Size() == 0 { 444 | continue 445 | } 446 | 447 | if err := rules.Validate(); err != nil { 448 | return err 449 | } 450 | } 451 | return nil 452 | } 453 | 454 | // validateRulesOrPanic verifies that the Rules for each ResourceHandler 455 | // registered with the muxAPI are valid, meaning they specify fields that exist 456 | // and correct types. If a Rule is invalid, this will panic. 457 | func (r *muxAPI) validateRulesOrPanic() { 458 | if err := r.Validate(); err != nil { 459 | panic(err) 460 | } 461 | } 462 | 463 | // responseSerializer returns a ResponseSerializer for the given format type. If the format 464 | // is not implemented, the returned serializer will be nil and the error set. 465 | func (r *muxAPI) responseSerializer(format string) (ResponseSerializer, error) { 466 | r.mu.RLock() 467 | defer r.mu.RUnlock() 468 | if serializer, ok := r.serializerRegistry[format]; ok { 469 | return serializer, nil 470 | } 471 | return nil, fmt.Errorf("Format not implemented: %s", format) 472 | } 473 | 474 | // applyMiddleware wraps the Handler with the provided RequestMiddleware and returns another Handler. 475 | func applyMiddleware(h http.Handler, middleware []RequestMiddleware) http.Handler { 476 | for _, m := range middleware { 477 | h = m(h) 478 | } 479 | 480 | return h 481 | } 482 | --------------------------------------------------------------------------------