Skip to content

Commit

Permalink
Merge pull request #103 from wepala/feature/WEOS-1127
Browse files Browse the repository at this point in the history
Feature/weos 1127 - As a developer I should be able to get a list of content type
  • Loading branch information
akeemphilbert committed Feb 21, 2022
2 parents 9ae3a0e + dff1a02 commit 2c280b2
Show file tree
Hide file tree
Showing 13 changed files with 261 additions and 32 deletions.
22 changes: 22 additions & 0 deletions api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,28 @@ paths:
description: Health Response
500:
description: API Internal Error
/api/json:
get:
operationId: Get API Details
x-controller: APIDiscovery
responses:
200:
description: API Details
content:
application/json:
schema:
type: string
/api/html:
get:
operationId: Get API Details
x-controller: APIDiscovery
responses:
200:
description: API Details
content:
application/html:
schema:
type: string
/blogs:
post:
operationId: Add Blog
Expand Down
1 change: 1 addition & 0 deletions context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const FILTERS ContextKey = "_filters"
const SORTS ContextKey = "_sorts"
const PAYLOAD ContextKey = "_payload"
const SEQUENCE_NO string = "sequence_no"
const RESPONSE_PREFIX string = "_httpstatus"

//Path initializers are run per path and can be used to configure routes that are not defined in the open api spec
const METHODS_FOUND ContextKey = "_methods_found"
Expand Down
33 changes: 33 additions & 0 deletions controllers/rest/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"strings"
"time"

"github.com/rakyll/statik/fs"
weoscontext "github.com/wepala/weos/context"
"github.com/wepala/weos/projections/dialects"
"gorm.io/driver/clickhouse"
Expand Down Expand Up @@ -272,6 +273,31 @@ func (p *RESTAPI) GetEntityFactories() map[string]model.EntityFactory {
return p.entityFactories
}

const SWAGGERUIENDPOINT = "/_discover/"
const SWAGGERJSONENDPOINT = "/_discover_json"

//RegisterSwaggerAPI creates default swagger api from binary
func (p *RESTAPI) RegisterDefaultSwaggerAPI() error {
statikFS, err := fs.New()
if err != nil {
return NewControllerError("Got an error formatting response", err, http.StatusInternalServerError)
}
static := http.FileServer(statikFS)
sh := http.StripPrefix(SWAGGERUIENDPOINT, static)
handler := echo.WrapHandler(sh)
p.e.GET(SWAGGERUIENDPOINT+"*", handler)

return nil
}

//RegisterDefaultSwaggerJson registers a default swagger json response
func (p *RESTAPI) RegisterDefaultSwaggerJSON() error {
p.e.GET(SWAGGERJSONENDPOINT, func(c echo.Context) error {
return c.JSON(http.StatusOK, p.Swagger)
})
return nil
}

//Initialize and setup configurations for RESTAPI
func (p *RESTAPI) Initialize(ctxt context.Context) error {
//register standard controllers
Expand All @@ -282,6 +308,8 @@ func (p *RESTAPI) Initialize(ctxt context.Context) error {
p.RegisterController("DeleteController", DeleteController)
p.RegisterController("HealthCheck", HealthCheck)
p.RegisterController("CreateBatchController", CreateBatchController)
p.RegisterController("APIDiscovery", APIDiscovery)

//register standard middleware
p.RegisterMiddleware("Context", Context)
p.RegisterMiddleware("CreateMiddleware", CreateMiddleware)
Expand All @@ -299,6 +327,11 @@ func (p *RESTAPI) Initialize(ctxt context.Context) error {
p.RegisterOperationInitializer(RouteInitializer)
//register standard post path initializers
p.RegisterPostPathInitializer(CORsInitializer)

//make default endpoints for returning swagger configuration to user
p.RegisterDefaultSwaggerAPI()
p.RegisterDefaultSwaggerJSON()

//these are the dynamic struct builders for the schemas in the OpenAPI
var schemas map[string]ds.Builder

Expand Down
35 changes: 32 additions & 3 deletions controllers/rest/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ package rest_test
import (
"bytes"
"encoding/json"
"github.com/wepala/weos/model"
"github.com/wepala/weos/projections"
"golang.org/x/net/context"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"

"github.com/wepala/weos/model"
"github.com/wepala/weos/projections"
"golang.org/x/net/context"

api "github.com/wepala/weos/controllers/rest"
)

Expand Down Expand Up @@ -403,3 +404,31 @@ func TestRESTAPI_DefaultProjectionRegisteredBefore(t *testing.T) {
t.Errorf("expected the projection to be the one that was set")
}
}

func TestRESTAPI_Initialize_DiscoveryAddedToGet(t *testing.T) {
os.Remove("test.db")
tapi, err := api.New("./fixtures/blog.yaml")
if err != nil {
t.Fatalf("un expected error loading spec '%s'", err)
}
err = tapi.Initialize(nil)
if err != nil {
t.Fatalf("un expected error loading spec '%s'", err)
}
e := tapi.EchoInstance()

found := false
method := "GET"
path := "/api"
middleware := "APIDiscovery"
routes := e.Routes()
for _, route := range routes {
if route.Method == method && route.Path == path && strings.Contains(route.Name, middleware) {
found = true
break
}
}
if !found {
t.Errorf("expected to find get path")
}
}
17 changes: 17 additions & 0 deletions controllers/rest/controller_standard.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/segmentio/ksuid"
weoscontext "github.com/wepala/weos/context"
"github.com/wepala/weos/model"
_ "github.com/wepala/weos/swaggerui"
"golang.org/x/net/context"
)

Expand Down Expand Up @@ -278,6 +279,22 @@ func BulkUpdate(app model.Service, spec *openapi3.Swagger, path *openapi3.PathIt
}
}

func APIDiscovery(api *RESTAPI, projection projections.Projection, commandDispatcher model.CommandDispatcher, eventSource model.EventRepository, entityFactory model.EntityFactory) echo.HandlerFunc {
return func(ctxt echo.Context) error {
newContext := ctxt.Request().Context()

//get content type expected for 200 response
responseType := newContext.Value(weoscontext.RESPONSE_PREFIX + strconv.Itoa(http.StatusOK))
if responseType == "application/json" {
return ctxt.JSON(http.StatusOK, api.Swagger)
} else if responseType == "application/html" {
return ctxt.Redirect(http.StatusPermanentRedirect, SWAGGERUIENDPOINT)
}

return NewControllerError("No response format chosen for a valid response", nil, http.StatusBadRequest)
}
}

func ViewMiddleware(api *RESTAPI, projection projections.Projection, commandDispatcher model.CommandDispatcher, eventSource model.EventRepository, entityFactory model.EntityFactory, path *openapi3.PathItem, operation *openapi3.Operation) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctxt echo.Context) error {
Expand Down
11 changes: 11 additions & 0 deletions controllers/rest/fixtures/blog.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,17 @@ paths:
description: Health Response
500:
description: API Internal Error
/api:
get:
operationId: Get API Details
x-controller: APIDiscovery
responses:
200:
description: API Details
content:
application/json:
schema:
type: string
/blogs:
parameters:
- in: header
Expand Down
21 changes: 19 additions & 2 deletions controllers/rest/middleware_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/wepala/weos/model"
"github.com/wepala/weos/projections"
"io/ioutil"
"net/http"
"net/textproto"
"net/url"
"strconv"
"strings"

"github.com/wepala/weos/model"
"github.com/wepala/weos/projections"

"github.com/getkin/kin-openapi/openapi3"
"github.com/labstack/echo/v4"
weosContext "github.com/wepala/weos/context"
Expand All @@ -39,6 +40,10 @@ func Context(api *RESTAPI, projection projections.Projection, commandDispatcher
cc, err = parseParams(c, cc, parameter, entityFactory)
}

//use the operation information to get the parameter values and add them to the context

cc, err = parseResponses(c, cc, operation)

//parse request body based on content type
var payload []byte
ct := c.Request().Header.Get("Content-Type")
Expand Down Expand Up @@ -77,6 +82,18 @@ func Context(api *RESTAPI, projection projections.Projection, commandDispatcher
}
}

//parseResponses gets the expected response for cases where different valid responses are possible
func parseResponses(c echo.Context, cc context.Context, operation *openapi3.Operation) (context.Context, error) {
for code, r := range operation.Responses {
if r.Value != nil {
for contentName, _ := range r.Value.Content {
cc = context.WithValue(cc, weosContext.RESPONSE_PREFIX+code, contentName)
}
}
}
return cc, nil
}

//parseParams uses the parameter type to determine where to pull the value from
func parseParams(c echo.Context, cc context.Context, parameter *openapi3.ParameterRef, entityFactory model.EntityFactory) (context.Context, error) {
if entityFactory == nil {
Expand Down
20 changes: 20 additions & 0 deletions controllers/rest/middleware_context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http/httptest"
"os"
"regexp"
"strconv"
"strings"
"testing"

Expand Down Expand Up @@ -489,4 +490,23 @@ func TestContext(t *testing.T) {
e.POST("/blogs", handler)
e.ServeHTTP(resp, req)
})

t.Run("check that resonse type is added to the context", func(t *testing.T) {
path := swagger.Paths.Find("/blogs")
mw := rest.Context(restApi, nil, nil, nil, entityFactory, path, path.Get)
handler := mw(func(ctxt echo.Context) error {
//check that certain parameters are in the context
cc := ctxt.Request().Context()
responseType := cc.Value(context.RESPONSE_PREFIX + strconv.Itoa(http.StatusOK))
if responseType != "application/json" {
t.Errorf("expected the response type to be '%s', got '%s'", "application/json", responseType)
}
return nil
})
e := echo.New()
resp := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/blogs", nil)
e.GET("/blogs", handler)
e.ServeHTTP(resp, req)
})
}
36 changes: 36 additions & 0 deletions end2end_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -617,8 +617,16 @@ func aHeaderWithValue(key, value string) error {
func aResponseShouldBeReturned(statusCode int) error {
//check resp first
if resp != nil && resp.StatusCode != statusCode {
if statusCode == http.StatusOK && resp.StatusCode > 300 && resp.StatusCode < 310 {
//redirected
return nil
}
return fmt.Errorf("expected the status code to be '%d', got '%d'", statusCode, resp.StatusCode)
} else if rec != nil && rec.Result().StatusCode != statusCode {
if statusCode == http.StatusOK && rec.Result().StatusCode > 300 && rec.Result().StatusCode < 310 {
//redirected
return nil
}
return fmt.Errorf("expected the status code to be '%d', got '%d'", statusCode, rec.Result().StatusCode)
}
return nil
Expand Down Expand Up @@ -1426,6 +1434,31 @@ func theTotalNoEventsAndProcessedAndFailuresShouldBeReturned() error {
return nil
}

func theApiAsJsonShouldBeShown() error {
contentEntity := map[string]interface{}{}
err := json.NewDecoder(rec.Body).Decode(&contentEntity)

if err != nil {
return err
}

if len(contentEntity) == 0 {
return fmt.Errorf("expected a response to be returned")
}
if _, ok := contentEntity["openapi"]; !ok {
return fmt.Errorf("expected the content entity to have a content 'openapi'")
}
return nil
}

func theSwaggerUiShouldBeShown() error {
url := rec.HeaderMap.Get("Location")
if url != api.SWAGGERUIENDPOINT {
return fmt.Errorf("the html result should have been returned")
}
return nil
}

func InitializeScenario(ctx *godog.ScenarioContext) {
ctx.Before(reset)
ctx.After(func(ctx context.Context, sc *godog.Scenario, err error) (context.Context, error) {
Expand Down Expand Up @@ -1504,6 +1537,9 @@ func InitializeScenario(ctx *godog.ScenarioContext) {
ctx.Step(`^Sojourner" deletes the "([^"]*)" table$`, sojournerDeletesTheTable)
ctx.Step(`^the "([^"]*)" table should be populated with$`, theTableShouldBePopulatedWith)
ctx.Step(`^the total no\. events and processed and failures should be returned$`, theTotalNoEventsAndProcessedAndFailuresShouldBeReturned)
ctx.Step(`^the api as json should be shown$`, theApiAsJsonShouldBeShown)
ctx.Step(`^the swagger ui should be shown$`, theSwaggerUiShouldBeShown)

}

func TestBDD(t *testing.T) {
Expand Down
Loading

0 comments on commit 2c280b2

Please sign in to comment.