Skip to content

Instantly share code, notes, and snippets.

@scottcagno
Last active December 21, 2022 21:27
Show Gist options
  • Save scottcagno/e2719c9172c2970588b6c85884619be6 to your computer and use it in GitHub Desktop.
Save scottcagno/e2719c9172c2970588b6c85884619be6 to your computer and use it in GitHub Desktop.
API ideas and examples
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"reflect"
"runtime"
"strconv"
"strings"
"sync"
)
type BooksController struct {
books sync.Map
}
type BookT struct {
ID int `json:"id,omitempty"`
Title string `json:"title"`
}
func addBooks(bc *BooksController) {
book1 := BookT{1, "book 1"}
book2 := BookT{2, "book 2"}
book3 := BookT{3, "book 3"}
book4 := BookT{4, "book 4"}
book5 := BookT{5, "book 5"}
bc.books.Store(book1.ID, book1)
bc.books.Store(book2.ID, book2)
bc.books.Store(book3.ID, book3)
bc.books.Store(book4.ID, book4)
bc.books.Store(book5.ID, book5)
}
func (bc *BooksController) getAllBooks(c *Ctx) {
var books []BookT
bc.books.Range(
func(id, book any) bool {
if book == nil {
return false
}
books = append(books, book.(BookT))
return true
})
c.WriteJSON(books)
}
func (bc *BooksController) getBookByID(c *Ctx) {
v := c.r.URL.Query().Get("id")
bid, err := strconv.Atoi(v)
if err != nil {
Error(c, http.StatusExpectationFailed)
return
}
var foundBook BookT
bc.books.Range(
func(id, book any) bool {
if book.(BookT).ID == bid {
foundBook = book.(BookT)
return false
}
return true
})
c.WriteJSON(foundBook)
}
func main() {
bc := new(BooksController)
addBooks(bc)
api := NewAPIServer()
api.GET("/api/books", bc.getAllBooks, bc.getBookByID)
log.Println(http.ListenAndServe(":8080", api))
}
type APIServer struct {
ctxPool sync.Pool
routes sync.Map
}
func initAPIServer(api *APIServer) {
api.ctxPool = sync.Pool{
New: func() any {
return new(Ctx)
},
}
}
func NewAPIServer() *APIServer {
api := new(APIServer)
initAPIServer(api)
return api
}
type Route struct {
method string
path string
handler string
fn CtxHandlerFunc
}
func (rte *Route) isMatch(c *Ctx) bool {
return c.r.Method == rte.method && c.r.URL.Path == rte.path
}
var allowedMethods = strings.Join([]string{
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodDelete,
http.MethodOptions,
}, "")
func makeRoute(method, path string, fn CtxHandlerFunc) (*Route, error) {
if !strings.ContainsAny(method, allowedMethods) {
return nil, fmt.Errorf("method %q is not allowed", method)
}
if path == "" {
return nil, fmt.Errorf("empty path is not allowed")
}
if fn == nil {
return nil, fmt.Errorf("a CtxHandlerFunc is required")
}
return &Route{
method: method,
path: path,
handler: nameOfFunction(fn),
fn: fn,
}, nil
}
func nameOfFunction(f any) string {
return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
}
func (api *APIServer) register(method, path string, fn ...CtxHandlerFunc) {
for _, h := range fn {
r, err := makeRoute(method, path, h)
if err != nil {
panic(err)
}
api.routes.Store(r.handler, r)
}
}
func (api *APIServer) Handle(method, path string, fn CtxHandlerFunc) {
api.register(method, path, fn)
}
func (api *APIServer) GET(path string, fn ...CtxHandlerFunc) {
api.register(http.MethodGet, path, fn...)
}
func (api *APIServer) POST(path string, fn CtxHandlerFunc) {
api.register(http.MethodGet, path, fn)
}
func (api *APIServer) PUT(path string, fn CtxHandlerFunc) {
api.register(http.MethodGet, path, fn)
}
func (api *APIServer) DELETE(path string, fn CtxHandlerFunc) {
api.register(http.MethodGet, path, fn)
}
func (api *APIServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := api.ctxPool.Get().(*Ctx)
ctx.init(w, r)
api.ServeCtxHTTP(ctx)
api.ctxPool.Put(ctx)
}
func (api *APIServer) ServeCtxHTTP(c *Ctx) {
api.routes.Range(func(k, v any) bool {
rte, ok := v.(*Route)
if !ok {
return false
}
if rte.isMatch(c) {
rte.fn(c)
return false
}
return true
})
Error(c, http.StatusNotFound)
}
func Error(c *Ctx, code int) {
c.w.Header().Set("Content-Type", "text/plain; charset=utf-8")
c.w.Header().Set("X-Content-Type-Options", "nosniff")
c.w.WriteHeader(code)
c.w.Write([]byte(http.StatusText(code)))
}
func WrapHandlerFunc(hf http.HandlerFunc) CtxHandlerFunc {
return func(c *Ctx) {
hf(c.w, c.r)
}
}
func WrapHandler(h http.Handler) CtxHandlerFunc {
return func(c *Ctx) {
h.ServeHTTP(c.w, c.r)
}
}
type CtxHandler interface {
ServeCtxHTTP(c *Ctx)
}
type CtxHandlerFunc func(c *Ctx)
func (cfn CtxHandlerFunc) ServeCtxHTTP(c *Ctx) {
cfn(c)
}
type Response struct {
http.ResponseWriter
}
type Ctx struct {
r *http.Request
w *Response
mu sync.Mutex
args map[string]any
}
func (c *Ctx) init(w http.ResponseWriter, r *http.Request) {
c.r = r
c.w = &Response{w}
if c.args == nil {
c.args = make(map[string]any)
}
}
func (c *Ctx) WriteJSON(v any) {
c.w.Header().Set("Content-Type", "application/json")
c.w.WriteHeader(http.StatusOK)
err := json.NewEncoder(c.w).Encode(v)
if err != nil {
Error(c, http.StatusExpectationFailed)
return
}
}
func (c *Ctx) BindJSON(v any) {
if c.r.Header.Get("Content-Type") != "application/json" {
Error(c, http.StatusBadRequest)
return
}
err := json.NewDecoder(c.r.Body).Decode(v)
if err != nil {
Error(c, http.StatusExpectationFailed)
return
}
c.w.WriteHeader(http.StatusOK)
}
package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"strings"
"sync"
)
func main() {
bookController := &BookController{
service: new(BookService),
}
bookController.service.init()
http.Handle("/api/books", bookController)
http.Handle("/api/books/count", bookController)
log.Panic(http.ListenAndServe(":8080", nil))
}
type Book struct {
ID int `json:"id,omitempty"`
Title string `json:"title"`
}
type BookService struct {
repo sync.Map
nextID int
}
func (s *BookService) init() {
book1 := Book{1, "The Bible"}
book2 := Book{2, "Lord of the Rings"}
book3 := Book{3, "The Hobbit"}
book4 := Book{4, "A Scanner Darkly"}
book5 := Book{5, "1984"}
s.repo.Store(book1.ID, book1)
s.repo.Store(book2.ID, book2)
s.repo.Store(book3.ID, book3)
s.repo.Store(book4.ID, book4)
s.repo.Store(book5.ID, book5)
}
func (s *BookService) getBookCount() int {
var count int
s.repo.Range(
func(k, v any) bool {
if v != nil {
count++
}
return true
})
return count
}
func (s *BookService) addBook(book Book) bool {
if book.ID > 0 {
return false
}
s.nextID++
book.ID = s.nextID
s.repo.Store(book.ID, book)
return true
}
func (s *BookService) updateBook(book Book) bool {
if book.ID < 1 {
return false
}
s.repo.Store(book.ID, book)
return true
}
func (s *BookService) getAllBooks() []Book {
// get all the books
var allBooks []Book
s.repo.Range(func(k, v any) bool {
book, isBook := v.(Book)
if isBook {
allBooks = append(allBooks, book)
}
return true
})
// return list of books
return allBooks
}
func (s *BookService) getBook(id int) *Book {
// find the book using the id
v, found := s.repo.Load(id)
if found {
book, isBook := v.(Book)
// return the found book
if isBook && book.ID == id {
return &book
}
}
return nil
}
type RequestMapping struct {
Method string `json:"method"`
Path string `json:"path"`
Content string `json:"content"`
}
// BookController must somehow be able to be used with
// the standard library easily enough, so to make this
// http compatible, we will implement ServeHTTP(w, r)
type BookController struct {
service *BookService
}
func (c *BookController) ServeHTTP(w http.ResponseWriter, r *http.Request) {
handleErr := func(w http.ResponseWriter, r *http.Request, code int) {
http.Error(w, http.StatusText(code), code)
}
if !strings.HasPrefix(r.URL.Path, "/api/books") {
handleErr(w, r, http.StatusExpectationFailed)
return
}
// handle returning one or all books
if r.Method == http.MethodGet && r.URL.Path == "/api/books" {
if r.URL.Query().Has("id") {
// handle get book by id
id, err := strconv.Atoi(r.URL.Query().Get("id"))
if err != nil {
handleErr(w, r, http.StatusExpectationFailed)
return
}
book := c.service.getBook(id)
data, err := json.Marshal(book)
if err != nil {
handleErr(w, r, http.StatusExpectationFailed)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(data)
return
}
// handle get all books
books := c.service.getAllBooks()
data, err := json.Marshal(books)
if err != nil {
handleErr(w, r, http.StatusExpectationFailed)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(data)
return
}
// handle add new book
if r.Method == http.MethodPost && r.URL.Path == "/api/books" {
var book Book
err := json.NewDecoder(r.Body).Decode(&book)
if err != nil {
handleErr(w, r, http.StatusExpectationFailed)
return
}
if ok := c.service.addBook(book); !ok {
handleErr(w, r, http.StatusExpectationFailed)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
return
}
// handle update book
if r.Method == http.MethodPut && r.URL.Path == "/api/books" {
var book Book
err := json.NewDecoder(r.Body).Decode(&book)
if err != nil {
handleErr(w, r, http.StatusExpectationFailed)
return
}
if ok := c.service.updateBook(book); !ok {
handleErr(w, r, http.StatusExpectationFailed)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
return
}
// handle a special api endpoint case
if r.Method == http.MethodGet && r.URL.Path == "/api/books/count" {
count := c.service.getBookCount()
bookCount, err := json.Marshal(struct {
NumberOfBooks int `json:"number_of_books"`
}{
NumberOfBooks: count,
})
if err != nil {
handleErr(w, r, http.StatusExpectationFailed)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(bookCount)
return
}
}
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"sync"
)
func main() {
bookController := &BookController{
service: new(BookService),
}
bookController.service.init()
http.Handle("/api/books", bookController)
http.Handle("/api/books/count", bookController)
log.Panic(http.ListenAndServe(":8080", nil))
}
type Book struct {
ID int `json:"id,omitempty"`
Title string `json:"title"`
}
type BookService struct {
repo sync.Map
nextID int
}
func (s *BookService) init() {
book1 := Book{1, "The Bible"}
book2 := Book{2, "Lord of the Rings"}
book3 := Book{3, "The Hobbit"}
book4 := Book{4, "A Scanner Darkly"}
book5 := Book{5, "1984"}
s.repo.Store(book1.ID, book1)
s.repo.Store(book2.ID, book2)
s.repo.Store(book3.ID, book3)
s.repo.Store(book4.ID, book4)
s.repo.Store(book5.ID, book5)
}
func (s *BookService) getBookCount() int {
var count int
s.repo.Range(
func(k, v any) bool {
if v != nil {
count++
}
return true
})
return count
}
func (s *BookService) addBook(book Book) bool {
if book.ID > 0 {
return false
}
s.nextID++
book.ID = s.nextID
s.repo.Store(book.ID, book)
return true
}
func (s *BookService) updateBook(book Book) bool {
if book.ID < 1 {
return false
}
s.repo.Store(book.ID, book)
return true
}
func (s *BookService) getAllBooks() []Book {
// get all the books
var allBooks []Book
s.repo.Range(func(k, v any) bool {
book, isBook := v.(Book)
if isBook {
allBooks = append(allBooks, book)
}
return true
})
// return list of books
return allBooks
}
func (s *BookService) getBook(id int) *Book {
// find the book using the id
v, found := s.repo.Load(id)
if found {
book, isBook := v.(Book)
// return the found book
if isBook && book.ID == id {
return &book
}
}
return nil
}
type RequestMapping struct {
Method string `json:"method"`
Path string `json:"path"`
Content string `json:"content"`
}
func WriteAsJSON(w http.ResponseWriter, data map[string]any) {
w.Header().Set("Content-Type", "application/json")
b, err := json.Marshal(data)
if err != nil {
w.WriteHeader(http.StatusExpectationFailed)
w.Write([]byte(`{"error":"could not marshal supplied data"}`))
return
}
w.WriteHeader(http.StatusOK)
w.Write(b)
}
func HandleErr(w http.ResponseWriter, code int, data map[string]any) {
w.Header().Set("Content-Type", "application/json")
if data != nil {
b, err := json.Marshal(data)
if err != nil {
w.WriteHeader(http.StatusExpectationFailed)
w.Write([]byte(`{"error"": "could not marshal supplied data"}`))
return
}
w.WriteHeader(code)
w.Write(b)
return
}
w.WriteHeader(code)
w.Write([]byte(fmt.Sprintf(`{"error":%q,"code":%d}`, http.StatusText(code), code)))
}
// BookController must somehow be able to be used with
// the standard library easily enough, so to make this
// http compatible, we will implement ServeHTTP(w, r)
type BookController struct {
service *BookService
}
func (c *BookController) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// handle returning one or all books
if r.Method == http.MethodGet && r.URL.Path == "/api/books" {
if r.URL.Query().Has("id") {
// handle get book by id
id, err := strconv.Atoi(r.URL.Query().Get("id"))
if err != nil {
HandleErr(w, http.StatusExpectationFailed, nil)
return
}
book := c.service.getBook(id)
WriteAsJSON(w, map[string]any{"count": 1, "book": book})
return
}
// handle get all books
books := c.service.getAllBooks()
WriteAsJSON(w, map[string]any{"count": len(books), "books": books})
return
}
// handle add new book
if r.Method == http.MethodPost && r.URL.Path == "/api/books" {
var book Book
err := json.NewDecoder(r.Body).Decode(&book)
if err != nil {
HandleErr(w, http.StatusExpectationFailed, nil)
return
}
if ok := c.service.addBook(book); !ok {
HandleErr(w, http.StatusExpectationFailed, nil)
return
}
WriteAsJSON(w, map[string]any{"added": true})
return
}
// handle update book
if r.Method == http.MethodPut && r.URL.Path == "/api/books" {
var book Book
err := json.NewDecoder(r.Body).Decode(&book)
if err != nil {
HandleErr(w, http.StatusExpectationFailed, nil)
return
}
if ok := c.service.updateBook(book); !ok {
HandleErr(w, http.StatusExpectationFailed, nil)
return
}
WriteAsJSON(w, map[string]any{"book.id": book.ID, "updated": true})
return
}
// handle a special api endpoint case
if r.Method == http.MethodGet && r.URL.Path == "/api/books/count" {
count := c.service.getBookCount()
WriteAsJSON(w, map[string]any{"number_of_books": count})
return
}
}
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"sync"
)
func main() {
bookController := &BookController{
service: new(BookService),
}
bookController.service.init()
api := NewAPI()
api.RegisterController(bookController)
log.Panic(http.ListenAndServe(":8080", api))
}
type ControllerResource interface {
Prefix() string
http.Handler
}
type API struct {
controllers []ControllerResource
}
func NewAPI() *API {
return &API{
controllers: make([]ControllerResource, 0),
}
}
func (api *API) RegisterController(cr ControllerResource) {
api.controllers = append(api.controllers, cr)
}
func (api *API) ServeHTTP(w http.ResponseWriter, r *http.Request) {
for _, c := range api.controllers {
if strings.HasPrefix(r.URL.Path, c.Prefix()) {
c.ServeHTTP(w, r)
return
}
}
http.NotFound(w, r)
return
}
type Book struct {
ID int `json:"id,omitempty"`
Title string `json:"title"`
}
type BookService struct {
repo sync.Map
nextID int
}
func (s *BookService) init() {
book1 := Book{1, "The Bible"}
book2 := Book{2, "Lord of the Rings"}
book3 := Book{3, "The Hobbit"}
book4 := Book{4, "A Scanner Darkly"}
book5 := Book{5, "1984"}
s.repo.Store(book1.ID, book1)
s.repo.Store(book2.ID, book2)
s.repo.Store(book3.ID, book3)
s.repo.Store(book4.ID, book4)
s.repo.Store(book5.ID, book5)
}
func (s *BookService) getBookCount() int {
var count int
s.repo.Range(
func(k, v any) bool {
if v != nil {
count++
}
return true
})
return count
}
func (s *BookService) addBook(book Book) bool {
if book.ID > 0 {
return false
}
s.nextID++
book.ID = s.nextID
s.repo.Store(book.ID, book)
return true
}
func (s *BookService) updateBook(book Book) bool {
if book.ID < 1 {
return false
}
s.repo.Store(book.ID, book)
return true
}
func (s *BookService) getAllBooks() []Book {
// get all the books
var allBooks []Book
s.repo.Range(func(k, v any) bool {
book, isBook := v.(Book)
if isBook {
allBooks = append(allBooks, book)
}
return true
})
// return list of books
return allBooks
}
func (s *BookService) getBook(id int) *Book {
// find the book using the id
v, found := s.repo.Load(id)
if found {
book, isBook := v.(Book)
// return the found book
if isBook && book.ID == id {
return &book
}
}
return nil
}
type RequestMapping struct {
Method string `json:"method"`
Path string `json:"path"`
Content string `json:"content"`
}
func WriteAsJSON(w http.ResponseWriter, data map[string]any) {
w.Header().Set("Content-Type", "application/json")
b, err := json.Marshal(data)
if err != nil {
w.WriteHeader(http.StatusExpectationFailed)
w.Write([]byte(`{"error":"could not marshal supplied data"}`))
return
}
w.WriteHeader(http.StatusOK)
w.Write(b)
}
func HandleErr(w http.ResponseWriter, code int, data map[string]any) {
w.Header().Set("Content-Type", "application/json")
if data != nil {
b, err := json.Marshal(data)
if err != nil {
w.WriteHeader(http.StatusExpectationFailed)
w.Write([]byte(`{"error"": "could not marshal supplied data"}`))
return
}
w.WriteHeader(code)
w.Write(b)
return
}
w.WriteHeader(code)
w.Write([]byte(fmt.Sprintf(`{"error":%q,"code":%d}`, http.StatusText(code), code)))
}
// BookController must somehow be able to be used with
// the standard library easily enough, so to make this
// http compatible, we will implement ServeHTTP(w, r)
type BookController struct {
service *BookService
}
func (c *BookController) Prefix() string {
return "/api/books"
}
func (c *BookController) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// handle returning one or all books
if r.Method == http.MethodGet && r.URL.Path == "/api/books" {
if r.URL.Query().Has("id") {
// handle get book by id
id, err := strconv.Atoi(r.URL.Query().Get("id"))
if err != nil {
HandleErr(w, http.StatusExpectationFailed, nil)
return
}
book := c.service.getBook(id)
WriteAsJSON(w, map[string]any{"count": 1, "book": book})
return
}
// handle get all books
books := c.service.getAllBooks()
WriteAsJSON(w, map[string]any{"count": len(books), "books": books})
return
}
// handle add new book
if r.Method == http.MethodPost && r.URL.Path == "/api/books" {
var book Book
err := json.NewDecoder(r.Body).Decode(&book)
if err != nil {
HandleErr(w, http.StatusExpectationFailed, nil)
return
}
if ok := c.service.addBook(book); !ok {
HandleErr(w, http.StatusExpectationFailed, nil)
return
}
WriteAsJSON(w, map[string]any{"added": true})
return
}
// handle update book
if r.Method == http.MethodPut && r.URL.Path == "/api/books" {
var book Book
err := json.NewDecoder(r.Body).Decode(&book)
if err != nil {
HandleErr(w, http.StatusExpectationFailed, nil)
return
}
if ok := c.service.updateBook(book); !ok {
HandleErr(w, http.StatusExpectationFailed, nil)
return
}
WriteAsJSON(w, map[string]any{"book.id": book.ID, "updated": true})
return
}
// handle a special api endpoint case
if r.Method == http.MethodGet && r.URL.Path == "/api/books/count" {
count := c.service.getBookCount()
WriteAsJSON(w, map[string]any{"number_of_books": count})
return
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment