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 FoundationEssentials
2import Hummingbird
3import Logging
4
5/// How the binary was started: `--socket <path> --db <path>` on Carloku,
6/// or `--listen 127.0.0.1:<port> --db <path>` for local runs and tests.
7public struct Options: Sendable, Equatable {
8 public enum Address: Sendable, Equatable {
9 case socket(String)
10 case listen(host: String, port: Int)
11 }
12
13 public var address: Address
14 public var database: String
15 /// `CARLOS_HOST`, the instance's hostname, if the platform set it.
16 public var carlosHost: String?
17
18 public struct UsageError: Error, CustomStringConvertible {
19 public var description: String
20 }
21
22 public static let usage = "usage: todo (--socket <path> | --listen 127.0.0.1:<port>) --db <path>"
23
24 public init(arguments: [String], environment: [String: String]) throws {
25 var socket: String?
26 var listen: String?
27 var database: String?
28 var rest = arguments[...]
29 while let flag = rest.popFirst() {
30 guard let value = rest.popFirst() else { throw UsageError(description: "\(flag) needs a value\n\(Self.usage)") }
31 switch flag {
32 case "--socket", "-socket": socket = value
33 case "--listen", "-listen": listen = value
34 case "--db", "-db": database = value
35 default: throw UsageError(description: "unknown flag \(flag)\n\(Self.usage)")
36 }
37 }
38 guard let database else { throw UsageError(description: Self.usage) }
39 self.database = database
40 switch (socket, listen) {
41 case (let path?, nil):
42 address = .socket(path)
43 case (nil, let listen?):
44 guard let colon = listen.lastIndex(of: ":"), listen[..<colon] == "127.0.0.1",
45 let port = Int(listen[listen.index(after: colon)...]), (1...65535).contains(port)
46 else { throw UsageError(description: "--listen takes 127.0.0.1:<port>; nothing else is bound") }
47 address = .listen(host: "127.0.0.1", port: port)
48 default:
49 throw UsageError(description: Self.usage)
50 }
51 carlosHost = environment["CARLOS_HOST"].flatMap { $0.isEmpty ? nil : $0 }
52 }
53
54 /// The origin a browser sends with the app's own form posts.
55 var origin: String? {
56 switch address {
57 case .listen(let host, let port): "http://\(host):\(port)"
58 case .socket: carlosHost.map { "https://\($0)" }
59 }
60 }
61
62 /// On a socket the platform's edge serves the app over https.
63 var servedOverHTTPS: Bool {
64 if case .socket = address { return true }
65 return false
66 }
67}
68
69public enum Server {
70 /// Builds the router for a store; the tests drive this directly.
71 public static func router(store: Store, options: Options, version: String = buildVersion) -> Router<BasicRequestContext> {
72 let router = Router()
73 router.add(middleware: OriginCheck(origin: options.origin))
74 Routes(store: store, visitors: Visitors(store: store, secure: options.servedOverHTTPS), version: version)
75 .register(on: router)
76 return router
77 }
78
79 /// Serves until SIGTERM or SIGINT, then closes the database and returns.
80 public static func run(_ options: Options) async throws {
81 var logger = Logger(label: "todo-swift")
82 logger.logLevel = .info
83 let store = try await Store.open(path: options.database)
84
85 let address: BindAddress
86 switch options.address {
87 case .socket(let path):
88 // A socket file left behind by an earlier process would make bind fail.
89 try? FileManager.default.removeItem(atPath: path)
90 address = .unixDomainSocket(path: path)
91 case .listen(let host, let port):
92 address = .hostname(host, port: port)
93 }
94
95 let app = Application(
96 router: router(store: store, options: options),
97 configuration: .init(address: address),
98 logger: logger
99 )
100 do {
101 try await app.runService()
102 } catch {
103 try? await store.close()
104 throw error
105 }
106 try await store.close()
107 if case .socket(let path) = options.address {
108 try? FileManager.default.removeItem(atPath: path)
109 }
110 }
111}
112