Skip to content

Commit

Permalink
Add RPC*
Browse files Browse the repository at this point in the history
  • Loading branch information
googollee committed Apr 16, 2024
1 parent 8411f6a commit 11e61c9
Show file tree
Hide file tree
Showing 7 changed files with 287 additions and 56 deletions.
1 change: 0 additions & 1 deletion buildtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ var errRegisterContextCall = errors.New("should call Context.Endpoint() in the b

type buildtimeEndpoint struct {
endpoint *Endpoint
err BindErrors
}

func (b *buildtimeEndpoint) BindPath(key string, v any) EndpointBuilder {
Expand Down
12 changes: 10 additions & 2 deletions codecs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package espresso
import (
"context"
"encoding/json"
"fmt"
"io"
"mime"

Expand Down Expand Up @@ -42,12 +43,19 @@ func NewCodecs(codec ...Codec) *Codecs {
}

func (c *Codecs) DecodeRequest(ctx Context, v any) error {
return c.Request(ctx).Decode(ctx, ctx.Request().Body, v)
codec := c.Request(ctx)
if err := codec.Decode(ctx, ctx.Request().Body, v); err != nil {
return fmt.Errorf("decode with codec(%s) error: %w", codec.Mime(), err)
}
return nil
}

func (c *Codecs) EncodeResponse(ctx Context, v any) error {
codec := c.Response(ctx)
return codec.Encode(ctx, ctx.ResponseWriter(), v)
if err := codec.Encode(ctx, ctx.ResponseWriter(), v); err != nil {
return fmt.Errorf("encode with codec(%s) error: %w", codec.Mime(), err)
}
return nil
}

func (c *Codecs) Request(ctx Context) Codec {
Expand Down
18 changes: 11 additions & 7 deletions endpoint.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
package espresso

import "reflect"

type EndpointBuilder interface {
BindPath(key string, v any) EndpointBuilder
End() BindErrors
}

type Endpoint struct {
Method string
Path string
PathParams map[string]BindParam
QueryParams map[string]BindParam
FormParams map[string]BindParam
HeadParams map[string]BindParam
ChainFuncs []HandleFunc
Method string
Path string
PathParams map[string]BindParam
QueryParams map[string]BindParam
FormParams map[string]BindParam
HeadParams map[string]BindParam
RequestType reflect.Type
ResponseType reflect.Type
ChainFuncs []HandleFunc
}

func newEndpoint() *Endpoint {
Expand Down
209 changes: 163 additions & 46 deletions examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,102 @@ import (
"github.com/googollee/go-espresso"
)

func ProvideLogger(ctx context.Context) (*slog.Logger, error) {
removeTime := func(groups []string, a slog.Attr) slog.Attr {
// Remove time from the output for predictable test output.
if a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
}

opt := slog.HandlerOptions{
ReplaceAttr: removeTime,
}
return slog.New(slog.NewTextHandler(os.Stdout, &opt)), nil
}

type Book struct {
ID int `json:"id"`
Title string `json:"title"`
}

type Books map[int]Book

func (books Books) GetBookHTTP(ctx espresso.Context) error {
var id int
if err := ctx.Endpoint(http.MethodGet, "/book/{id}").
BindPath("id", &id).
End(); err != nil {
return err
}

book, ok := books[id]
if !ok {
return espresso.Error(http.StatusNotFound, fmt.Errorf("not found"))
}

if err := espresso.CodecsModule.Value(ctx).EncodeResponse(ctx, &book); err != nil {
return err
}

return nil
}

func (books Books) CreateBookHTTP(ctx espresso.Context) error {
if err := ctx.Endpoint(http.MethodPost, "/book").
End(); err != nil {
return err
}

codecs := espresso.CodecsModule.Value(ctx)

var book Book
if err := codecs.DecodeRequest(ctx, &book); err != nil {
return espresso.Error(http.StatusBadRequest, err)
}

book.ID = len(books)
books[book.ID] = book

if err := codecs.EncodeResponse(ctx, &book); err != nil {
return err
}

return nil
}

func (books Books) GetBookRPC(ctx espresso.Context) (*Book, error) {
var id int
if err := ctx.Endpoint(http.MethodGet, "/book/{id}").
BindPath("id", &id).
End(); err != nil {
return nil, err
}

book, ok := books[id]
if !ok {
return nil, espresso.Error(http.StatusNotFound, fmt.Errorf("not found"))
}

return &book, nil

}

func (books Books) CreateBookRPC(ctx espresso.Context, book *Book) (*Book, error) {
if err := ctx.Endpoint(http.MethodPost, "/book").
End(); err != nil {
return nil, err
}

book.ID = len(books)
books[book.ID] = *book

return book, nil
}

func ExampleEspresso() {
books := make(map[int]Book)
books := make(Books)
books[1] = Book{
ID: 1,
Title: "The Espresso Book",
Expand All @@ -31,68 +120,96 @@ func ExampleEspresso() {

espo := espresso.New()
// Log to stdout for Output
espo.AddModule(espresso.LogModule.ProvideWithFunc(func(ctx context.Context) (*slog.Logger, error) {
return slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// Remove time from the output for predictable test output.
if a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
},
})), nil
}))
espo.AddModule(espresso.LogModule.ProvideWithFunc(ProvideLogger))
espo.AddModule(espresso.ProvideCodecs)

espo.HandleFunc(func(ctx espresso.Context) error {
var id int
if err := ctx.Endpoint(http.MethodGet, "/book/{id}").
BindPath("id", &id).
End(); err != nil {
return err
router := espo.WithPrefix("/http")
router.HandleFunc(books.GetBookHTTP)
router.HandleFunc(books.CreateBookHTTP)

svr := httptest.NewServer(espo)
defer svr.Close()

func() {
var book Book
resp, err := http.Get(svr.URL + "/http/book/1")
if err != nil {
panic(err)
}
defer resp.Body.Close()

book, ok := books[id]
if !ok {
return espresso.Error(http.StatusNotFound, fmt.Errorf("not found"))
if resp.StatusCode != http.StatusOK {
panic(resp.Status)
}

if err := espresso.CodecsModule.Value(ctx).EncodeResponse(ctx, &book); err != nil {
return err
if err := json.NewDecoder(resp.Body).Decode(&book); err != nil {
panic(err)
}

return nil
})
fmt.Println("Book 1 title:", book.Title)
}()

espo.HandleFunc(func(ctx espresso.Context) error {
if err := ctx.Endpoint(http.MethodPost, "/book/").
End(); err != nil {
return err
}
func() {
arg := Book{Title: "The New Book"}

codecs := espresso.CodecsModule.Value(ctx)
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(&arg); err != nil {
panic(err)
}

var book Book
if err := codecs.DecodeRequest(ctx, &book); err != nil {
return espresso.Error(http.StatusBadRequest, err)
resp, err := http.Post(svr.URL+"/http/book", "application/json", &buf)
if err != nil {
panic(err)
}
defer resp.Body.Close()

book.ID = len(books)
books[book.ID] = book
if resp.StatusCode != http.StatusOK {
panic(resp.Status)
}

if err := codecs.EncodeResponse(ctx, &book); err != nil {
return err
var ret Book
if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil {
panic(err)
}

return nil
})
fmt.Println("The New Book id:", ret.ID)
}()

// Output:
// level=INFO msg="receive http" method=GET path=/http/book/1
// level=INFO msg="finish http" method=GET path=/http/book/1
// Book 1 title: The Espresso Book
// level=INFO msg="receive http" method=POST path=/http/book
// level=INFO msg="finish http" method=POST path=/http/book
// The New Book id: 2
}

func ExampleEspresso_rpc() {
books := make(Books)
books[1] = Book{
ID: 1,
Title: "The Espresso Book",
}
books[2] = Book{
ID: 2,
Title: "The Second Book",
}

espo := espresso.New()
// Log to stdout for Output
espo.AddModule(espresso.LogModule.ProvideWithFunc(ProvideLogger))
espo.AddModule(espresso.ProvideCodecs)

router := espo.WithPrefix("/rpc")
router.HandleFunc(espresso.RPCRetrive(books.GetBookRPC))
router.HandleFunc(espresso.RPC(books.CreateBookRPC))

svr := httptest.NewServer(espo)
defer svr.Close()

func() {
var book Book
resp, err := http.Get(svr.URL + "/book/1")
resp, err := http.Get(svr.URL + "/rpc/book/1")
if err != nil {
panic(err)
}
Expand All @@ -117,7 +234,7 @@ func ExampleEspresso() {
panic(err)
}

resp, err := http.Post(svr.URL+"/book/1", "application/json", &buf)
resp, err := http.Post(svr.URL+"/rpc/book", "application/json", &buf)
if err != nil {
panic(err)
}
Expand All @@ -136,10 +253,10 @@ func ExampleEspresso() {
}()

// Output:
// level=INFO msg="receive http" method=GET path=/book/1
// level=INFO msg="finish http" method=GET path=/book/1
// level=INFO msg="receive http" method=GET path=/rpc/book/1
// level=INFO msg="finish http" method=GET path=/rpc/book/1
// Book 1 title: The Espresso Book
// level=INFO msg="receive http" method=POST path=/book/1
// level=INFO msg="finish http" method=POST path=/book/1
// level=INFO msg="receive http" method=POST path=/rpc/book
// level=INFO msg="finish http" method=POST path=/rpc/book
// The New Book id: 2
}
1 change: 1 addition & 0 deletions router.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

type Router interface {
Use(middlewares ...HandleFunc)
WithPrefix(path string) Router
HandleFunc(handleFunc HandleFunc)
}

Expand Down
Loading

0 comments on commit 11e61c9

Please sign in to comment.