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