Initial commit
This commit is contained in:
commit
2cb1bf1881
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/coverage
|
24
Makefile
Normal file
24
Makefile
Normal file
@ -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
|
13
README.md
Normal file
13
README.md
Normal file
@ -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
|
32
middleware/debug.go
Normal file
32
middleware/debug.go
Normal file
@ -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)
|
||||
}
|
22
middleware/debug_test.go
Normal file
22
middleware/debug_test.go
Normal file
@ -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")
|
||||
}
|
||||
|
||||
}
|
28
middleware/invalid_host.go
Normal file
28
middleware/invalid_host.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
33
middleware/service_container.go
Normal file
33
middleware/service_container.go
Normal file
@ -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)
|
||||
}
|
41
middleware/service_container_test.go
Normal file
41
middleware/service_container_test.go
Normal file
@ -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)
|
||||
}
|
||||
|
||||
}
|
9
middleware/type.go
Normal file
9
middleware/type.go
Normal file
@ -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
|
25
script/coverage
Executable file
25
script/coverage
Executable file
@ -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
|
43
service/container.go
Normal file
43
service/container.go
Normal file
@ -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),
|
||||
}
|
||||
}
|
43
service/session/flash.go
Normal file
43
service/session/flash.go
Normal file
@ -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}
|
||||
}
|
38
service/session/service.go
Normal file
38
service/session/service.go
Normal file
@ -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
|
||||
}
|
16
service/session/session.go
Normal file
16
service/session/session.go
Normal file
@ -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
|
||||
}
|
19
service/template/data.go
Normal file
19
service/template/data.go
Normal file
@ -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
|
||||
}
|
38
service/template/service.go
Normal file
38
service/template/service.go
Normal file
@ -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
|
||||
}
|
52
session/gorilla/service.go
Normal file
52
session/gorilla/service.go
Normal file
@ -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,
|
||||
}
|
||||
}
|
70
session/gorilla/session.go
Normal file
70
session/gorilla/session.go
Normal file
@ -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}
|
||||
}
|
22
static/static.go
Normal file
22
static/static.go
Normal file
@ -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
|
||||
}
|
40
template/html/data.go
Normal file
40
template/html/data.go
Normal file
@ -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
|
||||
|
||||
}
|
||||
}
|
71
template/html/helper.go
Normal file
71
template/html/helper.go
Normal file
@ -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()
|
||||
}
|
58
template/html/option.go
Normal file
58
template/html/option.go
Normal file
@ -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
|
||||
}
|
89
template/html/service.go
Normal file
89
template/html/service.go
Normal file
@ -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
Block a user