Created
October 26, 2022 22:28
-
-
Save scottcagno/f8eb7f55d943e4225dc22740b5be8b61 to your computer and use it in GitHub Desktop.
RESTful API Idea in Go
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
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
"io" | |
"log" | |
"net/http" | |
"net/url" | |
) | |
type Book struct { | |
ID string | |
} | |
type BookResource struct { | |
books []Book | |
} | |
func (b *BookResource) GetAll() http.Handler { | |
fn := func(w http.ResponseWriter, r *http.Request) { | |
fmt.Fprintf(w, "[BooksResource] GetAll()") | |
} | |
return http.HandlerFunc(fn) | |
} | |
func (b *BookResource) GetOne(id string) http.Handler { | |
fn := func(w http.ResponseWriter, r *http.Request) { | |
fmt.Fprintf(w, "[BooksResource] GetOne(id: %s)", id) | |
} | |
return http.HandlerFunc(fn) | |
} | |
func (b *BookResource) AddOne(r *http.Request) http.Handler { | |
fn := func(w http.ResponseWriter, r *http.Request) { | |
fmt.Fprintf(w, "[BooksResource] AddOne()") | |
} | |
return http.HandlerFunc(fn) | |
} | |
func (b *BookResource) SetOne(r *http.Request, id string) http.Handler { | |
fn := func(w http.ResponseWriter, r *http.Request) { | |
fmt.Fprintf(w, "[BooksResource] SetOne(id: %s)", id) | |
} | |
return http.HandlerFunc(fn) | |
} | |
func (b *BookResource) DelOne(id string) http.Handler { | |
fn := func(w http.ResponseWriter, r *http.Request) { | |
fmt.Fprintf(w, "[BooksResource] DelOne(id: %s)", id) | |
} | |
return http.HandlerFunc(fn) | |
} | |
type BookHandler struct { | |
books []Book | |
} | |
func (b *BookHandler) GetAll(w http.ResponseWriter, r *http.Request) { | |
fmt.Fprintf(w, "[BookHandler] GetAll()") | |
} | |
func (b *BookHandler) GetOne(w http.ResponseWriter, r *http.Request) { | |
fmt.Fprintf(w, "[BookHandler] GetOne(id: %s)", r.URL.Query().Get("id")) | |
} | |
func (b *BookHandler) AddOne(w http.ResponseWriter, r *http.Request) { | |
fmt.Fprintf(w, "[BookHandler] AddOne()") | |
} | |
func (b *BookHandler) SetOne(w http.ResponseWriter, r *http.Request) { | |
fmt.Fprintf(w, "[BookHandler] SetOne(id: %s)", r.URL.Query().Get("id")) | |
} | |
func (b *BookHandler) DelOne(w http.ResponseWriter, r *http.Request) { | |
fmt.Fprintf(w, "[BookHandler] DelOne(id: %s)", r.URL.Query().Get("id")) | |
} | |
func main() { | |
mux := http.NewServeMux() | |
api := NewAPI("/api/", mux) | |
//api.RegisterResource("books", new(BookResource)) | |
api.RegisterHandler("books", new(BookHandler)) | |
log.Fatal(http.ListenAndServe(":8080", api)) | |
} | |
type API struct { | |
base string | |
mux *http.ServeMux | |
} | |
func NewAPI(base string, mux *http.ServeMux) *API { | |
if mux == nil { | |
mux = http.NewServeMux() | |
} | |
api := &API{ | |
base: clean(base), | |
mux: mux, | |
} | |
api.mux.Handle("/", http.RedirectHandler(api.base, http.StatusSeeOther)) | |
return api | |
} | |
func (api *API) RegisterResource(name string, re Resource) { | |
r := &resource{ | |
name: name, | |
path: clean(api.base + name), | |
Resource: re, | |
} | |
api.mux.Handle(r.path, r) | |
} | |
func (api *API) RegisterHandler(name string, re ResourceHandler) { | |
r := &resourceHandler{ | |
name: name, | |
path: clean(api.base + name), | |
re: re, | |
} | |
api.mux.Handle(r.path, r) | |
} | |
func (api *API) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |
// lookup resource handler | |
rh, pat := api.mux.Handler(r) | |
// do something with the pattern if we need to | |
_ = pat | |
// call the resource handler | |
rh.ServeHTTP(w, r) | |
} | |
type Resource interface { | |
// GetAll returns a http.Handler that locates and returns all | |
// the implementing resource items. | |
GetAll() http.Handler | |
// GetOne takes an identifier and returns a http.Handler that | |
// locates and returns the resource item with the matching | |
// identifier. | |
GetOne(id string) http.Handler | |
// AddOne takes a serialized resource item (written to the request | |
// body) and returns a http.Handler that adds the serialized item | |
// to the resource set. | |
AddOne(r *http.Request) http.Handler | |
// SetOne takes an identifier along with a serialized resource | |
// item (written to the request body) and returns a http.Handler | |
// that locates and updates the resource item that has a matching | |
// identifier. | |
SetOne(r *http.Request, id string) http.Handler | |
// DelOne takes an identifier and returns a http.Handler that | |
// locates and deletes the resource item with the matching | |
// identifier. | |
DelOne(id string) http.Handler | |
} | |
type resource struct { | |
name string | |
path string | |
Resource | |
} | |
func LogRequest(r *http.Request, msg string) { | |
log.Printf("method=%q, path=%q, msg=%q\n", r.Method, r.RequestURI, msg) | |
} | |
func (re *resource) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |
params := r.URL.Query() | |
var h http.Handler | |
switch r.Method { | |
case http.MethodGet: | |
if len(params) > 0 { | |
LogRequest(r, "get one") | |
h = re.Resource.GetOne(params.Get("id")) | |
goto serve | |
} | |
LogRequest(r, "get all") | |
h = re.GetAll() | |
goto serve | |
case http.MethodPost: | |
LogRequest(r, "add one") | |
h = re.AddOne(r) | |
goto serve | |
case http.MethodPut: | |
if len(params) > 0 { | |
LogRequest(r, "set one") | |
h = re.SetOne(r, params.Get("id")) | |
goto serve | |
} | |
case http.MethodDelete: | |
if len(params) > 0 { | |
LogRequest(r, "del one") | |
h = re.DelOne(params.Get("id")) | |
goto serve | |
} | |
default: | |
LogRequest(r, "not found") | |
h = http.NotFoundHandler() | |
goto serve | |
} | |
serve: | |
h.ServeHTTP(w, r) | |
} | |
type ResourceHandler interface { | |
// GetAll implements http.Handler and is responsible for locating | |
// and returns all the implementing resource items. | |
GetAll(w http.ResponseWriter, r *http.Request) | |
// GetOne implements http.Handler and is responsible for locating | |
// and returning the resource item with the matching identifier. | |
// Note: the user is responsible for obtaining the identifier from | |
// the request. | |
GetOne(w http.ResponseWriter, r *http.Request) | |
// AddOne implements http.Handler and is responsible for locating | |
// the provided serialized resource item (written to the request | |
// body) and adding the serialized item to the resource set. | |
AddOne(w http.ResponseWriter, r *http.Request) | |
// SetOne implements http.Handler and is responsible for locating | |
// an identifier along with a serialized resource item (written to | |
// the request body) and updating the resource item that has a | |
// matching identifier. | |
SetOne(w http.ResponseWriter, r *http.Request) | |
// DelOne implements http.Handler and is responsible for locating | |
// an identifier and deleting the resource item with the matching | |
// identifier. | |
DelOne(w http.ResponseWriter, r *http.Request) | |
} | |
type resourceHandler struct { | |
name string | |
path string | |
re ResourceHandler | |
} | |
func (rh *resourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | |
hasID := len(r.URL.Query()) > 0 | |
switch r.Method { | |
case http.MethodGet: | |
if hasID { | |
LogRequest(r, "get one") | |
rh.re.GetOne(w, r) | |
return | |
} | |
LogRequest(r, "get all") | |
rh.re.GetAll(w, r) | |
return | |
case http.MethodPost: | |
LogRequest(r, "add one") | |
rh.re.AddOne(w, r) | |
return | |
case http.MethodPut: | |
if hasID { | |
LogRequest(r, "set one") | |
rh.re.SetOne(w, r) | |
return | |
} | |
case http.MethodDelete: | |
if hasID { | |
LogRequest(r, "del one") | |
rh.re.DelOne(w, r) | |
} | |
default: | |
LogRequest(r, "not found") | |
http.NotFoundHandler().ServeHTTP(w, r) | |
return | |
} | |
} | |
func AsJSON(w http.ResponseWriter, data any) { | |
w.Header().Set("Content-Type", "application/json") | |
if err := json.NewEncoder(w).Encode(data); err != nil { | |
w.WriteHeader(http.StatusExpectationFailed) | |
return | |
} | |
w.WriteHeader(http.StatusOK) | |
return | |
} | |
type Request struct { | |
Method string | |
Path string | |
Params url.Values | |
Handler http.Handler | |
} | |
func NewRequest(method, path string, h http.Handler) *Request { | |
uri, err := url.Parse(path) | |
if err != nil { | |
panic(err) | |
} | |
return &Request{ | |
Method: method, | |
Path: uri.Path, | |
Params: uri.Query(), | |
Handler: h, | |
} | |
} | |
func match(r *http.Request, method, path string, params url.Values) bool { | |
if r.Method != method { | |
return false | |
} | |
if r.URL.Path != path { | |
return false | |
} | |
if params != nil && len(params) > 0 { | |
reqParams := r.URL.Query() | |
for k, _ := range params { | |
if !reqParams.Has(k) { | |
return false | |
} | |
} | |
} | |
return true | |
} | |
func (r *Request) Matches(req *http.Request) bool { | |
if r.Method != req.Method { | |
return false | |
} | |
if r.Path != req.URL.Path { | |
return false | |
} | |
if r.Params != nil && len(r.Params) > 0 { | |
reqParams := req.URL.Query() | |
for key, _ := range r.Params { | |
if !reqParams.Has(key) { | |
return false | |
} | |
} | |
} | |
return true | |
} | |
type Response struct { | |
Code int | |
Message string | |
ContentType string | |
Body io.Writer | |
} | |
func clean(path string) string { | |
if path == "" { | |
return "/" | |
} | |
if path[0] != '/' { | |
path = "/" + path | |
} | |
if path[len(path)-1] != '/' { | |
path = path + "/" | |
} | |
out := lazybuf{s: path} | |
r, n := 0, len(path) | |
for r < n { | |
switch { | |
case path[r] == '/': | |
// empty path element | |
r++ | |
default: | |
// real path element. | |
// add slash if needed | |
if out.w != 1 { | |
out.append('/') | |
} | |
// copy element | |
for ; r < n && path[r] != '/'; r++ { | |
out.append(path[r]) | |
} | |
} | |
} | |
if out.w == n-1 { | |
out.append('/') | |
} | |
return out.string() | |
} | |
// A lazybuf is a lazily constructed path buffer. | |
// It supports append, reading previously appended bytes, | |
// and retrieving the final string. It does not allocate a buffer | |
// to hold the output until that output diverges from s. | |
type lazybuf struct { | |
s string | |
buf []byte | |
w int | |
} | |
func (b *lazybuf) index(i int) byte { | |
if b.buf != nil { | |
return b.buf[i] | |
} | |
return b.s[i] | |
} | |
func (b *lazybuf) append(c byte) { | |
if b.buf == nil { | |
if b.w < len(b.s) && b.s[b.w] == c { | |
b.w++ | |
return | |
} | |
b.buf = make([]byte, len(b.s)) | |
copy(b.buf, b.s[:b.w]) | |
} | |
b.buf[b.w] = c | |
b.w++ | |
} | |
func (b *lazybuf) string() string { | |
if b.buf == nil { | |
return b.s[:b.w] | |
} | |
return string(b.buf[:b.w]) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment