Skip to content

Commit

Permalink
Merge pull request #12 from go-spring-projects/feat-web-methods
Browse files Browse the repository at this point in the history
Add server route method `Get/Head/Post/Put/Patch/Delete/Connect/Options/Trace`
  • Loading branch information
limpo1989 authored Dec 6, 2023
2 parents c545a23 + fa9bece commit ab3b8c6
Show file tree
Hide file tree
Showing 17 changed files with 166 additions and 60 deletions.
52 changes: 39 additions & 13 deletions web/bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,12 @@ func (fn RendererFunc) Render(ctx *Context, err error, result interface{}) {
//
// func(ctx context.Context) R
//
// func(ctx context.Context) error
//
// func(ctx context.Context, req T) R
//
// func(ctx context.Context, req T) error
//
// func(ctx context.Context, req T) (R, error)
//
// func(writer http.ResponseWriter, request *http.Request)
Expand All @@ -53,18 +57,20 @@ func Bind(fn interface{}, render Renderer) http.HandlerFunc {

switch h := fn.(type) {
case http.HandlerFunc:
return h
return warpHandlerCtx(h)
case http.Handler:
return h.ServeHTTP
return warpHandlerCtx(h.ServeHTTP)
case func(http.ResponseWriter, *http.Request):
return h
return warpHandlerCtx(h)
default:
// valid func
if err := validMappingFunc(fnType); nil != err {
panic(err)
}
}

firstOutIsErrorType := 1 == fnType.NumOut() && utils.IsErrorType(fnType.Out(0))

return func(writer http.ResponseWriter, request *http.Request) {

// param of context
Expand Down Expand Up @@ -128,14 +134,15 @@ func Bind(fn interface{}, render Renderer) http.HandlerFunc {
// nothing
return
case 1:
// write response
result = returnValues[0].Interface()
if firstOutIsErrorType {
err, _ = returnValues[0].Interface().(error)
} else {
result = returnValues[0].Interface()
}
case 2:
// check error
result = returnValues[0].Interface()
if e, ok := returnValues[1].Interface().(error); ok && nil != e {
err = e
}
err, _ = returnValues[1].Interface().(error)
default:
panic("unreachable here")
}
Expand All @@ -149,7 +156,9 @@ func Bind(fn interface{}, render Renderer) http.HandlerFunc {
func validMappingFunc(fnType reflect.Type) error {
// func(ctx context.Context)
// func(ctx context.Context) R
// func(ctx context.Context) error
// func(ctx context.Context, req T) R
// func(ctx context.Context, req T) error
// func(ctx context.Context, req T) (R, error)
if !utils.IsFuncType(fnType) {
return fmt.Errorf("%s: not a func", fnType.String())
Expand All @@ -174,13 +183,30 @@ func validMappingFunc(fnType reflect.Type) error {
}
}

if 0 < fnType.NumOut() && utils.IsErrorType(fnType.Out(0)) {
return fmt.Errorf("%s: first output param type not be error", fnType.String())
}
switch fnType.NumOut() {
case 0: // nothing
case 1: // R | error
case 2: // (R, error)
if utils.IsErrorType(fnType.Out(0)) {
return fmt.Errorf("%s: first output param type not be error", fnType.String())
}

if 1 < fnType.NumOut() && !utils.IsErrorType(fnType.Out(1)) {
return fmt.Errorf("%s: second output type (%s) must a error", fnType.String(), fnType.Out(1).String())
if !utils.IsErrorType(fnType.Out(1)) {
return fmt.Errorf("%s: second output type (%s) must a error", fnType.String(), fnType.Out(1).String())
}
}

return nil
}

func warpHandlerCtx(handler http.HandlerFunc) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
webCtx := &Context{Writer: writer, Request: request}
handler.ServeHTTP(writer, requestWithCtx(request, webCtx))
}
}

func requestWithCtx(r *http.Request, webCtx *Context) *http.Request {
ctx := WithContext(r.Context(), webCtx)
return r.WithContext(ctx)
}
7 changes: 6 additions & 1 deletion web/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,11 @@ func (c *Context) SetCookie(name, value string, maxAge int, path, domain string,
// Render writes the response headers and calls render.Render to render data.
func (c *Context) Render(code int, render render.Renderer) error {
if code > 0 {
if len(c.Writer.Header().Get("Content-Type")) <= 0 {
if contentType := render.ContentType(); len(contentType) > 0 {
c.Writer.Header().Set("Content-Type", contentType)
}
}
c.Writer.WriteHeader(code)
}
return render.Render(c.Writer)
Expand All @@ -203,7 +208,7 @@ func (c *Context) String(code int, format string, args ...interface{}) error {

// Data writes some data into the body stream and updates the HTTP code.
func (c *Context) Data(code int, contentType string, data []byte) error {
return c.Render(code, render.BinaryRenderer{ContentType: contentType, Data: data})
return c.Render(code, render.BinaryRenderer{DataType: contentType, Data: data})
}

// JSON serializes the given struct as JSON into the response body.
Expand Down
15 changes: 7 additions & 8 deletions web/examples/greeting/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package main

import (
"context"
"fmt"
"log/slog"
"math/rand"
"mime/multipart"
Expand All @@ -36,10 +35,10 @@ type Greeting struct {
}

func (g *Greeting) OnInit(ctx context.Context) error {
g.Server.Bind("/greeting", g.Greeting)
g.Server.Bind("/health", g.Health)
g.Server.Bind("/user/register/{username}/{password}", g.Register)
g.Server.Bind("/user/password", g.UpdatePassword)
g.Server.Get("/greeting", g.Greeting)
g.Server.Get("/health", g.Health)
g.Server.Post("/user/register/{username}/{password}", g.Register)
g.Server.Post("/user/password", g.UpdatePassword)

g.Server.Use(func(handler http.Handler) http.Handler {

Expand All @@ -61,11 +60,11 @@ func (g *Greeting) Greeting(ctx context.Context) string {
return "greeting!!!"
}

func (g *Greeting) Health(ctx context.Context) (string, error) {
func (g *Greeting) Health(ctx context.Context) error {
if 0 == rand.Int()%2 {
return "", fmt.Errorf("health check failed")
return web.Error(400, "health check failed")
}
return time.Now().String(), nil
return nil
}

func (g *Greeting) Register(
Expand Down
19 changes: 10 additions & 9 deletions web/render/binary.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,19 @@ import (
)

type BinaryRenderer struct {
ContentType string
Data []byte
DataType string // Content-Type
Data []byte
}

func (b BinaryRenderer) Render(writer http.ResponseWriter) error {
if header := writer.Header(); len(header.Get("Content-Type")) == 0 {
contentType := "application/octet-stream"
if len(b.ContentType) > 0 {
contentType = b.ContentType
}
header.Set("Content-Type", contentType)
func (b BinaryRenderer) ContentType() string {
contentType := "application/octet-stream"
if len(b.DataType) > 0 {
contentType = b.DataType
}
return contentType
}

func (b BinaryRenderer) Render(writer http.ResponseWriter) error {
_, err := writer.Write(b.Data)
return err
}
5 changes: 3 additions & 2 deletions web/render/binary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ func TestBinaryRenderer(t *testing.T) {

w := httptest.NewRecorder()

err := BinaryRenderer{ContentType: "application/octet-stream", Data: data}.Render(w)
render := BinaryRenderer{DataType: "application/octet-stream", Data: data}
err := render.Render(w)
assert.Nil(t, err)

assert.Equal(t, w.Header().Get("Content-Type"), "application/octet-stream")
assert.Equal(t, render.ContentType(), "application/octet-stream")
assert.Equal(t, w.Body.Bytes(), data)
}
7 changes: 4 additions & 3 deletions web/render/html.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ type HTMLRenderer struct {
Data interface{}
}

func (h HTMLRenderer) ContentType() string {
return "text/html; charset=utf-8"
}

func (h HTMLRenderer) Render(writer http.ResponseWriter) error {
if header := writer.Header(); len(header.Get("Content-Type")) == 0 {
header.Set("Content-Type", "text/html; charset=utf-8")
}
if len(h.Name) > 0 {
return h.Template.ExecuteTemplate(writer, h.Name, h.Data)
}
Expand Down
2 changes: 1 addition & 1 deletion web/render/html_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@ func TestHTMLRenderer(t *testing.T) {
err := htmlRender.Render(w)

assert.Nil(t, err)
assert.Equal(t, w.Header().Get("Content-Type"), "text/html; charset=utf-8")
assert.Equal(t, htmlRender.ContentType(), "text/html; charset=utf-8")
assert.Equal(t, w.Body.String(), "Hello asdklajhdasdd")
}
7 changes: 4 additions & 3 deletions web/render/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ type JsonRenderer struct {
Data interface{}
}

func (j JsonRenderer) ContentType() string {
return "application/json; charset=utf-8"
}

func (j JsonRenderer) Render(writer http.ResponseWriter) error {
if header := writer.Header(); len(header.Get("Content-Type")) == 0 {
header.Set("Content-Type", "application/json; charset=utf-8")
}
encoder := json.NewEncoder(writer)
if len(j.Prefix) > 0 || len(j.Indent) > 0 {
encoder.SetIndent(j.Prefix, j.Indent)
Expand Down
5 changes: 3 additions & 2 deletions web/render/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ func TestJSONRenderer(t *testing.T) {

w := httptest.NewRecorder()

err := JsonRenderer{Data: data}.Render(w)
render := JsonRenderer{Data: data}
err := render.Render(w)
assert.Nil(t, err)

assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8")
assert.Equal(t, render.ContentType(), "application/json; charset=utf-8")
assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}\n")
}
4 changes: 4 additions & 0 deletions web/render/redirect.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ type RedirectRenderer struct {
Location string
}

func (r RedirectRenderer) ContentType() string {
return ""
}

func (r RedirectRenderer) Render(writer http.ResponseWriter) error {
if (r.Code < http.StatusMultipleChoices || r.Code > http.StatusPermanentRedirect) && r.Code != http.StatusCreated {
panic(fmt.Sprintf("Cannot redirect with status code %d", r.Code))
Expand Down
1 change: 1 addition & 0 deletions web/render/redirect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func TestRedirectRenderer(t *testing.T) {
w := httptest.NewRecorder()
err = data1.Render(w)
assert.Nil(t, err)
assert.Equal(t, data1.ContentType(), "")

data2 := RedirectRenderer{
Code: http.StatusOK,
Expand Down
5 changes: 4 additions & 1 deletion web/render/renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@

package render

import "net/http"
import (
"net/http"
)

// Renderer writes data with custom ContentType and headers.
type Renderer interface {
ContentType() string
Render(writer http.ResponseWriter) error
}
7 changes: 4 additions & 3 deletions web/render/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ type TextRenderer struct {
Args []interface{}
}

func (t TextRenderer) ContentType() string {
return "text/plain; charset=utf-8"
}

func (t TextRenderer) Render(writer http.ResponseWriter) error {
if header := writer.Header(); len(header.Get("Content-Type")) == 0 {
header.Set("Content-Type", "text/plain; charset=utf-8")
}
_, err := io.Copy(writer, strings.NewReader(fmt.Sprintf(t.Format, t.Args...)))
return err
}
8 changes: 5 additions & 3 deletions web/render/text_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ import (
func TestTextRenderer(t *testing.T) {
w := httptest.NewRecorder()

err := (TextRenderer{
render := TextRenderer{
Format: "hello %s %d",
Args: []any{"bob", 2},
}).Render(w)
}

err := render.Render(w)

assert.Nil(t, err)
assert.Equal(t, w.Header().Get("Content-Type"), "text/plain; charset=utf-8")
assert.Equal(t, render.ContentType(), "text/plain; charset=utf-8")
assert.Equal(t, w.Body.String(), "hello bob 2")
}
8 changes: 4 additions & 4 deletions web/render/xml.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ type XmlRenderer struct {
Data interface{}
}

func (x XmlRenderer) Render(writer http.ResponseWriter) error {
if header := writer.Header(); len(header.Get("Content-Type")) == 0 {
header.Set("Content-Type", "application/xml; charset=utf-8")
}
func (x XmlRenderer) ContentType() string {
return "application/xml; charset=utf-8"
}

func (x XmlRenderer) Render(writer http.ResponseWriter) error {
encoder := xml.NewEncoder(writer)
if len(x.Prefix) > 0 || len(x.Indent) > 0 {
encoder.Indent(x.Prefix, x.Indent)
Expand Down
5 changes: 3 additions & 2 deletions web/render/xml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ func TestXmlRenderer(t *testing.T) {
"foo": "bar",
}

err := (XmlRenderer{Data: data}).Render(w)
render := (XmlRenderer{Data: data})
err := render.Render(w)

assert.Nil(t, err)
assert.Equal(t, w.Header().Get("Content-Type"), "application/xml; charset=utf-8")
assert.Equal(t, render.ContentType(), "application/xml; charset=utf-8")
assert.Equal(t, w.Body.String(), "<map><foo>bar</foo></map>")
}
Loading

0 comments on commit ab3b8c6

Please sign in to comment.