// ===----------------------------------------------------------------------=== // // 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. /// /// This middleware routes the contents of a `DocC` documentation container, defined by its resource URI paths, following these rules: /// /// 1. *Redirects the URI path `/` to the path `//`*; /// 2. *Redirects the URI path `//` to the path `//documentation`* /// 3. *Redirects the URI path `//documentation` to the path `//documentation/`* /// 4. *Redirects the URI path `//tutorials` to the path `//tutorials/`* /// 5. *Redirects the URI path `//documentation/` to the resource on `/.doccarchive/documentation//index.html`* /// 6. *Redirects the URI path `//tutorials/` to the resource on `/.doccarchive/tutorials//index.html`* /// 7. *Redirects the URI path `//data/documentation.json` to the resource on `/.doccarchive/data/documentation/.json`* /// 8. *Redirects the URI path `//favicon.ico` to the resource on `/.doccarchive/favicon.ico`* /// 9. *Redirects the URI path `//favicon.svg` to the resource on `/.doccarchive/favicon.svg`* /// 10. *Redirects the URI path `//theme-settings.json` to the resource on `/.doccarchive/theme-settings.json`* /// 11. *Redirects the URI path `//css/` to the resource on `/.doccarchive/css/`* /// 12. *Redirects the URI path `//data/` to the resource on `/.doccarchive/data/`* /// 13. *Redirects the URI path `//downloads/` to the resource on `/.doccarchive/downloads/`* /// 14. *Redirects the URI path `//images/` to the resource on `/.doccarchive/images/`* /// 15. *Redirects the URI path `//img/` to the resource on `/.doccarchive/img/`* /// 16. *Redirects the URI path `//index/` to the resource on `/.doccarchive/index/`* /// 17. *Redirects the URI path `//js/` to the resource on `/.doccarchive/js/`* /// 18. *Redirects the URI path `//videos/` to the resource on `/.doccarchive/videos/`* /// 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( _ request: Input, context: any Context, next: (Input, any Context) async throws -> Output ) async throws -> Output { guard let uriPath = checkURI(request.uri), let uriData = prepareURIPath(uriPath) else { return try await next(request, context) } let rootPaths: [String] = [ String(format: .Format.Path.root, uriData.archiveName), String(format: .Format.Path.folder, uriData.archiveName) ] if rootPaths.contains(uriData.resourcePath) { return redirectURI( uriPath.hasSuffix(.Path.forwardSlash) // Rule #2: Redirects the URI path // to the path //documentation ? String(format: .Format.Path.documentation, uriPath) // Rule #1: Redirects the URI path / to the path // : String(format: .Format.Path.forwardSlash, uriPath), with: (request, context) ) } for assetFile in AssetFile.allCases { if uriData.resourcePath.contains(assetFile.path) { return try await serveURI( assetFile == .documentation // Rule #7: Redirects the URI path //data/documentation.json to the resource on /.doccarchive/data/documentation/.json ? String(format: .Format.Path.documentationJSON, uriData.archiveReference) // Rule #8: Redirects the URI path `//favicon.ico` to the resource on `/.doccarchive/favicon.ico` // Rule #9: Redirects the URI path `//favicon.svg` to the resource on `/.doccarchive/favicon.svg` // Rule #10: Redirects the URI path `//theme-settings.json` to the resource on `/.doccarchive/theme-settings.json` : uriData.resourcePath, at: uriData.archivePath, with: (request, context) ) } } for assetFolder in AssetFolder.allCases { if uriData.resourcePath.contains(assetFolder.path) { // Rule #11: Redirects the URI path `//css/` to the resource on `/.doccarchive/css/` // Rule #12: Redirects the URI path `//data/` to the resource on `/.doccarchive/data/` // Rule #13: Redirects the URI path `//downloads/` to the resource on `/.doccarchive/downloads/` // Rule #14: Redirects the URI path `//images/` to the resource on `/.doccarchive/images/` // Rule #15: Redirects the URI path `//img/` to the resource on `/.doccarchive/img/` // Rule #16: Redirects the URI path `//index/` to the resource on `/.doccarchive/index/` // Rule #17: Redirects the URI path `//js/` to the resource on `/.doccarchive/js/` // Rule #18: Redirects the URI path `//videos/` to the resource on `/.doccarchive/videos/` return try await serveURI( uriData.resourcePath, at: uriData.archivePath, with: (request, context) ) } } for documentationFolder in DocumentationFolder.allCases { if uriData.resourcePath.contains(documentationFolder.path) { if uriData.resourcePath.hasSuffix(.Path.forwardSlash) { // Rule #5: Redirects the URI path //documentation/ to the resource on /.doccarchive/documentation//index.html // Rule #6: Redirects the URI path //tutorials/ to the resource on /.doccarchive/tutorials//index.html return try await serveURI( String(format: .Format.Path.index, documentationFolder.path, uriData.archiveReference), at: uriData.archivePath, with: (request, context) ) } else { // Rule #3: Redirects the URI path //documentation to the path //documentation/ // Rule #4: Redirects the URI path //tutorials to the path //tutorials/ return redirectURI( String(format: .Format.Path.forwardSlash, uriPath), with: (request, context) ) } } } return try await next(request, context) } }