// Package ref is the bakeoff's reference fixture: a minimal, in-memory // implementation of CONTRACT.md used only to prove the behaviour suite and // the harness before any builder starts. It is not a contestant, and // builders never see it. package ref import ( "crypto/rand" _ "embed" "encoding/hex" "html/template" "log" "net/http" "path/filepath" "strconv" "strings" "sync" ) //go:embed page.html.tmpl var pageSrc string //go:embed app.js var appJS []byte var page = template.Must(template.New("page").Parse(pageSrc)) const cookieName = "todo" type item struct { ID int Title string Completed bool } type list struct { next int items []item } type app struct { origin string shared string mu sync.Mutex lists map[string]*list } // NewApp returns the reference app's handler. origin is the app's own // origin for the Origin check; shared is the directory holding base.css // and index.css. func NewApp(origin, shared string) http.Handler { a := &app{origin: origin, shared: shared, lists: map[string]*list{}} mux := http.NewServeMux() mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) { w.Write([]byte("ok")) }) mux.HandleFunc("GET /api/version", func(w http.ResponseWriter, _ *http.Request) { w.Write([]byte("refapp")) }) mux.HandleFunc("GET /static/{file}", a.static) mux.HandleFunc("GET /{$}", a.show("all")) mux.HandleFunc("GET /active", a.show("active")) mux.HandleFunc("GET /completed", a.show("completed")) mux.HandleFunc("POST /todos", a.act(func(l *list, r *http.Request) int { if t := strings.TrimSpace(r.FormValue("title")); t != "" { l.next++ l.items = append(l.items, item{ID: l.next, Title: t}) } return 0 })) mux.HandleFunc("POST /todos/toggle-all", a.act(func(l *list, _ *http.Request) int { all := true for _, it := range l.items { all = all && it.Completed } for i := range l.items { l.items[i].Completed = !all } return 0 })) mux.HandleFunc("POST /todos/clear-completed", a.act(func(l *list, _ *http.Request) int { kept := l.items[:0] for _, it := range l.items { if !it.Completed { kept = append(kept, it) } } l.items = kept return 0 })) mux.HandleFunc("POST /todos/{id}/{op}", a.act(func(l *list, r *http.Request) int { i := l.find(r.PathValue("id")) if i < 0 { return http.StatusNotFound } switch r.PathValue("op") { case "toggle": l.items[i].Completed = !l.items[i].Completed case "edit": if t := strings.TrimSpace(r.FormValue("title")); t != "" { l.items[i].Title = t } else { l.items = append(l.items[:i], l.items[i+1:]...) } case "destroy": l.items = append(l.items[:i], l.items[i+1:]...) default: return http.StatusNotFound } return 0 })) return mux } func (l *list) find(id string) int { n, err := strconv.Atoi(id) if err != nil { return -1 } for i, it := range l.items { if it.ID == n { return i } } return -1 } func (a *app) static(w http.ResponseWriter, r *http.Request) { switch f := r.PathValue("file"); f { case "base.css", "index.css": w.Header().Set("Content-Type", "text/css; charset=utf-8") http.ServeFile(w, r, filepath.Join(a.shared, f)) case "app.js": w.Header().Set("Content-Type", "text/javascript; charset=utf-8") w.Write(appJS) default: http.NotFound(w, r) } } // listFor returns the caller's list, minting a fresh one (and its cookie) // when the cookie is missing or unknown. Caller holds a.mu. func (a *app) listFor(w http.ResponseWriter, r *http.Request) *list { if c, err := r.Cookie(cookieName); err == nil { if l, ok := a.lists[c.Value]; ok { return l } } b := make([]byte, 16) rand.Read(b) key := hex.EncodeToString(b) l := &list{} a.lists[key] = l http.SetCookie(w, &http.Cookie{Name: cookieName, Value: key, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, Secure: strings.HasPrefix(a.origin, "https://")}) return l } func filterPath(f string) string { switch f { case "active", "completed": return "/" + f } return "/" } func (a *app) act(f func(*list, *http.Request) int) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if o := r.Header.Get("Origin"); o != "" && o != a.origin { http.Error(w, "cross-site request refused", http.StatusForbidden) return } r.ParseForm() a.mu.Lock() status := f(a.listFor(w, r), r) a.mu.Unlock() if status != 0 { http.Error(w, http.StatusText(status), status) return } filter := r.FormValue("filter") if r.Header.Get("X-Requested-With") == "fetch" { a.render(w, r, filter) return } http.Redirect(w, r, filterPath(filter), http.StatusSeeOther) } } func (a *app) show(filter string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { a.render(w, r, filter) } } type view struct { Filter, Path string Items []item Total, Left, Done int AllDone bool EditID int } func (a *app) render(w http.ResponseWriter, r *http.Request, filter string) { if filter != "active" && filter != "completed" { filter = "all" } a.mu.Lock() l := a.listFor(w, r) v := view{Filter: filter, Path: filterPath(filter), Total: len(l.items)} for _, it := range l.items { if it.Completed { v.Done++ } else { v.Left++ } if filter == "all" || (filter == "completed") == it.Completed { v.Items = append(v.Items, it) } } a.mu.Unlock() v.AllDone = v.Total > 0 && v.Left == 0 v.EditID, _ = strconv.Atoi(r.URL.Query().Get("edit")) w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := page.Execute(w, v); err != nil { log.Print(err) } }