fn
fn copied to clipboard
fn
This library aims to simplify the construction of JSON API service,
fn.Wrap is able to wrap any function to adapt the interface of
http.Handler, which unmarshals POST data to a struct automatically.
Benchmark
BenchmarkIsBuiltinType-8 50000000 33.5 ns/op 0 B/op 0 allocs/op
BenchmarkSimplePlainAdapter_Invoke-8 2000000 757 ns/op 195 B/op 3 allocs/op
BenchmarkSimpleUnaryAdapter_Invoke-8 2000000 681 ns/op 946 B/op 5 allocs/op
BenchmarkGenericAdapter_Invoke-8 2000000 708 ns/op 946 B/op 5 allocs/op
Support types
io.ReadCloser // request.Body
http.Header // request.Header
fn.Form // request.Form
fn.PostForm // request.PostForm
*fn.Form // request.Form
*fn.PostForm // request.PostForm
*url.URL // request.URL
*multipart.Form // request.MultipartForm
*http.Request // raw request
Usage
http.Handle("/test", fn.Wrap(test))
func test(io.ReadCloser, http.Header, fn.Form, fn.PostForm, *CustomizedRequestType, *url.URL, *multipart.Form) (*CustomizedResponseType, error)
Examples
Basic
package examples
import (
"io"
"mime/multipart"
"net/http"
"net/url"
"github.com/pingcap/fn"
)
type Request struct {
Username string `json:"username"`
Password string `json:"password"`
}
type Response struct {
Token string `json:"token"`
}
func api1() (*Response, error) {
return &Response{Token: "token"}, nil
}
func api2(request *Request) (*Response, error) {
token := request.Username + request.Password
return &Response{Token: token}, nil
}
func api3(rawreq *http.Request, request *Request) (*Response, error) {
token := request.Username + request.Password
return &Response{Token: token}, nil
}
func api4(rawreq http.Header, request *Request) (*Response, error) {
token := request.Username + request.Password
return &Response{Token: token}, nil
}
func api5(form *fn.Form, request *Request) (*Response, error) {
token := request.Username + request.Password + form.Get("type")
return &Response{Token: token}, nil
}
func api6(body io.ReadCloser, request *Request) (*Response, error) {
token := request.Username + request.Password
return &Response{Token: token}, nil
}
func api7(form *multipart.Form, request *Request) (*Response, error) {
token := request.Username + request.Password
return &Response{Token: token}, nil
}
func api7(urls *url.URL, request *Request) (*Response, error) {
token := request.Username + request.Password
return &Response{Token: token}, nil
}
func api8(urls *url.URL, form *multipart.Form, body io.ReadCloser, rawreq http.Header, request *Request) (*Response, error) {
token := request.Username + request.Password
return &Response{Token: token}, nil
}
Plugins
package examples
import (
"context"
"errors"
"io"
"log"
"mime/multipart"
"net/http"
"net/url"
"strings"
"github.com/pingcap/fn"
)
var PermissionDenied = errors.New("permission denied")
func logger(ctx context.Context, req *http.Request) (context.Context, error) {
log.Println("Request", req.RemoteAddr, req.URL.String())
return ctx, nil
}
func ipWhitelist(ctx context.Context, req *http.Request) (context.Context, error) {
if strings.HasPrefix(req.RemoteAddr, "172.168") {
return ctx, PermissionDenied
}
return ctx, nil
}
func auth(ctx context.Context, req *http.Request) (context.Context, error) {
token := req.Header.Get("X-Auth-token")
_ = token // Validate token (e.g: query db)
if token != "valid" {
return ctx, fn.ErrorWithStatusCode(PermissionDenied, http.StatusForbidden)
}
return ctx, nil
}
type Request struct {
Username string `json:"username"`
Password string `json:"password"`
}
type Response struct {
Token string `json:"token"`
}
func example() {
fn.Plugin(logger, ipWhitelist, auth)
http.Handle("/api1", fn.Wrap(api1))
http.Handle("/api2", fn.Wrap(api2))
}
// api1 and api2 request have be validated by `ipWhitelist` and `auth`
func api1() (*Response, error) {
return &Response{Token: "token"}, nil
}
func api2(request *Request) (*Response, error) {
token := request.Username + request.Password
return &Response{Token: token}, nil
}
fn.Group
package examples
import (
"context"
"errors"
"io"
"log"
"mime/multipart"
"net/http"
"net/url"
"strings"
"github.com/pingcap/fn"
)
var PermissionDenied = errors.New("permission denied")
func logger(ctx context.Context, req *http.Request) (context.Context, error) {
log.Println("Request", req.RemoteAddr, req.URL.String())
return ctx, nil
}
func ipWhitelist(ctx context.Context, req *http.Request) (context.Context, error) {
if strings.HasPrefix(req.RemoteAddr, "172.168") {
return ctx, PermissionDenied
}
return ctx, nil
}
func auth(ctx context.Context, req *http.Request) (context.Context, error) {
token := req.Header.Get("X-Auth-token")
_ = token // Validate token (e.g: query db)
if token != "valid" {
return ctx, fn.ErrorWithStatusCode(PermissionDenied, http.StatusForbidden)
}
return ctx, nil
}
type User struct {
Balance int64
}
func queryUserFromRedis(ctx context.Context, req *http.Request) (context.Context, error) {
token := req.Header.Get("X-Auth-token")
_ = token // Validate token (e.g: query db)
if token != "valid" {
return ctx, fn.ErrorWithStatusCode(PermissionDenied, http.StatusForbidden)
}
user := &User{
Balance: 10000, // balance from redis
}
return context.WithValue(ctx, "user", user), nil
}
type Response struct {
Balance int64 `json:"balance"`
}
func example() {
// Global plugins
fn.Plugin(logger, ipWhitelist, auth)
group := fn.NewGroup()
// Group plugins
group.Plugin(queryUserFromRedis)
http.Handle("/user/balance", group.Wrap(fetchBalance))
http.Handle("/user/buy", group.Wrap(buy))
}
func fetchBalance(ctx context.Context) (*Response, error) {
user := ctx.Value("user").(*User)
return &Response{Balance: user.Balance}, nil
}
func buy(ctx context.Context) (*Response, error) {
user := ctx.Value("user").(*User)
if user.Balance < 100 {
return nil, errors.New("please check balance")
}
user.Balance -= 100
return &Response{Balance: user.Balance}, nil
}
ResponseEncoder
package examples
import (
"context"
"errors"
"io"
"log"
"mime/multipart"
"net/http"
"net/url"
"strings"
"github.com/pingcap/fn"
)
var PermissionDenied = errors.New("permission denied")
func logger(ctx context.Context, req *http.Request) (context.Context, error) {
log.Println("Request", req.RemoteAddr, req.URL.String())
return ctx, nil
}
func ipWhitelist(ctx context.Context, req *http.Request) (context.Context, error) {
if strings.HasPrefix(req.RemoteAddr, "172.168") {
return ctx, PermissionDenied
}
return ctx, nil
}
func auth(ctx context.Context, req *http.Request) (context.Context, error) {
token := req.Header.Get("X-Auth-token")
_ = token // Validate token (e.g: query db)
if token != "valid" {
return ctx, fn.ErrorWithStatusCode(PermissionDenied, http.StatusForbidden)
}
return ctx, nil
}
func injectRequest(ctx context.Context, req *http.Request) (context.Context, error) {
return context.WithValue(ctx, "_rawreq", req), nil
}
type User struct {
Balance int64
}
func queryUserFromRedis(ctx context.Context, req *http.Request) (context.Context, error) {
token := req.Header.Get("X-Auth-token")
_ = token // Validate token (e.g: query db)
if token != "valid" {
return ctx, fn.ErrorWithStatusCode(PermissionDenied, http.StatusForbidden)
}
user := &User{
Balance: 10000, // balance from redis
}
return context.WithValue(ctx, "user", user), nil
}
type Response struct {
Balance int64 `json:"balance"`
}
type ResponseMessage struct {
Code int `json:"code"`
Data interface{} `json:"data"`
}
type ErrorMessage struct {
Code int `json:"code"`
Error string `json:"error"`
}
func example() {
// Global plugins
fn.Plugin(logger, ipWhitelist, auth, injectRequest)
// Uniform all responses
fn.SetErrorEncoder(func(ctx context.Context, err error) interface{} {
req := ctx.Value("_rawreq").(*http.Request)
log.Println("Error occurred: ", req.URL, err)
return &ErrorMessage{
Code: -1,
Error: err.Error(),
}
})
fn.SetResponseEncoder(func(ctx context.Context, payload interface{}) interface{} {
return &ResponseMessage{
Code: 1,
Data: payload,
}
})
group := fn.NewGroup()
// Group plugins
group.Plugin(queryUserFromRedis)
http.Handle("/user/balance", group.Wrap(fetchBalance))
http.Handle("/user/buy", group.Wrap(buy))
}
func fetchBalance(ctx context.Context) (*Response, error) {
user := ctx.Value("user").(*User)
return &Response{Balance: user.Balance}, nil
}
func buy(ctx context.Context) (*Response, error) {
user := ctx.Value("user").(*User)
if user.Balance < 100 {
return nil, errors.New("please check balance")
}
user.Balance -= 100
return &Response{Balance: user.Balance}, nil
}