bow

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

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

Go to latest
Published: Oct 19, 2022 License: MIT Imports: 29 Imported by: 1

README

bow

Bow wraps a minimal amount of go libraries to quickly bootstrap a capable web project.

What’s more than net/http and html/template?

Go comes with a really solid standard library, powered with robust packages that you can directly use for your production web applications. The community is well-known to be against frameworks by definition, especially when it comes to web applications. And it is probably true to a certain degree. I am really in favour of using those packages, but in my opinion, there are still missing bits to build modern and secure web applications. I am talking about:

  • A HTTP router/muxer that supports patterns.
  • A fast template engine that supports partials and layouts.
  • A migration system for databases.
  • Middlewares to automatically apply security headers to our responses.
  • A session system to store state information about users.
  • Flash messages.
  • An easy way to create forms with validation, with CSRF protection.
  • A simple way to do translations.

So all of that to say that net/http and html/template and not sufficient to build a real web application. That’s why bow was initiated, to pack additional small and effective libraries to fill the gaps.

Getting started

Using bow cli

That is the quickest way to get you started. First, install the cli.

go install github.com/lobre/bow/cmd/bow@latest

Initialize a new project.

mkdir myproject && cd myproject
go mod init myproject

Generate all the necessary files.

bow init

Tidy your dependencies and build the project.

go mod tidy
go build

Run and check your browser at localhost:8080.

./myproject

You are now ready to start developing features!

Manually
See details

First, initialize a new project.

mkdir myproject && cd myproject
go mod init myproject

Gather the dependencies.

go get github.com/julienschmidt/httprouter
go get github.com/lobre/bow

You will then need to define a base HTML layout.

mkdir -p views/layouts
cat views/layouts/base.html

<!DOCTYPE html>
<html lang="us">
  <head>
    <meta charset="utf-8" />
    <meta name="csrf-token" content="{{ csrf }}" />
    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />

    <title>{{ template "title" . }}</title>

    <link href='/{{ hash "assets/style.css" }}' rel="stylesheet">
    <link rel="icon" href='/{{ hash "assets/favicon.ico" }}'>

    {{ block "head"  . }}{{ end }}
  </head>

  <body>
    <nav>
      <a href="/">{{ "Home" }}</a>
    </nav>

    <main>
      {{ template "main" . }}
    </main>
  </body>
</html>

NOTE: The folders views and layouts cannot be renamed, and the base layout should be named base.html.

Then, let’s create a first HTML page.

cat views/home.html

{{ define "title" }}{{ "Home" }}{{ end }}

<div>"Hello World"</div>

Now, let’s create an assets folder in which you can add your favicon and your css style which will be empty for now.

mkdir assets
cp <your_icon> assets/favicon.ico
touch assets/style.css

It is now the time to start implementing our go code! Create a main.go.

You will need a fs.FS for those just created assets and templates. It is recommended to use an embed, so that they will be contained in your final binary. Add this at the top of your main.go file.

//go:embed assets
//go:embed views
var fsys embed.FS

Bow brings a bow.Core structure that should be embedded in your own struct. I have defined this struct application here in the main.go as well.

type application struct {
	*bow.Core

	// your future own fields
}

Then create your main func, define an instance of this struct and configure bow.

func main() {
	app := application{}
	app.Core, err_ = bow.NewCore(fsys)
	if err != nil {
		panic(err)
	}
}

We now need to define our application routes. Add this other function to your main.go.

func (app *application) routes() http.Handler {
	chain := app.DynChain()
	router := httprouter.New()
	router.Handler(http.MethodGet, "/assets/*filepath", app.FileServer())
	router.Handler(http.MethodGet, "/", chain.ThenFunc(app.home))
	return app.StdChain().Then(router)
}

And also our home handler that tells to render the page named home and that will correspond to our views/home.html.

func (app *application) home(w http.ResponseWriter, r *http.Request) {
	app.Views.Render(w, r, http.StatusOK, "home", nil)
}

To finish, at the end of your main.go, create an http.Server and run the app.

func main() {
	app := application{}
	app.Core, err_ = bow.NewCore(fsys)
	if err != nil {
		panic(err)
	}
	
	srv := &http.Server{
		Addr:         ":8080",
		Handler:      app.routes(),
		IdleTimeout:  time.Minute,
		ReadTimeout:  10 * time.Second,
		WriteTimeout: 30 * time.Second,
	}
	
	err := app.Run(srv)
	if err != nil {
		panic(err)
	}
}

NOTE: Make sure to format your main.go and auto-import the dependencies.

Build the project.

go build

Finally, run and check your browser at localhost:8080.

./myproject

You are now ready to start developing features!

Other features

The getting started guide explains how to get started as quickly as possible. However, there are 3 other features that can be enabled through bow init.

  • bow init -with-db: To allow having a sqlite connection with a migration system.
  • bow init -with-session: To allow having a persistent secured session stored in a user cookie.
  • bow init -with-translator: To allow having simple translation capabilities from csv files.

Also, feel free to explore the go documentation of bow, to better understand what it brings to the table.

Framework or not?

When you use a library, you are in charge of the application flow. You choose when and where to call the library. When you use a framework, the framework is in charge of the flow.

Following that definition, I don’t consider bow to be a web framework. It is simply a set of libraries that are carefully wrapped to provide the necessary tools required to build a robust web application. You simply embed the main bow.Core structure in your application, and you still have the freedom to organize your go code as you wish.

Dependencies

To me, having a minimal set of dependencies is key. Less code, less maintenance, fewer security issues.

The choice of those dependencies has been made carefully to include only small, strongly built and focused libraries that were not worth reimplementing.

Acknowledgement

This project has been heavily inspired by the awesome work of:

  • Alex Edwards and his book Let’s Go.
  • Ben Johnson and its project wtf.

Projects

For a project using bow, check github.com/lobre/tdispo.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ApplyLayout

func ApplyLayout(layout string) func(http.Handler) http.Handler

ApplyLayout is a middleware that applies a specific layout for the rendering of the view. It returns a function which has the correct signature to be used with alice, but it can also be used without.

https://pkg.go.dev/github.com/justinas/alice#Constructor

func Format

func Format(dt time.Time, layout string, locale string) string

Format uses the package monday to format a time.Time according to a locale with month and days translated.

func WithLayout

func WithLayout(r *http.Request, layout string) *http.Request

WithLayout returns a shallow copy of the request but with the information of the layout to apply. It can be used in a handler before calling render to change the layout.

Types

type Core

type Core struct {
	Logger *log.Logger

	DB      *DB
	Views   *Views
	Session *sessions.Session
	// contains filtered or unexported fields
}

Core holds the core logic to configure and run a simple web app. It is meant to be embedded in a parent web app structure.

func NewCore

func NewCore(fsys fs.FS, options ...Option) (*Core, error)

NewCore creates a core with sane defaults. Options can be used for specific configurations.

func (*Core) DynChain

func (core *Core) DynChain() alice.Chain

DynChain returns a chain of middleware that can be applied to all dynamic routes. It injects a CSRF cookie and enable sessions.

func (*Core) FileServer

func (core *Core) FileServer() http.Handler

FileServer returns a handler for serving filesystem files. It enforces http cache by appending hashes to filenames. A hashName function is defined in templates to gather the hashed filename of a file.

func (*Core) Flash

func (core *Core) Flash(r *http.Request, msg string)

Flash sets a flash message to the session.

func (*Core) Run

func (core *Core) Run(srv *http.Server) error

Run runs the http server and launches a goroutine to listen to os.Interrupt before stopping it gracefully.

func (*Core) StdChain

func (core *Core) StdChain() alice.Chain

StdChain returns a chain of middleware that can be applied to all routes. It gracefully handles panics to avoid spinning down the whole app. It logs requests and add default secure headers.

type DB

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

DB represents the database connection.

func NewDB

func NewDB(dns string, fsys fs.FS) *DB

NewDB creates a new DB taking a data source name and a filesystem for migration files.

func (*DB) BeginTx

func (db *DB) BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error)

BeginTx calls the underlying method on the db.

func (*DB) Close

func (db *DB) Close() error

Close closes the database connection.

func (*DB) Open

func (db *DB) Open() (err error)

open opens a sqlite database specified by the data source name. It also enables WAL mode and foreign keys check, and finally execute pending SQL migrations.

type Form

type Form struct {
	url.Values
	// contains filtered or unexported fields
}

Form validates form data against a particular set of rules. If an error occurs, it will store an error message associated with the field.

func NewForm

func NewForm(data url.Values) *Form

New creates a new Form taking data as entry.

func (*Form) CustomError

func (f *Form) CustomError(field, msg string)

CustomError adds a specific error for a field.

func (*Form) Error

func (f *Form) Error(field string) string

Error retrieves the first error message for a given field from the errors map.

func (*Form) IsDate

func (f *Form) IsDate(fields ...string)

IsDate checks that a specific field in the form is a correct date.

func (*Form) IsEmail

func (f *Form) IsEmail(fields ...string)

IsEmail checks that a specific field in the form is a correct email.

func (*Form) IsInteger

func (f *Form) IsInteger(fields ...string)

IsInteger checks that a specific field in the form is an integer.

func (*Form) IsTime

func (f *Form) IsTime(fields ...string)

IsTime checks that a specific field in the form is a correct time.

func (*Form) MatchesPattern

func (f *Form) MatchesPattern(field string, pattern *regexp.Regexp)

MatchesPattern checks that a specific field in the form matches a regular expression. If the check fails, then add the appropriate message to the form errors.

func (*Form) MaxLength

func (f *Form) MaxLength(field string, d int)

MaxLength checks that a specific field in the form contains a maximum number of characters. If the check fails, then add the appropriate message to the form errors.

func (*Form) MinLength

func (f *Form) MinLength(field string, d int)

MinLength checks that a specific field in the form contains a minimum number of characters. If the check fails, then add the appropriate message to the form errors.

func (*Form) PermittedValues

func (f *Form) PermittedValues(field string, opts ...string)

PermittedValues checks that a specific field in the form matches one of a set of specific permitted values. If the check fails, then add the appropriate message to the form errors.

func (*Form) Required

func (f *Form) Required(fields ...string)

Required checks that specific fields in the form data are present and not blank. If any fields fail this check, add the appropriate message to the form errors.

func (*Form) Valid

func (f *Form) Valid() bool

Valid returns true if there are no errors in the form.

type Option

type Option func(*Core) error

Option configures a core.

func WithCSP

func WithCSP(csp map[string]string) Option

WithCSP is an option to set the csp rules that will be set on http responses. By default, only default-src : 'self' is defined.

func WithDB

func WithDB(dsn string) Option

WithDB is an option to enable and configure the database access.

func WithDebug

func WithDebug(debug bool) Option

WithDebug is an option to spit the server errors directly in http responses, instead of a generic 'Internal Server Error' message.

func WithFuncs

func WithFuncs(funcs template.FuncMap) Option

WithFuncs is an option to configure default functions that will be injected into views.

func WithGlobals

func WithGlobals(fn func(*http.Request) interface{}) Option

WithGlobals is an option that allows to define a function that is called at each rendering to inject data that can be retrieved using the "globals" helper template function.

func WithLogger

func WithLogger(logger *log.Logger) Option

WithLogger is an option to set the application logger.

func WithReqFuncs

func WithReqFuncs(funcs ReqFuncMap) Option

WithReqFuncs is an option similar to WithFuncs, but with functions that are request-aware.

func WithSession

func WithSession(key string) Option

WithSession is an option to enable cookie sessions. The key parameter is the secret you want to use to authenticate and encrypt sessions cookies, and should be 32 bytes long.

func WithTranslator

func WithTranslator(locale string) Option

WithTranslator is an option to enable and configure the translator. If the locale paramater value is "auto", the locale will be retrieved first from the "lang" cookie, then from the "Accept-Language" request header. If it cannot retrieve it, messages will be returned untranslated.

type ReqFuncMap

type ReqFuncMap map[string]func(r *http.Request) interface{}

ReqFuncMap is a dynamic version of template.FuncMap that is request-aware.

type Translator

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

Translator allows to translate a message from english to a predefined set of locales parsed from csv files. Il also deals with date and time formats.

func NewTranslator

func NewTranslator() *Translator

NewTranslator creates a translator.

func (*Translator) Parse

func (tr *Translator) Parse(fsys fs.FS) error

Parse parses all the csv files in the translations folder and build dictionnary maps that will serve as databases for translations. The name of csv file should be a string representing a locale (e.g. en_US). When % is used in a csv translation, it will serve as a placeholder and its value won’t be altered during the translation.

func (*Translator) ReqLocale

func (tr *Translator) ReqLocale(r *http.Request) string

ReqLocale tries to return the locale from the request. It tries to retrieve it first using the "lang" cookie and otherwise using the "Accept-Language" request header. If the locale is not recognized or not supported, it will return the default locale (en_US).

func (*Translator) Translate

func (tr *Translator) Translate(msg string, locale string) string

Translate translates a message into the language of the corresponding locale. If the locale or the message is not found, it will be returned untranslated.

type Views

type Views struct {
	Logger *log.Logger
	Debug  bool // to display errors in http responses
	// contains filtered or unexported fields
}

Views is an engine that will render Views from templates.

func NewViews

func NewViews() *Views

NewViews creates a views engine.

func (*Views) ClientError

func (views *Views) ClientError(w http.ResponseWriter, status int)

ClientError sends a specific status code and corresponding description to the user. This should be used to send responses when there's a problem with the request that the user sent.

func (*Views) Funcs

func (views *Views) Funcs(funcs template.FuncMap)

Funcs adds the elements of the argument map to the list of functions to inject into templates.

func (*Views) Parse

func (views *Views) Parse(fsys fs.FS) error

Parse walks a filesystem from the root folder to discover and parse html files into views. Files starting with an underscore are partial views. Files in the layouts folder not starting with underscore are layouts. The rest of html files are full page views. The funcs parameter is a list of functions that is attached to views.

Views, layouts and partials will be referred to with their path, but without the root folder, and without the file extension.

Layouts will be referred to without the layouts folder neither.

Partials files are named with a leading underscore to distinguish them from regular views, but will be referred to without the underscore.

func (*Views) Render

func (views *Views) Render(w http.ResponseWriter, r *http.Request, status int, name string, data interface{})

Render renders a given view or partial.

For page views, the layout can be set using the WithLayout function or using the ApplyLayout middleware. If no layout is defined, the "base" layout will be chosen. Partial views are rendered without any layout.

func (*Views) ReqFuncs

func (views *Views) ReqFuncs(reqFuncs ReqFuncMap)

ReqFuncs adds the elements of the argument map to the list of request-aware functions to inject into templates.

func (*Views) ServerError

func (views *Views) ServerError(w http.ResponseWriter, err error)

ServerError writes an error message and stack trace to the logger, then sends a generic 500 Internal Server Error response to the user.

Directories

Path Synopsis
cmd
bow

Jump to

Keyboard shortcuts

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