Skip to content

Instantly share code, notes, and snippets.

@scottcagno
Created October 26, 2022 22:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save scottcagno/f8eb7f55d943e4225dc22740b5be8b61 to your computer and use it in GitHub Desktop.
Save scottcagno/f8eb7f55d943e4225dc22740b5be8b61 to your computer and use it in GitHub Desktop.
RESTful API Idea in Go
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