diff --git a/Sources/HummingbirdDocC/Public/Middlewares/DocCMiddleware.swift b/Sources/HummingbirdDocC/Public/Middlewares/DocCMiddleware.swift index 045bf60..67712da 100644 --- a/Sources/HummingbirdDocC/Public/Middlewares/DocCMiddleware.swift +++ b/Sources/HummingbirdDocC/Public/Middlewares/DocCMiddleware.swift @@ -23,26 +23,28 @@ import struct Logging.Logger /// /// 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/`* +/// 1. *Redirects the URI path `/` or `/` to the path `//`*; +/// 2. *Redirects the URI path `//documentation` to the path `//documentation/`* +/// 3. *Redirects the URI path `//tutorials` to the path `//tutorials/`* +/// 4. *Redirects the URI path `//documentation/` to the resource on `/.doccarchive/documentation//index.html`* +/// 5. *Redirects the URI path `//tutorials/` to the resource on `/.doccarchive/tutorials//index.html`* +/// 6. *Redirects the URI path `//data/documentation.json` to the resource on `/.doccarchive/data/documentation/.json`* +/// 7. *Redirects the URI path `//favicon.ico` to the resource on `/.doccarchive/favicon.ico`* +/// 8. *Redirects the URI path `//favicon.svg` to the resource on `/.doccarchive/favicon.svg`* +/// 9. *Redirects the URI path `//theme-settings.json` to the resource on `/.doccarchive/theme-settings.json`* +/// 10. *Redirects the URI path `//css/` to the resource on `/.doccarchive/css/`* +/// 11. *Redirects the URI path `//data/` to the resource on `/.doccarchive/data/`* +/// 12. *Redirects the URI path `//downloads/` to the resource on `/.doccarchive/downloads/`* +/// 13. *Redirects the URI path `//images/` to the resource on `/.doccarchive/images/`* +/// 14. *Redirects the URI path `//img/` to the resource on `/.doccarchive/img/`* +/// 15. *Redirects the URI path `//index/` to the resource on `/.doccarchive/index/`* +/// 16. *Redirects the URI path `//js/` to the resource on `/.doccarchive/js/`* +/// 17. *Redirects the URI path `//videos/` to the resource on `/.doccarchive/videos/`* /// -public struct DocCMiddleware { +public struct DocCMiddleware< + Context: RequestContext, + FileSystemProvider: FileProvider +> { // MARK: Properties @@ -53,7 +55,7 @@ public struct DocCMiddleware { let logger: Logger /// A use case that checks whether a received URI could be processed or not. - private let checkURI: CheckURIUseCase = .init() + private let checkURI: CheckURIUseCase /// A use case that extracts data from a given URI path, essential for routing the documentation contents. private let prepareURIPath: PrepareURIPathUseCase @@ -97,6 +99,7 @@ public struct DocCMiddleware { ) { self.logger = logger self.fileProvider = fileProvider + self.checkURI = .init(uriRoot: configuration.uriRoot) self.prepareURIPath = .init(uriRoot: configuration.uriRoot) self.redirectURI = .init(logger: logger) self.serveURI = .init( @@ -105,95 +108,98 @@ public struct DocCMiddleware { ) } + // MARK: Computed + + /// A list of relative root URI paths to match against the relative path of a resource. + var rootPaths: [String] {[ + .empty, .Path.forwardSlash + ]} + } // 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 + _ request: Request, + context: Context, + next: (Request, Context) async throws -> Response ) async throws -> Output { guard let uriPath = checkURI(request.uri), - let uriData = prepareURIPath(uriPath) + let resource = 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) - ] + // Root URI Paths matching. + if rootPaths.contains(resource.relativePath) { + let uriRoot: String = if resource.relativePath.isEmpty { + .init(format: .Format.Path.forwardSlash, uriPath) + } else { + uriPath + } - if rootPaths.contains(uriData.resourcePath) { + // Rule #1: Redirects the URI path / or // to the path //documentation 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), + String(format: .Format.Path.documentation, uriRoot), with: (request, context) ) } + // Asset files matching. for assetFile in AssetFile.allCases { - if uriData.resourcePath.contains(assetFile.path) { + if resource.relativePath.hasPrefix(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, + // Rule #6: Redirects the URI path //data/documentation.json to the resource on /.doccarchive/data/documentation/.json + ? String(format: .Format.Path.documentationJSON, resource.archiveReference) + // Rule #7: Redirects the URI path `//favicon.ico` to the resource on `/.doccarchive/favicon.ico` + // Rule #8: Redirects the URI path `//favicon.svg` to the resource on `/.doccarchive/favicon.svg` + // Rule #9: Redirects the URI path `//theme-settings.json` to the resource on `/.doccarchive/theme-settings.json` + : resource.relativePath, + at: resource.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/` + if resource.relativePath.hasPrefix(assetFolder.path) { + // Rule #10: Redirects the URI path `//css/` to the resource on `/.doccarchive/css/` + // Rule #11: Redirects the URI path `//data/` to the resource on `/.doccarchive/data/` + // Rule #12: Redirects the URI path `//downloads/` to the resource on `/.doccarchive/downloads/` + // Rule #13: Redirects the URI path `//images/` to the resource on `/.doccarchive/images/` + // Rule #14: Redirects the URI path `//img/` to the resource on `/.doccarchive/img/` + // Rule #15: Redirects the URI path `//index/` to the resource on `/.doccarchive/index/` + // Rule #16: Redirects the URI path `//js/` to the resource on `/.doccarchive/js/` + // Rule #17: Redirects the URI path `//videos/` to the resource on `/.doccarchive/videos/` return try await serveURI( - uriData.resourcePath, - at: uriData.archivePath, + resource.relativePath, + at: resource.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 + if resource.relativePath.hasPrefix(documentationFolder.path) { + let pathSuffix: String = .init(format: .Format.Path.forwardSlash, documentationFolder.path) + + if uriPath.hasSuffix(pathSuffix) { + // Rule #4: Redirects the URI path //documentation/ to the resource on /.doccarchive/documentation//index.html + // Rule #5: 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, + String(format: .Format.Path.index, documentationFolder.path, resource.archiveReference), + at: resource.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/ + // Rule #2: Redirects the URI path //documentation to the path //documentation/ + // Rule #3: Redirects the URI path //tutorials to the path //tutorials/ return redirectURI( String(format: .Format.Path.forwardSlash, uriPath), with: (request, context) diff --git a/Tests/HummingbirdDocC/Tests/Public/Middlewares/DocCMiddlewareTests.swift b/Tests/HummingbirdDocC/Tests/Public/Middlewares/DocCMiddlewareTests.swift index 965599e..d73c2d5 100644 --- a/Tests/HummingbirdDocC/Tests/Public/Middlewares/DocCMiddlewareTests.swift +++ b/Tests/HummingbirdDocC/Tests/Public/Middlewares/DocCMiddlewareTests.swift @@ -296,16 +296,12 @@ private extension DocCMiddlewareTests { ) { // GIVEN // WHEN - let middleware = DocCMiddleware( + let middleware = DocCMiddleware( configuration: configuration, logger: logger ) // THEN - #expect(middleware.configuration.folderRoot == configuration.folderRoot) - #expect(middleware.configuration.uriRoot == configuration.uriRoot) - #expect(middleware.configuration.threadPool === configuration.threadPool) - #expect(middleware.logger.label == logger.label) #expect(middleware.logger.logLevel == logger.logLevel) #expect(middleware.logger.metadataProvider == nil) @@ -325,17 +321,13 @@ private extension DocCMiddlewareTests { ) { // GIVEN // WHEN - let middleware = DocCMiddleware( + let middleware = DocCMiddleware( configuration: configuration, fileProvider: fileProvider, logger: logger ) // THEN - #expect(middleware.configuration.folderRoot == configuration.folderRoot) - #expect(middleware.configuration.uriRoot == configuration.uriRoot) - #expect(middleware.configuration.threadPool === configuration.threadPool) - #expect(middleware.logger.label == logger.label) #expect(middleware.logger.logLevel == logger.logLevel) #expect(middleware.logger.metadataProvider == nil) @@ -363,13 +355,13 @@ private extension DocCMiddlewareTests { handler: logHandler ) - let context: any RequestContext = RequestContextMock(logger: logger) + let context: RequestContextMock = .init(logger: logger) let request: Request = .test( method: .get, path: uriPath ) - let middleware = DocCMiddleware( + let middleware = DocCMiddleware( configuration: .init( uriRoot: .Sample.uriRoot, folderRoot: .Sample.uriFolder @@ -455,7 +447,7 @@ private extension DocCMiddlewareTests { path: uriPath ) - let middleware = DocCMiddleware( + let middleware = DocCMiddleware( configuration: .init( uriRoot: .Sample.uriRoot, folderRoot: .Sample.uriFolder @@ -476,7 +468,7 @@ private extension DocCMiddlewareTests { .contentLength: (statusCode == .ok ? "36" : "0") ]) - let contentLength = #require(result.body.contentLength) + let contentLength = try #require(result.body.contentLength) if statusCode == .ok { #expect(contentLength > 0) @@ -553,7 +545,12 @@ private extension DocCMiddlewareTests { private extension Input { /// A list of relative URI paths to match against the URI path redirections done by the middleware. - static let redirectURIPaths: [String] = [.empty, .Path.forwardSlash, "/documentation", "/tutorials"] + static let redirectURIPaths: [String] = [ + .empty, + .Path.forwardSlash, + "/documentation", + "/tutorials" + ] /// A list of relative URI paths to match against the URI path servings done by the middleware. static let serveURIPaths: [String] = [ "/documentation/", @@ -575,22 +572,27 @@ private extension Input { private extension Output { /// A list of expected relative URI path redirections outputs coming out of the URI path redirections done by the middleware. - static let redirectURIPaths: [String] = [.Path.forwardSlash, "/documentation", "/documentation/", "/tutorials/"] + static let redirectURIPaths: [String] = [ + "/documentation", + "/documentation", + "/documentation/", + "/tutorials/" + ] /// A list of expected relative file URI paths of the logged messages coming out of the URI path servings done by the middleware. static let serveURIFilePaths: [String] = [ "/SomeDocument.doccarchive/documentation/somedocument/index.html", "/SomeDocument.doccarchive/tutorials/somedocument/index.html", "/SomeDocument.doccarchive/data/documentation/somedocument.json", - "/SomeDocument.doccarchive/SomeDocument/favicon.ico", - "/SomeDocument.doccarchive/SomeDocument/favicon.svg", - "/SomeDocument.doccarchive/SomeDocument/theme-settings.json", - "/SomeDocument.doccarchive/SomeDocument/css/file.css", - "/SomeDocument.doccarchive/SomeDocument/data/data.bin", - "/SomeDocument.doccarchive/SomeDocument/downloads/file.txt", - "/SomeDocument.doccarchive/SomeDocument/images/image.png", - "/SomeDocument.doccarchive/SomeDocument/img/image.jpg", - "/SomeDocument.doccarchive/SomeDocument/index/file", - "/SomeDocument.doccarchive/SomeDocument/js/file.js", - "/SomeDocument.doccarchive/SomeDocument/videos/video.mp4" + "/SomeDocument.doccarchive/favicon.ico", + "/SomeDocument.doccarchive/favicon.svg", + "/SomeDocument.doccarchive/theme-settings.json", + "/SomeDocument.doccarchive/css/file.css", + "/SomeDocument.doccarchive/data/data.bin", + "/SomeDocument.doccarchive/downloads/file.txt", + "/SomeDocument.doccarchive/images/image.png", + "/SomeDocument.doccarchive/img/image.jpg", + "/SomeDocument.doccarchive/index/file", + "/SomeDocument.doccarchive/js/file.js", + "/SomeDocument.doccarchive/videos/video.mp4" ] }