carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

1// Package ref is the bakeoff's reference fixture: a minimal, in-memory
2// implementation of CONTRACT.md used only to prove the behaviour suite and
3// the harness before any builder starts. It is not a contestant, and
4// builders never see it.
5package ref
6
7import (
8 "crypto/rand"
9 _ "embed"
10 "encoding/hex"
11 "html/template"
12 "log"
13 "net/http"
14 "path/filepath"
15 "strconv"
16 "strings"
17 "sync"
18)
19
20//go:embed page.html.tmpl
21var pageSrc string
22
23//go:embed app.js
24var appJS []byte
25
26var page = template.Must(template.New("page").Parse(pageSrc))
27
28const cookieName = "todo"
29
30type item struct {
31 ID int
32 Title string
33 Completed bool
34}
35
36type list struct {
37 next int
38 items []item
39}
40
41type app struct {
42 origin string
43 shared string
44 mu sync.Mutex
45 lists map[string]*list
46}
47
48// NewApp returns the reference app's handler. origin is the app's own
49// origin for the Origin check; shared is the directory holding base.css
50// and index.css.
51func NewApp(origin, shared string) http.Handler {
52 a := &app{origin: origin, shared: shared, lists: map[string]*list{}}
53 mux := http.NewServeMux()
54 mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) { w.Write([]byte("ok")) })
55 mux.HandleFunc("GET /api/version", func(w http.ResponseWriter, _ *http.Request) { w.Write([]byte("refapp")) })
56 mux.HandleFunc("GET /static/{file}", a.static)
57 mux.HandleFunc("GET /{$}", a.show("all"))
58 mux.HandleFunc("GET /active", a.show("active"))
59 mux.HandleFunc("GET /completed", a.show("completed"))
60 mux.HandleFunc("POST /todos", a.act(func(l *list, r *http.Request) int {
61 if t := strings.TrimSpace(r.FormValue("title")); t != "" {
62 l.next++
63 l.items = append(l.items, item{ID: l.next, Title: t})
64 }
65 return 0
66 }))
67 mux.HandleFunc("POST /todos/toggle-all", a.act(func(l *list, _ *http.Request) int {
68 all := true
69 for _, it := range l.items {
70 all = all && it.Completed
71 }
72 for i := range l.items {
73 l.items[i].Completed = !all
74 }
75 return 0
76 }))
77 mux.HandleFunc("POST /todos/clear-completed", a.act(func(l *list, _ *http.Request) int {
78 kept := l.items[:0]
79 for _, it := range l.items {
80 if !it.Completed {
81 kept = append(kept, it)
82 }
83 }
84 l.items = kept
85 return 0
86 }))
87 mux.HandleFunc("POST /todos/{id}/{op}", a.act(func(l *list, r *http.Request) int {
88 i := l.find(r.PathValue("id"))
89 if i < 0 {
90 return http.StatusNotFound
91 }
92 switch r.PathValue("op") {
93 case "toggle":
94 l.items[i].Completed = !l.items[i].Completed
95 case "edit":
96 if t := strings.TrimSpace(r.FormValue("title")); t != "" {
97 l.items[i].Title = t
98 } else {
99 l.items = append(l.items[:i], l.items[i+1:]...)
100 }
101 case "destroy":
102 l.items = append(l.items[:i], l.items[i+1:]...)
103 default:
104 return http.StatusNotFound
105 }
106 return 0
107 }))
108 return mux
109}
110
111func (l *list) find(id string) int {
112 n, err := strconv.Atoi(id)
113 if err != nil {
114 return -1
115 }
116 for i, it := range l.items {
117 if it.ID == n {
118 return i
119 }
120 }
121 return -1
122}
123
124func (a *app) static(w http.ResponseWriter, r *http.Request) {
125 switch f := r.PathValue("file"); f {
126 case "base.css", "index.css":
127 w.Header().Set("Content-Type", "text/css; charset=utf-8")
128 http.ServeFile(w, r, filepath.Join(a.shared, f))
129 case "app.js":
130 w.Header().Set("Content-Type", "text/javascript; charset=utf-8")
131 w.Write(appJS)
132 default:
133 http.NotFound(w, r)
134 }
135}
136
137// listFor returns the caller's list, minting a fresh one (and its cookie)
138// when the cookie is missing or unknown. Caller holds a.mu.
139func (a *app) listFor(w http.ResponseWriter, r *http.Request) *list {
140 if c, err := r.Cookie(cookieName); err == nil {
141 if l, ok := a.lists[c.Value]; ok {
142 return l
143 }
144 }
145 b := make([]byte, 16)
146 rand.Read(b)
147 key := hex.EncodeToString(b)
148 l := &list{}
149 a.lists[key] = l
150 http.SetCookie(w, &http.Cookie{Name: cookieName, Value: key, Path: "/", HttpOnly: true,
151 SameSite: http.SameSiteLaxMode, Secure: strings.HasPrefix(a.origin, "https://")})
152 return l
153}
154
155func filterPath(f string) string {
156 switch f {
157 case "active", "completed":
158 return "/" + f
159 }
160 return "/"
161}
162
163func (a *app) act(f func(*list, *http.Request) int) http.HandlerFunc {
164 return func(w http.ResponseWriter, r *http.Request) {
165 if o := r.Header.Get("Origin"); o != "" && o != a.origin {
166 http.Error(w, "cross-site request refused", http.StatusForbidden)
167 return
168 }
169 r.ParseForm()
170 a.mu.Lock()
171 status := f(a.listFor(w, r), r)
172 a.mu.Unlock()
173 if status != 0 {
174 http.Error(w, http.StatusText(status), status)
175 return
176 }
177 filter := r.FormValue("filter")
178 if r.Header.Get("X-Requested-With") == "fetch" {
179 a.render(w, r, filter)
180 return
181 }
182 http.Redirect(w, r, filterPath(filter), http.StatusSeeOther)
183 }
184}
185
186func (a *app) show(filter string) http.HandlerFunc {
187 return func(w http.ResponseWriter, r *http.Request) { a.render(w, r, filter) }
188}
189
190type view struct {
191 Filter, Path string
192 Items []item
193 Total, Left, Done int
194 AllDone bool
195 EditID int
196}
197
198func (a *app) render(w http.ResponseWriter, r *http.Request, filter string) {
199 if filter != "active" && filter != "completed" {
200 filter = "all"
201 }
202 a.mu.Lock()
203 l := a.listFor(w, r)
204 v := view{Filter: filter, Path: filterPath(filter), Total: len(l.items)}
205 for _, it := range l.items {
206 if it.Completed {
207 v.Done++
208 } else {
209 v.Left++
210 }
211 if filter == "all" || (filter == "completed") == it.Completed {
212 v.Items = append(v.Items, it)
213 }
214 }
215 a.mu.Unlock()
216 v.AllDone = v.Total > 0 && v.Left == 0
217 v.EditID, _ = strconv.Atoi(r.URL.Query().Get("edit"))
218 w.Header().Set("Content-Type", "text/html; charset=utf-8")
219 if err := page.Execute(w, v); err != nil {
220 log.Print(err)
221 }
222}
223