fup: create serve subcommand and fuphttp package
This is the skeleton of the application. Let's goooo!
This commit is contained in:
parent
a1bda601a9
commit
bcd39fae10
3 changed files with 155 additions and 0 deletions
41
web/fup/cmd/serve.go
Normal file
41
web/fup/cmd/serve.go
Normal 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)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
79
web/fup/fuphttp/fuphttp.go
Normal file
79
web/fup/fuphttp/fuphttp.go
Normal 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
|
||||||
|
}
|
35
web/fup/fuphttp/fuphttp_test.go
Normal file
35
web/fup/fuphttp/fuphttp_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue