Skip to content

Commit

Permalink
Add POST support (#66)
Browse files Browse the repository at this point in the history
This patch adds support for the `POST` HTTP method. The adapter will now
accept the `POST` method and call the `Add` method defined in the
`AddHandler` interface. An implementation of this interface will receive
the request body already parsed, and should add the object to the
collection and return it. For example, a simple implementation that
stores the object in a map in memory could look like this:

```go
func (h *MyHandler) Add(ctx context.Context, request *AddRequest) (respone *AddResponse, err error) {
	h.storeLock.Lock()
	defer h.storeLock.Unlock()
	h.storeMap[request.Variables[0]] = request.Object
	response := &AddResponse{
		Object: request.Object,
	}
	return
}
```

In order to make naming of the interfaces more consistent this patch
also renames `ObjectHandler` to `GetHandler` and `CollectionHandler` to
`ListHandler`. That way all interfaces are named after the operation
they represent.

Signed-off-by: Juan Hernandez <[email protected]>
  • Loading branch information
jhernand authored Feb 21, 2024
1 parent 702c678 commit 579913e
Show file tree
Hide file tree
Showing 4 changed files with 301 additions and 63 deletions.
157 changes: 134 additions & 23 deletions internal/service/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ import (
"errors"
"fmt"
"log/slog"
"mime"
"net/http"
"slices"
"strings"

"github.com/gorilla/mux"
jsoniter "github.com/json-iterator/go"
Expand All @@ -46,8 +48,9 @@ type AdapterBuilder struct {
type Adapter struct {
logger *slog.Logger
pathVariables []string
collectionHandler CollectionHandler
objectHandler ObjectHandler
listHandler ListHandler
getHandler GetHandler
addHandler AddHandler
includeFields []search.Path
excludeFields []search.Path
pathsParser *search.PathsParser
Expand Down Expand Up @@ -114,20 +117,21 @@ func (b *AdapterBuilder) Build() (result *Adapter, err error) {
}

// Check that the handler implements at least one of the handler interfaces:
collectionHandler, _ := b.handler.(CollectionHandler)
objectHandler, _ := b.handler.(ObjectHandler)
if collectionHandler == nil && objectHandler == nil {
listHandler, _ := b.handler.(ListHandler)
getHandler, _ := b.handler.(GetHandler)
addHandler, _ := b.handler.(AddHandler)
if listHandler == nil && getHandler == nil && addHandler == nil {
err = errors.New("handler doesn't implement any of the handler interfaces")
return
}

// If the handler implements the collection and object handler interfaces then we need to
// have at least one path variable because we use it to decide if the request is for the
// collection or for a specific object.
if collectionHandler != nil && objectHandler != nil && len(b.pathVariables) == 0 {
// If the handler implements the list and get handler interfaces then we need to have at
// least one path variable because we use it to decide if the request is for the collection
// or for a specific object.
if listHandler != nil && getHandler != nil && len(b.pathVariables) == 0 {
err = errors.New(
"at least one path variable is required when both the collection and " +
"object handlers are implemented",
"at least one path variable is required when both the list and " +
"get handlers are implemented",
)
return
}
Expand Down Expand Up @@ -193,8 +197,9 @@ func (b *AdapterBuilder) Build() (result *Adapter, err error) {
logger: b.logger,
pathVariables: variables,
pathsParser: pathsParser,
collectionHandler: collectionHandler,
objectHandler: objectHandler,
listHandler: listHandler,
getHandler: getHandler,
addHandler: addHandler,
includeFields: includePaths,
excludeFields: excludePaths,
selectorParser: selectorParser,
Expand All @@ -213,21 +218,50 @@ func (a *Adapter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
pathVariables[i] = muxVariables[name]
}

// Serve according to the HTTP method:
switch r.Method {
case http.MethodGet:
a.serveGetMethod(w, r, pathVariables)
case http.MethodPost:
a.servePostMethod(w, r, pathVariables)
default:
SendError(w, http.StatusMethodNotAllowed, "Method '%s' is not allowed", r.Method)
}
}

func (a *Adapter) serveGetMethod(w http.ResponseWriter, r *http.Request, pathVariables []string) {
// Check that we have a compatible handler:
if a.listHandler == nil && a.getHandler == nil {
SendError(w, http.StatusMethodNotAllowed, "Method '%s' is not allowed", r.Method)
return
}

// If the handler only implements one of the interfaces then we use it unconditionally.
// Otherwise we select the collection handler only if the first variable is empty.
switch {
case a.collectionHandler != nil && a.objectHandler == nil:
a.serveCollection(w, r, pathVariables)
case a.collectionHandler == nil && a.objectHandler != nil:
a.serveObject(w, r, pathVariables)
case a.listHandler != nil && a.getHandler == nil:
a.serveList(w, r, pathVariables)
case a.listHandler == nil && a.getHandler != nil:
a.serveGet(w, r, pathVariables)
case pathVariables[0] == "":
a.serveCollection(w, r, pathVariables[1:])
a.serveList(w, r, pathVariables[1:])
default:
a.serveObject(w, r, pathVariables)
a.serveGet(w, r, pathVariables)
}
}

func (a *Adapter) serveObject(w http.ResponseWriter, r *http.Request, pathVariables []string) {
func (a *Adapter) servePostMethod(w http.ResponseWriter, r *http.Request, pathVariables []string) {
// Check that we have a compatible handler:
if a.addHandler == nil {
SendError(w, http.StatusMethodNotAllowed, "Method '%s' is not allowed", r.Method)
return
}

// Call the handler:
a.serveAdd(w, r, pathVariables)
}

func (a *Adapter) serveGet(w http.ResponseWriter, r *http.Request, pathVariables []string) {
// Get the context:
ctx := r.Context()

Expand All @@ -244,7 +278,7 @@ func (a *Adapter) serveObject(w http.ResponseWriter, r *http.Request, pathVariab
}

// Call the handler:
response, err := a.objectHandler.Get(ctx, request)
response, err := a.getHandler.Get(ctx, request)
if err != nil {
a.logger.Error(
"Failed to get object",
Expand Down Expand Up @@ -288,7 +322,7 @@ func (a *Adapter) serveObject(w http.ResponseWriter, r *http.Request, pathVariab
a.sendObject(ctx, w, object)
}

func (a *Adapter) serveCollection(w http.ResponseWriter, r *http.Request, pathVariables []string) {
func (a *Adapter) serveList(w http.ResponseWriter, r *http.Request, pathVariables []string) {
// Get the context:
ctx := r.Context()

Expand All @@ -309,7 +343,7 @@ func (a *Adapter) serveCollection(w http.ResponseWriter, r *http.Request, pathVa
}

// Call the handler:
response, err := a.collectionHandler.List(ctx, request)
response, err := a.listHandler.List(ctx, request)
if err != nil {
a.logger.Error(
"Failed to get items",
Expand Down Expand Up @@ -338,6 +372,83 @@ func (a *Adapter) serveCollection(w http.ResponseWriter, r *http.Request, pathVa
a.sendItems(ctx, w, items)
}

func (a *Adapter) serveAdd(w http.ResponseWriter, r *http.Request, pathVariables []string) {
// Get the context:
ctx := r.Context()

// Check that the content type is acceptable:
contentType := r.Header.Get("Content-Type")
if contentType == "" {
a.logger.Error(
"Received empty content type header",
)
SendError(
w, http.StatusBadRequest,
"Content type is mandatory, use 'application/json'",
)
return
}
mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil {
a.logger.Error(
"Failed to parse content type",
slog.String("header", contentType),
slog.String("error", err.Error()),
)
SendError(w, http.StatusBadRequest, "Failed to parse content type '%s'", contentType)
}
if !strings.EqualFold(mediaType, "application/json") {
a.logger.Error(
"Unsupported content type",
slog.String("header", contentType),
slog.String("media", mediaType),
)
SendError(
w, http.StatusBadRequest,
"Content type '%s' isn't supported, use 'application/json'",
mediaType,
)
return
}

// Parse the request body:
decoder := a.jsonAPI.NewDecoder(r.Body)
var object data.Object
err = decoder.Decode(&object)
if err != nil {
a.logger.Error(
"Failed to decode input",
slog.String("error", err.Error()),
)
SendError(w, http.StatusBadRequest, "Failed to decode input")
return
}

// Create the request:
request := &AddRequest{
Variables: pathVariables,
Object: object,
}

// Call the handler:
response, err := a.addHandler.Add(ctx, request)
if err != nil {
a.logger.Error(
"Failed to add item",
"error", err,
)
SendError(
w,
http.StatusInternalServerError,
"Failed to add item",
)
return
}

// Send the added object:
a.sendObject(ctx, w, response.Object)
}

// extractSelector tries to extract the selector from the request. It return the selector and a
// flag indicating if it is okay to continue processing the request. When this flag is false the
// error response was already sent to the client, and request processing should stop.
Expand Down
43 changes: 43 additions & 0 deletions internal/service/adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"io"
"net/http"
"net/http/httptest"
"strings"

"github.com/gorilla/mux"
. "github.com/onsi/ginkgo/v2/dsl/core"
Expand Down Expand Up @@ -1040,6 +1041,48 @@ var _ = Describe("Adapter", func() {
})
})

Describe("Object creation", func() {
It("Creates object that doesn't exist", func() {
// Prepare the handler:
body := func(ctx context.Context,
request *AddRequest) (response *AddResponse, err error) {
response = &AddResponse{
Object: data.Object{
"myattr": "myvalue",
},
}
return
}
handler := NewMockAddHandler(ctrl)
handler.EXPECT().Add(gomock.Any(), gomock.Any()).DoAndReturn(body)

// Create the adapter:
adapter, err := NewAdapter().
SetLogger(logger).
SetPathVariables("id").
SetHandler(handler).
Build()
Expect(err).ToNot(HaveOccurred())
router := mux.NewRouter()
router.Handle("/mycollection/{id}", adapter)

// Send the request:
request := httptest.NewRequest(
http.MethodPost,
"/mycollection/123",
strings.NewReader("{}"),
)
request.Header.Set("Content-Type", "application/json")
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)

// Verify the response:
Expect(recorder.Body).To(MatchJSON(`{
"myattr": "myvalue"
}`))
})
})

DescribeTable(
"JSON generation",
func(items data.Stream, expected string) {
Expand Down
55 changes: 43 additions & 12 deletions internal/service/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ type ListResponse struct {
Items data.Stream
}

// ListHandler is the interface implemented by objects that know how to get list
// of items of a collection of objects.
type ListHandler interface {
List(ctx context.Context, request *ListRequest) (response *ListResponse, err error)
}

// GetRequest represents a request for an individual object.
type GetRequest struct {
// Variables contains the values of the path variables. For example, if the request path is
Expand All @@ -78,21 +84,46 @@ type GetResponse struct {
Object data.Object
}

// CollectionHandler is the interface implemented by objects that know how to handle requests to
// list the items of a collection of objects.
type CollectionHandler interface {
List(ctx context.Context, request *ListRequest) (response *ListResponse, err error)
// GetHandler is the interface implemented by objects that now how to get the details of an object.
type GetHandler interface {
Get(ctx context.Context, request *GetRequest) (response *GetResponse, err error)
}

// ObjectHandler is the interface implemented by objects that now how to handle requests to get the
// details of an object.
type ObjectHandler interface {
Get(ctx context.Context, request *GetRequest) (response *GetResponse, err error)
// AddRequest represents a request to create a new object inside a collection.
type AddRequest struct {
// Variables contains the values of the path variables. For example, if the request path is
// like this:
//
// /o2ims-infrastructureInventory/v1/resourcePools/123/resources/456
//
// Then it will contain '456' and '123'.
//
// These path variables are ordered from more specific to less specific, the opposite of
// what appears in the request path. This is intended to simplify things because most
// handlers will only be interested in the most specific identifier and therefore they
// can just use index zero.
Variables []string

// Object is the definition of the object.
Object data.Object
}

// AddResponse represents the response to the request to create a new object inside a collection.
type AddResponse struct {
// Object is the definition of the object that was created.
Object data.Object
}

// AddHandler is the interface implemented by objects that know how add items to a collection
// of objects.
type AddHandler interface {
Add(ctx context.Context, request *AddRequest) (response *AddResponse, err error)
}

// Handler is the interface implemented by objects that knows how to handle requests to list the
// items of a collection, as well as requests to get a specific object.
// Handler aggregates all the other specific handlers. This is intended for unit/ tests, where it
// is convenient to have a single mock that implements all the operations.
type Handler interface {
CollectionHandler
ObjectHandler
ListHandler
GetHandler
AddHandler
}
Loading

0 comments on commit 579913e

Please sign in to comment.