github.com/justinjmoses/evergreen@v0.0.0-20170530173719-1d50e381ff0d/web/app.go (about) 1 package web 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "html/template" 8 "net/http" 9 "os" 10 "path/filepath" 11 "reflect" 12 "strings" 13 14 "github.com/codegangsta/inject" 15 "github.com/mongodb/grip" 16 "github.com/pkg/errors" 17 ) 18 19 const JSONMarshalError = "Could not marshal data to JSON" 20 21 type App struct { 22 Router *http.Handler 23 TemplateFolder string 24 TemplateFuncs map[string]interface{} 25 TemplateCache map[string]*template.Template 26 CacheTemplates bool 27 inject.Injector 28 } 29 30 type HandlerApp interface { 31 RespondStreamTemplate(templateNames []string, entryPointName string, data interface{}) HTTPResponse 32 RespondTemplate(templateNames []string, entryPointName string, data interface{}) HTTPResponse 33 MakeHandler(handlers ...interface{}) http.HandlerFunc 34 } 35 36 func NewApp() *App { 37 return &App{ 38 TemplateFuncs: make(map[string]interface{}), 39 TemplateCache: make(map[string]*template.Template), 40 CacheTemplates: true, 41 Injector: inject.New(), 42 } 43 } 44 45 func (ae *App) RespondTemplate(templateNames []string, entryPointName string, data interface{}) HTTPResponse { 46 cacheKey := strings.Join(templateNames, ":") + ":" + entryPointName 47 tmpl := ae.TemplateCache[cacheKey] 48 49 if ae.CacheTemplates && tmpl != nil { //cache hit 50 return &TemplateResponse{entryPointName, tmpl, data, false} 51 } else { // cache miss. 52 templatePaths := make([]string, len(templateNames)) 53 for i, v := range templateNames { 54 templatePaths[i] = filepath.Join(ae.TemplateFolder, v) 55 } 56 tmpl := template.New(templateNames[0]).Funcs(ae.TemplateFuncs) 57 tmpl, err := tmpl.ParseFiles(templatePaths...) 58 if err != nil { 59 grip.Error(errors.Wrapf(err, "encountered error parsing template %v", templatePaths)) 60 return StringResponse{"Error: " + err.Error(), 500} 61 } 62 ae.TemplateCache[cacheKey] = tmpl 63 return &TemplateResponse{entryPointName, tmpl, data, false} 64 } 65 } 66 67 func (ae *App) RespondStreamTemplate(templateNames []string, entryPointName string, data interface{}) HTTPResponse { 68 cacheKey := strings.Join(templateNames, ":") + ":" + entryPointName 69 tmpl := ae.TemplateCache[cacheKey] 70 71 if tmpl != nil { //cache hit 72 return &TemplateResponse{entryPointName, tmpl, data, true} 73 } else { // cache miss. 74 templatePaths := make([]string, len(templateNames)) 75 for i, v := range templateNames { 76 templatePaths[i] = filepath.Join(ae.TemplateFolder, v) 77 } 78 tmpl := template.New(templateNames[0]).Funcs(ae.TemplateFuncs) 79 tmpl, err := tmpl.ParseFiles(templatePaths...) 80 if err != nil { 81 return StringResponse{"Error: " + err.Error(), 500} 82 } 83 ae.TemplateCache[cacheKey] = tmpl 84 return &TemplateResponse{entryPointName, tmpl, data, true} 85 } 86 } 87 88 func (ae *App) MakeHandler(handlers ...interface{}) http.HandlerFunc { 89 return func(w http.ResponseWriter, r *http.Request) { 90 child := inject.New() 91 child.SetParent(ae.Injector) 92 child.MapTo(w, (*http.ResponseWriter)(nil)) 93 child.Map(r) 94 handlerLoop: 95 for _, reqProcessor := range handlers { 96 result, err := child.Invoke(reqProcessor) 97 if err != nil { 98 w.WriteHeader(http.StatusInternalServerError) 99 fmt.Fprintf(os.Stderr, "error in url %v: %v\n", r.URL, err) 100 return 101 } 102 103 if len(result) == 0 { 104 continue 105 } else { 106 valAsInterface := result[0].Interface() 107 if resp, ok := valAsInterface.(HTTPResponse); ok { 108 err := resp.Render(w) 109 if err != nil { 110 w.WriteHeader(http.StatusInternalServerError) 111 fmt.Fprintln(os.Stderr, "error: %v", err) 112 } 113 if reflect.TypeOf(valAsInterface) != reflect.TypeOf(ChainHttpResponse{}) { 114 break handlerLoop 115 } 116 } 117 } 118 } 119 } 120 } 121 122 //HTTPResponse is an interface that is responsible for encapsulating how a response is rendered to an HTTP client. 123 type HTTPResponse interface { 124 Render(w http.ResponseWriter) error 125 } 126 127 //StringResponse will write a simple string directly to the output 128 type StringResponse struct { 129 Body string 130 StatusCode int 131 } 132 133 //RawHTTPResponse will write a sequence of bytes directly to the output. 134 type RawHTTPResponse []byte 135 136 //JSONResponse will marshal "Data" as json to the output, and set StatusCode accordingly. 137 type JSONResponse struct { 138 Data interface{} 139 StatusCode int 140 } 141 142 //YAMLResponse will marshal "Data" as json to the output, and set StatusCode accordingly. 143 type YAMLResponse struct { 144 Data interface{} 145 StatusCode int 146 } 147 148 //TemplateResponse will execute TemplateSet against Data with the root template named by TemplateName. 149 type TemplateResponse struct { 150 TemplateName string 151 TemplateSet *template.Template 152 Data interface{} 153 Stream bool 154 } 155 156 //NullHttpResponse doesn't do anything to the response. 157 //It can be used if the handler has already written the response directly 158 //so no further action needs to be taken. 159 type NullHttpResponse struct{} 160 161 type ChainHttpResponse struct{} 162 163 func (rw RawHTTPResponse) Render(w http.ResponseWriter) error { 164 _, err := w.Write(rw) 165 return err 166 } 167 168 func (nh NullHttpResponse) Render(w http.ResponseWriter) error { 169 return nil 170 } 171 172 func (nh ChainHttpResponse) Render(w http.ResponseWriter) error { 173 return nil 174 } 175 176 func (sr StringResponse) Render(w http.ResponseWriter) error { 177 if sr.StatusCode != 0 { 178 w.WriteHeader(sr.StatusCode) 179 } 180 _, err := w.Write([]byte(sr.Body)) 181 return err 182 } 183 184 func (tr TemplateResponse) Render(w http.ResponseWriter) error { 185 var buf bytes.Buffer 186 if tr.Stream { 187 err := tr.TemplateSet.ExecuteTemplate(w, tr.TemplateName, tr.Data) 188 if err != nil { 189 w.WriteHeader(500) 190 w.Write([]byte(fmt.Sprintf("error: \"%v\"", err))) 191 return err 192 } 193 } else { 194 err := tr.TemplateSet.ExecuteTemplate(&buf, tr.TemplateName, tr.Data) 195 if err != nil { 196 w.WriteHeader(500) 197 w.Write([]byte(fmt.Sprintf("error: \"%v\"", err))) 198 return err 199 } 200 w.Write(buf.Bytes()) 201 } 202 return nil 203 } 204 205 func (jr JSONResponse) Render(w http.ResponseWriter) error { 206 w.Header().Set("Content-Type", "application/json; charset=utf-8") 207 208 statusCode := jr.StatusCode 209 jsonBytes, err := json.Marshal(jr.Data) 210 if err != nil { 211 grip.Errorf("marshaling json: %+v", err) 212 statusCode = http.StatusInternalServerError 213 jsonBytes = []byte(fmt.Sprintf(`{"error":"%v"}`, JSONMarshalError)) 214 } 215 216 if statusCode != 0 { 217 w.WriteHeader(statusCode) 218 } 219 220 w.Write(jsonBytes) 221 return nil 222 }