Skip to content

Commit

Permalink
Merge pull request #1 from jvcoutinho/feature/result-package
Browse files Browse the repository at this point in the history
Result package
  • Loading branch information
jvcoutinho authored May 6, 2023
2 parents d4adc47 + 698e7aa commit b9f3fc9
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 32 deletions.
9 changes: 9 additions & 0 deletions result.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package lit

// Result of an HTTP request.
//
// Use the result package in order to produce Result implementations.
type Result interface {
// Write writes the result into the HTTP response managed by ctx.
Write(ctx *Context)
}
16 changes: 16 additions & 0 deletions result/result.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Package result contains functions that return responses for incoming requests.
package result

import (
"net/http"
)

// Ok responds the request with Status Code 200 (OK).
func Ok() *TerminalResponse {
return newTerminalResponse(http.StatusOK, nil)
}

// BadRequest responds the request with Status Code 400 (OK).
func BadRequest() *TerminalResponse {
return newTerminalResponse(http.StatusBadRequest, nil)
}
37 changes: 37 additions & 0 deletions result/terminal_response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package result

import (
"github.com/jvcoutinho/lit"
)

// TerminalResponse writes a single chunk of bytes into the response and closes the request.
type TerminalResponse struct {
statusCode int
body []byte
header map[string]string
}

func newTerminalResponse(statusCode int, body []byte) *TerminalResponse {
return &TerminalResponse{statusCode: statusCode, body: body}
}

func (r *TerminalResponse) AddHeader(key string, value string) {
if r.header == nil {
r.header = make(map[string]string)
}

r.header[key] = value
}

func (r *TerminalResponse) Write(ctx *lit.Context) {
responseWriter := ctx.ResponseWriter

header := responseWriter.Header()
for key, value := range r.header {
header.Set(key, value)
}

responseWriter.WriteHeader(r.statusCode)

_, _ = responseWriter.Write(r.body)
}
8 changes: 6 additions & 2 deletions router.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
const DefaultReadHeaderTimeout = 3 * time.Second

// HandleFunc is a function that handles requests.
type HandleFunc func(ctx *Context)
type HandleFunc func(ctx *Context) Result

// Router manages API routes.
//
Expand Down Expand Up @@ -62,7 +62,11 @@ func (r *Router) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
ctx := newContext(writer, request)
ctx.setArguments(match.Parameters)

handler(ctx)
result := handler(ctx)

if result != nil {
result.Write(ctx)
}
}

// Server this router uses for listening and serving requests.
Expand Down
68 changes: 38 additions & 30 deletions router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ func TestRouter_Handle(t *testing.T) {
Handler lit.HandleFunc
}

emptyHandler := func(ctx *lit.Context) lit.Result { return nil }

tests := []struct {
name string

Expand All @@ -31,124 +33,124 @@ func TestRouter_Handle(t *testing.T) {
{
name: "RouteDoesNotExist_SameMethod",
currentRoutes: []route{
{Pattern: "/users", Method: http.MethodGet, Handler: func(ctx *lit.Context) {}},
{Pattern: "/users", Method: http.MethodGet, Handler: emptyHandler},
},
routeToHandle: route{Pattern: "/users/owner", Method: http.MethodGet, Handler: func(ctx *lit.Context) {}},
routeToHandle: route{Pattern: "/users/owner", Method: http.MethodGet, Handler: emptyHandler},
panics: false,
expectedError: "",
},
{
name: "RouteDoesNotExist_SamePattern",
currentRoutes: []route{
{Pattern: "/users", Method: http.MethodGet, Handler: func(ctx *lit.Context) {}},
{Pattern: "/users", Method: http.MethodGet, Handler: emptyHandler},
},
routeToHandle: route{Pattern: "/users", Method: http.MethodPost, Handler: func(ctx *lit.Context) {}},
routeToHandle: route{Pattern: "/users", Method: http.MethodPost, Handler: emptyHandler},
panics: false,
expectedError: "",
},
{
name: "RouteDoesNotExist_Subpattern",
currentRoutes: []route{
{Pattern: "/users/:user_id", Method: http.MethodGet, Handler: func(ctx *lit.Context) {}},
{Pattern: "/users/:user_id", Method: http.MethodGet, Handler: emptyHandler},
},
routeToHandle: route{Pattern: "/users", Method: http.MethodGet, Handler: func(ctx *lit.Context) {}},
routeToHandle: route{Pattern: "/users", Method: http.MethodGet, Handler: emptyHandler},
panics: false,
expectedError: "",
},
{
name: "RouteDoesNotExist_Superpattern",
currentRoutes: []route{
{Pattern: "/users", Method: http.MethodGet, Handler: func(ctx *lit.Context) {}},
{Pattern: "/users", Method: http.MethodGet, Handler: emptyHandler},
},
routeToHandle: route{Pattern: "/users/:user_id", Method: http.MethodGet, Handler: func(ctx *lit.Context) {}},
routeToHandle: route{Pattern: "/users/:user_id", Method: http.MethodGet, Handler: emptyHandler},
panics: false,
expectedError: "",
},
{
name: "RouteAlreadyExists",
currentRoutes: []route{
{Pattern: "/users", Method: http.MethodGet, Handler: func(ctx *lit.Context) {}},
{Pattern: "/users", Method: http.MethodGet, Handler: emptyHandler},
},
routeToHandle: route{Pattern: "/users", Method: http.MethodGet, Handler: func(ctx *lit.Context) {}},
routeToHandle: route{Pattern: "/users", Method: http.MethodGet, Handler: emptyHandler},
panics: true,
expectedError: "GET /users has been already defined",
},
{
name: "RouteAlreadyExists_DifferentMethodCase",
currentRoutes: []route{
{Pattern: "/users", Method: http.MethodGet, Handler: func(ctx *lit.Context) {}},
{Pattern: "/users", Method: http.MethodGet, Handler: emptyHandler},
},
routeToHandle: route{Pattern: "/users", Method: "get", Handler: func(ctx *lit.Context) {}},
routeToHandle: route{Pattern: "/users", Method: "get", Handler: emptyHandler},
panics: true,
expectedError: "GET /users has been already defined",
},
{
name: "RouteAlreadyExists_MissingLeadingSlash",
currentRoutes: []route{
{Pattern: "/users", Method: http.MethodGet, Handler: func(ctx *lit.Context) {}},
{Pattern: "/users", Method: http.MethodGet, Handler: emptyHandler},
},
routeToHandle: route{Pattern: "users", Method: http.MethodGet, Handler: func(ctx *lit.Context) {}},
routeToHandle: route{Pattern: "users", Method: http.MethodGet, Handler: emptyHandler},
panics: true,
expectedError: "GET /users has been already defined",
},
{
name: "RouteAlreadyExists_PresentTrailingSlash",
currentRoutes: []route{
{Pattern: "/users", Method: http.MethodGet, Handler: func(ctx *lit.Context) {}},
{Pattern: "/users", Method: http.MethodGet, Handler: emptyHandler},
},
routeToHandle: route{Pattern: "users/", Method: http.MethodGet, Handler: func(ctx *lit.Context) {}},
routeToHandle: route{Pattern: "users/", Method: http.MethodGet, Handler: emptyHandler},
panics: true,
expectedError: "GET /users has been already defined",
},
{
name: "RouteAlreadyExists_MultiplePaths",
currentRoutes: []route{
{Pattern: "/users/owner", Method: http.MethodGet, Handler: func(ctx *lit.Context) {}},
{Pattern: "/users/owner", Method: http.MethodGet, Handler: emptyHandler},
},
routeToHandle: route{Pattern: "/users/owner", Method: http.MethodGet, Handler: func(ctx *lit.Context) {}},
routeToHandle: route{Pattern: "/users/owner", Method: http.MethodGet, Handler: emptyHandler},
panics: true,
expectedError: "GET /users/owner has been already defined",
},
{
name: "RouteAlreadyExists_MultiplePaths_MissingLeadingSlash",
currentRoutes: []route{
{Pattern: "/users/owner", Method: http.MethodGet, Handler: func(ctx *lit.Context) {}},
{Pattern: "/users/owner", Method: http.MethodGet, Handler: emptyHandler},
},
routeToHandle: route{Pattern: "users/owner", Method: http.MethodGet, Handler: func(ctx *lit.Context) {}},
routeToHandle: route{Pattern: "users/owner", Method: http.MethodGet, Handler: emptyHandler},
panics: true,
expectedError: "GET /users/owner has been already defined",
},
{
name: "RouteAlreadyExists_MultiplePaths_PresentTrailingSlash",
currentRoutes: []route{
{Pattern: "/users/owner", Method: http.MethodGet, Handler: func(ctx *lit.Context) {}},
{Pattern: "/users/owner", Method: http.MethodGet, Handler: emptyHandler},
},
routeToHandle: route{Pattern: "users/owner/", Method: http.MethodGet, Handler: func(ctx *lit.Context) {}},
routeToHandle: route{Pattern: "users/owner/", Method: http.MethodGet, Handler: emptyHandler},
panics: true,
expectedError: "GET /users/owner has been already defined",
},
{
name: "RouteAlreadyExists_MultiplePaths_DifferentArguments",
currentRoutes: []route{
{Pattern: "/users/:id", Method: http.MethodGet, Handler: func(ctx *lit.Context) {}},
{Pattern: "/users/:id", Method: http.MethodGet, Handler: emptyHandler},
},
routeToHandle: route{Pattern: "/users/:user_id", Method: http.MethodGet, Handler: func(ctx *lit.Context) {}},
routeToHandle: route{Pattern: "/users/:user_id", Method: http.MethodGet, Handler: emptyHandler},
panics: true,
expectedError: "GET /users/:user_id has been already defined",
},
{
name: "RouteAlreadyExists_MultiplePaths_DifferentArguments_ArgumentInMiddle",
currentRoutes: []route{
{Pattern: "/users/:user_id/items", Method: http.MethodGet, Handler: func(ctx *lit.Context) {}},
{Pattern: "/users/:user_id/items", Method: http.MethodGet, Handler: emptyHandler},
},
routeToHandle: route{Pattern: "/users/:id/items", Method: http.MethodGet, Handler: func(ctx *lit.Context) {}},
routeToHandle: route{Pattern: "/users/:id/items", Method: http.MethodGet, Handler: emptyHandler},
panics: true,
expectedError: "GET /users/:id/items has been already defined",
},
{
name: "InvalidRoute_DuplicateArgument",
currentRoutes: nil,
routeToHandle: route{Pattern: "/users/:id/items/:id", Method: http.MethodGet, Handler: func(ctx *lit.Context) {}},
routeToHandle: route{Pattern: "/users/:id/items/:id", Method: http.MethodGet, Handler: emptyHandler},
panics: true,
expectedError: "a pattern can not contain two arguments with the same name (:id)",
},
Expand Down Expand Up @@ -190,8 +192,12 @@ func TestRouter_ServeHTTP(t *testing.T) {
Handler lit.HandleFunc
}

okHandler := func(_ *lit.Context) {}
notFoundHandler := func(ctx *lit.Context) { http.NotFound(ctx.ResponseWriter, ctx.Request) }
okHandler := func(_ *lit.Context) lit.Result { return nil }
notFoundHandler := func(ctx *lit.Context) lit.Result {
http.NotFound(ctx.ResponseWriter, ctx.Request)

return nil
}

tests := []struct {
name string
Expand Down Expand Up @@ -352,10 +358,12 @@ func TestRouter_ServeHTTP(t *testing.T) {
router := lit.NewRouter()

for _, currentRoute := range test.currentRoutes {
router.Handle(currentRoute.Pattern, currentRoute.Method, func(ctx *lit.Context) {
router.Handle(currentRoute.Pattern, currentRoute.Method, func(ctx *lit.Context) lit.Result {
require.Equal(t, test.expectedArguments, ctx.URIArguments())

currentRoute.Handler(ctx)

return nil
})
}

Expand Down

0 comments on commit b9f3fc9

Please sign in to comment.