Initial commit
This commit is contained in:
commit
2cb1bf1881
|
@ -0,0 +1 @@
|
||||||
|
/coverage
|
|
@ -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