{"id":13692231,"url":"https://github.com/pingcap/fn","last_synced_at":"2025-07-19T17:15:28.333Z","repository":{"id":40004445,"uuid":"192450655","full_name":"pingcap/fn","owner":"pingcap","description":null,"archived":false,"fork":false,"pushed_at":"2023-08-30T14:25:25.000Z","size":30,"stargazers_count":35,"open_issues_count":3,"forks_count":14,"subscribers_count":28,"default_branch":"master","last_synced_at":"2025-04-09T23:17:41.040Z","etag":null,"topics":["http-api","json-api"],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pingcap.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-06-18T02:27:31.000Z","updated_at":"2024-05-23T08:29:38.000Z","dependencies_parsed_at":"2024-06-18T15:14:57.858Z","dependency_job_id":"07ff7e65-e40b-4fa4-bcaa-dd64afe6343f","html_url":"https://github.com/pingcap/fn","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/pingcap/fn","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pingcap%2Ffn","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pingcap%2Ffn/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pingcap%2Ffn/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pingcap%2Ffn/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pingcap","download_url":"https://codeload.github.com/pingcap/fn/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pingcap%2Ffn/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264503947,"owners_count":23618763,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["http-api","json-api"],"created_at":"2024-08-02T17:00:55.042Z","updated_at":"2025-07-09T21:35:33.994Z","avatar_url":"https://github.com/pingcap.png","language":"Go","readme":"# fn\n\nThis library aims to simplify the construction of JSON API service,\n`fn.Wrap` is able to wrap any function to adapt the interface of\n`http.Handler`, which unmarshals POST data to a struct automatically.\n\n## Benchmark\n\n```\nBenchmarkIsBuiltinType-8                50000000                33.5 ns/op             0 B/op          0 allocs/op\nBenchmarkSimplePlainAdapter_Invoke-8     2000000               757 ns/op             195 B/op          3 allocs/op\nBenchmarkSimpleUnaryAdapter_Invoke-8     2000000               681 ns/op             946 B/op          5 allocs/op\nBenchmarkGenericAdapter_Invoke-8         2000000               708 ns/op             946 B/op          5 allocs/op\n```\n\n## Support types\n\n```\nio.ReadCloser      // request.Body\nhttp.Header        // request.Header\nfn.Form        // request.Form\nfn.PostForm    // request.PostForm\n*fn.Form       // request.Form\n*fn.PostForm   // request.PostForm\n*url.URL           // request.URL\n*multipart.Form    // request.MultipartForm\n*http.Request      // raw request\n```\n\n## Usage\n\n```go\nhttp.Handle(\"/test\", fn.Wrap(test))\n\nfunc test(io.ReadCloser, http.Header, fn.Form, fn.PostForm, *CustomizedRequestType, *url.URL, *multipart.Form) (*CustomizedResponseType, error)\n```\n\n## Examples\n\n### Basic\n\n```go\npackage examples\n\nimport (\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/pingcap/fn\"\n)\n\ntype Request struct {\n\tUsername string `json:\"username\"`\n\tPassword string `json:\"password\"`\n}\n\ntype Response struct {\n\tToken string `json:\"token\"`\n}\n\nfunc api1() (*Response, error) {\n\treturn \u0026Response{Token: \"token\"}, nil\n}\n\nfunc api2(request *Request) (*Response, error) {\n\ttoken := request.Username + request.Password\n\treturn \u0026Response{Token: token}, nil\n}\n\nfunc api3(rawreq *http.Request, request *Request) (*Response, error) {\n\ttoken := request.Username + request.Password\n\treturn \u0026Response{Token: token}, nil\n}\n\nfunc api4(rawreq http.Header, request *Request) (*Response, error) {\n\ttoken := request.Username + request.Password\n\treturn \u0026Response{Token: token}, nil\n}\n\nfunc api5(form *fn.Form, request *Request) (*Response, error) {\n\ttoken := request.Username + request.Password + form.Get(\"type\")\n\treturn \u0026Response{Token: token}, nil\n}\n\nfunc api6(body io.ReadCloser, request *Request) (*Response, error) {\n\ttoken := request.Username + request.Password\n\treturn \u0026Response{Token: token}, nil\n}\n\nfunc api7(form *multipart.Form, request *Request) (*Response, error) {\n\ttoken := request.Username + request.Password\n\treturn \u0026Response{Token: token}, nil\n}\n\nfunc api7(urls *url.URL, request *Request) (*Response, error) {\n\ttoken := request.Username + request.Password\n\treturn \u0026Response{Token: token}, nil\n}\n\nfunc api8(urls *url.URL, form *multipart.Form, body io.ReadCloser, rawreq http.Header, request *Request) (*Response, error) {\n\ttoken := request.Username + request.Password\n\treturn \u0026Response{Token: token}, nil\n}\n```\n\n### Plugins\n\n```go\npackage examples\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"log\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/pingcap/fn\"\n)\n\nvar PermissionDenied = errors.New(\"permission denied\")\n\nfunc logger(ctx context.Context, req *http.Request) (context.Context, error) {\n\tlog.Println(\"Request\", req.RemoteAddr, req.URL.String())\n\treturn ctx, nil\n}\n\nfunc ipWhitelist(ctx context.Context, req *http.Request) (context.Context, error) {\n\tif strings.HasPrefix(req.RemoteAddr, \"172.168\") {\n\t\treturn ctx, PermissionDenied\n\t}\n\treturn ctx, nil\n}\n\nfunc auth(ctx context.Context, req *http.Request) (context.Context, error) {\n\ttoken := req.Header.Get(\"X-Auth-token\")\n\t_ = token // Validate token (e.g: query db)\n\tif token != \"valid\" {\n\t\treturn ctx, fn.ErrorWithStatusCode(PermissionDenied, http.StatusForbidden)\n\t}\n\treturn ctx, nil\n}\n\ntype Request struct {\n\tUsername string `json:\"username\"`\n\tPassword string `json:\"password\"`\n}\n\ntype Response struct {\n\tToken string `json:\"token\"`\n}\n\nfunc example() {\n\tfn.Plugin(logger, ipWhitelist, auth)\n\thttp.Handle(\"/api1\", fn.Wrap(api1))\n\thttp.Handle(\"/api2\", fn.Wrap(api2))\n}\n\n// api1 and api2 request have be validated by `ipWhitelist` and `auth`\n\nfunc api1() (*Response, error) {\n\treturn \u0026Response{Token: \"token\"}, nil\n}\n\nfunc api2(request *Request) (*Response, error) {\n\ttoken := request.Username + request.Password\n\treturn \u0026Response{Token: token}, nil\n}\n```\n\n### `fn.Group`\n\n```go\npackage examples\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"log\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/pingcap/fn\"\n)\n\nvar PermissionDenied = errors.New(\"permission denied\")\n\nfunc logger(ctx context.Context, req *http.Request) (context.Context, error) {\n\tlog.Println(\"Request\", req.RemoteAddr, req.URL.String())\n\treturn ctx, nil\n}\n\nfunc ipWhitelist(ctx context.Context, req *http.Request) (context.Context, error) {\n\tif strings.HasPrefix(req.RemoteAddr, \"172.168\") {\n\t\treturn ctx, PermissionDenied\n\t}\n\treturn ctx, nil\n}\n\nfunc auth(ctx context.Context, req *http.Request) (context.Context, error) {\n\ttoken := req.Header.Get(\"X-Auth-token\")\n\t_ = token // Validate token (e.g: query db)\n\tif token != \"valid\" {\n\t\treturn ctx, fn.ErrorWithStatusCode(PermissionDenied, http.StatusForbidden)\n\t}\n\treturn ctx, nil\n}\n\ntype User struct {\n\tBalance int64\n}\n\nfunc queryUserFromRedis(ctx context.Context, req *http.Request) (context.Context, error) {\n\ttoken := req.Header.Get(\"X-Auth-token\")\n\t_ = token // Validate token (e.g: query db)\n\tif token != \"valid\" {\n\t\treturn ctx, fn.ErrorWithStatusCode(PermissionDenied, http.StatusForbidden)\n\t}\n\tuser := \u0026User{\n\t\tBalance: 10000, // balance from redis\n\t}\n\treturn context.WithValue(ctx, \"user\", user), nil\n}\n\ntype Response struct {\n\tBalance int64 `json:\"balance\"`\n}\n\nfunc example() {\n\t// Global plugins\n\tfn.Plugin(logger, ipWhitelist, auth)\n\n\tgroup := fn.NewGroup()\n\n\t// Group plugins\n\tgroup.Plugin(queryUserFromRedis)\n\thttp.Handle(\"/user/balance\", group.Wrap(fetchBalance))\n\thttp.Handle(\"/user/buy\", group.Wrap(buy))\n}\n\nfunc fetchBalance(ctx context.Context) (*Response, error) {\n\tuser := ctx.Value(\"user\").(*User)\n\treturn \u0026Response{Balance: user.Balance}, nil\n}\n\nfunc buy(ctx context.Context) (*Response, error) {\n\tuser := ctx.Value(\"user\").(*User)\n\tif user.Balance \u003c 100 {\n\t\treturn nil, errors.New(\"please check balance\")\n\t}\n\tuser.Balance -= 100\n\treturn \u0026Response{Balance: user.Balance}, nil\n}\n```\n\n### ResponseEncoder\n\n```go\npackage examples\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"log\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/pingcap/fn\"\n)\n\nvar PermissionDenied = errors.New(\"permission denied\")\n\nfunc logger(ctx context.Context, req *http.Request) (context.Context, error) {\n\tlog.Println(\"Request\", req.RemoteAddr, req.URL.String())\n\treturn ctx, nil\n}\n\nfunc ipWhitelist(ctx context.Context, req *http.Request) (context.Context, error) {\n\tif strings.HasPrefix(req.RemoteAddr, \"172.168\") {\n\t\treturn ctx, PermissionDenied\n\t}\n\treturn ctx, nil\n}\n\nfunc auth(ctx context.Context, req *http.Request) (context.Context, error) {\n\ttoken := req.Header.Get(\"X-Auth-token\")\n\t_ = token // Validate token (e.g: query db)\n\tif token != \"valid\" {\n\t\treturn ctx, fn.ErrorWithStatusCode(PermissionDenied, http.StatusForbidden)\n\t}\n\treturn ctx, nil\n}\n\nfunc injectRequest(ctx context.Context, req *http.Request) (context.Context, error) {\n\treturn context.WithValue(ctx, \"_rawreq\", req), nil\n}\n\ntype User struct {\n\tBalance int64\n}\n\nfunc queryUserFromRedis(ctx context.Context, req *http.Request) (context.Context, error) {\n\ttoken := req.Header.Get(\"X-Auth-token\")\n\t_ = token // Validate token (e.g: query db)\n\tif token != \"valid\" {\n\t\treturn ctx, fn.ErrorWithStatusCode(PermissionDenied, http.StatusForbidden)\n\t}\n\tuser := \u0026User{\n\t\tBalance: 10000, // balance from redis\n\t}\n\treturn context.WithValue(ctx, \"user\", user), nil\n}\n\ntype Response struct {\n\tBalance int64 `json:\"balance\"`\n}\n\ntype ResponseMessage struct {\n\tCode int         `json:\"code\"`\n\tData interface{} `json:\"data\"`\n}\n\ntype ErrorMessage struct {\n\tCode  int    `json:\"code\"`\n\tError string `json:\"error\"`\n}\n\nfunc example() {\n\t// Global plugins\n\tfn.Plugin(logger, ipWhitelist, auth, injectRequest)\n\t// Uniform all responses\n\tfn.SetErrorEncoder(func(ctx context.Context, err error) interface{} {\n\t\treq := ctx.Value(\"_rawreq\").(*http.Request)\n\t\tlog.Println(\"Error occurred: \", req.URL, err)\n\t\treturn \u0026ErrorMessage{\n\t\t\tCode:  -1,\n\t\t\tError: err.Error(),\n\t\t}\n\t})\n\n\tfn.SetResponseEncoder(func(ctx context.Context, payload interface{}) interface{} {\n\t\treturn \u0026ResponseMessage{\n\t\t\tCode: 1,\n\t\t\tData: payload,\n\t\t}\n\t})\n\n\tgroup := fn.NewGroup()\n\n\t// Group plugins\n\tgroup.Plugin(queryUserFromRedis)\n\thttp.Handle(\"/user/balance\", group.Wrap(fetchBalance))\n\thttp.Handle(\"/user/buy\", group.Wrap(buy))\n}\n\nfunc fetchBalance(ctx context.Context) (*Response, error) {\n\tuser := ctx.Value(\"user\").(*User)\n\treturn \u0026Response{Balance: user.Balance}, nil\n}\n\nfunc buy(ctx context.Context) (*Response, error) {\n\tuser := ctx.Value(\"user\").(*User)\n\tif user.Balance \u003c 100 {\n\t\treturn nil, errors.New(\"please check balance\")\n\t}\n\tuser.Balance -= 100\n\treturn \u0026Response{Balance: user.Balance}, nil\n}\n```","funding_links":[],"categories":["开源类库","Open source library"],"sub_categories":["Web 框架","Web Framework"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpingcap%2Ffn","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpingcap%2Ffn","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpingcap%2Ffn/lists"}