oef/renderer/renderer.go
2019-11-13 13:57:41 +01:00

309 lines
7 KiB
Go

package renderer
import (
"bufio"
"encoding/json"
"fmt"
"html/template"
"io"
"log"
"mime"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"reflect"
"strings"
"time"
"github.com/gocarina/gocsv"
"github.com/gorilla/schema"
)
type Renderer interface {
Render(http.ResponseWriter, *http.Request, interface{}, ...url.Values) error
}
type JSONRenderer struct{}
type CSVRenderer struct{}
type PDFRenderer struct{}
type htmlTemplateData struct {
Data interface{}
Options url.Values
}
type JsonResponse struct {
Result []byte
Error []byte
}
var (
currRenderer Renderer
Render map[string]func(http.ResponseWriter, *http.Request, interface{}, ...url.Values)
contentTypeToFormat = map[string]string{
"application/x-www-form-urlencoded": "html",
"application/json": "json",
"text/csv; charset=utf-8": "csv",
"application/pdf": "pdf",
}
)
func init() {
htmlRenderer, err := NewHTMLRenderer("templates/")
if err != nil {
panic(err)
}
jsonRenderer, err := NewJSONRenderer()
if err != nil {
panic(err)
}
csvRenderer, err := NewCSVRenderer()
if err != nil {
panic(err)
}
pdfRenderer, err := NewPDFRenderer()
if err != nil {
panic(err)
}
Render = make(map[string]func(http.ResponseWriter, *http.Request, interface{}, ...url.Values))
Render["html"] = func(w http.ResponseWriter, r *http.Request, data interface{}, options ...url.Values) {
htmlRenderer.Render(w, r, data, options...)
}
Render["json"] = func(w http.ResponseWriter, r *http.Request, data interface{}, options ...url.Values) {
jsonRenderer.Render(w, r, data, options...)
}
Render["csv"] = func(w http.ResponseWriter, r *http.Request, data interface{}, options ...url.Values) {
csvRenderer.Render(w, r, data, options...)
}
Render["pdf"] = func(w http.ResponseWriter, r *http.Request, data interface{}, options ...url.Values) {
pdfRenderer.Render(w, r, data, options...)
}
}
func Query(values ...string) template.URL {
return query(values...)
}
// FIXME: Not safe. We should check if it responds to the error interface instead.
func isErrorType(data interface{}) bool {
return strings.Contains(strings.ToLower(reflect.TypeOf(data).String()), "error")
}
func NewJSONRenderer() (*JSONRenderer, error) {
return &JSONRenderer{}, nil
}
func (rend *JSONRenderer) Render(w http.ResponseWriter, r *http.Request, data interface{}, options ...url.Values) error {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if isErrorType(data) {
j, err := json.Marshal(JsonResponse{nil, []byte(data.(error).Error())})
if err != nil {
return err
}
w.WriteHeader(http.StatusInternalServerError)
w.Write(j)
} else {
result, err := json.Marshal(data)
if err != nil {
return err
}
response, err := json.Marshal(JsonResponse{result, nil})
if err != nil {
return err
}
w.Write(response)
}
return nil
}
func NewCSVRenderer() (*CSVRenderer, error) {
return &CSVRenderer{}, nil
}
func (rend *CSVRenderer) Render(w http.ResponseWriter, r *http.Request, data interface{}, options ...url.Values) error {
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
if isErrorType(data) {
j, err := json.Marshal(JsonResponse{nil, []byte(data.(error).Error())})
if err != nil {
return err
}
w.WriteHeader(http.StatusInternalServerError)
w.Write(j)
} else {
result, err := gocsv.MarshalString(data)
if err != nil {
return err
}
response, err := json.Marshal(JsonResponse{[]byte(result), nil})
if err != nil {
return err
}
w.Write(response)
}
return nil
}
func NewPDFRenderer() (*PDFRenderer, error) {
return &PDFRenderer{}, nil
}
func (rend *PDFRenderer) Render(w http.ResponseWriter, r *http.Request, data interface{}, options ...url.Values) error {
fileInfo := data.(map[string]string)
filename := fileInfo["filename"]
w.Header().Set("Content-Type", "application/pdf")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filepath.Base(filename)))
f, err := os.Open(filename)
if err != nil {
panic(err)
}
reader := bufio.NewReader(f)
io.Copy(w, reader)
return nil
}
type HTMLRenderer struct {
TemplatePath string
templates map[string]*template.Template
}
func NewHTMLRenderer(templatePath string) (*HTMLRenderer, error) {
r := &HTMLRenderer{
TemplatePath: templatePath,
templates: make(map[string]*template.Template),
}
fns, err := filepath.Glob(filepath.Join(templatePath, "*.tpl"))
if err != nil {
panic(err)
}
lfns, err := filepath.Glob(filepath.Join(templatePath, "layout", "*.tpl"))
if err != nil {
panic(err)
}
for _, fn := range fns {
tplName := filepath.Base(fn)
tplName = strings.TrimSuffix(tplName, path.Ext(tplName))
tplName = strings.TrimSuffix(tplName, path.Ext(tplName))
files := append(lfns, fn)
if err != nil {
return nil, err
}
r.templates[tplName] = template.New(tplName)
r.templates[tplName] = template.Must(r.templates[tplName].Funcs(funcMap).ParseFiles(files...))
}
return r, nil
}
func Use(r Renderer) {
currRenderer = r
}
func (rend *HTMLRenderer) writeError(w http.ResponseWriter, r *http.Request, data interface{}) {
t, ok := rend.templates["error"]
if !ok {
panic(fmt.Errorf("Error template not found! Can't proceed, sorry."))
}
log.Println(data.(*htmlTemplateData).Data.(error))
w.Header().Set("Content-Type", "text/html; charset=utf-8")
e := t.ExecuteTemplate(w, "base", data)
if e != nil {
panic(e)
}
}
func (rend *HTMLRenderer) Render(w http.ResponseWriter, r *http.Request, data interface{}, options ...url.Values) {
if data != nil && isErrorType(data) {
rend.writeError(w, r, &htmlTemplateData{data.(error), options[0]})
} else {
t, ok := rend.templates[options[0]["tpl_content"][0]]
if !ok {
err := fmt.Errorf("Template %s not found", options[0]["tpl_content"][0])
rend.writeError(w, r, &htmlTemplateData{err, options[0]})
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
err := t.ExecuteTemplate(w, options[0]["tpl_layout"][0], &htmlTemplateData{data, options[0]})
if err != nil {
rend.writeError(w, r, &htmlTemplateData{err, options[0]})
}
}
}
func GetContentFormat(r *http.Request) string {
var (
t string
err error
)
contentType := r.Header.Get("Content-type")
if contentType == "" {
return "application/octet-stream"
}
t, _, err = mime.ParseMediaType(contentType)
if err != nil {
return ""
}
return contentTypeToFormat[t]
}
func Decode(dst interface{}, r *http.Request) error {
switch GetContentFormat(r) {
case "html":
var timeConverter = func(value string) reflect.Value {
if value == "" {
return reflect.ValueOf(time.Time{})
}
if v, err := time.Parse("2006-01-02", value); err == nil {
return reflect.ValueOf(v)
}
return reflect.Value{}
}
if err := r.ParseForm(); err != nil {
return err
}
decoder := schema.NewDecoder()
decoder.RegisterConverter(time.Time{}, timeConverter)
if err := decoder.Decode(dst, r.PostForm); err != nil {
return err
}
case "json":
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&dst)
if err != nil {
return err
}
}
return nil
}