diff --git a/web/fup/cmd/serve.go b/web/fup/cmd/serve.go new file mode 100644 index 0000000000..810e7ffa3b --- /dev/null +++ b/web/fup/cmd/serve.go @@ -0,0 +1,41 @@ +package cmd + +import ( + "context" + "fmt" + "net/http" + + "github.com/spf13/cobra" + "hg.lukegb.com/lukegb/depot/web/fup/fuphttp" + "hg.lukegb.com/lukegb/depot/web/fup/fupstatic" +) + +func init() { + rootCmd.AddCommand(serveCmd) + + serveCmd.Flags().StringVarP(&serveBind, "listen", "l", ":8191", "Bind address for HTTP server.") +} + +var ( + serveBind string + + serveCmd = &cobra.Command{ + Use: "serve", + Short: "Serve HTTP", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + cfg := &fuphttp.Config{ + Templates: fupstatic.Templates, + Static: fupstatic.Static, + StaticRoot: "/static/", + } + a, err := fuphttp.New(ctx, cfg) + if err != nil { + return fmt.Errorf("constructing application: %w", err) + } + http.Handle("/", a.Handler()) + http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(fupstatic.Static)))) + return http.ListenAndServe(serveBind, nil) + }, + } +) diff --git a/web/fup/fuphttp/fuphttp.go b/web/fup/fuphttp/fuphttp.go new file mode 100644 index 0000000000..fea1b11924 --- /dev/null +++ b/web/fup/fuphttp/fuphttp.go @@ -0,0 +1,79 @@ +package fuphttp + +import ( + "context" + "fmt" + "io/fs" + "net/http" + + "github.com/google/safehtml/template" + "github.com/google/safehtml/template/uncheckedconversions" + "github.com/gorilla/mux" +) + +type Config struct { + Templates fs.FS + Static fs.FS + StaticRoot string +} + +type Application struct { + indexTmpl *template.Template + notFoundTmpl *template.Template +} + +func (a *Application) Handler() http.Handler { + r := mux.NewRouter() + + r.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.WriteHeader(http.StatusNotFound) + a.notFoundTmpl.Execute(rw, nil) + }) + + return r +} + +func parseTemplate(t *template.Template, fsys fs.FS, name string) (*template.Template, error) { + bs, err := fs.ReadFile(fsys, name) + if err != nil { + return nil, fmt.Errorf("reading template %q: %w", name, err) + } + + return t.ParseFromTrustedTemplate( + uncheckedconversions.TrustedTemplateFromStringKnownToSatisfyTypeContract(string(bs)), + ) +} + +func loadTemplate(fsys fs.FS, name string) (*template.Template, error) { + t := template.New(name) + var err error + if t, err = parseTemplate(t, fsys, "base.html"); err != nil { + return nil, fmt.Errorf("loading base template: %w", err) + } + if t, err = parseTemplate(t, fsys, fmt.Sprintf("%s.html", name)); err != nil { + return nil, fmt.Errorf("loading leaf template: %w", err) + } + return t, nil +} + +func New(ctx context.Context, cfg *Config) (*Application, error) { + a := new(Application) + + tmpls := []struct { + t **template.Template + name string + }{ + {&a.indexTmpl, "index"}, + {&a.notFoundTmpl, "404"}, + } + + for _, tmpl := range tmpls { + t, err := loadTemplate(cfg.Templates, tmpl.name) + if err != nil { + return nil, fmt.Errorf("loading template %q: %w", tmpl.name, err) + } + *tmpl.t = t + } + + return a, nil +} diff --git a/web/fup/fuphttp/fuphttp_test.go b/web/fup/fuphttp/fuphttp_test.go new file mode 100644 index 0000000000..410c406903 --- /dev/null +++ b/web/fup/fuphttp/fuphttp_test.go @@ -0,0 +1,35 @@ +package fuphttp_test + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "hg.lukegb.com/lukegb/depot/web/fup/fuphttp" + "hg.lukegb.com/lukegb/depot/web/fup/fupstatic" +) + +var cfg = &fuphttp.Config{ + Templates: fupstatic.Templates, +} + +func TestNotFound(t *testing.T) { + ctx := context.Background() + + a, err := fuphttp.New(ctx, cfg) + if err != nil { + t.Fatalf("fuphttp.New: %v", err) + } + s := httptest.NewServer(a.Handler()) + defer s.Close() + + resp, err := s.Client().Get(fmt.Sprintf("%s/not-found", s.URL)) + if err != nil { + t.Fatalf("Get: %v", err) + } + if resp.StatusCode != http.StatusNotFound { + t.Errorf("response status was %v; want %v", resp.StatusCode, http.StatusNotFound) + } +}