import Configuration import Hummingbird import Logging import WebsiteCore /// Builds the website application. /// /// Reads the log level, server name, and static files location from the configuration, /// then assembles the router, server configuration, and logger. /// - Parameter reader: the configuration reader the values are read from. /// - Returns: the configured application, ready to run as a service. func application( reader: ConfigReader ) async -> some ApplicationProtocol { let cacheControl = cacheControl( textMaxAge: reader.int( forKey: .Cache.maxAgeText, default: .Cache.maxAgeText ), imageMaxAge: reader.int( forKey: .Cache.maxAgeImage, default: .Cache.maxAgeImage ), defaultMaxAge: reader.int( forKey: .Cache.maxAgeDefault, default: .Cache.maxAgeDefault ) ) let logLevel = reader.string( forKey: .Log.level, as: Logger.Level.self, default: .info ) let serverName = reader.string( forKey: .HTTP.serverName, default: .Server.name ) let staticFilesPath = reader.string( forKey: .Path.staticFiles, default: .Path.staticResources ) return Application( router: router( staticFilesPath: staticFilesPath, cacheControl: cacheControl, logLevel: logLevel ), configuration: ApplicationConfiguration( reader: reader.scoped(to: "http") ), logger: logger( serverName: serverName, logLevel: logLevel ) ) } // MARK: - Helpers // Request context used by application private typealias AppRequestContext = BasicRequestContext /// Builds the cache-control policy applied to the served static files. /// /// Static files are public and validated by `FileMiddleware` through their `ETag` and /// `Last-Modified` headers, so each media type is given a `max-age` after which the browser /// revalidates. Text-based assets (CSS, JavaScript) are additionally marked `must-revalidate` /// since they change between deployments while keeping their filenames. /// - Parameters: /// - textMaxAge: the max-age, in seconds, applied to text-based static files (CSS, JavaScript, plain text). /// - imageMaxAge: the max-age, in seconds, applied to image static files (ICO, PNG, SVG). /// - defaultMaxAge: the max-age, in seconds, applied to all other static files (e.g. the web manifest). /// - Returns: the configured cache-control policy. private func cacheControl( textMaxAge: Int, imageMaxAge: Int, defaultMaxAge: Int ) -> CacheControl { .init([ (.text, [.public, .maxAge(textMaxAge), .mustRevalidate]), (.image, [.public, .maxAge(imageMaxAge)]), (.init(type: .any), [.public, .maxAge(defaultMaxAge)]), ]) } /// Builds the application's logger. /// - Parameters: /// - serverName: the label applied to the logger. /// - logLevel: the minimum level the logger emits. /// - Returns: the configured logger. private func logger( serverName: String, logLevel: Logger.Level ) -> Logger { var logger = Logger(label: serverName) logger.logLevel = logLevel return logger } /// Builds the application's router. /// /// Registers the request-logging middleware, the not-found middleware that serves the error /// page, and the static file middleware that serves the contents of `staticFilesPath` (tagging /// responses with the given `cacheControl` directives), then adds the `RootController` routes /// that render the landing page. /// - Parameters: /// - staticFilesPath: the folder, relative to the working directory, the static files are served from. /// - cacheControl: the cache-control directives applied to the served static files. /// - logLevel: the level the request-logging middleware logs at. /// - Returns: the configured router. private func router( staticFilesPath: String, cacheControl: CacheControl, logLevel: Logger.Level ) -> Router { let router = Router(context: AppRequestContext.self) router.addMiddleware { LogRequestsMiddleware(logLevel) NotFoundMiddleware() FileMiddleware( staticFilesPath, cacheControl: cacheControl ) } router.addRoutes(RootController().routes) return router }