commit
2cb1bf1881
23 changed files with 827 additions and 0 deletions
@ -0,0 +1,24 @@
|
||||
test: |
||||
go clean -testcache
|
||||
go test -v ./...
|
||||
|
||||
deps: |
||||
go get -u golang.org/x/tools/cmd/godoc
|
||||
go get -u github.com/cortesi/modd/cmd/modd
|
||||
go get -u github.com/golangci/golangci-lint/cmd/golangci-lint
|
||||
|
||||
coverage: |
||||
@script/coverage
|
||||
|
||||
lint: |
||||
golangci-lint run --tests=false --enable-all
|
||||
|
||||
clean: |
||||
rm -rf ./bin ./release ./coverage
|
||||
|
||||
doc: |
||||
@echo "open your browser to http://localhost:6060/pkg/forge.cadoles.com/wpetit/goweb to see the documentation"
|
||||
godoc -http=:6060
|
||||
|
||||
|
||||
.PHONY: test clean lint coverage doc |
@ -0,0 +1,13 @@
|
||||
# goweb |
||||
|
||||
Librairie d'aide à la création d'applications web pour Go |
||||
|
||||
**Librairie en cours de développement.** Des changements d'API peuvent intervenir à tout moment. |
||||
|
||||
## Documentation |
||||
|
||||
https://godoc.org/forge.cadoles.com/wpetit/goweb |
||||
|
||||
## License |
||||
|
||||
AGPL-3.0 |
@ -0,0 +1,32 @@
|
||||
package middleware |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
|
||||
"github.com/go-chi/chi/middleware" |
||||
) |
||||
|
||||
const ( |
||||
// KeyDebug is the context key associated with the debug value
|
||||
KeyDebug ContextKey = "debug" |
||||
) |
||||
|
||||
// ErrInvalidDebug is returned when no debug value
|
||||
// could be found on the given context
|
||||
var ErrInvalidDebug = errors.New("invalid debug") |
||||
|
||||
// GetDebug retrieves the debug value from the given context
|
||||
func GetDebug(ctx context.Context) (bool, error) { |
||||
debug, ok := ctx.Value(KeyDebug).(bool) |
||||
if !ok { |
||||
return false, ErrInvalidDebug |
||||
} |
||||
return debug, nil |
||||
} |
||||
|
||||
// Debug expose the given debug flag as a context value
|
||||
// on the HTTP requests
|
||||
func Debug(debug bool) Middleware { |
||||
return middleware.WithValue(KeyDebug, debug) |
||||
} |
@ -0,0 +1,22 @@
|
||||
package middleware |
||||
|
||||
import ( |
||||
"context" |
||||
"testing" |
||||
) |
||||
|
||||
func TestContextDebug(t *testing.T) { |
||||
|
||||
debug := false |
||||
ctx := context.WithValue(context.Background(), KeyDebug, debug) |
||||
|
||||
dbg, err := GetDebug(ctx) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if dbg { |
||||
t.Fatal("debug should be false") |
||||
} |
||||
|
||||
} |
@ -0,0 +1,28 @@
|
||||
package middleware |
||||
|
||||
import "net/http" |
||||
|
||||
// InvalidHostRedirect returns a middleware that redirects incoming
|
||||
// requests that do not use the expected host/schemes
|
||||
func InvalidHostRedirect(expectedHost string, useTLS bool) Middleware { |
||||
return func(next http.Handler) http.Handler { |
||||
fn := func(w http.ResponseWriter, r *http.Request) { |
||||
invalidScheme := (useTLS && r.TLS == nil) || (!useTLS && r.TLS != nil) |
||||
invalidHost := expectedHost != r.Host |
||||
if invalidHost || invalidScheme { |
||||
if expectedHost != "" { |
||||
r.URL.Host = expectedHost |
||||
} |
||||
if useTLS && r.TLS == nil { |
||||
r.URL.Scheme = "https" |
||||
} else if !useTLS && r.TLS != nil { |
||||
r.URL.Scheme = "http" |
||||
} |
||||
http.Redirect(w, r, r.URL.String(), http.StatusTemporaryRedirect) |
||||
return |
||||
} |
||||
next.ServeHTTP(w, r) |
||||
} |
||||
return http.HandlerFunc(fn) |
||||
} |
||||
} |
@ -0,0 +1,33 @@
|
||||
package middleware |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
|
||||
"forge.cadoles.com/wpetit/goweb/service" |
||||
"github.com/go-chi/chi/middleware" |
||||
) |
||||
|
||||
const ( |
||||
// KeyServiceContainer is the context key associated with the ServiceContainer value
|
||||
KeyServiceContainer ContextKey = "serviceContainer" |
||||
) |
||||
|
||||
// ErrInvalidServiceContainer is returned when no service container
|
||||
// could be found on the given context
|
||||
var ErrInvalidServiceContainer = errors.New("invalid service container") |
||||
|
||||
// GetServiceContainer retrieves the service container from the given context
|
||||
func GetServiceContainer(ctx context.Context) (*service.Container, error) { |
||||
container, ok := ctx.Value(KeyServiceContainer).(*service.Container) |
||||
if !ok { |
||||
return nil, ErrInvalidServiceContainer |
||||
} |
||||
return container, nil |
||||
} |
||||
|
||||
// ServiceContainer expose the given service container as a context value
|
||||
// on the HTTP requests
|
||||
func ServiceContainer(container *service.Container) Middleware { |
||||
return middleware.WithValue(KeyServiceContainer, container) |
||||
} |
@ -0,0 +1,41 @@
|
||||
package middleware |
||||
|
||||
import ( |
||||
"context" |
||||
"testing" |
||||
|
||||
"forge.cadoles.com/wpetit/goweb/service" |
||||
) |
||||
|
||||
func TestContextServiceContainer(t *testing.T) { |
||||
|
||||
container := service.NewContainer() |
||||
ctx := context.WithValue(context.Background(), KeyServiceContainer, container) |
||||
|
||||
ctn, err := GetServiceContainer(ctx) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if ctn == nil { |
||||
t.Fatal("container should not be nil") |
||||
} |
||||
|
||||
} |
||||
|
||||
func TestContextInvalidServiceContainer(t *testing.T) { |
||||
|
||||
invalidContainer := struct{}{} |
||||
ctx := context.WithValue(context.Background(), KeyServiceContainer, invalidContainer) |
||||
|
||||
container, err := GetServiceContainer(ctx) |
||||
|
||||
if g, e := err, ErrInvalidServiceContainer; g != e { |
||||
t.Errorf("err: got '%v', expected '%v'", g, e) |
||||
} |
||||
|
||||
if container != nil { |
||||
t.Errorf("container: got '%v', expected '%v'", container, nil) |
||||
} |
||||
|
||||
} |
@ -0,0 +1,9 @@
|
||||
package middleware |
||||
|
||||
import "net/http" |
||||
|
||||
// ContextKey are values exposed on the request context
|
||||
type ContextKey string |
||||
|
||||
// Middleware An HTTP middleware
|
||||
type Middleware func(http.Handler) http.Handler |
@ -0,0 +1,25 @@
|
||||
#!/bin/bash |
||||
|
||||
set -eo pipefail |
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" |
||||
GO_PACKAGE_DIRS=$(find . -not -path './vendor/*' -name '*.go' -printf '%h\n' | uniq) |
||||
COVERAGE_DIR="$DIR/../coverage" |
||||
|
||||
rm -rf "$COVERAGE_DIR" |
||||
mkdir -p "$COVERAGE_DIR" |
||||
|
||||
for dir in $GO_PACKAGE_DIRS; do |
||||
pushd $dir > /dev/null |
||||
go test -coverprofile="$DIR/../coverage/${dir//\//_}.out" > /dev/null |
||||
popd > /dev/null |
||||
done |
||||
|
||||
OUTPUT_FILES=$(find "$COVERAGE_DIR" -name '*.out') |
||||
for out in $OUTPUT_FILES; do |
||||
package=$(realpath --relative-to="$DIR/../coverage" "${out}" | sed -e 's/\.out$//' -e 's/^\._//') |
||||
echo "-- Package '${package//_/\/}' --" |
||||
echo |
||||
go tool cover -func="$out" |
||||
echo |
||||
done |
@ -0,0 +1,43 @@
|
||||
package service |
||||
|
||||
import ( |
||||
"errors" |
||||
) |
||||
|
||||
var ( |
||||
// ErrNotImplemented is the error when a service is accessed
|
||||
// and no implementation was provided
|
||||
ErrNotImplemented = errors.New("service not implemented") |
||||
) |
||||
|
||||
// Provider is a provider for a service
|
||||
type Provider func(ctn *Container) (interface{}, error) |
||||
|
||||
// Container is a simple service container for dependency injection
|
||||
type Container struct { |
||||
providers map[Name]Provider |
||||
} |
||||
|
||||
// Name is a name of a service
|
||||
type Name string |
||||
|
||||
// Provide registers a provider for the survey.Service
|
||||
func (c *Container) Provide(name Name, provider Provider) { |
||||
c.providers[name] = provider |
||||
} |
||||
|
||||
// Service retrieves a service implementation based on its name
|
||||
func (c *Container) Service(name Name) (interface{}, error) { |
||||
provider, exists := c.providers[name] |
||||
if !exists { |
||||
return nil, ErrNotImplemented |
||||
} |
||||
return provider(c) |
||||
} |
||||
|
||||
// NewContainer returns a new empty service container
|
||||
func NewContainer() *Container { |
||||
return &Container{ |
||||
providers: make(map[Name]Provider), |
||||
} |
||||
} |
@ -0,0 +1,43 @@
|
||||
package session |
||||
|
||||
const ( |
||||
// FlashError defines an "error" flash message
|
||||
FlashError FlashType = "error" |
||||
// FlashWarn defines an "warning" flash message
|
||||
FlashWarn FlashType = "warn" |
||||
// FlashSuccess defines an "success" flash message
|
||||
FlashSuccess FlashType = "success" |
||||
// FlashInfo defines an "info" flash message
|
||||
FlashInfo FlashType = "info" |
||||
) |
||||
|
||||
// FlashType defines the type of a flash message
|
||||
type FlashType string |
||||
|
||||
// Flash is a ephemeral message that lives in a session
|
||||
// until it's read
|
||||
type Flash interface { |
||||
Type() FlashType |
||||
Message() string |
||||
} |
||||
|
||||
// BaseFlash is a base implementation of a flash message
|
||||
type BaseFlash struct { |
||||
flashType FlashType |
||||
message string |
||||
} |
||||
|
||||
// Type returns the type of the flash
|
||||
func (f *BaseFlash) Type() FlashType { |
||||
return f.flashType |
||||
} |
||||
|
||||
// Message returns the message of the flash
|
||||
func (f *BaseFlash) Message() string { |
||||
return f.message |
||||
} |
||||
|
||||
// NewBaseFlash returns a new BaseFlash
|
||||
func NewBaseFlash(flashType FlashType, message string) *BaseFlash { |
||||
return &BaseFlash{flashType, message} |
||||
} |
@ -0,0 +1,38 @@
|
||||
package session |
||||
|
||||
import ( |
||||
"net/http" |
||||
|
||||
"forge.cadoles.com/wpetit/goweb/service" |
||||
"github.com/pkg/errors" |
||||
) |
||||
|
||||
// ServiceName defines the Session service name
|
||||
const ServiceName service.Name = "session" |
||||
|
||||
// Service defines the API of a "http session" service
|
||||
type Service interface { |
||||
Get(http.ResponseWriter, *http.Request) (Session, error) |
||||
} |
||||
|
||||
// From retrieves the session service in the given container
|
||||
func From(container *service.Container) (Service, error) { |
||||
service, err := container.Service(ServiceName) |
||||
if err != nil { |
||||
return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName) |
||||
} |
||||
sessionService, ok := service.(Service) |
||||
if !ok { |
||||
return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName) |
||||
} |
||||
return sessionService, nil |
||||
} |
||||
|
||||
// Must retrieves the session service in the given container or panic otherwise
|
||||
func Must(container *service.Container) Service { |
||||
service, err := From(container) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
return service |
||||
} |
@ -0,0 +1,16 @@
|
||||
package session |
||||
|
||||
import ( |
||||
"net/http" |
||||
) |
||||
|
||||
// Session defines the API of a Session
|
||||
type Session interface { |
||||
Set(string, interface{}) |
||||
Unset(string) |
||||
Get(string) interface{} |
||||
AddFlash(flashType FlashType, message string) |
||||
Flashes(flashTypes ...FlashType) []Flash |
||||
Save(http.ResponseWriter, *http.Request) error |
||||
Delete(http.ResponseWriter, *http.Request) error |
||||
} |
@ -0,0 +1,19 @@
|
||||
package template |
||||
|
||||
// Data is some data to inject into the template
|
||||
type Data map[string]interface{} |
||||
|
||||
// DataExtFunc is some extensions to a template's data
|
||||
type DataExtFunc func(data Data) (Data, error) |
||||
|
||||
// Extend returns a template's data with the given extensions
|
||||
func Extend(data Data, extensions ...DataExtFunc) (Data, error) { |
||||
var err error |
||||
for _, ext := range extensions { |
||||
data, err = ext(data) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
return data, nil |
||||
} |
@ -0,0 +1,38 @@
|
||||
package template |
||||
|
||||
import ( |
||||
"net/http" |
||||
|
||||
"forge.cadoles.com/wpetit/goweb/service" |
||||
"github.com/pkg/errors" |
||||
) |
||||
|
||||
// ServiceName defines the Tempate service name
|
||||
const ServiceName service.Name = "template" |
||||
|
||||
// Service is a templating service
|
||||
type Service interface { |
||||
RenderPage(w http.ResponseWriter, templateName string, data interface{}) error |
||||
} |
||||
|
||||
// From retrieves the template service in the given container
|
||||
func From(container *service.Container) (Service, error) { |
||||
service, err := container.Service(ServiceName) |
||||
if err != nil { |
||||
return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName) |
||||
} |
||||
templateService, ok := service.(Service) |
||||
if !ok { |
||||
return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName) |
||||
} |
||||
return templateService, nil |
||||
} |
||||
|
||||
// Must retrieves the template service in the given container or panic otherwise
|
||||
func Must(container *service.Container) Service { |
||||
service, err := From(container) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
return service |
||||
} |
@ -0,0 +1,52 @@
|
||||
package gorilla |
||||
|
||||
import ( |
||||
"net/http" |
||||
|
||||
"github.com/gorilla/securecookie" |
||||
|
||||
"forge.cadoles.com/wpetit/goweb/service/session" |
||||
"github.com/gorilla/sessions" |
||||
"github.com/pkg/errors" |
||||
) |
||||
|
||||
// SessionService is an implementation of service.Session
|
||||
// based on the github.com/gorilla/sessions
|
||||
type SessionService struct { |
||||
sessionName string |
||||
store sessions.Store |
||||
defaultOptions *sessions.Options |
||||
} |
||||
|
||||
// Get returns a Session associated with the given HTTP request
|
||||
func (s *SessionService) Get(w http.ResponseWriter, r *http.Request) (session.Session, error) { |
||||
sess, err := s.store.Get(r, s.sessionName) |
||||
if err != nil { |
||||
multiErr, ok := err.(securecookie.MultiError) |
||||
if !ok || multiErr.Error() != securecookie.ErrMacInvalid.Error() { |
||||
return nil, errors.Wrap(err, "error while retrieving the session from the request") |
||||
} |
||||
} |
||||
if err != nil { |
||||
defaultOptions := s.defaultOptionsCopy() |
||||
sess.Options = &defaultOptions |
||||
if err := sess.Save(r, w); err != nil { |
||||
return nil, errors.Wrap(err, "error while saving session") |
||||
} |
||||
} |
||||
return NewSession(sess), nil |
||||
} |
||||
|
||||
func (s *SessionService) defaultOptionsCopy() sessions.Options { |
||||
return *s.defaultOptions |
||||
} |
||||
|
||||
// NewSessionService returns a new SessionService backed
|
||||
// by the given Store
|
||||
func NewSessionService(sessionName string, store sessions.Store, defaultOptions *sessions.Options) *SessionService { |
||||
return &SessionService{ |
||||
sessionName: sessionName, |
||||
store: store, |
||||
defaultOptions: defaultOptions, |
||||
} |
||||
} |
@ -0,0 +1,70 @@
|
||||
package gorilla |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net/http" |
||||
|
||||
"forge.cadoles.com/wpetit/goweb/service/session" |
||||
"github.com/gorilla/sessions" |
||||
) |
||||
|
||||
// Session is an implementation of the session.Session service
|
||||
// backed by github.com/gorilla/sessions Session
|
||||
type Session struct { |
||||
sess *sessions.Session |
||||
} |
||||
|
||||
// Get retrieve a value from the session
|
||||
func (s *Session) Get(key string) interface{} { |
||||
return s.sess.Values[key] |
||||
} |
||||
|
||||
// Set updates a value in the session
|
||||
func (s *Session) Set(key string, value interface{}) { |
||||
s.sess.Values[key] = value |
||||
} |
||||
|
||||
// Unset remove a value in the session
|
||||
func (s *Session) Unset(key string) { |
||||
delete(s.sess.Values, key) |
||||
} |
||||
|
||||
// AddFlash adds the given flash message to the stack
|
||||
func (s *Session) AddFlash(flashType session.FlashType, message string) { |
||||
s.sess.AddFlash(message, s.flashKey(flashType)) |
||||
} |
||||
|
||||
// Flashes retrieves the flash messages of the given types in the stack
|
||||
func (s *Session) Flashes(flashTypes ...session.FlashType) []session.Flash { |
||||
flashes := make([]session.Flash, 0) |
||||
for _, ft := range flashTypes { |
||||
rawFlashes := s.sess.Flashes(s.flashKey(ft)) |
||||
for _, f := range rawFlashes { |
||||
message := fmt.Sprintf("%v", f) |
||||
flashes = append(flashes, session.NewBaseFlash(ft, message)) |
||||
} |
||||
} |
||||
return flashes |
||||
} |
||||
|
||||
func (s *Session) flashKey(flashType session.FlashType) string { |
||||
return fmt.Sprintf("_%s_flash", flashType) |
||||
} |
||||
|
||||
// Save saves the session with its current values
|
||||
func (s *Session) Save(w http.ResponseWriter, r *http.Request) error { |
||||
return s.sess.Save(r, w) |
||||
} |
||||
|
||||
// Delete deletes the session
|
||||
func (s *Session) Delete(w http.ResponseWriter, r *http.Request) error { |
||||
s.sess.Options = &sessions.Options{ |
||||
MaxAge: -1, |
||||
} |
||||
return s.sess.Save(r, w) |
||||
} |
||||
|
||||
// NewSession returns a new Session
|
||||
func NewSession(sess *sessions.Session) *Session { |
||||
return &Session{sess} |
||||
} |
@ -0,0 +1,22 @@
|
||||
package static |
||||
|
||||
import ( |
||||
"net/http" |
||||
"os" |
||||
) |
||||
|
||||
// Dir serves the files in the given directory or
|
||||
// uses the given handler to handles missing files
|
||||
func Dir(dirPath string, stripPrefix string, notFoundHandler http.Handler) http.Handler { |
||||
root := http.Dir(dirPath) |
||||
fs := http.FileServer(root) |
||||
fn := func(w http.ResponseWriter, r *http.Request) { |
||||
if _, err := os.Stat(dirPath + r.RequestURI); os.IsNotExist(err) { |
||||
notFoundHandler.ServeHTTP(w, r) |
||||
} else { |
||||
fs.ServeHTTP(w, r) |
||||
} |
||||
} |
||||
handler := http.StripPrefix(stripPrefix, http.HandlerFunc(fn)) |
||||
return handler |
||||
} |
@ -0,0 +1,40 @@
|
||||
package html |
||||
|
||||
import ( |
||||
"net/http" |
||||
|
||||
"forge.cadoles.com/wpetit/goweb/service" |
||||
"forge.cadoles.com/wpetit/goweb/service/session" |
||||
"forge.cadoles.com/wpetit/goweb/service/template" |
||||
"github.com/pkg/errors" |
||||
) |
||||
|
||||
// WithFlashes extends the template's data with session's flash messages
|
||||
func WithFlashes(w http.ResponseWriter, r *http.Request, container *service.Container) template.DataExtFunc { |
||||
return func(data template.Data) (template.Data, error) { |
||||
|
||||
sessionService, err := session.From(container) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "error while retrieving session service") |
||||
} |
||||
|
||||
sess, err := sessionService.Get(w, r) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "error while retrieving session") |
||||
} |
||||
|
||||
flashes := sess.Flashes( |
||||
session.FlashError, session.FlashWarn, |
||||
session.FlashSuccess, session.FlashInfo, |
||||
) |
||||
|
||||
data["Flashes"] = flashes |
||||
|
||||
if err := sess.Save(w, r); err != nil { |
||||
return nil, errors.Wrap(err, "error while saving session") |
||||
} |
||||
|
||||
return data, nil |
||||
|
||||
} |
||||
} |
@ -0,0 +1,71 @@
|
||||
package html |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"errors" |
||||
"reflect" |
||||
"unicode/utf8" |
||||
) |
||||
|
||||
func dump(args ...interface{}) (string, error) { |
||||
out := "" |
||||
for _, a := range args { |
||||
str, err := json.MarshalIndent(a, "", " ") |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
out += string(str) + "\n" |
||||
} |
||||
return out, nil |
||||
} |
||||
|
||||
func increment(value int) int { |
||||
return value + 1 |
||||
} |
||||
|
||||
func ellipsis(str string, max int) string { |
||||
if utf8.RuneCountInString(str) > max { |
||||
return string([]rune(str)[0:(max-3)]) + "..." |
||||
} |
||||
return str |
||||
} |
||||
|
||||
func customMap(values ...interface{}) (map[string]interface{}, error) { |
||||
if len(values)%2 != 0 { |
||||
return nil, errors.New("invalid custoMmap call") |
||||
} |
||||
customMap := make(map[string]interface{}, len(values)/2) |
||||
for i := 0; i < len(values); i += 2 { |
||||
key, ok := values[i].(string) |
||||
if !ok { |
||||
return nil, errors.New("map keys must be strings") |
||||
} |
||||
customMap[key] = values[i+1] |
||||
} |
||||
return customMap, nil |
||||
} |
||||
|
||||
func defaultVal(src interface{}, defaultValue interface{}) interface{} { |
||||
switch typ := src.(type) { |
||||
case string: |
||||
if typ == "" { |
||||
return defaultValue |
||||
} |
||||
default: |
||||
if src == nil { |
||||
return defaultValue |
||||
} |
||||
} |
||||
return src |
||||
} |
||||
|
||||
func has(v interface{}, name string) bool { |
||||
rv := reflect.ValueOf(v) |
||||
if rv.Kind() == reflect.Ptr { |
||||
rv = rv.Elem() |
||||
} |
||||
if rv.Kind() != reflect.Struct { |
||||
return false |
||||
} |
||||
return rv.FieldByName(name).IsValid() |
||||
} |
@ -0,0 +1,58 @@
|
||||
package html |
||||
|
||||
import "html/template" |
||||
|
||||
// Options are configuration options for the template service
|
||||
type Options struct { |
||||
Helpers template.FuncMap |
||||
PoolSize int |
||||
} |
||||
|
||||
// OptionFunc configures options for the template service
|
||||
type OptionFunc func(*Options) |
||||
|
||||
// WithDefaultHelpers configures the template service
|
||||
// to expose the default helpers
|
||||
func WithDefaultHelpers() OptionFunc { |
||||
return func(opts *Options) { |
||||
helpers := template.FuncMap{ |
||||
"dump": dump, |
||||
"ellipsis": ellipsis, |
||||
"map": customMap, |
||||
"default": defaultVal, |
||||
"inc": increment, |
||||
"has": has, |
||||
} |
||||
for name, fn := range helpers { |
||||
WithHelper(name, fn)(opts) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// WithHelper configures the template service
|
||||
// to expose the default helpers
|
||||
func WithHelper(name string, fn interface{}) OptionFunc { |
||||
return func(opts *Options) { |
||||
opts.Helpers[name] = fn |
||||
} |
||||
} |
||||
|
||||
// WithPoolSize configures the template service
|
||||
// to use the given pool size
|
||||
func WithPoolSize(size int) OptionFunc { |
||||
return func(opts *Options) { |
||||
opts.PoolSize = size |
||||
} |
||||
} |
||||
|
||||
func defaultOptions() *Options { |
||||
options := &Options{} |
||||
funcs := []OptionFunc{ |
||||
WithPoolSize(64), |
||||
WithDefaultHelpers(), |
||||
} |
||||
for _, f := range funcs { |
||||
f(options) |
||||
} |
||||
return options |
||||
} |
@ -0,0 +1,89 @@
|
||||
package html |
||||
|
||||
import ( |
||||
"fmt" |
||||
"html/template" |
||||
"net/http" |
||||
"path/filepath" |
||||
|
||||
"github.com/oxtoacart/bpool" |
||||
) |
||||
|
||||
// TemplateService is a template/html based templating service
|
||||
type TemplateService struct { |
||||
templates map[string]*template.Template |
||||
pool *bpool.BufferPool |
||||
helpers template.FuncMap |
||||
} |
||||
|
||||
// LoadTemplates loads the templates used by the service
|
||||
func (t *TemplateService) LoadTemplates(templatesDir string) error { |
||||
|
||||
layouts, err := filepath.Glob(filepath.Join(templatesDir, "layouts", "*.tmpl")) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
blocks, err := filepath.Glob(filepath.Join(templatesDir, "blocks", "*.tmpl")) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// Generate our templates map from our layouts/ and blocks/ directories
|
||||
for _, layout := range layouts { |
||||
|
||||
var err error |
||||
files := append(blocks, layout) |
||||
|
||||
tmpl := template.New("") |
||||
tmpl.Funcs(t.helpers) |
||||
|
||||
tmpl, err = tmpl.ParseFiles(files...) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
t.templates[filepath.Base(layout)] = tmpl |
||||
|
||||
} |
||||
|
||||
return nil |
||||
|
||||
} |
||||
|
||||
// RenderPage renders a template to the given http.ResponseWriter
|
||||
func (t *TemplateService) RenderPage(w http.ResponseWriter, templateName string, data interface{}) error { |
||||
|
||||
// Ensure the template exists in the map.
|
||||
tmpl, ok := t.templates[templateName] |
||||
if !ok { |
||||
return fmt.Errorf("the template '%s' does not exist", templateName) |
||||
} |
||||
|
||||
// Create a buffer to temporarily write to and check if any errors were encountered.
|
||||
buf := t.pool.Get() |
||||
defer t.pool.Put(buf) |
||||
|
||||
if err := tmpl.ExecuteTemplate(buf, templateName, data); err != nil { |
||||
return err |
||||
} |
||||
|
||||
// Set the header and write the buffer to the http.ResponseWriter
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8") |
||||
_, err := buf.WriteTo(w) |
||||
return err |
||||
|
||||
} |
||||
|
||||
// NewTemplateService returns a new Service
|
||||
func NewTemplateService(funcs ...OptionFunc) *TemplateService { |
||||
options := defaultOptions() |
||||
for _, f := range funcs { |
||||
f(options) |
||||
} |
||||
return &TemplateService{ |
||||
templates: make(map[string]*template.Template), |
||||
pool: bpool.NewBufferPool(options.PoolSize), |
||||
helpers: options.Helpers, |
||||
} |
||||
} |
Loading…
Reference in new issue