├── .gitignore ├── LICENSE ├── README.md ├── api.go ├── config.go ├── decode.go ├── decoder ├── cached.go ├── compile.go ├── decode.go ├── map.go └── params.go ├── example ├── __debug_bin ├── main.go └── test.http ├── go.mod ├── go.sum ├── group.go ├── handler.go ├── problem ├── problem.go └── util.go └── tagcheck.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Dependency directories (remove the comment below to include it) 12 | # vendor/ 13 | 14 | # goland 15 | .idea/ 16 | 17 | .vscode/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Adam Bouqdib 4 | Copyright (c) 2022 Jarrett Vance 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # japi is a JSON HTTP API go library 2 | 3 | Japi is a fast & simple HTTP API library that will automatically marshal JSON payloads to/from 4 | your request and response structs. It follows [RFC7807](https://datatracker.ietf.org/doc/html/rfc7807) 5 | standard for returning useful problem details. 6 | 7 | This library focuses on happy path to minimize code and dependencies. For more complex use cases, 8 | we recommend sticking to a larger web framework. However, this library supports the standard 9 | net/http ecosystem. 10 | 11 | This library requires Go 1.18 to work as it utilizes generics. 12 | 13 | This library was forked from https://github.com/AbeMedia/go-don 14 | 15 | ## Contents 16 | 17 | - [Basic Example](#basic-example) 18 | - [Configuration](#configuration) 19 | - [Request parsing](#request-parsing) 20 | - [Customize response](#customize-response) 21 | - [Problem details](#problem-details) 22 | - [Sub-routers](#sub-routers) 23 | - [Middleware](#middleware) 24 | 25 | ## Basic Example 26 | 27 | ```go 28 | package main 29 | 30 | import ( 31 | "context" 32 | "errors" 33 | "fmt" 34 | "net/http" 35 | 36 | "github.com/jarrettv/go-japi" 37 | ) 38 | 39 | type GreetRequest struct { 40 | Name string `path:"name"` // Get name from the URL path. 41 | Age int `header:"X-User-Age"` // Get age from HTTP header. 42 | } 43 | 44 | type GreetResponse struct { 45 | // Remember to add tags for automatic marshalling 46 | Greeting string `json:"data"` 47 | } 48 | 49 | func Greet(ctx context.Context, req GreetRequest) (*GreetResponse, error) { 50 | if req.Name == "" { 51 | return nil, problem.Validation(map[string]string{ 52 | "name": "required", 53 | }) 54 | } 55 | res := &GreetResponse{ 56 | Greeting: fmt.Sprintf("Hello %s, you're %d years old.", req.Name, req.Age), 57 | } 58 | 59 | return res, nil 60 | } 61 | 62 | func Pong(context.Context, japi.Empty) (string, error) { 63 | return "pong", nil 64 | } 65 | 66 | func main() { 67 | r := japi.New(nil) 68 | r.Get("/ping", japi.H(Pong)) // Handlers are wrapped with `japi.H`. 69 | r.Post("/greet/:name", japi.H(Greet)) 70 | r.ListenAndServe(":8080") 71 | } 72 | ``` 73 | 74 | ## Configuration 75 | 76 | Japi is configured by passing in the `Config` struct to `japi.New`. We recommend you setup `ProblemConfig` at a minimum. 77 | 78 | ```go 79 | r := japi.New(&japi.Config{ 80 | ProblemConfig: problem.ProblemConfig{ 81 | ProblemTypeUrlFormat: "https://example.com/errors/%s", 82 | ProblemInstanceFunc: func(ctx context.Context) string { 83 | return fmt.Sprintf("https://example.com/trace/%d", time.Now().UnixMilli()) 84 | }, 85 | }, 86 | }) 87 | ``` 88 | ### RouteLogFunc 89 | 90 | A function to easily log the route name and route variables. 91 | 92 | ### ProblemLogFunc 93 | 94 | A function to easily log when problems occur. 95 | 96 | ### ProblemConfig.ProblemTypeUrlFormat 97 | 98 | The format for the problem details type URI. See [RFC7807](https://datatracker.ietf.org/doc/html/rfc7807) 99 | 100 | ### ProblemConfig.ProblemInstanceFunc 101 | 102 | A function for generating a unique trace URI. Defaults to a timestamp. See [RFC7807](https://datatracker.ietf.org/doc/html/rfc7807) 103 | 104 | ## Request parsing 105 | 106 | Automatically unmarshals values from headers, URL query, URL path & request body into your request 107 | struct. 108 | 109 | ```go 110 | type MyRequest struct { 111 | // Get from the URL path. 112 | ID int64 `path:"id"` 113 | 114 | // Get from the URL query. 115 | Filter string `query:"filter"` 116 | 117 | // Get from the JSON or form body. 118 | Content float64 `form:"bar" json:"bar"` 119 | 120 | // Get from the HTTP header. 121 | Lang string `header:"Accept-Language"` 122 | } 123 | ``` 124 | 125 | Please note that using a pointer as the request type negatively affects performance. 126 | 127 | ## Customize Response 128 | 129 | Implement the `StatusCoder` and `Headerer` interfaces to customise headers and response codes. 130 | 131 | ```go 132 | type MyResponse struct { 133 | Foo string `json:"foo"` 134 | } 135 | 136 | // Set a custom HTTP response code. 137 | func (nr *MyResponse) StatusCode() int { 138 | return 201 139 | } 140 | 141 | // Add custom headers to the response. 142 | func (nr *MyResponse) Header() http.Header { 143 | header := http.Header{} 144 | header.Set("foo", "bar") 145 | return header 146 | } 147 | ``` 148 | 149 | ## Problems 150 | 151 | Return a `problem.Problem` error when something goes wrong. For example: 152 | 153 | ```go 154 | return nil, problem.Unexpected(err) // 500 155 | // or 156 | return nil, problem.NotFound() // 404 157 | // or 158 | return nil, problem.NotPermitted(username) // 403 159 | // or 160 | return nil, problem.Validation(map[string]string{ // 400 161 | "name": "required", 162 | }) 163 | // or 164 | return nil, problem.RuleViolantion("item is on backorder") // 400 165 | // or 166 | return nil, problem.NotCurrent() // 407 167 | ``` 168 | 169 | 170 | ## Sub-routers 171 | 172 | You can create sub-routers using the `Group` function: 173 | 174 | ```go 175 | r := japi.New(nil) 176 | sub := r.Group("/api") 177 | sub.Get("/hello") 178 | ``` 179 | 180 | ## Middleware 181 | 182 | Japi uses the standard http middleware format of 183 | `func(http.RequestHandler) http.RequestHandler`. 184 | 185 | For example: 186 | 187 | ```go 188 | func loggingMiddleware(next http.Handler) http.Handler { 189 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 190 | log.Println(r.URL) 191 | next(ctx) 192 | }) 193 | } 194 | ``` 195 | 196 | It is registered on a router using `Use` e.g. 197 | 198 | ```go 199 | r := japi.New(nil) 200 | r.Post("/", japi.H(handler)) 201 | r.Use(loggingMiddleware) 202 | ``` 203 | 204 | Middleware registered on a group only applies to routes in that group and child groups. 205 | 206 | ```go 207 | r := japi.New(nil) 208 | r.Get("/login", japi.H(loginHandler)) 209 | r.Use(loggingMiddleware) // applied to all routes 210 | 211 | api := r.Group("/api") 212 | api.Get("/hello", japi.H(helloHandler)) 213 | api.Use(authMiddleware) // applied to routes `/api/hello` and `/api/v2/bye` 214 | 215 | 216 | v2 := api.Group("/v2") 217 | v2.Get("/bye", japi.H(byeHandler)) 218 | v2.Use(corsMiddleware) // only applied to `/api/v2/bye` 219 | 220 | ``` 221 | 222 | To pass values from the middleware to the handler extend the context e.g. 223 | 224 | ```go 225 | func myMiddleware(next http.Handler) http.Handler { 226 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 227 | ctx := context.WithValue(r.Context(), ContextUserKey, "my_user") 228 | next.ServeHTTP(w, r.WithContext(ctx)) 229 | }) 230 | } 231 | ``` 232 | 233 | This can now be accessed in the handler: 234 | 235 | ```go 236 | user := ctx.Value(ContextUserKey).(string) 237 | ``` 238 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package japi 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/julienschmidt/httprouter" 7 | 8 | "github.com/jarrettv/go-japi/problem" 9 | ) 10 | 11 | type Empty struct{} 12 | 13 | const JsonEncoding = "application/json" 14 | 15 | type Middleware func(http.Handler) http.Handler 16 | 17 | type Router interface { 18 | Get(path string, handle http.Handler) 19 | Post(path string, handle http.Handler) 20 | Put(path string, handle http.Handler) 21 | Delete(path string, handle http.Handler) 22 | Handle(method, path string, handle http.Handler) 23 | HandleFunc(method, path string, handle http.HandlerFunc) 24 | Group(path string) Router 25 | Use(mw ...Middleware) 26 | } 27 | 28 | type API struct { 29 | router *httprouter.Router 30 | config *Config 31 | mw []Middleware 32 | 33 | NotFound http.Handler 34 | MethodNotAllowed http.Handler 35 | PanicHandler func(http.ResponseWriter, *http.Request, interface{}) 36 | } 37 | 38 | // New creates a new API instance. 39 | func New(c *Config) *API { 40 | if c == nil { 41 | c = GetDefaultConfig() 42 | } 43 | 44 | r := httprouter.New() 45 | r.RedirectTrailingSlash = true 46 | r.SaveMatchedRoutePath = true 47 | 48 | return &API{ 49 | router: r, 50 | config: c, 51 | NotFound: withConfig(E(problem.NotFound()), c), 52 | MethodNotAllowed: withConfig(E(problem.Status(http.StatusMethodNotAllowed)), c), 53 | PanicHandler: func(w http.ResponseWriter, r *http.Request, err any) { 54 | withConfig(E(problem.Status(http.StatusInternalServerError)), c).ServeHTTP(w, r) 55 | }, 56 | } 57 | } 58 | 59 | // Router creates a http.Handler for the API. 60 | func (r *API) Router() http.Handler { 61 | r.router.NotFound = r.NotFound 62 | r.router.MethodNotAllowed = r.MethodNotAllowed 63 | r.router.PanicHandler = r.PanicHandler 64 | r.router.SaveMatchedRoutePath = true 65 | 66 | h := http.Handler(r.router) 67 | // TODO (jv) understand why need to reverse middleware 68 | for i := len(r.mw) - 1; i >= 0; i-- { 69 | h = r.mw[i](h) 70 | } 71 | return h 72 | } 73 | 74 | // Get handles GET requests. 75 | func (r *API) Get(path string, handle http.Handler) { 76 | r.Handle(http.MethodGet, path, handle) 77 | } 78 | 79 | // Post handles POST requests. 80 | func (r *API) Post(path string, handle http.Handler) { 81 | r.Handle(http.MethodPost, path, handle) 82 | } 83 | 84 | // Put handles PUT requests. 85 | func (r *API) Put(path string, handle http.Handler) { 86 | r.Handle(http.MethodPut, path, handle) 87 | } 88 | 89 | // Patch handles PATCH requests. 90 | func (r *API) Patch(path string, handle http.Handler) { 91 | r.Handle(http.MethodPatch, path, handle) 92 | } 93 | 94 | // Delete handles DELETE requests. 95 | func (r *API) Delete(path string, handle http.Handler) { 96 | r.Handle(http.MethodDelete, path, handle) 97 | } 98 | 99 | // Handle can be used to wrap regular handlers. 100 | func (r *API) Handle(method, path string, handle http.Handler) { 101 | var hh httprouter.Handle 102 | if h, ok := handle.(Handler); ok { 103 | hh = withConfig(h, r.config).handle 104 | } else { 105 | hh = wrapHandler(handle) 106 | } 107 | 108 | r.router.Handle(method, path, hh) 109 | } 110 | 111 | // HandleFunc handles the requests with the specified method. 112 | func (r *API) HandleFunc(method, path string, handle http.HandlerFunc) { 113 | r.Handle(method, path, handle) 114 | } 115 | 116 | // Group creates a new sub-router with the given prefix. 117 | func (r *API) Group(path string) Router { 118 | return &group{prefix: path, r: r} 119 | } 120 | 121 | // Use will register middleware to run prior to the handlers. 122 | func (r *API) Use(mw ...Middleware) { 123 | r.mw = append(r.mw, mw...) 124 | } 125 | 126 | func withConfig(handle Handler, c *Config) Handler { 127 | if h, ok := handle.(interface{ setConfig(*Config) }); ok { 128 | h.setConfig(c) 129 | } 130 | 131 | return handle 132 | } 133 | 134 | func wrapHandler(h http.Handler) httprouter.Handle { 135 | return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 136 | h.ServeHTTP(w, r) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package japi 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | "github.com/goccy/go-json" 10 | 11 | "github.com/jarrettv/go-japi/problem" 12 | ) 13 | 14 | type Config struct { 15 | // the function to call for logging route details 16 | RouteLogFunc func(ctx context.Context, route string, params map[string]string) 17 | // the function to call for logging problems 18 | ProblemLogFunc func(ctx context.Context, p *problem.Problem) 19 | problem.ProblemConfig 20 | } 21 | 22 | // GetDefaultConfig will return default problem config. 23 | func GetDefaultConfig() *Config { 24 | return &Config{ 25 | RouteLogFunc: func(ctx context.Context, route string, params map[string]string) { 26 | if params != nil && len(params) > 0 { 27 | data, err := json.Marshal(params) 28 | if err == nil { 29 | log.Printf("%s %s", route, string(data)) 30 | return 31 | } 32 | } 33 | log.Print(route) 34 | }, 35 | ProblemLogFunc: func(ctx context.Context, p *problem.Problem) { 36 | log.Printf("%v type=%v", p.Title, p.Type) 37 | }, 38 | ProblemConfig: problem.ProblemConfig{ 39 | ProblemTypeUrlFormat: "https://example.com/errors/%s", 40 | ProblemInstanceFunc: func(ctx context.Context) string { 41 | return fmt.Sprintf("https://example.com/trace/%d", time.Now().UnixMilli()) 42 | }, 43 | }, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /decode.go: -------------------------------------------------------------------------------- 1 | package japi 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | 10 | "github.com/goccy/go-json" 11 | ) 12 | 13 | func init() { 14 | RegisterDecoder(JsonEncoding, decodeJSON) 15 | } 16 | 17 | type ( 18 | Unmarshaler = func(data []byte, v interface{}) error 19 | ContextUnmarshaler = func(ctx context.Context, data []byte, v interface{}) error 20 | DecoderFactory = func(io.Reader) interface{ Decode(interface{}) error } 21 | RequestParser = func(r *http.Request, v interface{}) error 22 | ) 23 | 24 | type DecoderConstraint interface { 25 | Unmarshaler | ContextUnmarshaler | DecoderFactory | RequestParser 26 | } 27 | 28 | // RegisterDecoder registers a request decoder. 29 | func RegisterDecoder[T DecoderConstraint](contentType string, dec T, aliases ...string) { 30 | switch d := any(dec).(type) { 31 | case Unmarshaler: 32 | decoders[contentType] = func(r *http.Request, v interface{}) error { 33 | b, err := ioutil.ReadAll(r.Body) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | return d(b, v) 39 | } 40 | 41 | case ContextUnmarshaler: 42 | decoders[contentType] = func(r *http.Request, v interface{}) error { 43 | b, err := ioutil.ReadAll(r.Body) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | return d(r.Context(), b, v) 49 | } 50 | 51 | case DecoderFactory: 52 | decoders[contentType] = func(r *http.Request, v interface{}) error { 53 | return d(r.Body).Decode(v) 54 | } 55 | 56 | case RequestParser: 57 | decoders[contentType] = d 58 | } 59 | 60 | for _, alias := range aliases { 61 | decoderAliases[alias] = contentType 62 | } 63 | } 64 | 65 | func getDecoder(mime string) (RequestParser, error) { 66 | if enc := decoders[mime]; enc != nil { 67 | return enc, nil 68 | } 69 | 70 | if name := decoderAliases[mime]; name != "" { 71 | return decoders[name], nil 72 | } 73 | 74 | return nil, errors.New("decoder not found") 75 | } 76 | 77 | func decodeJSON(r *http.Request, v interface{}) error { 78 | return json.NewDecoder(r.Body).DecodeContext(r.Context(), v) 79 | } 80 | 81 | var ( 82 | decoders = map[string]RequestParser{} 83 | decoderAliases = map[string]string{} 84 | ) 85 | -------------------------------------------------------------------------------- /decoder/cached.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | type Getter interface { 8 | Get(string) string 9 | Values(string) []string 10 | } 11 | 12 | type CachedDecoder struct { 13 | dec decoder 14 | } 15 | 16 | func NewCachedDecoder(v interface{}, tag string) (*CachedDecoder, error) { 17 | t, k, ptr := typeKind(reflect.TypeOf(v)) 18 | if k != reflect.Struct { 19 | return nil, ErrUnsupportedType 20 | } 21 | 22 | dec, err := compile(t, tag, ptr) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | return &CachedDecoder{dec}, nil 28 | } 29 | 30 | func (d *CachedDecoder) Decode(data Getter, v interface{}) error { 31 | return d.dec(reflect.ValueOf(v).Elem(), data) 32 | } 33 | -------------------------------------------------------------------------------- /decoder/compile.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "strconv" 7 | "unsafe" 8 | ) 9 | 10 | var ErrUnsupportedType = errors.New("decoder: unsupported type") 11 | 12 | type decoder func(reflect.Value, Getter) error 13 | 14 | //nolint:cyclop 15 | func compile(typ reflect.Type, tagKey string, isPtr bool) (decoder, error) { 16 | decoders := []decoder{} 17 | 18 | for i := 0; i < typ.NumField(); i++ { 19 | f := typ.Field(i) 20 | if f.PkgPath != "" { 21 | continue // skip unexported fields 22 | } 23 | 24 | t, k, ptr := typeKind(f.Type) 25 | 26 | tag, ok := f.Tag.Lookup(tagKey) 27 | if !ok && k != reflect.Struct { 28 | continue 29 | } 30 | 31 | switch k { 32 | case reflect.Struct: 33 | dec, err := compile(t, tagKey, ptr) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | index := i 39 | 40 | decoders = append(decoders, func(v reflect.Value, m Getter) error { 41 | return dec(v.Field(index), m) 42 | }) 43 | case reflect.String: 44 | decoders = append(decoders, decodeString(set[string](ptr, i, t), tag)) 45 | case reflect.Int: 46 | decoders = append(decoders, decodeInt(set[int](ptr, i, t), tag)) 47 | case reflect.Int8: 48 | decoders = append(decoders, decodeInt8(set[int8](ptr, i, t), tag)) 49 | case reflect.Int16: 50 | decoders = append(decoders, decodeInt16(set[int16](ptr, i, t), tag)) 51 | case reflect.Int32: 52 | decoders = append(decoders, decodeInt32(set[int32](ptr, i, t), tag)) 53 | case reflect.Int64: 54 | decoders = append(decoders, decodeInt64(set[int64](ptr, i, t), tag)) 55 | case reflect.Uint: 56 | decoders = append(decoders, decodeUint(set[uint](ptr, i, t), tag)) 57 | case reflect.Uint8: 58 | decoders = append(decoders, decodeUint8(set[uint8](ptr, i, t), tag)) 59 | case reflect.Uint16: 60 | decoders = append(decoders, decodeUint16(set[uint16](ptr, i, t), tag)) 61 | case reflect.Uint32: 62 | decoders = append(decoders, decodeUint32(set[uint32](ptr, i, t), tag)) 63 | case reflect.Uint64: 64 | decoders = append(decoders, decodeUint64(set[uint64](ptr, i, t), tag)) 65 | case reflect.Float32: 66 | decoders = append(decoders, decodeFloat32(set[float32](ptr, i, t), tag)) 67 | case reflect.Float64: 68 | decoders = append(decoders, decodeFloat64(set[float64](ptr, i, t), tag)) 69 | case reflect.Bool: 70 | decoders = append(decoders, decodeBool(set[bool](ptr, i, t), tag)) 71 | case reflect.Slice: 72 | _, sk, _ := typeKind(t.Elem()) 73 | switch sk { 74 | case reflect.String: 75 | decoders = append(decoders, decodeStrings(set[[]string](ptr, i, t), tag)) 76 | case reflect.Uint8: 77 | decoders = append(decoders, decodeBytes(set[[]byte](ptr, i, t), tag)) 78 | } 79 | default: 80 | return nil, ErrUnsupportedType 81 | } 82 | } 83 | 84 | if len(decoders) == 0 { 85 | return func(reflect.Value, Getter) error { return nil }, nil 86 | } 87 | 88 | return func(v reflect.Value, d Getter) error { 89 | if isPtr { 90 | if v.IsNil() { 91 | v.Set(reflect.New(typ)) 92 | } 93 | 94 | v = v.Elem() 95 | } 96 | 97 | for _, dec := range decoders { 98 | if err := dec(v, d); err != nil { 99 | return err 100 | } 101 | } 102 | 103 | return nil 104 | }, nil 105 | } 106 | 107 | func typeKind(t reflect.Type) (reflect.Type, reflect.Kind, bool) { 108 | var isPtr bool 109 | 110 | k := t.Kind() 111 | if k == reflect.Pointer { 112 | t = t.Elem() 113 | k = t.Kind() 114 | isPtr = true 115 | } 116 | 117 | return t, k, isPtr 118 | } 119 | 120 | func set[T any](ptr bool, i int, t reflect.Type) func(reflect.Value, T) { 121 | if ptr { 122 | return func(v reflect.Value, d T) { 123 | f := v.Field(i) 124 | if f.IsNil() { 125 | f.Set(reflect.New(t)) 126 | } 127 | 128 | *(*T)(unsafe.Pointer(f.Elem().UnsafeAddr())) = d 129 | } 130 | } 131 | 132 | return func(v reflect.Value, d T) { 133 | *(*T)(unsafe.Pointer(v.Field(i).UnsafeAddr())) = d 134 | } 135 | } 136 | 137 | func decodeString(set func(reflect.Value, string), k string) decoder { 138 | return func(v reflect.Value, g Getter) error { 139 | if s := g.Get(k); s != "" { 140 | set(v, s) 141 | } 142 | 143 | return nil 144 | } 145 | } 146 | 147 | func decodeInt(set func(reflect.Value, int), k string) decoder { 148 | return func(v reflect.Value, g Getter) error { 149 | if s := g.Get(k); s != "" { 150 | n, err := strconv.Atoi(s) 151 | if err != nil { 152 | return err 153 | } 154 | 155 | set(v, n) 156 | } 157 | 158 | return nil 159 | } 160 | } 161 | 162 | func decodeInt8(set func(reflect.Value, int8), k string) decoder { 163 | return func(v reflect.Value, g Getter) error { 164 | if s := g.Get(k); s != "" { 165 | n, err := strconv.ParseInt(s, 10, 8) 166 | if err != nil { 167 | return err 168 | } 169 | 170 | set(v, int8(n)) 171 | } 172 | 173 | return nil 174 | } 175 | } 176 | 177 | func decodeInt16(set func(reflect.Value, int16), k string) decoder { 178 | return func(v reflect.Value, g Getter) error { 179 | if s := g.Get(k); s != "" { 180 | n, err := strconv.ParseInt(s, 10, 16) 181 | if err != nil { 182 | return err 183 | } 184 | 185 | set(v, int16(n)) 186 | } 187 | 188 | return nil 189 | } 190 | } 191 | 192 | func decodeInt32(set func(reflect.Value, int32), k string) decoder { 193 | return func(v reflect.Value, g Getter) error { 194 | if s := g.Get(k); s != "" { 195 | n, err := strconv.ParseInt(s, 10, 32) 196 | if err != nil { 197 | return err 198 | } 199 | 200 | set(v, int32(n)) 201 | } 202 | 203 | return nil 204 | } 205 | } 206 | 207 | func decodeInt64(set func(reflect.Value, int64), k string) decoder { 208 | return func(v reflect.Value, g Getter) error { 209 | if s := g.Get(k); s != "" { 210 | n, err := strconv.Atoi(s) 211 | if err != nil { 212 | return err 213 | } 214 | 215 | set(v, int64(n)) 216 | } 217 | 218 | return nil 219 | } 220 | } 221 | 222 | func decodeFloat32(set func(reflect.Value, float32), k string) decoder { 223 | return func(v reflect.Value, g Getter) error { 224 | if s := g.Get(k); s != "" { 225 | f, err := strconv.ParseFloat(s, 32) 226 | if err != nil { 227 | return err 228 | } 229 | 230 | set(v, float32(f)) 231 | } 232 | 233 | return nil 234 | } 235 | } 236 | 237 | func decodeFloat64(set func(reflect.Value, float64), k string) decoder { 238 | return func(v reflect.Value, g Getter) error { 239 | if s := g.Get(k); s != "" { 240 | f, err := strconv.ParseFloat(s, 64) 241 | if err != nil { 242 | return err 243 | } 244 | 245 | set(v, f) 246 | } 247 | 248 | return nil 249 | } 250 | } 251 | 252 | func decodeUint(set func(reflect.Value, uint), k string) decoder { 253 | return func(v reflect.Value, g Getter) error { 254 | if s := g.Get(k); s != "" { 255 | n, err := strconv.ParseUint(s, 10, strconv.IntSize) 256 | if err != nil { 257 | return err 258 | } 259 | 260 | set(v, uint(n)) 261 | } 262 | 263 | return nil 264 | } 265 | } 266 | 267 | func decodeUint8(set func(reflect.Value, uint8), k string) decoder { 268 | return func(v reflect.Value, g Getter) error { 269 | if s := g.Get(k); s != "" { 270 | n, err := strconv.ParseUint(s, 10, 8) 271 | if err != nil { 272 | return err 273 | } 274 | 275 | set(v, uint8(n)) 276 | } 277 | 278 | return nil 279 | } 280 | } 281 | 282 | func decodeUint16(set func(reflect.Value, uint16), k string) decoder { 283 | return func(v reflect.Value, g Getter) error { 284 | if s := g.Get(k); s != "" { 285 | n, err := strconv.ParseUint(s, 10, 16) 286 | if err != nil { 287 | return err 288 | } 289 | 290 | set(v, uint16(n)) 291 | } 292 | 293 | return nil 294 | } 295 | } 296 | 297 | func decodeUint32(set func(reflect.Value, uint32), k string) decoder { 298 | return func(v reflect.Value, g Getter) error { 299 | if s := g.Get(k); s != "" { 300 | n, err := strconv.ParseUint(s, 10, 32) 301 | if err != nil { 302 | return err 303 | } 304 | 305 | set(v, uint32(n)) 306 | } 307 | 308 | return nil 309 | } 310 | } 311 | 312 | func decodeUint64(set func(reflect.Value, uint64), k string) decoder { 313 | return func(v reflect.Value, g Getter) error { 314 | if s := g.Get(k); s != "" { 315 | n, err := strconv.ParseUint(s, 10, 64) 316 | if err != nil { 317 | return err 318 | } 319 | 320 | set(v, n) 321 | } 322 | 323 | return nil 324 | } 325 | } 326 | 327 | func decodeBool(set func(reflect.Value, bool), k string) decoder { 328 | return func(v reflect.Value, g Getter) error { 329 | if s := g.Get(k); s != "" { 330 | b, err := strconv.ParseBool(s) 331 | if err != nil { 332 | return err 333 | } 334 | 335 | set(v, b) 336 | } 337 | 338 | return nil 339 | } 340 | } 341 | 342 | func decodeBytes(set func(reflect.Value, []byte), k string) decoder { 343 | return func(v reflect.Value, g Getter) error { 344 | if s := g.Get(k); s != "" { 345 | sp := unsafe.Pointer(&s) 346 | b := *(*[]byte)(sp) 347 | (*reflect.SliceHeader)(unsafe.Pointer(&b)).Cap = (*reflect.StringHeader)(sp).Len 348 | 349 | set(v, b) 350 | } 351 | 352 | return nil 353 | } 354 | } 355 | 356 | func decodeStrings(set func(reflect.Value, []string), k string) decoder { 357 | return func(v reflect.Value, g Getter) error { 358 | if s := g.Values(k); s != nil { 359 | set(v, s) 360 | } 361 | 362 | return nil 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /decoder/decode.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import ( 4 | "reflect" 5 | "sync" 6 | ) 7 | 8 | type Decoder struct { 9 | tag string 10 | cache sync.Map 11 | } 12 | 13 | func NewDecoder(tag string) *Decoder { 14 | return &Decoder{tag: tag} 15 | } 16 | 17 | func (d *Decoder) Decode(data Getter, v any) (err error) { 18 | val := reflect.ValueOf(v).Elem() 19 | if val.Kind() != reflect.Struct { 20 | return ErrUnsupportedType 21 | } 22 | 23 | t := val.Type() 24 | 25 | dec, ok := d.cache.Load(t) 26 | if !ok { 27 | dec, err = compile(t, d.tag, t.Kind() == reflect.Ptr) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | d.cache.Store(t, dec) 33 | } 34 | 35 | return dec.(decoder)(val, data) 36 | } 37 | -------------------------------------------------------------------------------- /decoder/map.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | type MapDecoder struct { 4 | dec *CachedDecoder 5 | } 6 | 7 | func NewMapDecoder(v interface{}, tag string) (*MapDecoder, error) { 8 | dec, err := NewCachedDecoder(v, tag) 9 | if err != nil { 10 | return nil, err 11 | } 12 | 13 | return &MapDecoder{dec}, nil 14 | } 15 | 16 | func (d *MapDecoder) Decode(data map[string][]string, v interface{}) error { 17 | return d.dec.Decode(MapGetter(data), v) 18 | } 19 | 20 | type MapGetter map[string][]string 21 | 22 | func (m MapGetter) Get(key string) string { 23 | if m == nil { 24 | return "" 25 | } 26 | 27 | vs := m[key] 28 | if len(vs) == 0 { 29 | return "" 30 | } 31 | 32 | return vs[0] 33 | } 34 | 35 | func (m MapGetter) Values(key string) []string { 36 | if m == nil { 37 | return nil 38 | } 39 | 40 | return m[key] 41 | } 42 | -------------------------------------------------------------------------------- /decoder/params.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import "github.com/julienschmidt/httprouter" 4 | 5 | type ParamsDecoder struct { 6 | dec *CachedDecoder 7 | } 8 | 9 | func NewParamsDecoder(v interface{}, tag string) (*ParamsDecoder, error) { 10 | dec, err := NewCachedDecoder(v, tag) 11 | if err != nil { 12 | return nil, err 13 | } 14 | 15 | return &ParamsDecoder{dec}, nil 16 | } 17 | 18 | func (d *ParamsDecoder) Decode(data []httprouter.Param, v interface{}) error { 19 | return d.dec.Decode(ParamsGetter(data), v) 20 | } 21 | 22 | type ParamsGetter []httprouter.Param 23 | 24 | func (ps ParamsGetter) Get(key string) string { 25 | for i := range ps { 26 | if ps[i].Key == key { 27 | return ps[i].Value 28 | } 29 | } 30 | 31 | return "" 32 | } 33 | 34 | func (ps ParamsGetter) Values(key string) []string { 35 | for i := range ps { 36 | if ps[i].Key == key { 37 | return []string{ps[i].Value} 38 | } 39 | } 40 | 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /example/__debug_bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jarrettv/go-japi/864355bc37998580dc7994dadb6d32d06b47dc21/example/__debug_bin -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | "time" 9 | 10 | "github.com/jarrettv/go-japi" 11 | "github.com/jarrettv/go-japi/problem" 12 | ) 13 | 14 | func Empty(context.Context, *japi.Empty) (interface{}, error) { 15 | return nil, nil 16 | } 17 | 18 | func Ping(context.Context, *japi.Empty) (string, error) { 19 | return "pong", nil 20 | } 21 | 22 | type GreetRequest struct { 23 | Name string `json:"name"` // Get name from JSON body 24 | Age int `header:"X-User-Age"` // Get age from HTTP header 25 | } 26 | 27 | type GreetResponse struct { 28 | Greeting string `json:"data"` 29 | } 30 | 31 | type HelloRequest struct { 32 | First string `path:"first"` // Get name from JSON body 33 | Last string `path:"last"` // Get name from JSON body 34 | } 35 | 36 | type HelloResponse struct { 37 | Hello string `json:"hello"` 38 | } 39 | 40 | func (gr *GreetResponse) StatusCode() int { 41 | return http.StatusTeapot 42 | } 43 | 44 | func (gr *GreetResponse) Header() http.Header { 45 | header := http.Header{} 46 | header.Set("foo", "bar") 47 | return header 48 | } 49 | 50 | func Greet(ctx context.Context, req *GreetRequest) (*GreetResponse, error) { 51 | if strings.TrimSpace(req.Name) == "" { 52 | return nil, problem.Validation(map[string]string{ 53 | "name": "required", 54 | }) 55 | } 56 | res := &GreetResponse{ 57 | Greeting: fmt.Sprintf("Hello %s, you're %d years old.", req.Name, req.Age), 58 | } 59 | return res, nil 60 | } 61 | 62 | func Hello(ctx context.Context, req *HelloRequest) (*HelloResponse, error) { 63 | res := &HelloResponse{ 64 | Hello: fmt.Sprintf("Hello %s %s", req.First, req.Last), 65 | } 66 | return res, nil 67 | } 68 | 69 | func main() { 70 | r := japi.New(&japi.Config{ 71 | ProblemConfig: problem.ProblemConfig{ 72 | ProblemTypeUrlFormat: "https://example.com/errors/%s", 73 | ProblemInstanceFunc: func(ctx context.Context) string { 74 | return fmt.Sprintf("https://example.com/trace/%d", time.Now().UnixMilli()) 75 | }, 76 | }, 77 | }) 78 | r.Get("/", japi.H(Empty)) 79 | 80 | g := r.Group("/api") 81 | g.Get("/ping", japi.H(Ping)) 82 | g.Post("/greet", japi.H(Greet)) 83 | g.Post("/hello/:first/:last", japi.H(Hello)) 84 | http.ListenAndServe(":8080", r.Router()) 85 | } 86 | -------------------------------------------------------------------------------- /example/test.http: -------------------------------------------------------------------------------- 1 | GET http://localhost:8080/ 2 | 3 | ### 4 | 5 | GET http://localhost:8080/api/ping 6 | Accept: application/json 7 | 8 | ### 9 | 10 | GET http://localhost:8080/not-found 11 | Accept: application/json 12 | 13 | ### 14 | 15 | POST http://localhost:8080/api/greet 16 | Accept: application/json 17 | 18 | {"name": ""} 19 | 20 | ### 21 | 22 | POST http://localhost:8080/api/greet 23 | Accept: application/json 24 | X-User-Age: 80 25 | 26 | {"name": "John"} 27 | 28 | ### 29 | 30 | POST http://localhost:8080/api/hello/John/Doe 31 | Accept: application/json -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jarrettv/go-japi 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/goccy/go-json v0.9.6 7 | github.com/julienschmidt/httprouter v1.3.1-0.20200921135023-fe77dd05ab5a 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/goccy/go-json v0.9.6 h1:5/4CtRQdtsX0sal8fdVhTaiMN01Ri8BExZZ8iRmHQ6E= 2 | github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 3 | github.com/julienschmidt/httprouter v1.3.1-0.20200921135023-fe77dd05ab5a h1:VTF3sHLbpm2PdWMPKVWUMwKg85VE7Ep7wgBw8ETYri8= 4 | github.com/julienschmidt/httprouter v1.3.1-0.20200921135023-fe77dd05ab5a/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 5 | -------------------------------------------------------------------------------- /group.go: -------------------------------------------------------------------------------- 1 | package japi 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | ) 7 | 8 | type group struct { 9 | r *API 10 | prefix string 11 | } 12 | 13 | func (g *group) Get(path string, handle http.Handler) { 14 | g.Handle(http.MethodGet, path, handle) 15 | } 16 | 17 | func (g *group) Post(path string, handle http.Handler) { 18 | g.Handle(http.MethodPost, path, handle) 19 | } 20 | 21 | func (g *group) Put(path string, handle http.Handler) { 22 | g.Handle(http.MethodPut, path, handle) 23 | } 24 | 25 | func (g *group) Patch(path string, handle http.Handler) { 26 | g.Handle(http.MethodPatch, path, handle) 27 | } 28 | 29 | func (g *group) Delete(path string, handle http.Handler) { 30 | g.Handle(http.MethodDelete, path, handle) 31 | } 32 | 33 | func (g *group) Handle(method, path string, handle http.Handler) { 34 | g.r.Handle(method, g.prefix+path, handle) 35 | } 36 | 37 | func (g *group) HandleFunc(method, path string, handle http.HandlerFunc) { 38 | g.Handle(method, path, handle) 39 | } 40 | 41 | func (g *group) Group(path string) Router { 42 | return &group{prefix: g.prefix + path, r: g.r} 43 | } 44 | 45 | func (g *group) Use(mw ...Middleware) { 46 | g.r.Use(func(next http.Handler) http.Handler { 47 | mwNext := next 48 | 49 | // TODO (jv) do we need to reverse? 50 | for _, fn := range mw { 51 | mwNext = fn(mwNext) 52 | } 53 | 54 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 55 | // Only use the middleware if path belongs to group. 56 | if strings.HasPrefix(r.URL.Path, g.prefix) { 57 | mwNext.ServeHTTP(w, r) 58 | } else { 59 | next.ServeHTTP(w, r) 60 | } 61 | }) 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package japi 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/goccy/go-json" 8 | 9 | "github.com/jarrettv/go-japi/decoder" 10 | "github.com/jarrettv/go-japi/problem" 11 | "github.com/julienschmidt/httprouter" 12 | ) 13 | 14 | // StatusCoder allows you to customise the HTTP response code. 15 | type StatusCoder interface { 16 | StatusCode() int 17 | } 18 | 19 | // Headerer allows you to customise the HTTP headers. 20 | type Headerer interface { 21 | Header() http.Header 22 | } 23 | 24 | // Problemer allows you to customize the error problem details. 25 | type Problemer interface { 26 | Problem() problem.Problem 27 | } 28 | 29 | // Handler allows you to handle request with the route params 30 | type Handler interface { 31 | http.Handler 32 | handle(http.ResponseWriter, *http.Request, httprouter.Params) 33 | } 34 | 35 | // H wraps your handler function with the Go generics magic. 36 | func H[T any, O any](handle Handle[T, O]) Handler { 37 | h := &handler[T, O]{handler: handle} 38 | 39 | var t T 40 | 41 | if hasTag(t, headerTag) { 42 | dec, err := decoder.NewCachedDecoder(t, headerTag) 43 | if err == nil { 44 | h.decodeHeader = dec 45 | } 46 | } 47 | 48 | if hasTag(t, queryTag) { 49 | dec, err := decoder.NewMapDecoder(t, queryTag) 50 | if err == nil { 51 | h.decodeQuery = dec 52 | } 53 | } 54 | 55 | if hasTag(t, pathTag) { 56 | dec, err := decoder.NewParamsDecoder(t, pathTag) 57 | if err == nil { 58 | h.decodePath = dec 59 | } 60 | } 61 | 62 | return h 63 | } 64 | 65 | // E creates a Handler that returns the error 66 | func E(err error) Handler { 67 | return H(func(context.Context, *Empty) (*Empty, error) { 68 | return nil, err 69 | }) 70 | } 71 | 72 | // LoadRawJson loads raw json to response useful for swagger docs. 73 | func LoadRawJson(loadRawJson func() ([]byte, error)) http.HandlerFunc { 74 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 75 | json, e := loadRawJson() 76 | if e != nil { 77 | http.Error(w, e.Error(), http.StatusInternalServerError) 78 | return 79 | } 80 | RawJson(json) 81 | }) 82 | } 83 | 84 | // RawJson sends raw json to response useful for swagger docs. 85 | func RawJson(data []byte) http.HandlerFunc { 86 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 87 | w.Header().Set("Content-Type", "application/json") 88 | _, e := w.Write(data) 89 | if e != nil { 90 | http.Error(w, e.Error(), http.StatusInternalServerError) 91 | } 92 | }) 93 | } 94 | 95 | // Handle is the type for your handlers. 96 | type Handle[T any, O any] func(ctx context.Context, request T) (O, error) 97 | 98 | type handler[T any, O any] struct { 99 | config *Config 100 | handler Handle[T, O] 101 | decodeHeader *decoder.CachedDecoder 102 | decodePath *decoder.ParamsDecoder 103 | decodeQuery *decoder.MapDecoder 104 | isNil func(v any) bool 105 | } 106 | 107 | func (h *handler[T, O]) ServeHTTP(w http.ResponseWriter, r *http.Request) { 108 | h.handle(w, r, nil) 109 | } 110 | 111 | //nolint:gocognit,cyclop 112 | func (h *handler[T, O]) handle(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 113 | if h.config.RouteLogFunc != nil { 114 | route := p.MatchedRoutePath() 115 | vars := make(map[string]string, len(p)) 116 | for _, param := range p { 117 | if param.Value != route { 118 | vars[param.Key] = param.Value 119 | } 120 | } 121 | h.config.RouteLogFunc(r.Context(), route, vars) // TODO (jv) get route 122 | } 123 | 124 | serveProblem := func(p *problem.Problem) { 125 | h.config.Enrich(r.Context(), p) 126 | if h.config.ProblemLogFunc != nil { 127 | h.config.ProblemLogFunc(r.Context(), p) 128 | } 129 | p.ServeJSON(w) 130 | } 131 | 132 | serveRequestProblem := func(e error) { 133 | p := problem.BadRequest(e) 134 | serveProblem(p) 135 | } 136 | 137 | req := new(T) 138 | 139 | // Decode the header 140 | if h.decodeHeader != nil { 141 | e := h.decodeHeader.Decode(r.Header, req) 142 | if e != nil { 143 | serveRequestProblem(e) 144 | return 145 | } 146 | } 147 | 148 | // Decode the URL query 149 | if h.decodeQuery != nil && r.URL.RawQuery != "" { 150 | e := h.decodeQuery.Decode(r.URL.Query(), req) 151 | if e != nil { 152 | serveRequestProblem(e) 153 | return 154 | } 155 | } 156 | 157 | // Decode the path params 158 | if h.decodePath != nil && len(p) != 0 { 159 | e := h.decodePath.Decode(p, req) 160 | if e != nil { 161 | serveRequestProblem(e) 162 | return 163 | } 164 | } 165 | 166 | // Decode the body 167 | if r.ContentLength > 0 { 168 | dec, e := getDecoder(JsonEncoding) 169 | if e != nil { 170 | serveRequestProblem(e) // http.ErrNotSupported 171 | return 172 | } 173 | 174 | if e := dec(r, req); e != nil { 175 | serveRequestProblem(e) 176 | return 177 | } 178 | } 179 | 180 | var res any 181 | res, e := h.handler(r.Context(), *req) 182 | w.Header().Set("Content-Type", JsonEncoding+"; charset=utf-8") 183 | if e != nil { 184 | if pb, ok := e.(Problemer); ok { 185 | p := pb.Problem() 186 | serveProblem(&p) 187 | return 188 | } else if p, ok := e.(*problem.Problem); ok { 189 | serveProblem(p) 190 | return 191 | } else { 192 | p := problem.Unexpected(e) 193 | serveProblem(p) 194 | return 195 | } 196 | } 197 | 198 | if h, ok := res.(Headerer); ok { 199 | headers := w.Header() 200 | for k, v := range h.Header() { 201 | headers[k] = v 202 | } 203 | } 204 | 205 | if sc, ok := res.(StatusCoder); ok { 206 | w.WriteHeader(sc.StatusCode()) 207 | } 208 | 209 | if e = json.NewEncoder(w).Encode(res); e != nil { 210 | p := problem.Unexpected(e) 211 | serveProblem(p) 212 | } 213 | } 214 | 215 | func (h *handler[T, O]) setConfig(r *Config) { 216 | h.config = r 217 | } 218 | 219 | const ( 220 | headerTag = "header" 221 | pathTag = "path" 222 | queryTag = "query" 223 | ) 224 | -------------------------------------------------------------------------------- /problem/problem.go: -------------------------------------------------------------------------------- 1 | package problem 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | // Problem is the struct definition of a problem details object 9 | type Problem struct { 10 | // Type is a URI reference [RFC3986] that identifies the 11 | // problem type. This specification encourages that, when 12 | // dereferenced, it provide human-readable documentation for the 13 | // problem type (e.g., using HTML [W3C.REC-html5-20141028]). When 14 | // this member is not present, its value is assumed to be 15 | // "about:blank". 16 | Type string `json:"type"` 17 | // Title is a short, human-readable summary of the problem 18 | // type. It SHOULD NOT change from occurrence to occurrence of the 19 | // problem, except for purposes of localization (e.g., using 20 | // proactive content negotiation; see [RFC7231], Section 3.4). 21 | Title string `json:"title"` 22 | // Status is the HTTP status code ([RFC7231], Section 6) 23 | // generated by the origin server for this occurrence of the problem. 24 | Status int `json:"status,omitempty"` 25 | // Detail is a human-readable explanation specific to this 26 | // occurrence of the problem. 27 | // If present, it ought to focus on helping the client 28 | // correct the problem, rather than giving debugging information. 29 | Detail string `json:"detail,omitempty"` 30 | // Instance is a URI reference that identifies the specific 31 | // occurrence of the problem. It may or may not yield further 32 | // information if dereferenced. 33 | Instance string `json:"instance,omitempty"` 34 | // Params are request input field level errors. They communicate 35 | // back to client hints as to the exact problem. 36 | Params map[string]string `json:"params,omitempty"` 37 | } 38 | 39 | // Error implements the error interface 40 | func (pd *Problem) Error() string { 41 | if pd.Title == "" && pd.Detail == "" { 42 | return fmt.Sprintf("Status %d", pd.Status) 43 | } 44 | 45 | if pd.Detail == "" { 46 | return fmt.Sprintf("%s", pd.Title) 47 | } 48 | return fmt.Sprintf("%s: %s", pd.Title, pd.Detail) 49 | } 50 | 51 | // New creates a new Problem with given info. 52 | func New(statusCode int, problemType, title, detail, instance string, params map[string]string) *Problem { 53 | // When this member is not present, its value is assumed to be 54 | // "about:blank". 55 | if problemType == "" { 56 | problemType = "about:blank" 57 | } 58 | 59 | // When "about:blank" is used, the title SHOULD be the same as the 60 | // recommended HTTP status phrase for that code (e.g., "Not Found" for 61 | // 404, and so on), although it MAY be localized to suit client 62 | // preferences (expressed with the Accept-Language request header). 63 | if problemType == "about:blank" { 64 | title = http.StatusText(statusCode) 65 | } 66 | 67 | return &Problem{ 68 | Type: problemType, 69 | Title: title, 70 | Status: statusCode, 71 | Detail: detail, 72 | Instance: instance, 73 | Params: params, 74 | } 75 | } 76 | 77 | // Status creates a new ProblemDetails error based just on the HTTP status code. 78 | func Status(statusCode int) *Problem { 79 | return New(statusCode, "", "", "", "", nil) 80 | } 81 | 82 | // Unexpected will create a new problem for unexpected errors. 83 | func Unexpected(err error) *Problem { 84 | return New(http.StatusInternalServerError, "unexpected", "Unexpected problem", 85 | err.Error(), "", nil) 86 | } 87 | 88 | // NotFound will create a new problem for when the record is not found. 89 | func NotFound() *Problem { 90 | return New(http.StatusNotFound, "not-found", "Record not found", 91 | "", "", nil) 92 | } 93 | 94 | // NotPermitted will create a new problem for when the user is forbidden access. 95 | func NotPermitted(username string) *Problem { 96 | detail := fmt.Sprintf("%s does not have proper permissions", username) 97 | return New(http.StatusForbidden, "not-permitted", "User not permitted", 98 | detail, "", nil) 99 | } 100 | 101 | // BadRequest will create a new problem for reqeust marshalling errors. 102 | func BadRequest(err error) *Problem { 103 | return New(http.StatusBadRequest, "bad-request", "Bad or malformed request", 104 | "Fix the error and try again", "", nil) 105 | } 106 | 107 | // Validation will create a new problem for when request has field validation errors. 108 | func Validation(params map[string]string) *Problem { 109 | return New(http.StatusBadRequest, "validation", "Validation failed", 110 | "Fix the errors and try again", "", params) 111 | } 112 | 113 | // RuleViolated will create a new problem for when a business rule is violated. 114 | func RuleViolated(rule string) *Problem { 115 | return New(http.StatusBadRequest, "rule-violated", "Rule violated", 116 | rule, "", nil) 117 | } 118 | 119 | // NotCurrent will create a new problem for optimistic concurrency errors. 120 | func NotCurrent() *Problem { 121 | return New(http.StatusConflict, "not-current", "Record not current", 122 | "Reload and try your changes again", "", nil) 123 | } 124 | -------------------------------------------------------------------------------- /problem/util.go: -------------------------------------------------------------------------------- 1 | package problem 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "strings" 9 | ) 10 | 11 | type ProblemConfig struct { 12 | // the URI to be string formatted with default type codes 13 | ProblemTypeUrlFormat string 14 | // the function to return URI for this problem instance 15 | ProblemInstanceFunc func(ctx context.Context) string 16 | } 17 | 18 | // Enrich will alter the type and instance of the problem as configured. 19 | func (cfg ProblemConfig) Enrich(ctx context.Context, p *Problem) { 20 | if cfg.ProblemTypeUrlFormat != "" && p.Type != "about:blank" && !strings.HasPrefix(p.Type, "http") { 21 | p.Type = fmt.Sprintf(cfg.ProblemTypeUrlFormat, p.Type) 22 | } 23 | if cfg.ProblemInstanceFunc != nil { 24 | p.Instance = cfg.ProblemInstanceFunc(ctx) 25 | } 26 | } 27 | 28 | // ServeJSON will output Problem Details json to the response writer. 29 | func (pd *Problem) ServeJSON(w http.ResponseWriter) error { 30 | w.Header().Set("Content-Type", "application/problem+json") 31 | w.WriteHeader(pd.Status) 32 | if err := json.NewEncoder(w).Encode(pd); err != nil { 33 | return err 34 | } 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /tagcheck.go: -------------------------------------------------------------------------------- 1 | package japi 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | func hasTag(v interface{}, tag string) bool { 8 | t := reflect.TypeOf(v) 9 | 10 | if t.Kind() == reflect.Ptr { 11 | t = t.Elem() 12 | } 13 | 14 | if t.Kind() != reflect.Struct { 15 | return false 16 | } 17 | 18 | return typeHasTag(t, tag) 19 | } 20 | 21 | func typeHasTag(t reflect.Type, tag string) bool { 22 | for i := 0; i < t.NumField(); i++ { 23 | f := t.Field(i) 24 | 25 | if f.PkgPath != "" { 26 | continue // skip unexported fields 27 | } 28 | 29 | if _, ok := f.Tag.Lookup(tag); ok { 30 | return true 31 | } 32 | 33 | ft := f.Type 34 | if ft.Kind() == reflect.Ptr { 35 | ft = ft.Elem() 36 | } 37 | 38 | if ft.Kind() == reflect.Struct { 39 | if typeHasTag(ft, tag) { 40 | return true 41 | } 42 | } 43 | } 44 | 45 | return false 46 | } 47 | --------------------------------------------------------------------------------