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

Add multitype and runtime validations #16

Merged
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
6 changes: 3 additions & 3 deletions .github/workflows/lint-test-cover.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ jobs:
- name: Install Golang
uses: actions/setup-go@v3
with:
go-version: 1.18
go-version: 1.20.4
check-latest: true

- name: Print Go Version
run: go version

- name: Cache go modules
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
~/.cache/go-build
Expand All @@ -46,7 +46,7 @@ jobs:

- name: Test and produce coverage profile
id: coverage
run: echo "::set-output name=total::$(make test-coverage)"
run: echo "total=$(make test-coverage)" >> $GITHUB_OUTPUT

- name: Update README.md with coverage
run: |
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.DS_Store

# Jet Brains
.idea

Expand Down
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ COVERAGE_PROFILE_FILE := profile.cov
COVERAGE_HTML_FILE := coverage.html
SHELL := bash -eo pipefail -c


$(COVERAGE_PROFILE_FILE): $(shell find router examples)
@go test -v -race -failfast \
-coverprofile=$@ \
Expand All @@ -17,13 +16,18 @@ test-coverage: $(COVERAGE_PROFILE_FILE)
$(COVERAGE_HTML_FILE): $(COVERAGE_PROFILE_FILE)
@go tool cover -html=$(COVERAGE_PROFILE_FILE) -o $(COVERAGE_HTML_FILE)

FILE ?= file0

.PHONY: show-coverage
show-coverage: $(COVERAGE_HTML_FILE)
$(eval ANCHOR:=$(shell cat $(COVERAGE_HTML_FILE) | grep -E '<option value=".*">.*</option>' | grep $(FILE) | sed -n 's/^.*value="\(.*\)".*$$/\1/p'))
@sed -E -i '' 's/select\("file[0-9]+"\);/select("$(ANCHOR)");/g' $(COVERAGE_HTML_FILE)
@open $(COVERAGE_HTML_FILE)


.PHONY: test
test:
@go test -race -failfast ./...
@go test -race ./...

.PHONY: clean-coverage
clean-coverage:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Cellotape - Beta - OpenAPI Router for Go

![](https://badgen.net/badge/coverage/25/green?icon=github)
![99.2%](https://badgen.net/badge/coverage/99.2%25/green?icon=github)

Cellotape requires Go 1.18 or above.

Expand Down
1 change: 0 additions & 1 deletion examples/hello_world_example/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ func TestHelloWorldExample(t *testing.T) {
handler, err := router.NewOpenAPIRouter(spec).
WithOperation("greet", api.GreetOperationHandler).
AsHandler()
fmt.Println(err)
require.NoError(t, err)

ts := httptest.NewServer(handler)
Expand Down
2 changes: 1 addition & 1 deletion examples/todo_list_app_example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func mainHandler() error {
return err
}
port := 8080
fmt.Printf("Starting HTTP server on port %d\n", port)
log.Printf("Starting HTTP server on port %d\n", port)
if err = http.ListenAndServe(fmt.Sprintf(":%d", port), handler); err != nil {
return err
}
Expand Down
63 changes: 61 additions & 2 deletions examples/todo_list_app_example/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func TestGetAllTasks(t *testing.T) {
assert.JSONEq(t, `{
"results": [],
"page": 0,
"pageSize": 0,
"pageSize": 10,
"isLast": true
}`, string(response))
}
Expand All @@ -52,7 +52,7 @@ func TestCreateNewTaskAndGetIt(t *testing.T) {
req, err := http.NewRequest("POST", fmt.Sprintf("%s/tasks", ts.URL), request)
require.NoError(t, err)
req.Header.Set("Authorization", "Bearer secret")
//req.Header.Set("Content-Type", "application/json")
req.Header.Set("Content-Type", "application/json")
client := http.Client{}
resp, err := client.Do(req)
require.NoError(t, err)
Expand All @@ -77,6 +77,65 @@ func TestCreateNewTaskAndGetIt(t *testing.T) {
assert.JSONEq(t, taskJson, string(data))
}

func TestRequestQueryParamViolateSchemaValidations(t *testing.T) {
ts := initAPI(t)
defer ts.Close()
req, err := http.NewRequest("GET", fmt.Sprintf("%s/tasks?pageSize=30", ts.URL), nil)
require.NoError(t, err)
req.Header.Set("Authorization", "Bearer secret")
client := http.Client{}
resp, err := client.Do(req)
require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode)
response, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t,
`invalid request query param. parameter "pageSize" in query has an error: number must be at most 20`,
string(response))
}

func TestRequestPathParamViolateSchemaValidations(t *testing.T) {
ts := initAPI(t)
defer ts.Close()
req, err := http.NewRequest("GET", fmt.Sprintf("%s/tasks/123", ts.URL), nil)
require.NoError(t, err)
req.Header.Set("Authorization", "Bearer secret")
client := http.Client{}
resp, err := client.Do(req)
require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode)
response, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t,
`invalid request path param. parameter "id" in path has an error: minimum string length is 36`,
string(response))
}

func TestRequestBodyViolateSchemaValidations(t *testing.T) {
ts := initAPI(t)
defer ts.Close()
taskJson := `{
"summary": "code first approach",
"description": "add support for code first approach",
"status": "archived"
}`
request := bytes.NewBufferString(taskJson)

req, err := http.NewRequest("POST", fmt.Sprintf("%s/tasks", ts.URL), request)
require.NoError(t, err)
req.Header.Set("Authorization", "Bearer secret")
req.Header.Set("Content-Type", "application/json")
client := http.Client{}
resp, err := client.Do(req)
require.NoError(t, err)
assert.Equal(t, 400, resp.StatusCode)
response, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t,
`invalid request body. request body has an error: doesn't match schema #/components/schemas/Task: value "archived" is not one of the allowed values`,
string(response))
}

func initAPI(t *testing.T) *httptest.Server {
spec, err := router.NewSpecFromData(specData)
require.NoError(t, err)
Expand Down
3 changes: 2 additions & 1 deletion examples/todo_list_app_example/middlewares/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import (

"github.com/piiano/cellotape/examples/todo_list_app_example/models"
r "github.com/piiano/cellotape/router"
"github.com/piiano/cellotape/router/utils"
)

const token = "secret"

var authHeader = fmt.Sprintf("Bearer %s", token)

var AuthMiddleware = r.NewHandler(func(c *r.Context, req r.Request[r.Nil, r.Nil, r.Nil]) (r.Response[authResponses], error) {
var AuthMiddleware = r.NewHandler(func(c *r.Context, req r.Request[utils.Nil, utils.Nil, utils.Nil]) (r.Response[authResponses], error) {
if req.Headers.Get("Authorization") != authHeader {
return r.SendJSON(authResponses{Unauthorized: models.HttpError{
Error: "Unauthorized",
Expand Down
8 changes: 4 additions & 4 deletions examples/todo_list_app_example/middlewares/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ import (
r "github.com/piiano/cellotape/router"
)

var LoggerMiddleware = r.NewHandler(loggerHandler)
var LoggerMiddleware = r.RawHandler(loggerHandler)

func loggerHandler(c *r.Context, request r.Request[r.Nil, r.Nil, r.Nil]) (r.Response[any], error) {
func loggerHandler(c *r.Context) error {
start := time.Now()
response, err := c.Next()
duration := time.Since(start)
if err != nil {
log.Printf("[ERROR] error occurred: %s. - %s - [%s] %s\n", err.Error(), duration, c.Request.Method, c.Request.URL.Path)
return r.Response[any]{}, nil
return err
}
log.Printf("[INFO] (status %d | %d bytes | %s) - [%s] %s\n", response.Status, len(response.Body), duration, c.Request.Method, c.Request.URL.Path)
return r.Response[any]{}, nil
return nil
}
3 changes: 2 additions & 1 deletion examples/todo_list_app_example/middlewares/powered_by.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ package middlewares

import (
r "github.com/piiano/cellotape/router"
"github.com/piiano/cellotape/router/utils"
)

var PoweredByMiddleware = r.NewHandler(poweredByHandler)

func poweredByHandler(c *r.Context, _ r.Request[r.Nil, r.Nil, r.Nil]) (r.Response[any], error) {
func poweredByHandler(c *r.Context, _ r.Request[utils.Nil, utils.Nil, utils.Nil]) (r.Response[any], error) {
c.Writer.Header().Add("X-Powered-By", "Piiano OpenAPI Router")
_, err := c.Next()
return r.Response[any]{}, err
Expand Down
2 changes: 2 additions & 0 deletions examples/todo_list_app_example/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ components:
Id:
type: string
format: uuid
minLength: 36
maxLength: 36
nullable: false
Identifiable:
type: object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import (
m "github.com/piiano/cellotape/examples/todo_list_app_example/models"
"github.com/piiano/cellotape/examples/todo_list_app_example/services"
r "github.com/piiano/cellotape/router"
"github.com/piiano/cellotape/router/utils"
)

func createNewTaskOperation(tasks services.TasksService) r.Handler {
return r.NewHandler(func(c *r.Context, request r.Request[m.Task, r.Nil, r.Nil]) (r.Response[createNewTaskResponses], error) {
return r.NewHandler(func(c *r.Context, request r.Request[m.Task, utils.Nil, utils.Nil]) (r.Response[createNewTaskResponses], error) {
id := tasks.CreateTask(request.Body)
return r.SendOKJSON(createNewTaskResponses{OK: m.Identifiable{ID: id}}), nil
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import (
m "github.com/piiano/cellotape/examples/todo_list_app_example/models"
"github.com/piiano/cellotape/examples/todo_list_app_example/services"
r "github.com/piiano/cellotape/router"
"github.com/piiano/cellotape/router/utils"
)

func deleteTaskByIDOperation(tasks services.TasksService) r.Handler {
return r.NewHandler(func(_ *r.Context, request r.Request[r.Nil, idPathParam, r.Nil]) (r.Response[deleteTaskByIDResponses], error) {
return r.NewHandler(func(_ *r.Context, request r.Request[utils.Nil, idPathParam, utils.Nil]) (r.Response[deleteTaskByIDResponses], error) {
id, err := uuid.Parse(request.PathParams.ID)
if err != nil {
return r.SendJSON(deleteTaskByIDResponses{
Expand All @@ -35,7 +36,7 @@ func deleteTaskByIDOperation(tasks services.TasksService) r.Handler {
}

type deleteTaskByIDResponses struct {
NoContent r.Nil `status:"204"`
NoContent utils.Nil `status:"204"`
BadRequest m.HttpError `status:"400"`
Gone m.HttpError `status:"410"`
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import (
m "github.com/piiano/cellotape/examples/todo_list_app_example/models"
"github.com/piiano/cellotape/examples/todo_list_app_example/services"
r "github.com/piiano/cellotape/router"
"github.com/piiano/cellotape/router/utils"
)

func getTaskByIDOperation(tasks services.TasksService) r.Handler {
return r.NewHandler(func(_ *r.Context, request r.Request[r.Nil, idPathParam, r.Nil]) (r.Response[getTaskByIDResponses], error) {
return r.NewHandler(func(_ *r.Context, request r.Request[utils.Nil, idPathParam, utils.Nil]) (r.Response[getTaskByIDResponses], error) {
id, err := uuid.Parse(request.PathParams.ID)
if err != nil {
return r.SendJSON(getTaskByIDResponses{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import (
m "github.com/piiano/cellotape/examples/todo_list_app_example/models"
"github.com/piiano/cellotape/examples/todo_list_app_example/services"
r "github.com/piiano/cellotape/router"
"github.com/piiano/cellotape/router/utils"
)

func getTasksPageOperation(tasks services.TasksService) r.Handler {
return r.NewHandler(func(_ *r.Context, request r.Request[r.Nil, r.Nil, paginationQueryParams]) (r.Response[getTasksPageResponses], error) {
return r.NewHandler(func(_ *r.Context, request r.Request[utils.Nil, utils.Nil, paginationQueryParams]) (r.Response[getTasksPageResponses], error) {
tasksPage := tasks.GetTasksPage(request.QueryParams.Page, request.QueryParams.PageSize)
return r.SendOKJSON(getTasksPageResponses{OK: tasksPage}, http.Header{"Cache-Control": {"max-age=10"}}), nil
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import (
m "github.com/piiano/cellotape/examples/todo_list_app_example/models"
"github.com/piiano/cellotape/examples/todo_list_app_example/services"
r "github.com/piiano/cellotape/router"
"github.com/piiano/cellotape/router/utils"
)

func updateTaskByIDOperation(tasks services.TasksService) r.Handler {
return r.NewHandler(func(_ *r.Context, request r.Request[m.Task, idPathParam, r.Nil]) (r.Response[updateTaskByIDResponses], error) {
return r.NewHandler(func(_ *r.Context, request r.Request[m.Task, idPathParam, utils.Nil]) (r.Response[updateTaskByIDResponses], error) {
id, err := uuid.Parse(request.PathParams.ID)
if err != nil {
return r.SendJSON(updateTaskByIDResponses{
Expand All @@ -35,7 +36,7 @@ func updateTaskByIDOperation(tasks services.TasksService) r.Handler {
}

type updateTaskByIDResponses struct {
NoContent r.Nil `status:"204"`
NoContent utils.Nil `status:"204"`
BadRequest m.HttpError `status:"400"`
NotFound m.HttpError `status:"404"`
}
14 changes: 6 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
module github.com/piiano/cellotape

go 1.18
go 1.20

retract (
v1.0.0 // Published accidentally.
v2.0.0 // Published accidentally.
)
retract v1.0.0 // Published accidentally.

require (
github.com/getkin/kin-openapi v0.94.0
github.com/getkin/kin-openapi v0.112.0
github.com/gin-gonic/gin v1.7.7
github.com/google/uuid v1.3.0
github.com/invopop/jsonschema v0.4.0
github.com/julienschmidt/httprouter v1.3.0
github.com/stretchr/testify v1.7.1
github.com/stretchr/testify v1.8.1
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/swag v0.19.5 // indirect
github.com/go-playground/locales v0.14.0 // indirect
Expand All @@ -27,12 +23,14 @@ require (
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.7 // indirect
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect
github.com/invopop/yaml v0.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect
Expand Down
Loading