nerdweb

package module
v2.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 10, 2022 License: MIT Imports: 16 Imported by: 1

README

nerdweb

A small set of utility functions for writing Go HTTP applications. Most of these utilities are designed for working with Gorilla Mux. This library has minimal dependencies, and only really requires logrus.

Usage

go get github.com/ResurgenceIT/nerdweb/v2

HTTP Servers

nerdweb has a few options for creating HTTP servers. These options are mostly to reduce boilerplate. They make use of Gorilla Mux and the standard HTTP library under the hood. nerdweb has methods for creating basic REST servers and Single Page Application servers (web apps).

Endpoints

Regardless of which server option you choose both accept a configuration, and these configurations needs a slice of endpoints. An endpoint has three requirements: a path, a slice of accepted methods, and either a handler function or handler interface.

type Endpoint struct {
  Path        string
  Methods     []string
  HandlerFunc http.HandlerFunc
  Handler     http.Handler
}

See the examples below on how one can configure endpoints.

REST Server

Here is an example of creating a basic REST server.

package main

import (
  "context"
  "net/http"
  "time"

  "github.com/ResurgenceIT/nerdweb/v2"
  "github.com/sirupsen/logrus"
)

var (
  logger *logrus.Entry
)

func main() {
  logger := logrus.New().WithField("who", "example")
  restConfig := nerdweb.DefaultRESTConfig("localhost:8080")

  restConfig.Endpoints = nerdweb.Endpoints{
    {Path: "/version", Methods: []string{http.MethodGet}, HandlerFunc: versionHandler},
  }

  router, server := nerdweb.NewRESTRouterAndServer(restConfig)

  /*
   * Start the server in a goroutine
   */
  go func() {
    err := server.ListenAndServe()

    if err != nil && err != http.ErrServerClosed {
      logger.WithError(err).Fatal("error starting server")
    }
  }()

  <-nerdweb.WaitForKill()

  ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  defer cancel()

  if err = server.Shutdown(ctx); err != nil {
    logger.WithError(err).Fatal("error shutting down server")
  }

  logger.Info("server stopped")
}

func versionHandler(w http.ResponseWriter, r *http.Request) {
  nerdweb.WriteString(logger, w, http.StatusOK, "version 1")
}
SPA Server

Here is an example of creating a basic server with a single page application built-in.

package main

import (
  "context"
  "embed"
  "net/http"
  "time"

  "github.com/ResurgenceIT/nerdweb/v2"
  "github.com/sirupsen/logrus"
)

var (
  // Version should be set during build using build flags
  Version string = "development"

  logger *logrus.Entry

  //go:embed app
  appFs embed.FS

  //go:embed app/index.html
  indexHTML []byte

  //go:embed app/main.js
  mainJS []byte

  //go:embed app/manifest.json
  manifestJSON []byte
)

func main() {
  logger := logrus.New().WithField("who", "example")
  spaConfig := nerdweb.DefaultSPAConfig("localhost:8080", Version, appFs, indexHTML, mainJS, manifestJSON)

  spaConfig.Endpoints = nerdweb.Endpoints{
    {Path: "/version", Methods: []string{http.MethodGet}, HandlerFunc: versionHandler},
  }

  router, server := nerdweb.NewSPARouterAndServer(restConfig)

  /*
   * Start the server in a goroutine
   */
  go func() {
    err := server.ListenAndServe()

    if err != nil && err != http.ErrServerClosed {
      logger.WithError(err).Fatal("error starting server")
    }
  }()

  <-nerdweb.WaitForKill()

  ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  defer cancel()

  if err = server.Shutdown(ctx); err != nil {
    logger.WithError(err).Fatal("error shutting down server")
  }

  logger.Info("server stopped")
}

func versionHandler(w http.ResponseWriter, r *http.Request) {
  nerdweb.WriteString(logger, w, http.StatusOK, "version 1")
}

Requests

Methods for working with HTTP requests.

RealIP

RealIP attempts to return the client's real IP address. The default value is RemoteAddr. If a X-Forwarded-For header is found the value there will be used. This is useful for requests coming through proxies.

ip := nerdweb.RealIP(r) // r is *http.Request
ValidateHTTPMethod

ValidateHTTPMethod checks the request method against an expected value. If they do not match an error message is written back to the client. The error message takes the format of:

{
  "message": "method not allowed"
}
logger := logrus.New().WithField("who", "example")

if err := nerdweb.ValidateHTTPMethod(r, w, http.MethodPost, logger); err != nil {
  // Do something if the method is invalid. An error has already
  // been written back to the client.
}
ReadJSONBody

ReadJSONBody reads the body from an HTTP reponse as JSON data into a provided destinationn variable. In this example the body is read into SampleStruct.

type SampleStruct struct {
  Name string `json:"name"`
  Age int `json:"age"`
}

result := SampleStruct{}

if err := nerdweb.ReadJSONBody(r, &result); err != nil {
  // Do something with the error
}

Responses

Methods for working with HTTP responses.

WriteJSON

WriteJSON writes JSON content to the caller. It expects the value you write to be JSON serializable.

logger := logrus.New().WithField("who", "example")

type SampleStruct struct {
  Name string `json:"name"`
  Age int `json:"age"`
}

result := SampleStruct{
  Name: "Adam",
  Age: 10,
}

nerdweb.WriteJSON(logger, w, http.StatusOK, result)
WriteString

WriteString writes string content to the caller.

logger := logrus.New().WithField("who", "example")
nerdweb.WriteString(logger, w, http.StatusInternalServerError, "Bad!")

Middlewares

nerdweb comes with a few middlewares. You can easily create your own as well.

Making Your Own

There are two types of middlewares. The first is one you attach to a single handler. The other you attach to the server mux (affects all handlers).

Single Handler Middleware
func MyMiddleware(next http.HandlerFunc) http.HandlerFunc {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    fmt.Printf("In my middleware\n")
    next.ServeHTTP(w, r)
  })
}
Server Mux Middleware
type example struct {
  handler http.Handler
}

func (m *example) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  fmt.Printf("In my middleware")
  m.handler.ServeHTTP(w, r)
}

func ExampleMiddleware() mux.MiddlewareFunc {
  return func(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
      handler := &example{
        handler: next,
      }

      handler.ServeHTTP(w, r)
    })
  }
}

Bundled Middlewares

Access Control

AccessControl wraps an HTTP mux with a middleware that sets headers for access control and allowed headers.

mux := nerdweb.NewServeMux()
mux.HandleFunc("/endpoint", handler)

mux.Use(middlewares.AccessControl(middlewares.AllowAllOrigins, middlewares.AllowAllMethods, middlewares.AllowAllHeaders)
Allow

Allow verifies if the caller method matches the provided method. If the caller's method does not match what is allowed, the string "method not allowed" is written back to the caller.

mux := nerdweb.NewServeMux()
mux.HandleFunc("/endpoint", middlewares.Allow(myHandler, http.MethodPost))
CaptureAuth

CaptureAuth captures an authorization token from an Authorization header and stored it in a context variable named "authtoken". This middleware expect the header to be in the format of:

Authorization: Bearer

If the header format is invalid, the provided error method is called. Here is an example:

onInvalidHeader = func(logger *logrus.Entry, w http.ResponseWriter) {
  result := map[string]string{
    "error": "invalid JWT header!",
  }

  nerdweb.WriteJSON(logger, w, http.StatusBadRequest, result)
}

// Now, in your handler definition
http.HandleFunc("/endpoint", middlewares.CaptureAuth(handlerFunc, logger, onInvalidHeader))

Then to get the captured authrozation token:

func handler(w http.ResponseWriter, r *http.Request) {
  ctx := r.Context()

  ip := ctx.Value("authtoken").(string)
}
CaptureIP

CaptureIP captures the caller's IP address and puts it into the context as "ip". Example:

mux := nerdweb.NewServeMux()
mux.HandleFunc("/endpoint", handler)

mux.Use(middlewares.CaptureIP())

Then to get the IP from the context:

func handler(w http.ResponseWriter, r *http.Request) {
  ctx := r.Context()

  ip := ctx.Value("ip").(string)
}
RequestLogger

RequestLogger returns a middleware for logging all requests. It logs using an Entry struct from Logrus.

mux := nerdweb.NewServeMux()
mux.HandleFunc("/endpoint", handler)

mux.Use(middlewares.RequestLogger(logger))
License

Copyright 2022 App Nerds LLC

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AdjustPage

func AdjustPage(page int) int

AdjustPage decrements the value of "page" because we want to use zero-based pages for the math. Make sure page is never less than zero.

func HasNextPage

func HasNextPage(page, pageSize, recordCount int) bool

HasNextPage returns true when the result of page multiplied by pageSize is less than the total recordCount.

func NewRESTRouterAndServer

func NewRESTRouterAndServer(config RESTConfig) (*mux.Router, *http.Server)

NewRESTRouterAndServer creates a new Gorilla router and HTTP server with some preconfigured defaults for REST applications. The HTTP server is setup to use the resulting router.

func NewSPARouterAndServer

func NewSPARouterAndServer(config SPAConfig) (*mux.Router, *http.Server)

NewSPARouterAndServer creates a new Gorilla router and HTTP server with some preconfigured defaults for single page applications. The HTTP server is setup to use the resulting router.

func ReadJSONBody

func ReadJSONBody(r *http.Request, dest interface{}) error

ReadJSONBody reads the body content from an http.Request as JSON data into dest.

func RealIP

func RealIP(r *http.Request) string

RealIP attempts to return the IP address of the caller. The result will default to the RemoteAddr from http.Request. It will also check the request headers for an "X-Forwarded-For" value and use that. This is useful for when requests come through proxies or other non-direct means.

func TotalPages

func TotalPages(pageSize, recordCount int) int

TotalPages returns how many pages are available in a paged result based pageSize and the total recordCount.

func ValidateHTTPMethod

func ValidateHTTPMethod(r *http.Request, w http.ResponseWriter, expectedMethod string, logger *logrus.Entry) error

ValidateHTTPMethod checks the request METHOD against expectedMethod. If they do not match an error message is written back to the client.

func WaitForKill

func WaitForKill() chan os.Signal

WaitForKill returns a channel that waits for an OS interrupt or terminate.

func WriteJSON

func WriteJSON(logger *logrus.Entry, w http.ResponseWriter, status int, value interface{})

WriteJSON writes JSON content to the response writer.

func WriteString

func WriteString(logger *logrus.Entry, w http.ResponseWriter, status int, value string)

WriteString writes string content to the response writer.

Types

type Endpoint

type Endpoint struct {
	Path        string
	Methods     []string
	HandlerFunc http.HandlerFunc
	Handler     http.Handler
}

Endpoint defines a single HTTP endpoint. Each endpoint is used to configure a Gorilla Mux route.

type Endpoints

type Endpoints []*Endpoint

Endpoints represents an Endpoint slice.

func (Endpoints) Len

func (a Endpoints) Len() int

func (Endpoints) Less

func (a Endpoints) Less(i, j int) bool

func (Endpoints) Swap

func (a Endpoints) Swap(i, j int)

type RESTConfig

type RESTConfig struct {
	Endpoints    Endpoints
	Host         string
	IdleTimeout  int
	ReadTimeout  int
	WriteTimeout int
}

RESTConfig is used to configure a router for basic REST servers

func DefaultRESTConfig

func DefaultRESTConfig(host string) RESTConfig

DefaultRESTConfig creates a REST configuration with default values. In this configuration the HTTP server is configured with an idle timeout of 60 seconds, and a read and write timeout of 30 seconds.

type SPAConfig

type SPAConfig struct {
	AppDirectory  string
	AppFileSystem embed.FS
	Endpoints     Endpoints
	Host          string
	IdleTimeout   int
	IndexHTML     []byte
	MainJS        []byte
	ManifestJSON  []byte
	ReadTimeout   int
	Version       string
	WriteTimeout  int
}

SPAConfig is used to configure a single page application router

func DefaultSPAConfig

func DefaultSPAConfig(host, version string, appFileSystem embed.FS, indexHTML, mainJS, manifestJSON []byte) SPAConfig

DefaultSPAConfig creates a single page application configuration with default values. In this configuration the directory holding the front-end application is "app". The HTTP server is configured with an idle timeout of 60 seconds, and a read and write timeout of 30 seconds.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL