medium

package module
v0.0.0-...-1535236 Latest Latest
Warning

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

Go to latest
Published: Aug 20, 2023 License: MIT Imports: 7 Imported by: 0

README

Medium

Experimental Go code for writing web applications. This is a collection of packages I ocassionally hack on to write some Go and play around with, trying to use Go as a web application framework.

These packages are likely to change often and without warning given the (current) experimental nature.

Packages

  • middleware/rescue - Basic rescue middleware for router.
  • middleware/httpmethod - Rewrites the HTTP method based on the _method parameter. This is used to allow browsers to make PUT, PATCH, and DELETE requests.
  • middleware/httplogger - Basic logger middleware for router.
  • view - Wraps the html/template package to provide a slightly more friendly and ergonimic interface for web application usage. Use bat instead.
  • session - Struct based, cookie backed session management using HMAC signatures to validate session contents.
  • mail - Provides a basic mailer package that utilizes template for templating. Additionally provides a basic interface that can be used with router to see sent emails in development.
  • mlog - Simple structured logger usable directly, or through context compatible API's.
  • set - Basic Set data structure.
  • webpack - Middleware that allows you to use webpack to serve assets in development.

Medium (web framework)

Formerly router, this is a basic router that provides a few basic features:

  • Middleware - Middleware allows you to change the request and response before and after the handler is called. (logging, authentication, session management, etc.)
  • Custom Handler Types - Most other frameworks pass their own context object. In medium, generics are used to allow you to define your own handler types. This allows you to pass in any type of context you want, and have it be type safe.
  • Subrouters and Groups - Medium allows you to consolidate behavior at the route level, allowing you to create subrouters for things like authentication, API versioning, or requiring a specific resource to be present and authorized.
Getting started

To get started, install medium via go get github.com/blakewilliams/medium.

From there, you can create a new router and add a handler:

import (
  "fmt"
  "html/template"
  "net/http"
  "github.com/blakewilliams/medium"
)
// Requests in medium can store a generic data type that is passed to each
// BeforeFunc and handler. This is useful for storing things like the current
// user, global rendering data, etc.
type ReqData struct {
  currentUser *User
}

// Routers are generic and must specify the type of Data they will pass to
// HandlerFunc/BeforeFuncs.
router := medium.New(func(req RootRequest) *ReqData {
  return &ReqData
})
// Fill in ReqData with the current user before each request. The BeforeFunc
// must return a Response that will be used to render the response. Calling next
// will continue to the next BeforeFunc or HandlerFunc.
router.Before(func(ctx context.Context, req *medium.Request[ReqData], next medium.Next) Response{
  req.Data.currentUser = findCurrentUser(req.Request)
  return next(ctx)
})

// Add a hello route
router.Get("/hello/:name", func(ctx context.Context, req *medium.Request[ReqData]) Response {
  return Render(req, "hello.html", map[string]any{"name": req.Params["name"], "currentUser": req.Data.currentUser})
})

fmt.Println("Listening on :8080")
server := http.Server{Addr: ":8080", Handler: router}
_ = server.ListenAndServe()
Groups and Subrouters

Groups and subrouters allow you to consolidate behavior at the route level. For example, you can create a group that requires a user to be logged in.

router := medium.New(func(req RootRequest) *ReqData {
  return &ReqData
})

router.Before(func(ctx context.Context, req *medium.Request[ReqData], next medium.Next) Response {
  req.Data.currentUser = findCurrentUser(req.Request)
  return next(ctx)
})

// Create a group that requires a user to be logged in
authGroup := router.Group(func(r *medium.Request[ReqData]) *ReqData {})
authGroup.Before(func(ctx context.Context, req *medium.Request[ReqData], next medium.Next) Response {
  // If there is no current user, return a 404
  if a.currentUser != nil {
    res := medium.NewResponse()
    res.WriteStatus(http.StatusNotFound)
    res.WriteString("Not Found")

    return res
  }

  // Otherwise, continue to the next BeforeFunc/HandlerFunc
  return next(ctx)
}

// Add a route to the group that will redirect if the user is not logged in
authGroup.Get("/welcome", func(ctx context.Context, req *medium.Request[ReqData]) Response {
  return Render(ctx, "hello.html", map[string]any{"CurrentUser": a.currentUser})
})

Subrouters are similar to groups, but allow you to create a new router that has a path prefix. This is useful for patterns like API versioning or requiring a specific resource to be present and authorized.

// Create a new router
router := medium.New(func(req RootRequest) *ReqData {
  currentUser := findCurrentUser(req.Request)
  return &ReqData{currentUser: currentUser}
})

// Create a type that will hold on to the current team
type TeamData struct {
  currentTeam *Team
  // Embed parent data type if you want to access the current user, or pass it
  // explicitly in the data creator function passed to SubRouter
  ReqData
}

// Create a subrouter that ensures a team is present and authorized
teamRouter := router.SubRouter("/teams/:teamID", func(r *medium.Request[ReqData]) *TeamData {
  team := findTeam(r.Params["teamID"])
  return &TeamData{ReqData: data, currentTeam: team}
})

// Ensure routes in the team router have a current team and that the current
// user is a member of the team
teamRouter.Before(func(ctx context.Context, req *medium.Request[TeamData], next medium.Next) Response {
  // If there is no current team, return a 404
  if req.Data.currentTeam == nil {
    res := medium.NewResponse()
    res.WriteStatus(http.StatusNotFound)
    res.WriteString("Not Found")

    return res
  }

  // If the current user is not a member of the team, return a 403
  if !team.IsMember(a.currentUser) {
    res := medium.NewResponse()
    res.WriteStatus(http.StatusForbidden)
    res.WriteString("Forbidden")

    return res
  }

  // Otherwise, continue to the next BeforeFunc/HandlerFunc
  return next(ctx)
})

// Add a route to render the team show page
teamRouter.Get("/", func (ctx context.Context, req *medium.Request[TeamData]) Response {
  return Render(ctx, "team.html", map[string]any{"Team": req.Data.currentTeam})
})


// Add a subrouter to the team subrouter that will render the team settings page
// if the current user is an admin
teamSettingsRouter := teamRouter.SubRouter("/settings", func(r *medium.Request[TeamData]) *TeamData { return r.Data })
teamSettingsRouter.Before(func(ctx context.Context, req *medium.Request[TeamData], next medium.Next) Response {
  if !r.Data.currentTeam.IsAdmin(a.currentUser) {
    res := medium.NewResponse()
    res.WriteStatus(http.StatusForbidden)
    res.WriteString("Forbidden")
  }

  return next(ctx)
})

This allows for flexible and safe composition of routes based on the current state of the request.

Middleware

Middleware are functions that use the Go http package types to modify the request and response before and after the handler is called. This is useful for compatibility with existing Go middleware packages and for adding generic behavior to the router.

// Create a new router
router := medium.New(func(req RootRequest) *ReqData {
  currentUser := findCurrentUser(req.Request)
  return &ReqData{currentUser: currentUser}
})

// Add a middleware that logs the request. Middleware work on raw HTTP types, not medium types.
router.Use(func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
  now := time.Now()
  log.Printf("Started: %s %s", a.Request.Method, a.Request.URL.Path)

  next(a)

  log.Printf("Served: %s %s in %s", a.Request.Method, a.Request.URL.Path, time.Since(now))
})

Contributing

Contributions are welcome via pull requests and issues.

Documentation

Overview

The router package provides a simple interface that maps routes to handlers. The router accepts a type argument which must implement the Action interface. This allows clients to define extend and enhance the BaseAction type with additional data and functionality.

Example:

package main

type MyAction struct {
	requestId string
	*router.BaseAction
}

func Run() {
	// Router that defines a context creator that returns a new MyAction for each request.
	router := New(func(ac *MyAction) {
		ac.requestId = randomString()
	}))

	// Add an Around handler that sets the requestId header
	router.Around(func(ac *MyAction, next func()) {
		ac.Response.Header().Add("X-Request-ID", ac.requestId)
		next()
	})

	// Echo back the requestId header
	router.Get("/echo", func(ac *MyAction) {
		ac.Write([]byte(ac.requestId))
	})

	http.ListenAndServe(":8080", router)
}

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type BeforeFunc

type BeforeFunc[T any] (func(ctx context.Context, req *Request[T], next Next) Response)

BeforeFunc is a function that is called before the action is executed.

type HandlerFunc

type HandlerFunc[T any] func(context.Context, *Request[T]) Response

A function that handles a request.

type Middleware

type Middleware func(http.ResponseWriter, *http.Request, http.HandlerFunc)

Middleware is a function that is called before the action is executed. See Router.Use for more information. type Middleware func(c Action, next HandlerFunc[Action])

type Next

type Next func(ctx context.Context) Response

Next is a function that calls the next BeforeFunc or HandlerFunc in the chain. It accepts a context and returns a Response.

type NoData

type NoData struct{}

NoData is a placeholder type for the default action creator.

func WithNoData

func WithNoData(rootRequest *RootRequest) NoData

WithNoData is a convenience function for creating a NoData type for use with groups and routers.

type Request

type Request[Data any] struct {
	Data Data
	// contains filtered or unexported fields
}

Request is a wrapper around http.Request that contains the original Request object and the response writer. It also can contain application specific data that is passed to the handlers.

func NewRequest

func NewRequest[Data any](originalRequest *http.Request, data Data, routeData *RouteData) *Request[Data]

func (Request[Data]) Body

func (r Request[Data]) Body() io.ReadCloser

Body returns the request body.

func (Request[Data]) ContentLength

func (r Request[Data]) ContentLength() int64

ContentLength returns the length of the request body.

func (Request[Data]) Cookie

func (r Request[Data]) Cookie(name string) (*http.Cookie, error)

Cookie returns the named cookie provided in the request.

func (Request[Data]) Cookies

func (r Request[Data]) Cookies() []*http.Cookie

Cookies parses and returns the HTTP cookies sent with the request.

func (Request[Data]) FormData

func (r Request[Data]) FormData() (map[string][]string, error)

FormData returns the parsed form data from the request body.

func (Request[Data]) FormValue

func (r Request[Data]) FormValue(key string) string

FormValue returns the first value for the named component of the request body, ignoring errors from ParseForm.

func (Request[Data]) Header

func (r Request[Data]) Header() http.Header

Header returns the header field

func (Request[Data]) Host

func (r Request[Data]) Host() string

Host returns the host of the request.

func (Request[Data]) MatchedPath

func (r Request[Data]) MatchedPath() string

MatchedPath returns the route path pattern that was matched.

func (Request[Data]) Method

func (r Request[Data]) Method() string

Method returns the HTTP method of the request.

func (Request[Data]) Params

func (r Request[Data]) Params() map[string]string

Params returns the route parameters that were matched.

func (Request[Data]) PostFormData

func (r Request[Data]) PostFormData() (map[string][]string, error)

PostFormData returns the parsed form data from the request body.

func (Request[Data]) Proto

func (r Request[Data]) Proto() string

Proto returns the HTTP protocol version of the request.

func (Request[Data]) ProtoMajor

func (r Request[Data]) ProtoMajor() int

ProtoMajor returns the HTTP protocol major version of the request.

func (Request[Data]) ProtoMinor

func (r Request[Data]) ProtoMinor() int

ProtoMinor returns the HTTP protocol minor version of the request.

func (Request[Data]) Query

func (r Request[Data]) Query() url.Values

Query returns the parsed query string from the request URL.

func (Request[Data]) QueryParam

func (r Request[Data]) QueryParam(name string) string

QueryParam returns the first value for the named component of the query.

func (Request[Data]) QueryParams

func (r Request[Data]) QueryParams(name string) []string

QueryParams returns the values for the named component of the query.

func (Request[Data]) Referer

func (r Request[Data]) Referer() string

Referrer returns the referer for the request.

func (Request[Data]) RemoteAddr

func (r Request[Data]) RemoteAddr() string

RemoteAddr returns the remote address of the request.

func (Request[Data]) Request

func (r Request[Data]) Request() *http.Request

Request returns the original request.

func (Request[Data]) RequestURI

func (r Request[Data]) RequestURI() string

RequestURI returns the unmodified request-target of the request.

func (Request[Data]) URL

func (r Request[Data]) URL() *url.URL

URL returns the url.URL of the *http.Request.

type Response

type Response interface {
	Status() int
	Header() http.Header
	Body() io.Reader
}

ResponseWriter is an interface that represents the response that will be sent to the client.

The default status if not provided is 200, and the default headers are an empty map.

func OK

func OK() Response

OK returns a response with a 200 status code and a body of "OK".

func Redirect

func Redirect(to string) Response

Redirect returns an HTTP response to redirect the client to the provided URL.

func StringResponse

func StringResponse(status int, body string) Response

type ResponseBuilder

type ResponseBuilder struct {
	// contains filtered or unexported fields
}

ResponseBuilder is a helper struct that can be used to build a response. It implements the response interface and can be returned directly from handlers.

func NewResponse

func NewResponse() *ResponseBuilder

NewResponse returns a new ResponseBuilder that can be used to build a response.

func (*ResponseBuilder) Body

func (rb *ResponseBuilder) Body() io.Reader

Body returns the body of the response.

func (*ResponseBuilder) Header

func (rb *ResponseBuilder) Header() http.Header

Header returns the header map for the response.

func (*ResponseBuilder) Status

func (rb *ResponseBuilder) Status() int

Status returns the status code of the response.

func (*ResponseBuilder) Write

func (rb *ResponseBuilder) Write(p []byte) (int, error)

Write writes the provided bytes to the response body.

func (*ResponseBuilder) WriteStatus

func (rb *ResponseBuilder) WriteStatus(status int)

WriteStatus sets the status code for the response. It does not prevent the status code from being changed by a middleware or writing additional headers.

func (*ResponseBuilder) WriteString

func (rb *ResponseBuilder) WriteString(s string) (int, error)

WriteString writes the provided string to the response body.

type RootRequest

type RootRequest = Request[NoData]

RootRequest is a wrapper around http.Request that contains the original Request object and the response writer. This is used for the root router since there is no application specific data to store.

type Route

type Route[C any] struct {
	Method string
	Raw    string
	// contains filtered or unexported fields
}

A Route is a single route that can be matched against a request and holds a reference to the handler used to handle the reques and holds a reference to the handler used to handle the request.

func (*Route[C]) IsMatch

func (r *Route[C]) IsMatch(req *RootRequest) (bool, map[string]string)

Given a request, returns true if the route matches the request and false if not.

type RouteData

type RouteData struct {
	// Params holds the route parameters that were matched.
	Params map[string]string
	// HandlerPath holds the path that was matched.
	HandlerPath string
}

RouteData holds data about the matched route.

type RouteGroup

type RouteGroup[ParentData any, Data any] struct {
	// contains filtered or unexported fields
}

RouteGroup represents a collection of routes that share a common set of Around/Before/After callbacks and Action type (T)

func Group

func Group[
	ParentData any,
	Data any,
	Parent registerable[ParentData],
](
	parent Parent,
	creator func(r *Request[ParentData]) Data,
) *RouteGroup[ParentData, Data]

NewGroup creates a new route Group that can be used to group around filters and other common behavior.

An action creator function is passed to the NewGroup, so that it can reference fields from the parent action type.

func GroupWithContext

func GroupWithContext[
	ParentData any,
	Data any,
	Parent registerable[ParentData],
](
	parent Parent,
	creator func(context.Context, *Request[ParentData]) (context.Context, Data),
) *RouteGroup[ParentData, Data]

GroupWithContext has the same behavior as Group but passes the context to the data creator and requires a context to be returned.

func SubRouter

func SubRouter[
	ParentData any,
	Data any,
	Parent registerable[ParentData],
](
	parent Parent,
	prefix string,
	creator func(r *Request[ParentData]) Data,
) *RouteGroup[ParentData, Data]

SubRouter creates a new grouping of routes that will be routed to in addition to the routes defined on the primery router. These routes will be prefixed using the given prefix.

Subrouters provide their own action creator, so common behavior can be grouped via a custom action.

Diagram of what the "inheritance" chain can look like:

router[GlobalAction]
|
|_Group[GlobalAction, LoggedInAction]
|
|-Group[LoggedInAction, Teams]
|
|-Group[LoggedInAction, Settings]
|
|-Group[LoggedInAction, Admin]
  |
  |_ Group[Admin, SuperAdmin]

func SubRouterWithContext

func SubRouterWithContext[
	ParentData any,
	Data any,
	Parent registerable[ParentData],
](
	parent Parent,
	prefix string,
	creator func(ctx context.Context, r *Request[ParentData]) (context.Context, Data),
) *RouteGroup[ParentData, Data]

SubRouterWithContext has the same behavior as SubRouter but passes the context to the data creator and

func (*RouteGroup[ParentData, Data]) Before

func (r *RouteGroup[ParentData, Data]) Before(before BeforeFunc[Data])

func (*RouteGroup[ParentData, Data]) Delete

func (g *RouteGroup[ParentData, Data]) Delete(path string, handler HandlerFunc[Data])

Defines a new Route that responds to DELETE requests.

func (*RouteGroup[ParentData, Data]) Get

func (g *RouteGroup[ParentData, Data]) Get(path string, handler HandlerFunc[Data])

Defines a new Route that responds to GET requests.

func (*RouteGroup[ParentData, Data]) Match

func (g *RouteGroup[ParentData, Data]) Match(method string, path string, handler HandlerFunc[Data])

Match defines a new Route that responds to requests that match the given method and path.

func (*RouteGroup[ParentData, Data]) Patch

func (g *RouteGroup[ParentData, Data]) Patch(path string, handler HandlerFunc[Data])

Defines a new Route that responds to PATCH requests.

func (*RouteGroup[ParentData, Data]) Post

func (g *RouteGroup[ParentData, Data]) Post(path string, handler HandlerFunc[Data])

Defines a new Route that responds to POST requests.

func (*RouteGroup[ParentData, Data]) Put

func (t *RouteGroup[ParentData, Data]) Put(path string, handler HandlerFunc[Data])

Defines a new Route that responds to PUT requests.

type Router

type Router[T any] struct {
	// contains filtered or unexported fields
}

Router is a collection of Routes and is used to dispatch requests to the correct Route handler.

func New

func New[T any](dataCreator func(*RootRequest) T) *Router[T]

Creates a new Router with the given action creator used to create the application's root type.

func NewWithContext

func NewWithContext[T any](dataCreator func(context.Context, *RootRequest) (context.Context, T)) *Router[T]

NewWithContext behaves the same as New, but is passed a context and expects a context to be returned from the data creator.

func (*Router[T]) Before

func (r *Router[T]) Before(before BeforeFunc[T])

func (*Router[T]) Delete

func (r *Router[T]) Delete(path string, handler HandlerFunc[T])

Defines a new Route that responds to DELETE requests.

func (*Router[T]) Get

func (r *Router[T]) Get(path string, handler HandlerFunc[T])

Defines a new Route that responds to GET requests.

func (*Router[T]) Match

func (r *Router[T]) Match(method string, path string, handler HandlerFunc[T])

Match is used to add a new Route to the Router

func (*Router[T]) Missing

func (r *Router[T]) Missing(handler HandlerFunc[T])

Defines a handler that is called when no route matches the request.

func (*Router[T]) Patch

func (r *Router[T]) Patch(path string, handler HandlerFunc[T])

Defines a new Route that responds to PATCH requests.

func (*Router[T]) Post

func (r *Router[T]) Post(path string, handler HandlerFunc[T])

Defines a new Route that responds to POST requests.

func (*Router[T]) Put

func (r *Router[T]) Put(path string, handler HandlerFunc[T])

Defines a new Route that responds to PUT requests.

func (*Router[T]) ServeHTTP

func (router *Router[T]) ServeHTTP(rw http.ResponseWriter, r *http.Request)

func (*Router[T]) Use

func (r *Router[T]) Use(middleware Middleware)

Defines a new middleware that is called in each request before the matching route is called, if one exists. Middleware are only passed a router.BaseAction and not the application specific action. This is due to middleware being treated as a low-level API.

If you need access to the application specific action, you can use the actionCreator function passed to New or NewGroup.

Middleware is called in the order that they are added. Middleware must call next in order to continue the request, otherwise the request is halted.

Directories

Path Synopsis
middleware
slashnormalizer
Package slashnormalizer provides a middleware that normalizes the URL path by removing duplicate slashes and redirects that normalized URL path.
Package slashnormalizer provides a middleware that normalizes the URL path by removing duplicate slashes and redirects that normalized URL path.
The mlog package provides a simple structured logger.
The mlog package provides a simple structured logger.

Jump to

Keyboard shortcuts

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