// ===----------------------------------------------------------------------=== // // This source file is part of the Hummingbird DocC Middleware open source project // // Copyright (c) 2025 Röck+Cöde VoF. and the Hummingbird DocC Middleware project authors // Licensed under the EUPL 1.2 or later. // // See LICENSE for license information // See CONTRIBUTORS for the list of Hummingbird DocC Middleware project authors // // ===----------------------------------------------------------------------=== import protocol Hummingbird.FileProvider import protocol Hummingbird.RequestContext import protocol Hummingbird.RouterMiddleware import struct Hummingbird.LocalFileSystem import struct Hummingbird.Request import struct Hummingbird.Response import struct Logging.Logger /// A middleware that proxies requests to `DocC` documentation containers within a hosting app. public struct DocCMiddleware { // MARK: Properties /// A type that contains the parameters to configure the middleware. let configuration: Configuration /// A type that conforms to a protocol that defines file system interactions. let fileProvider: FileSystemProvider /// A type that interacts with the logging system. let logger: Logger /// A use case that checks whether a received URI could be processed or not. private let checkURI: CheckURIUseCase = .init() /// A use case that extracts data from a given URI path, essential for routing the documentation contents. private let prepareURIPath: PrepareURIPathUseCase /// A use case that produces a redirect response based on a given URI path. private let redirectURI: RedirectURIUseCase /// A use case that serves a resource, defined by its URI path, from a physical location. private let serveURI: ServeURIUseCase // MARK: Initializers /// Initializes this middleware. /// - Parameters: /// - configuration: A type that contains the parameters to configure the middleware. /// - logger: A type that interacts with the logging system. public init( configuration: Configuration, logger: Logger ) where FileSystemProvider == LocalFileSystem { self.init( configuration: configuration, fileProvider: LocalFileSystem( rootFolder: configuration.folderRoot, threadPool: configuration.threadPool, logger: logger ), logger: logger, ) } /// Initializes this middleware with a concrete file provider type. /// - Parameters: /// - configuration: A type that contains the parameters to configure the middleware. /// - fileProvider: A type that conforms to the protocol that defines file system interactions. /// - logger: A type that interacts with the logging system. init( configuration: Configuration, fileProvider: FileSystemProvider, logger: Logger, ) { self.logger = logger self.configuration = configuration self.fileProvider = fileProvider self.prepareURIPath = .init(uriRoot: configuration.uriRoot) self.redirectURI = .init(logger: logger) self.serveURI = .init( fileProvider: fileProvider, logger: logger ) } } // MARK: - RouterMiddleware extension DocCMiddleware: RouterMiddleware { // MARK: Type aliases public typealias Context = RequestContext public typealias Input = Request public typealias Output = Response // MARK: Functions public func handle( _ input: Input, context: any Context, next: (Input, any Context) async throws -> Output ) async throws -> Output { guard let uriPath = checkURI(input.uri), let uriData = prepareURIPath(uriPath) else { return try await next(input, context) } if uriData.resourcePath == .Path.forwardSlash { // rule #1: Redirects URI root to `/`. // rule #2: Redirects URI resources with `/` to `/documentation`. return redirectURI( uriPath.hasSuffix(.Path.forwardSlash) ? String(format: .Format.Path.documentation, uriPath) : String(format: .Format.Path.forwardSlash, uriPath), with: (input, context) ) } for assetFile in AssetFile.allCases { if uriData.resourcePath.contains(assetFile.path) { return try await serveURI( assetFile == .documentation // Rule #6: Redirects URI resources with `/data/documentation.json` to the file in the `data/documentation/` // folder that has the name of the module and ends with the `.json` extension in the *DocC* archive container. ? String(format: .Format.Path.documentationJSON, uriData.archiveName) // Rule #7: Redirect URI resources for static files (`favicon.ico`, `favicon.svg`, and `theme-settings.json`) // to their respective files in the *DocC* archive container. : uriData.resourcePath, at: uriData.archivePath, with: (input, context) ) } } for assetFolder in AssetFolder.allCases { if uriData.resourcePath.contains(assetFolder.path) { // Rule #8: Redirect URI resources for asset files (`/css/`, `/data/`, `/downloads/`, `/images/`, `/img/`, // `/index/`, `/js/`, or `/videos/`) to their respective files in the *DocC* archive container. return try await serveURI( uriData.resourcePath, at: uriData.archivePath, with: (input, context) ) } } for documentationFolder in DocumentationFolder.allCases { if uriData.resourcePath.contains(documentationFolder.path) { if uriData.resourcePath.hasSuffix(.Path.forwardSlash) { // Rule #5: Redirect URI resources for `/documentation/` and `/tutorials/` folders to their respective `index.html` file. return try await serveURI( String(format: .Format.Path.index, documentationFolder.path, uriData.archiveName), at: uriData.archivePath, with: (input, context) ) } else { // rule #3: Redirects URI resources with `/documentation` to `/documentation/`. // rule #4: Redirects URI resources with `/tutorials` to `/tutorials/`. return redirectURI( String(format: .Format.Path.forwardSlash, uriPath), with: (input, context) ) } } } return try await next(input, context) } }