package renderer import ( "bufio" "encoding/json" "fmt" "html/template" "io" "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, err error) { t, ok := rend.templates["error"] if !ok { panic(fmt.Errorf("Error template not found! Can't proceed, sorry.")) } w.Header().Set("Content-Type", "text/html; charset=utf-8") e := t.ExecuteTemplate(w, "base", err) 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, data.(error)) } else { t, ok := rend.templates[options[0]["tpl_content"][0]] if !ok { rend.writeError(w, r, fmt.Errorf("Template %s not found", options[0]["tpl_content"][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, err) } } } 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 }