carlos / todomvc-bakeoff Public

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

Plain git — no account needed to clone.

Download

Download this file

1import HTTPTypes
2import Hummingbird
3
4/// `make bump` changes this text so the next build has to recompile this
5/// file, the one holding the `GET /` handler; `make unbump` puts it back.
6let bumpNote = "unbumped"
7
8/// The request header the page's JavaScript sends with each action. With
9/// it, an action answers 200 with the new `section.main` and
10/// `footer.footer` instead of 303.
11let partialHeader = HTTPField.Name("X-Todo-Partial")!
12
13/// Every route the contract lists.
14struct Routes: Sendable {
15 let store: Store
16 let visitors: Visitors
17 let version: String
18
19 func register(on router: Router<BasicRequestContext>) {
20 router.get("/healthz") { _, _ in "ok\n" }
21 router.get("/api/version") { _, _ in self.version }
22
23 router.get("/static/base.css") { _, _ in Assets.response(Assets.baseCSS, type: "text/css; charset=utf-8") }
24 router.get("/static/index.css") { _, _ in Assets.response(Assets.indexCSS, type: "text/css; charset=utf-8") }
25 router.get("/static/app.css") { _, _ in Assets.response(Assets.appCSS, type: "text/css; charset=utf-8") }
26 router.get("/static/app.js") { _, _ in Assets.response(Assets.appJS, type: "text/javascript; charset=utf-8") }
27
28 for filter in Filter.allCases {
29 router.get(RouterPath(filter.path)) { request, _ in
30 try await self.page(request, filter: filter)
31 }
32 }
33
34 router.post("/todos") { request, context in
35 try await self.action(request, context) { list, form in
36 let title = form.title?.trimmingWhitespace() ?? ""
37 if !title.isEmpty { try await self.store.add(list: list, title: title) }
38 return true
39 }
40 }
41 router.post("/todos/toggle-all") { request, context in
42 try await self.action(request, context) { list, _ in
43 try await self.store.toggleAll(list: list)
44 return true
45 }
46 }
47 router.post("/todos/clear-completed") { request, context in
48 try await self.action(request, context) { list, _ in
49 try await self.store.clearCompleted(list: list)
50 return true
51 }
52 }
53 router.post("/todos/:id/toggle") { request, context in
54 let id = try context.parameters.require("id", as: Int.self)
55 return try await self.action(request, context) { list, _ in
56 try await self.store.toggle(list: list, id: id)
57 }
58 }
59 router.post("/todos/:id/edit") { request, context in
60 let id = try context.parameters.require("id", as: Int.self)
61 return try await self.action(request, context) { list, form in
62 let title = form.title?.trimmingWhitespace() ?? ""
63 return title.isEmpty
64 ? try await self.store.destroy(list: list, id: id)
65 : try await self.store.rename(list: list, id: id, title: title)
66 }
67 }
68 router.post("/todos/:id/destroy") { request, context in
69 let id = try context.parameters.require("id", as: Int.self)
70 return try await self.action(request, context) { list, _ in
71 try await self.store.destroy(list: list, id: id)
72 }
73 }
74 }
75
76 /// `GET /`, `/active` and `/completed`, with `?edit=<id>` opening that
77 /// item's editor.
78 func page(_ request: Request, filter: Filter) async throws -> Response {
79 let visitor = try await visitors.resolve(request)
80 let todos = try await store.todos(list: visitor.list)
81 let editing = request.uri.queryParameters["edit"].flatMap { Int(String($0)) }
82 let html = Page.document(todos: todos, filter: filter, editing: editing, version: version)
83 var response = Response.html(html)
84 visitors.attach(visitor, to: &response)
85 return response
86 }
87
88 struct Form: Decodable {
89 var title: String?
90 var filter: String?
91 }
92
93 /// Runs one form action for the caller's own list. `change` answers
94 /// false when the item it names isn't in that list, which is a 404.
95 func action(
96 _ request: Request,
97 _ context: BasicRequestContext,
98 change: (Int, Form) async throws -> Bool
99 ) async throws -> Response {
100 var request = request
101 let body = try await request.collectBody(upTo: context.maxUploadSize)
102 guard let form = try? URLEncodedFormDecoder().decode(Form.self, from: String(buffer: body)) else {
103 throw HTTPError(.badRequest, message: "Expected a form.")
104 }
105 let filter = Filter(field: form.filter)
106 let visitor = try await visitors.resolve(request)
107
108 var response: Response
109 if try await change(visitor.list, form) {
110 if request.headers[partialHeader] != nil {
111 let todos = try await store.todos(list: visitor.list)
112 response = .html(Page.body(todos: todos, filter: filter, editing: nil))
113 } else {
114 response = Response(status: .seeOther, headers: [.location: filter.path])
115 }
116 } else {
117 response = Response(status: .notFound, headers: [.contentType: "text/plain; charset=utf-8"],
118 body: .init(byteBuffer: ByteBuffer(string: "No such item in your list.\n")))
119 }
120 visitors.attach(visitor, to: &response)
121 return response
122 }
123}
124
125extension Response {
126 static func html(_ html: String) -> Response {
127 Response(
128 status: .ok,
129 headers: [.contentType: "text/html; charset=utf-8", .cacheControl: "no-store"],
130 body: .init(byteBuffer: ByteBuffer(string: html))
131 )
132 }
133}
134
135extension String {
136 func trimmingWhitespace() -> String {
137 String(self[...].trimmingWhitespace())
138 }
139}
140