Skip to content

Commit

Permalink
Merge pull request #44 from go-andiamo/path-auto-options
Browse files Browse the repository at this point in the history
Add `Path.AutoOptionsMethod`
  • Loading branch information
marrow16 authored Nov 23, 2023
2 parents 620c1b5 + db84aad commit 8fda997
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 25 deletions.
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ Notes:
* statics for `Redoc` are served from CDNs
* statics for `Swagger` are all served directly from *Chioas*

### Added Bonus Features
Chioas comes with many bonus features that help in building complete APIs and specs...
* Highly extensible - e.g. if there are parts of the OAS spec that are not directly supported by Chioas, then they can be added using the `Additional` field on each part
* Optionally check that OAS refs (`$ref`) are valid _(see `DocOptions.CheckRefs`)_
* Optional typed handlers - see [typed README](https://github.com/go-andiamo/chioas/blob/main/typed/README.md)
* Optional automatically added `HEAD` methods for `GET` methods _(see `Definition.AutoHeadMethods`)_
* Optional automatically added `OPTIONS` methods - with `Allow` header populated with actual allowed methods _(see `Definition.AutoOptionsMethods` and `Path.AutoOptionsMethod`)_
* Optional automatically added Chi `MethodNotAllowed` handler to each path - with `Allow` header populated with actual allowed methods _(see `Definition.AutoMethodNotAllowed`)_

## Installation
To install chioas, use go get:

Expand All @@ -47,7 +56,7 @@ To update chioas to the latest version, run:

go get -u github.com/go-andiamo/chioas

### Basic Example
## Basic Example
```go
package main

Expand Down Expand Up @@ -259,6 +268,3 @@ func deleteFoo(writer http.ResponseWriter, request *http.Request) {
}
```
[try on go-playground](https://go.dev/play/p/0zaWsmsw2FD)

## Typed Handlers
see [typed README](https://github.com/go-andiamo/chioas/blob/main/typed/README.md)
59 changes: 39 additions & 20 deletions definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,6 @@ import (
type Definition struct {
// DocOptions is the documentation options for the spec
DocOptions DocOptions
// AutoHeadMethods when set to true, automatically adds HEAD methods for GET methods (where HEAD method not explicitly specified)
AutoHeadMethods bool
// AutoOptionsMethods when set to true, automatically adds OPTIONS methods for each path (and because Chioas knows the methods for each path can correctly set the Allow header)
//
// Note: If an OPTIONS method is already defined for the path then no OPTIONS method is automatically added
AutoOptionsMethods bool
// AutoMethodNotAllowed when set to true, automatically adds a method not allowed (405) handler for each path (and because Chioas knows the methods for each path can correctly set the Allow header)
AutoMethodNotAllowed bool
// MethodHandlerBuilder is an optional MethodHandlerBuilder which is called to build the
// http.HandlerFunc for the method
//
// If MethodHandlerBuilder is nil then the default method handler builder is used
MethodHandlerBuilder MethodHandlerBuilder
// Info is the OAS info for the spec
Info Info
// Servers is the OAS servers for the spec
Expand All @@ -50,6 +37,27 @@ type Definition struct {
Additional Additional
// Comment is any comment(s) to appear in the OAS spec yaml
Comment string
// AutoHeadMethods when set to true, automatically adds HEAD methods for GET methods (where HEAD method not explicitly specified)
//
// If you don't want these automatically added HEAD methods to appear in the OAS spec - then set DocOptions.HideAutoOptionsMethods
AutoHeadMethods bool
// AutoOptionsMethods when set to true, automatically adds OPTIONS methods for each path (and because Chioas knows the methods for each path can correctly set the Allow header)
//
// Note: If an OPTIONS method is already defined for the path then no OPTIONS method is automatically added
AutoOptionsMethods bool
// OptionsMethodPayloadBuilder is an optional implementation of OptionsMethodPayloadBuilder that can provide body payloads for the automatically created OPTIONS methods
OptionsMethodPayloadBuilder OptionsMethodPayloadBuilder
// RootAutoOptionsMethod when set to true, automatically adds OPTIONS method for the root path (and because Chioas knows the methods for each path can correctly set the Allow header)
//
// Note: If an OPTIONS method is already defined for the root path then no OPTIONS method is automatically added
RootAutoOptionsMethod bool
// AutoMethodNotAllowed when set to true, automatically adds a method not allowed (405) handler for each path (and because Chioas knows the methods for each path can correctly set the Allow header)
AutoMethodNotAllowed bool
// MethodHandlerBuilder is an optional MethodHandlerBuilder which is called to build the
// http.HandlerFunc for the method
//
// If MethodHandlerBuilder is nil then the default method handler builder is used
MethodHandlerBuilder MethodHandlerBuilder
}

// SetupRoutes sets up the API routes on the supplied chi.Router
Expand All @@ -65,7 +73,7 @@ func (d *Definition) SetupRoutes(router chi.Router, thisApi any) error {
middlewares = append(middlewares, d.ApplyMiddlewares(thisApi)...)
}
subRoute.Use(middlewares...)
if err := d.setupMethods(root, d.Methods, subRoute, thisApi); err != nil {
if err := d.setupMethods(root, nil, d.Methods, d.RootAutoOptionsMethod, subRoute, thisApi); err != nil {
return err
}
if err := d.setupPaths(nil, d.Paths, subRoute, thisApi); err != nil {
Expand All @@ -91,7 +99,7 @@ func (d *Definition) setupPaths(ancestry []string, paths Paths, route chi.Router
subRoute.MethodNotAllowed(d.methodNotAllowedHandler(pDef.Methods))
}
subRoute.Use(middlewares...)
if err := d.setupMethods(strings.Join(newAncestry, ""), pDef.Methods, subRoute, thisApi); err != nil {
if err := d.setupMethods(strings.Join(newAncestry, ""), &pDef, pDef.Methods, d.AutoOptionsMethods || pDef.AutoOptionsMethod, subRoute, thisApi); err != nil {
return err
}
if err := d.setupPaths(newAncestry, pDef.Paths, subRoute, thisApi); err != nil {
Expand All @@ -103,37 +111,48 @@ func (d *Definition) setupPaths(ancestry []string, paths Paths, route chi.Router
return nil
}

func (d *Definition) setupMethods(path string, methods Methods, route chi.Router, thisApi any) error {
if methods != nil {
func (d *Definition) setupMethods(path string, pathDef *Path, methods Methods, pathAutoOptions bool, route chi.Router, thisApi any) error {
if methods != nil && len(methods) > 0 {
for m, mDef := range methods {
if h, err := getMethodHandlerBuilder(d.MethodHandlerBuilder).BuildHandler(path, m, mDef, thisApi); err == nil {
route.MethodFunc(m, root, h)
} else {
return err
}
}
if d.AutoOptionsMethods && !methods.hasOptions() {
route.MethodFunc(http.MethodOptions, root, d.optionsHandler(methods))
if (d.AutoOptionsMethods || pathAutoOptions) && !methods.hasOptions() {
route.MethodFunc(http.MethodOptions, root, d.optionsHandler(methods, path, pathDef))
}
if d.AutoHeadMethods {
if mDef, ok := methods.getWithoutHead(); ok {
h, _ := getMethodHandlerBuilder(d.MethodHandlerBuilder).BuildHandler(path, http.MethodHead, mDef, thisApi)
route.MethodFunc(http.MethodHead, root, h)
}
}
} else if pathAutoOptions {
route.MethodFunc(http.MethodOptions, root, d.optionsHandler(Methods{}, path, pathDef))
}
return nil
}

func (d *Definition) optionsHandler(methods Methods) http.HandlerFunc {
func (d *Definition) optionsHandler(methods Methods, path string, pathDef *Path) http.HandlerFunc {
add := []string{http.MethodOptions}
if _, hasGet := methods.getWithoutHead(); hasGet && d.AutoHeadMethods {
add = append(add, http.MethodHead)
}
allow := strings.Join(methods.sorted(add...), ", ")
return func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set(hdrAllow, allow)
payloadData := make([]byte, 0)
if d.OptionsMethodPayloadBuilder != nil {
var addHdrs map[string]string
payloadData, addHdrs = d.OptionsMethodPayloadBuilder.BuildPayload(path, pathDef, d)
for k, v := range addHdrs {
writer.Header().Set(k, v)
}
}
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write(payloadData)
}
}

Expand Down
146 changes: 146 additions & 0 deletions definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,152 @@ func TestDefinition_SetupRoutes_AutoOptions(t *testing.T) {
assert.Equal(t, "GET, HEAD, POST, OPTIONS", res.Result().Header.Get(hdrAllow))
}

func TestDefinition_SetupRoutes_AutoOptions_WithRootPayload(t *testing.T) {
d := Definition{
AutoOptionsMethods: true,
OptionsMethodPayloadBuilder: NewRootOptionsMethodPayloadBuilder(),
DocOptions: DocOptions{
HideAutoOptionsMethods: true,
},
Methods: Methods{
http.MethodGet: {
Handler: func(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusNotFound)
},
},
},
Paths: Paths{
"/subs": {
Methods: Methods{
http.MethodGet: {
Handler: func(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusConflict)
},
},
http.MethodPost: {
Handler: func(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusConflict)
},
},
},
},
},
}
router := chi.NewRouter()
err := d.SetupRoutes(router, nil)
assert.NoError(t, err)

req, err := http.NewRequest(http.MethodOptions, "/", nil)
require.NoError(t, err)
res := httptest.NewRecorder()
router.ServeHTTP(res, req)
assert.Equal(t, http.StatusOK, res.Code)
assert.Equal(t, "GET, OPTIONS", res.Result().Header.Get(hdrAllow))
assert.Equal(t, contentTypeYaml, res.Result().Header.Get(hdrContentType))
const expectYaml = `openapi: "3.0.3"
info:
title: "API Documentation"
version: "1.0.0"
paths:
"/":
get:
responses:
200:
description: OK
content:
"application/json":
schema:
type: object
"/subs":
get:
responses:
200:
description: OK
content:
"application/json":
schema:
type: object
post:
responses:
200:
description: OK
content:
"application/json":
schema:
type: object
`
assert.Equal(t, expectYaml, res.Body.String())

d.DocOptions.AsJson = true
req, err = http.NewRequest(http.MethodOptions, "/", nil)
require.NoError(t, err)
res = httptest.NewRecorder()
router.ServeHTTP(res, req)
assert.Equal(t, http.StatusOK, res.Code)
assert.Equal(t, "GET, OPTIONS", res.Result().Header.Get(hdrAllow))
assert.Equal(t, contentTypeJson, res.Result().Header.Get(hdrContentType))
assert.Contains(t, res.Body.String(), `"title":"API Documentation"`)

d.DocOptions.specData = []byte("null")
req, err = http.NewRequest(http.MethodOptions, "/", nil)
require.NoError(t, err)
res = httptest.NewRecorder()
router.ServeHTTP(res, req)
assert.Equal(t, http.StatusOK, res.Code)
assert.Equal(t, "GET, OPTIONS", res.Result().Header.Get(hdrAllow))
assert.Equal(t, contentTypeJson, res.Result().Header.Get(hdrContentType))
assert.Equal(t, "null", res.Body.String())

req, err = http.NewRequest(http.MethodOptions, "/subs", nil)
require.NoError(t, err)
res = httptest.NewRecorder()
router.ServeHTTP(res, req)
assert.Equal(t, http.StatusOK, res.Code)
assert.Equal(t, "GET, POST, OPTIONS", res.Result().Header.Get(hdrAllow))
assert.Equal(t, "", res.Body.String())
}

func TestDefinition_SetupRoutes_PathAutoOptions(t *testing.T) {
d := Definition{
AutoHeadMethods: true,
RootAutoOptionsMethod: true,
Paths: Paths{
"/subs": {
AutoOptionsMethod: true,
Methods: Methods{
http.MethodGet: {
Handler: func(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusConflict)
},
},
http.MethodPost: {
Handler: func(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusConflict)
},
},
},
},
},
}
router := chi.NewRouter()
err := d.SetupRoutes(router, nil)
assert.NoError(t, err)

req, err := http.NewRequest(http.MethodOptions, "/", nil)
require.NoError(t, err)
res := httptest.NewRecorder()
router.ServeHTTP(res, req)
assert.Equal(t, http.StatusOK, res.Code)
assert.Equal(t, "OPTIONS", res.Result().Header.Get(hdrAllow))

req, err = http.NewRequest(http.MethodOptions, "/subs", nil)
require.NoError(t, err)
res = httptest.NewRecorder()
router.ServeHTTP(res, req)
assert.Equal(t, http.StatusOK, res.Code)
assert.Equal(t, "GET, HEAD, POST, OPTIONS", res.Result().Header.Get(hdrAllow))
}

func TestDefinition_SetupRoutes_AuthMethodNotAllowed(t *testing.T) {
d := Definition{
AutoHeadMethods: true,
Expand Down
35 changes: 35 additions & 0 deletions options_payload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package chioas

// OptionsMethodPayloadBuilder is an interface that can be provided to Definition.OptionsMethodPayloadBuilder and
// allows automatically created OPTIONS methods to return a body payload
type OptionsMethodPayloadBuilder interface {
BuildPayload(path string, pathDef *Path, def *Definition) (data []byte, headers map[string]string)
}

// NewRootOptionsMethodPayloadBuilder provides an OptionsMethodPayloadBuilder that can be used for Definition.OptionsMethodPayloadBuilder
// and provides the OPTIONS payload body on API root as the OAS spec
func NewRootOptionsMethodPayloadBuilder() OptionsMethodPayloadBuilder {
return &rootOptionsPayload{}
}

type rootOptionsPayload struct{}

func (r *rootOptionsPayload) BuildPayload(path string, pathDef *Path, def *Definition) (data []byte, headers map[string]string) {
data = make([]byte, 0)
headers = map[string]string{}
if path == root {
if def.DocOptions.AsJson {
headers[hdrContentType] = contentTypeJson
} else {
headers[hdrContentType] = contentTypeYaml
}
if def.DocOptions.specData != nil {
data = def.DocOptions.specData
} else if def.DocOptions.AsJson {
data, _ = def.AsJson()
} else {
data, _ = def.AsYaml()
}
}
return
}
6 changes: 5 additions & 1 deletion path.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ type Path struct {
Additional Additional
// Comment is any comment(s) to appear in the OAS spec yaml
Comment string
// AutoOptionsMethod when set to true, automatically adds OPTIONS method for the path (and because Chioas knows the methods for each path can correctly set the Allow header)
//
// Note: If an OPTIONS method is already defined for the path then no OPTIONS method is automatically added
AutoOptionsMethod bool
}

type flatPath struct {
Expand All @@ -64,7 +68,7 @@ func (p flatPath) writeYaml(opts *DocOptions, autoHeads bool, autoOptions bool,
w.WritePathStart(context, template.Template(true)).
WriteComments(p.def.Comment)
if p.def.Methods != nil {
p.def.Methods.writeYaml(opts, autoHeads, autoOptions, template, p.getPathParams(), p.tag, w)
p.def.Methods.writeYaml(opts, autoHeads, autoOptions || p.def.AutoOptionsMethod, template, p.getPathParams(), p.tag, w)
}
writeExtensions(p.def.Extensions, w)
writeAdditional(p.def.Additional, p.def, w)
Expand Down

0 comments on commit 8fda997

Please sign in to comment.