fup: create serve subcommand and fuphttp package

This is the skeleton of the application. Let's goooo!
This commit is contained in:
Luke Granger-Brown 2021-03-20 19:53:43 +00:00
parent a1bda601a9
commit bcd39fae10
3 changed files with 155 additions and 0 deletions

41
web/fup/cmd/serve.go Normal file
View file

@ -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)
},
}
)

View file

@ -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
}

View file

@ -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)
}
}