Skip to content

Commit

Permalink
Merge pull request #131 from wepala/dev
Browse files Browse the repository at this point in the history
Sprint 22
  • Loading branch information
akeemphilbert authored Mar 4, 2022
2 parents 2f085d6 + 723c832 commit 6fb7de3
Show file tree
Hide file tree
Showing 50 changed files with 6,943 additions and 183 deletions.
1 change: 1 addition & 0 deletions .github/workflows/dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ env:
SLACK_ICON: https://github.com/wepala.png?size=48
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_FOOTER: copyright 2022 Wepala
OAUTH_TEST_KEY: ${{ secrets.OAUTH_TEST_KEY }}

jobs:
build-api:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ env:
SLACK_ICON: https://github.com/wepala.png?size=48
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_FOOTER: copyright 2022 Wepala
OAUTH_TEST_KEY: ${{ secrets.OAUTH_TEST_KEY }}

jobs:
build-api:
Expand Down
78 changes: 75 additions & 3 deletions api.yaml
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ components:
url:
type: string
format: uri
x-unique: true
title:
type: string
description:
Expand All @@ -76,9 +77,9 @@ components:
type: string
nullable: true
enum:
- null
- unpublished
- published
- "null"
- one
- two
image:
type: string
format: byte
Expand Down Expand Up @@ -116,6 +117,55 @@ components:
type: string
format: date-time
paths:
/:
get:
operationId: Homepage
responses:
200:
description: Application Homepage
content:
text/html:
example: |
<html>
<head>
<title>Health</title>
</head>
<body>
This is a landing page
</body>
</html>
/page:
get:
summary: Test
parameters:
- in: header
name: Accept
schema:
type: string
responses:
'200':
description: Test Page
content:
text/html:
example: |
<html>
<head>
<title>Test Page</title>
</head>
<body>
This is a test page
</body>
</html>
schema:
type: string
application/json:
example:
blog:
value: {
"title": "API Testing",
"url": "www.example.com",
"email": "[email protected]"
}
/health:
summary: Health Check
get:
Expand All @@ -125,6 +175,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
64 changes: 64 additions & 0 deletions conf/tasks.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
openapi: 3.0.3
info:
title: Tasks API
description: Tasks API
version: 1.0.0
servers:
- url: 'http://localhost:8681'
x-weos-config:
database:
driver: sqlite3
database: e2e.db
components:
securitySchemes:
Auth0:
type: openIdConnect
openIdConnectUrl: https://samples.auth0.com/.well-known/openid-configuration
schemas:
Task:
type: object
properties:
title:
type: string
security:
- Auth0: ["email","name"]
paths:
/tasks:
get:
description: Get a list of tasks
operationId: getTasks
security:
- Auth0: []
responses:
200:
description: List of tasks
content:
application/json:
schema:
type: object
properties:
total:
type: integer
page:
type: integer
items:
$ref: "#/components/schemas/Task"

post:
description: Create task
operationId: createTask
requestBody:
description: Task
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Task"
application/x-www-form-urlencoded:
schema:
$ref: "#/components/schemas/Task"
responses:
201:
description: Created task


3 changes: 3 additions & 0 deletions context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ const FILTERS ContextKey = "_filters"
const SORTS ContextKey = "_sorts"
const PAYLOAD ContextKey = "_payload"
const SEQUENCE_NO string = "sequence_no"
const RESPONSE_PREFIX string = "_httpstatus"
const AUTHORIZATION string = "Authorization"
const ACCEPT string = "Accept"

//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
106 changes: 101 additions & 5 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 @@ -47,6 +48,7 @@ type RESTAPI struct {
eventStores map[string]model.EventRepository
commandDispatchers map[string]model.CommandDispatcher
projections map[string]projections.Projection
globalInitializers []GlobalInitializer
operationInitializers []OperationInitializer
registeredInitializers map[reflect.Value]int
prePathInitializers []PathInitializer
Expand Down Expand Up @@ -120,6 +122,20 @@ func (p *RESTAPI) RegisterEventStore(name string, repository model.EventReposito
p.eventStores[name] = repository
}

//RegisterGlobalInitializer add global initializer if it's not already there
func (p *RESTAPI) RegisterGlobalInitializer(initializer GlobalInitializer) {
if p.registeredInitializers == nil {
p.registeredInitializers = make(map[reflect.Value]int)
}
//only add initializer if it doesn't already exist
tpoint := reflect.ValueOf(initializer)
if _, ok := p.registeredInitializers[tpoint]; !ok {
p.globalInitializers = append(p.globalInitializers, initializer)
p.registeredInitializers[tpoint] = len(p.globalInitializers)
}

}

//RegisterOperationInitializer add operation initializer if it's not already there
func (p *RESTAPI) RegisterOperationInitializer(initializer OperationInitializer) {
if p.registeredInitializers == nil {
Expand Down Expand Up @@ -244,6 +260,11 @@ func (p *RESTAPI) GetProjection(name string) (projections.Projection, error) {
return nil, fmt.Errorf("projection '%s' not found", name)
}

//GetGlobalInitializers get global intializers in the order they were registered
func (p *RESTAPI) GetGlobalInitializers() []GlobalInitializer {
return p.globalInitializers
}

//GetOperationInitializers get operation intializers in the order they were registered
func (p *RESTAPI) GetOperationInitializers() []OperationInitializer {
return p.operationInitializers
Expand Down Expand Up @@ -272,6 +293,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(pathMiddleware []echo.MiddlewareFunc) 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, pathMiddleware...)

return nil
}

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

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

//register standard middleware
p.RegisterMiddleware("Context", Context)
p.RegisterMiddleware("OpenIDMiddleware", OpenIDMiddleware)
p.RegisterMiddleware("CreateMiddleware", CreateMiddleware)
p.RegisterMiddleware("CreateBatchMiddleware", CreateBatchMiddleware)
p.RegisterMiddleware("UpdateMiddleware", UpdateMiddleware)
p.RegisterMiddleware("ListMiddleware", ListMiddleware)
p.RegisterMiddleware("ViewMiddleware", ViewMiddleware)
p.RegisterMiddleware("DeleteMiddleware", DeleteMiddleware)
p.RegisterMiddleware("Recover", Recover)
//register standard global initializers
p.RegisterGlobalInitializer(Security)
p.RegisterMiddleware("DefaultResponseMiddleware", DefaultResponseMiddleware)
//register standard operation initializers
p.RegisterOperationInitializer(ContextInitializer)
p.RegisterOperationInitializer(EntityFactoryInitializer)
Expand All @@ -299,6 +352,7 @@ func (p *RESTAPI) Initialize(ctxt context.Context) error {
p.RegisterOperationInitializer(RouteInitializer)
//register standard post path initializers
p.RegisterPostPathInitializer(CORsInitializer)

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

Expand All @@ -323,6 +377,40 @@ func (p *RESTAPI) Initialize(ctxt context.Context) error {
}
p.RegisterProjection("Default", defaultProjection)
}

//This will check the enum types on run and output an error
for _, scheme := range p.Swagger.Components.Schemas {
for pName, prop := range scheme.Value.Properties {
if prop.Value.Enum != nil {
t := prop.Value.Type
for _, v := range prop.Value.Enum {
switch t {
case "string":
if reflect.TypeOf(v).String() != "string" {
return fmt.Errorf("Expected field: %s, of type %s, to have enum options of the same type", pName, t)
}
case "integer":
if reflect.TypeOf(v).String() != "float64" {
if v.(string) == "null" {
continue
} else {
return fmt.Errorf("Expected field: %s, of type %s, to have enum options of the same type", pName, t)
}
}
case "number":
if reflect.TypeOf(v).String() != "float64" {
if v.(string) == "null" {
continue
} else {
return fmt.Errorf("Expected field: %s, of type %s, to have enum options of the same type", pName, t)
}
}
}
}
}
}
}

//get the database schema
schemas = CreateSchema(ctxt, p.EchoInstance(), p.Swagger)
p.Schemas = schemas
Expand Down Expand Up @@ -417,12 +505,20 @@ func (p *RESTAPI) Initialize(ctxt context.Context) error {
//setup routes
knownActions := []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "TRACE", "CONNECT"}
var err error
globalContext := context.Background()
//run global initializers
for _, initializer := range p.GetGlobalInitializers() {
globalContext, err = initializer(globalContext, p, p.Swagger)
if err != nil {
return err
}
}
for path, pathData := range p.Swagger.Paths {
var methodsFound []string
pathContext := context.Background()

//run pre path initializers
for _, initializer := range p.GetPrePathInitializers() {
pathContext, err = initializer(pathContext, p, path, p.Swagger, pathData)
globalContext, err = initializer(globalContext, p, path, p.Swagger, pathData)
if err != nil {
return err
}
Expand All @@ -432,7 +528,7 @@ func (p *RESTAPI) Initialize(ctxt context.Context) error {
operationData := pathData.GetOperation(strings.ToUpper(method))
if operationData != nil {
methodsFound = append(methodsFound, strings.ToUpper(method))
operationContext := context.WithValue(context.Background(), weoscontext.SCHEMA_BUILDERS, schemas) //TODO fix this because this feels hacky
operationContext := context.WithValue(globalContext, weoscontext.SCHEMA_BUILDERS, schemas) //TODO fix this because this feels hacky
for _, initializer := range p.GetOperationInitializers() {
operationContext, err = initializer(operationContext, p, path, method, p.Swagger, pathData, operationData)
if err != nil {
Expand All @@ -443,9 +539,9 @@ func (p *RESTAPI) Initialize(ctxt context.Context) error {
}

//run post path initializers
pathContext = context.WithValue(pathContext, weoscontext.METHODS_FOUND, methodsFound)
globalContext = context.WithValue(globalContext, weoscontext.METHODS_FOUND, methodsFound)
for _, initializer := range p.GetPostPathInitializers() {
pathContext, err = initializer(pathContext, p, path, p.Swagger, pathData)
globalContext, err = initializer(globalContext, p, path, p.Swagger, pathData)
}
//output registered endpoints for debugging purposes
for _, route := range p.EchoInstance().Routes() {
Expand Down
Loading

0 comments on commit 6fb7de3

Please sign in to comment.