├── .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 |
66 | {{#handlers}}
67 | {{name}}
68 | {{/handlers}}
69 |
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 | 
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 |
--------------------------------------------------------------------------------