espresso

package module
v0.6.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 16, 2024 License: MIT Imports: 16 Imported by: 0

README

go-espresso

Go Reference Go Report Card CI

An web/API framework.

  • For individual developers and small teams.
  • Code first.
    • Focus on code, instead of switching between schemas and code.
  • Type safe.
    • No casting from any.
  • Support IDE completion.
  • As small dependencies as possible.
    • httprouter
    • exp/slog for logging
      • This may go to std in the future.
    • testing
      • go-cmp

Examples to show the usage:

Requirement:

  • Go >= 1.22
    • Require generics.
    • errors.Is() supports interface{ Unwrap() []error }
    • With GODEBUG=httpmuxgo121=0

Documentation

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	CodecsModule  = module.New[*Codecs]()
	ProvideCodecs = CodecsModule.ProvideWithFunc(func(context.Context) (*Codecs, error) {
		return NewCodecs(JSON{}, YAML{}), nil
	})
)
View Source
var (
	LogModule = module.New[*slog.Logger]()
	LogText   = LogModule.ProvideWithFunc(func(ctx context.Context) (*slog.Logger, error) {
		handler := slog.NewTextHandler(os.Stderr, nil)
		return slog.New(handler), nil
	})
	LogJSON = LogModule.ProvideWithFunc(func(ctx context.Context) (*slog.Logger, error) {
		handler := slog.NewJSONHandler(os.Stderr, nil)
		return slog.New(handler), nil
	})
)

Functions

func DEBUG added in v0.6.0

func DEBUG(ctx context.Context, msg string, args ...any)

func ERROR added in v0.6.0

func ERROR(ctx context.Context, msg string, args ...any)

func Error added in v0.4.0

func Error(code int, err error) error

func INFO added in v0.6.0

func INFO(ctx context.Context, msg string, args ...any)

func WARN added in v0.6.0

func WARN(ctx context.Context, msg string, args ...any)

Types

type BindError added in v0.3.1

type BindError struct {
	Key  string
	From BindSource
	Type reflect.Type
	Err  error
}

BindError describes the error when binding a param.

func (BindError) Error added in v0.3.1

func (b BindError) Error() string

func (BindError) Unwrap added in v0.3.1

func (b BindError) Unwrap() error

type BindErrors added in v0.3.1

type BindErrors []BindError

BindErrors describes all errors when binding params.

func (BindErrors) Error added in v0.3.1

func (e BindErrors) Error() string

func (BindErrors) Unwrap added in v0.3.1

func (e BindErrors) Unwrap() []error

type BindFunc added in v0.6.0

type BindFunc func(any, string) error

type BindParam added in v0.4.0

type BindParam struct {
	Key  string
	From BindSource
	Type reflect.Type
	Func BindFunc
}

type BindSource added in v0.6.0

type BindSource int

BindSource describes the type of bind.

const (
	BindPathParam BindSource = iota
	BindFormParam
	BindQueryParam
	BindHeadParam
)

func (BindSource) String added in v0.6.0

func (b BindSource) String() string

func (BindSource) Valid added in v0.6.0

func (b BindSource) Valid() bool

type Codec added in v0.3.1

type Codec interface {
	Mime() string
	Encode(ctx context.Context, w io.Writer, v any) error
	Decode(ctx context.Context, r io.Reader, v any) error
}

type Codecs added in v0.6.0

type Codecs struct {
	// contains filtered or unexported fields
}

func NewCodecs added in v0.6.0

func NewCodecs(codec ...Codec) *Codecs

func (*Codecs) DecodeRequest added in v0.6.0

func (c *Codecs) DecodeRequest(ctx Context, v any) error

func (*Codecs) EncodeResponse added in v0.6.0

func (c *Codecs) EncodeResponse(ctx Context, v any) error

func (*Codecs) Request added in v0.6.0

func (c *Codecs) Request(ctx Context) Codec

func (*Codecs) Response added in v0.6.0

func (c *Codecs) Response(ctx Context) Codec

type Context

type Context interface {
	context.Context
	ContextExtender
	Endpoint(method string, path string, middlewares ...HandleFunc) EndpointBuilder

	Next()

	Request() *http.Request
	ResponseWriter() http.ResponseWriter
}

type ContextExtender added in v0.6.0

type ContextExtender interface {
	WithParent(ctx context.Context) Context
	WithResponseWriter(w http.ResponseWriter) Context
}

type Endpoint added in v0.4.0

type Endpoint struct {
	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
}

type EndpointBuilder added in v0.4.0

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

type Espresso added in v0.6.0

type Espresso struct {
	// contains filtered or unexported fields
}
Example
type Book struct {
	ID    int    `json:"id"`
	Title string `json:"title"`
}
type Books map[int]Book

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(func(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
}))

espo.AddModule(espresso.ProvideCodecs)

router := espo.WithPrefix("/http")
router.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
	}

	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
})
router.HandleFunc(func(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
})

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()

	if resp.StatusCode != http.StatusOK {
		panic(resp.Status)
	}

	if err := json.NewDecoder(resp.Body).Decode(&book); err != nil {
		panic(err)
	}

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

func() {
	arg := Book{Title: "The New Book"}

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

	resp, err := http.Post(svr.URL+"/http/book", "application/json", &buf)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		panic(resp.Status)
	}

	var ret Book
	if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil {
		panic(err)
	}

	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
Example (Rpc)
type Book struct {
	ID    int    `json:"id"`
	Title string `json:"title"`
}
type Books map[int]Book

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(func(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
}))
espo.AddModule(espresso.ProvideCodecs)

router := espo.WithPrefix("/rpc")
router.HandleFunc(espresso.RPCRetrive(func(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
}))
router.HandleFunc(espresso.RPC(func(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
}))

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

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

	if resp.StatusCode != http.StatusOK {
		panic(resp.Status)
	}

	if err := json.NewDecoder(resp.Body).Decode(&book); err != nil {
		panic(err)
	}

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

func() {
	arg := Book{Title: "The New Book"}

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

	resp, err := http.Post(svr.URL+"/rpc/book", "application/json", &buf)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		panic(resp.Status)
	}

	var ret Book
	if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil {
		panic(err)
	}

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

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=/rpc/book
level=INFO msg="finish http" method=POST path=/rpc/book
The New Book id: 2

func New added in v0.4.0

func New() *Espresso

func (*Espresso) AddModule added in v0.6.0

func (s *Espresso) AddModule(provider ...module.Provider)

func (*Espresso) HandleFunc added in v0.6.0

func (s *Espresso) HandleFunc(handleFunc HandleFunc)

func (*Espresso) ServeHTTP added in v0.6.0

func (s *Espresso) ServeHTTP(w http.ResponseWriter, r *http.Request)

func (*Espresso) Use added in v0.6.0

func (s *Espresso) Use(middlewares ...HandleFunc)

func (*Espresso) WithPrefix added in v0.6.0

func (s *Espresso) WithPrefix(path string) Router

type HTTPError added in v0.6.0

type HTTPError interface {
	HTTPCode() int
}

type HandleFunc added in v0.4.0

type HandleFunc func(Context) error

func RPC added in v0.6.0

func RPC[Request, Response any](fn func(Context, Request) (Response, error)) HandleFunc

func RPCConsume added in v0.6.0

func RPCConsume[Request any](fn func(Context, Request) error) HandleFunc

func RPCRetrive added in v0.6.0

func RPCRetrive[Response any](fn func(Context) (Response, error)) HandleFunc

type JSON added in v0.6.0

type JSON struct{}

func (JSON) Decode added in v0.6.0

func (JSON) Decode(ctx context.Context, r io.Reader, v any) error

func (JSON) Encode added in v0.6.0

func (JSON) Encode(ctx context.Context, w io.Writer, v any) error

func (JSON) Mime added in v0.6.0

func (JSON) Mime() string

type MiddlewareProvider added in v0.6.0

type MiddlewareProvider interface {
	Middlewares() []HandleFunc
}

type Router added in v0.3.1

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

type YAML added in v0.6.0

type YAML struct{}

func (YAML) Decode added in v0.6.0

func (YAML) Decode(ctx context.Context, r io.Reader, v any) error

func (YAML) Encode added in v0.6.0

func (YAML) Encode(ctx context.Context, w io.Writer, v any) error

func (YAML) Mime added in v0.6.0

func (YAML) Mime() string

Directories

Path Synopsis
Package module provides a way to do dependency injection, with type-safe, without performance penalty.
Package module provides a way to do dependency injection, with type-safe, without performance penalty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL