-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
support for new ServeMux functionality (#35)
- Loading branch information
1 parent
221cb72
commit 147fca2
Showing
5 changed files
with
382 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
package main | ||
|
||
import ( | ||
"encoding/json" | ||
"github.com/jakecoffman/crud" | ||
"log" | ||
"math/rand" | ||
"net/http" | ||
) | ||
|
||
// This example uses the built-in ServeMux with added functionality in Go 1.22. | ||
// To see examples using other routers go to the adapaters directory. | ||
|
||
func main() { | ||
r := crud.NewRouter("Widget API", "1.0.0", crud.NewServeMuxAdapter()) | ||
|
||
if err := r.Add(Routes...); err != nil { | ||
log.Fatal(err) | ||
} | ||
|
||
log.Println("Serving http://127.0.0.1:8080") | ||
err := r.Serve("127.0.0.1:8080") | ||
if err != nil { | ||
log.Println(err) | ||
} | ||
} | ||
|
||
var tags = []string{"Widgets"} | ||
|
||
var Routes = []crud.Spec{{ | ||
Method: "GET", | ||
Path: "/widgets", | ||
Handler: ok, | ||
Description: "Lists widgets", | ||
Tags: tags, | ||
Validate: crud.Validate{ | ||
Query: crud.Object(map[string]crud.Field{ | ||
"limit": crud.Number().Required().Min(0).Max(25).Description("Records to return"), | ||
"ids": crud.Array().Items(crud.Number()), | ||
}), | ||
}, | ||
}, { | ||
Method: "POST", | ||
Path: "/widgets", | ||
PreHandlers: fakeAuthPreHandler, | ||
Handler: bindAndOk, | ||
Description: "Adds a widget", | ||
Tags: tags, | ||
Validate: crud.Validate{ | ||
Body: crud.Object(map[string]crud.Field{ | ||
"name": crud.String().Required().Example("Bob"), | ||
"arrayMatey": crud.Array().Items(crud.Number()), | ||
}), | ||
}, | ||
Responses: map[string]crud.Response{ | ||
"200": { | ||
Schema: crud.JsonSchema{ | ||
Type: crud.KindObject, | ||
Properties: map[string]crud.JsonSchema{ | ||
"hello": {Type: crud.KindString}, | ||
}, | ||
}, | ||
Description: "OK", | ||
}, | ||
}, | ||
}, { | ||
Method: "GET", | ||
Path: "/widgets/{id}", | ||
Handler: ok, | ||
Description: "Updates a widget", | ||
Tags: tags, | ||
Validate: crud.Validate{ | ||
Path: crud.Object(map[string]crud.Field{ | ||
"id": crud.Number().Required(), | ||
}), | ||
}, | ||
}, { | ||
Method: "PUT", | ||
Path: "/widgets/{id}", | ||
Handler: bindAndOk, | ||
Description: "Updates a widget", | ||
Tags: tags, | ||
Validate: crud.Validate{ | ||
Path: crud.Object(map[string]crud.Field{ | ||
"id": crud.Number().Required(), | ||
}), | ||
Body: crud.Object(map[string]crud.Field{ | ||
"name": crud.String().Required(), | ||
}), | ||
}, | ||
}, { | ||
Method: "DELETE", | ||
Path: "/widgets/{id}", | ||
Handler: ok, | ||
Description: "Deletes a widget", | ||
Tags: tags, | ||
Validate: crud.Validate{ | ||
Path: crud.Object(map[string]crud.Field{ | ||
"id": crud.Number().Required(), | ||
}), | ||
}, | ||
}, | ||
} | ||
|
||
func fakeAuthPreHandler(next http.Handler) http.Handler { | ||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
if rand.Intn(2) == 0 { | ||
w.WriteHeader(http.StatusTeapot) | ||
_, _ = w.Write([]byte("Random rejection from PreHandler")) | ||
return | ||
} | ||
next.ServeHTTP(w, r) | ||
}) | ||
} | ||
|
||
func ok(w http.ResponseWriter, r *http.Request) { | ||
_ = json.NewEncoder(w).Encode(r.URL.Query()) | ||
} | ||
|
||
func bindAndOk(w http.ResponseWriter, r *http.Request) { | ||
var widget interface{} | ||
if err := json.NewDecoder(r.Body).Decode(&widget); err != nil { | ||
w.WriteHeader(400) | ||
_ = json.NewEncoder(w).Encode(err.Error()) | ||
return | ||
} | ||
_ = json.NewEncoder(w).Encode(widget) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
package crud | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/url" | ||
"reflect" | ||
) | ||
|
||
type ServeMuxAdapter struct { | ||
Engine *http.ServeMux | ||
} | ||
|
||
func NewServeMuxAdapter() *ServeMuxAdapter { | ||
return &ServeMuxAdapter{ | ||
Engine: http.NewServeMux(), | ||
} | ||
} | ||
|
||
type MiddlewareFunc func(http.Handler) http.Handler | ||
|
||
func (a *ServeMuxAdapter) Install(r *Router, spec *Spec) error { | ||
middlewares := []MiddlewareFunc{ | ||
validateHandlerMiddleware(r, spec), | ||
} | ||
|
||
switch v := spec.PreHandlers.(type) { | ||
case nil: | ||
case []MiddlewareFunc: | ||
middlewares = append(middlewares, v...) | ||
case MiddlewareFunc: | ||
middlewares = append(middlewares, v) | ||
case func(http.Handler) http.Handler: | ||
middlewares = append(middlewares, v) | ||
default: | ||
return fmt.Errorf("PreHandlers must be MiddlewareFunc, got: %v", reflect.TypeOf(spec.Handler)) | ||
} | ||
|
||
var finalHandler http.Handler | ||
switch v := spec.Handler.(type) { | ||
case nil: | ||
return fmt.Errorf("handler must not be nil") | ||
case http.HandlerFunc: | ||
finalHandler = v | ||
case func(http.ResponseWriter, *http.Request): | ||
finalHandler = http.HandlerFunc(v) | ||
case http.Handler: | ||
finalHandler = v | ||
default: | ||
return fmt.Errorf("handler must be http.HandlerFunc, got %v", reflect.TypeOf(spec.Handler)) | ||
} | ||
|
||
// install the route, use a subrouter so the "use" is scoped | ||
path := fmt.Sprintf("%s %s", spec.Method, spec.Path) | ||
subrouter := http.NewServeMux() | ||
subrouter.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { | ||
handler := finalHandler | ||
for i := len(middlewares) - 1; i >= 0; i-- { | ||
handler = middlewares[i](handler) | ||
} | ||
handler.ServeHTTP(w, r) | ||
}) | ||
a.Engine.Handle(path, subrouter) | ||
|
||
return nil | ||
} | ||
|
||
func (a *ServeMuxAdapter) Serve(swagger *Swagger, addr string) error { | ||
a.Engine.HandleFunc("GET /swagger.json", func(w http.ResponseWriter, r *http.Request) { | ||
_ = json.NewEncoder(w).Encode(swagger) | ||
}) | ||
|
||
a.Engine.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { | ||
w.Header().Set("content-type", "text/html") | ||
_, err := w.Write(SwaggerUiTemplate) | ||
if err != nil { | ||
panic(err) | ||
} | ||
}) | ||
|
||
return http.ListenAndServe(addr, a.Engine) | ||
} | ||
|
||
func validateHandlerMiddleware(router *Router, spec *Spec) MiddlewareFunc { | ||
return func(next http.Handler) http.Handler { | ||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
val := spec.Validate | ||
var query url.Values | ||
var body interface{} | ||
var path map[string]string | ||
|
||
if val.Path.Initialized() { | ||
path = map[string]string{} | ||
for name := range val.Path.obj { | ||
path[name] = r.PathValue(name) | ||
} | ||
} | ||
|
||
var rewriteBody bool | ||
if val.Body.Initialized() && val.Body.Kind() != KindFile { | ||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil { | ||
w.WriteHeader(400) | ||
_ = json.NewEncoder(w).Encode("failure decoding body: " + err.Error()) | ||
return | ||
} | ||
rewriteBody = true | ||
} | ||
|
||
var rewriteQuery bool | ||
if val.Query.Initialized() { | ||
query = r.URL.Query() | ||
rewriteQuery = true | ||
} | ||
|
||
if err := router.Validate(val, query, body, path); err != nil { | ||
w.WriteHeader(400) | ||
_ = json.NewEncoder(w).Encode(err.Error()) | ||
return | ||
} | ||
|
||
// Validate can strip values that are not valid, so we rewrite them | ||
// after validation is complete. Can't use defer as in other adapters | ||
// because next.ServeHTTP calls the next handler and defer hasn't | ||
// run yet. | ||
if rewriteBody { | ||
data, _ := json.Marshal(body) | ||
_ = r.Body.Close() | ||
r.Body = io.NopCloser(bytes.NewReader(data)) | ||
} | ||
if rewriteQuery { | ||
r.URL.RawQuery = query.Encode() | ||
} | ||
|
||
next.ServeHTTP(w, r) | ||
}) | ||
} | ||
} |
Oops, something went wrong.