Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/weos 1127 - As a developer I should be able to get a list of content type #103

Merged
merged 15 commits into from
Feb 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -75,6 +80,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