From 5a3ec20fe905cf549bac7c9d04fc89a0144e83e4 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Thu, 18 Sep 2025 23:04:55 +0200 Subject: [PATCH 01/29] Removed boilerplates from library and test targets. --- Sources/DocCMiddleware/hummingbird_docc_middleware.swift | 2 -- .../DocCMiddleware/hummingbird_docc_middlewareTests.swift | 7 ------- 2 files changed, 9 deletions(-) delete mode 100644 Sources/DocCMiddleware/hummingbird_docc_middleware.swift delete mode 100644 Tests/DocCMiddleware/hummingbird_docc_middlewareTests.swift diff --git a/Sources/DocCMiddleware/hummingbird_docc_middleware.swift b/Sources/DocCMiddleware/hummingbird_docc_middleware.swift deleted file mode 100644 index 08b22b8..0000000 --- a/Sources/DocCMiddleware/hummingbird_docc_middleware.swift +++ /dev/null @@ -1,2 +0,0 @@ -// The Swift Programming Language -// https://docs.swift.org/swift-book diff --git a/Tests/DocCMiddleware/hummingbird_docc_middlewareTests.swift b/Tests/DocCMiddleware/hummingbird_docc_middlewareTests.swift deleted file mode 100644 index 730ed9c..0000000 --- a/Tests/DocCMiddleware/hummingbird_docc_middlewareTests.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Testing - -@testable import DocCMiddleware - -@Test func example() async throws { - // Write your test here and use APIs like `#expect(...)` to check expected conditions. -} -- 2.52.0 From a2483b9fd67996ece10a19f44632fdb406c6deb2 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Thu, 18 Sep 2025 23:08:52 +0200 Subject: [PATCH 02/29] Implemented the initialization functions for the DocCMiddleware type in the library target. --- .../Public/Middlewares/DocCMiddleware.swift | 61 ++++++++++++ .../Middlewares/DocCMiddlewareTests.swift | 99 +++++++++++++++++++ .../Types/Extensions/Logger+Constants.swift | 29 ++++++ .../Types/Extensions/Tag+Constants.swift | 22 +++++ .../Types/Stubs/FileProviderStub.swift | 53 ++++++++++ 5 files changed, 264 insertions(+) create mode 100644 Sources/DocCMiddleware/Public/Middlewares/DocCMiddleware.swift create mode 100644 Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift create mode 100644 Tests/DocCMiddleware/Types/Extensions/Logger+Constants.swift create mode 100644 Tests/DocCMiddleware/Types/Extensions/Tag+Constants.swift create mode 100644 Tests/DocCMiddleware/Types/Stubs/FileProviderStub.swift diff --git a/Sources/DocCMiddleware/Public/Middlewares/DocCMiddleware.swift b/Sources/DocCMiddleware/Public/Middlewares/DocCMiddleware.swift new file mode 100644 index 0000000..9d6b03b --- /dev/null +++ b/Sources/DocCMiddleware/Public/Middlewares/DocCMiddleware.swift @@ -0,0 +1,61 @@ +// ===----------------------------------------------------------------------=== +// +// 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 Hummingbird + +import class NIOPosix.NIOThreadPool +import struct Logging.Logger + +/// A middleware that proxies requests to `DocC` documentation containers within a hosting app. +public struct DocCMiddleware { + + // MARK: Properties + + /// A protocol that defines file system interactions. + let fileProvider: any FileProvider + + /// A type that interacts with the logging system. + let logger: Logger + + // MARK: Initializers + + /// Initializes this middleware with the root path to the `DocC` documentation containers in the file system. + /// - Parameters: + /// - pathToRoot: A path to the root folder in which the `DocC` documentation container are located. + /// - logger: A type that interacts with the logging system. + /// - threadPool: A representation of the thread pool that should be used in case some blocking work needs to be performed for which no non-blocking API exists. + init( + pathToRoot rootFolder: String, + logger: Logger, + threadPool: NIOThreadPool = .singleton + ) { + self.fileProvider = LocalFileSystem( + rootFolder: rootFolder, + threadPool: threadPool, + logger: logger + ) + self.logger = logger + } + + /// Initializes this middleware with a type conforming to the `FileProvider` protocol. + /// - Parameters: + /// - fileProvider: A type that conforms to the protocol that defines file system interactions. + /// - logger: A type that interacts with the logging system. + init( + fileProvider: any FileProvider, + logger: Logger + ) { + self.fileProvider = fileProvider + self.logger = logger + } + +} diff --git a/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift b/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift new file mode 100644 index 0000000..77c253b --- /dev/null +++ b/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift @@ -0,0 +1,99 @@ +// ===----------------------------------------------------------------------=== +// +// 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 Testing + +import struct Hummingbird.LocalFileSystem +import struct Logging.Logger +import protocol Hummingbird.FileProvider + +@testable import DocCMiddleware + +@Suite("DocC Middleware") +struct DocCMiddlewareTests { + + // MARK: Initializers tests + +#if swift(>=6.2) + @Test(.tags(.initializer)) + func `initialize with path to root`() { + assertInit(pathToRoot: "/path/to/root/docc/documentation") + } + + @Test(.tags(.initializer)) + func `initialize with type that conforms to the FileProvider protocol`() { + // GIVEN + assertInit(fileProvider: FileProviderStub()) + } +#else + @Test("initialize with path to root", .tags(.initializer)) + func initWithRootPath() { + assertInit(pathToRoot: "/path/to/root/docc/documentation") + } + + @Test("initialize with type that conforms to the FileProvider protocol", .tags(.initializer)) + func initWithFileProviderType() { + assertInit(fileProvider: FileProviderStub()) + } +#endif + +} + +// MARK: - Assertions + +private extension DocCMiddlewareTests { + + // MARK: Functions + + /// Asserts the initialization of a `DocCMiddleware` type with a root path in the file system. + /// - Parameters: + /// - pathToRoot: A path to the root `DocC` documentation containers. + /// - logger: A type that interacts with the logging system. + func assertInit( + pathToRoot: String, + logger: Logger = .test + ) { + // GIVEN + // WHEN + let middleware = DocCMiddleware( + pathToRoot: pathToRoot, + logger: logger + ) + + // THEN + #expect(middleware.fileProvider is LocalFileSystem) + #expect(middleware.logger.label == logger.label) + #expect(middleware.logger.logLevel == logger.logLevel) + } + + /// Asserts the initialization of a `DocCMiddleware` type with a type that conforms to the `FileProvider` protocol. + /// - Parameters: + /// - fileProvider: A type that conforms to the protocol that defines file system interactions. + /// - logger: A type that interacts with the logging system. + func assertInit( + fileProvider: any FileProvider, + logger: Logger = .test + ) { + // GIVEN + // WHEN + let middleware = DocCMiddleware( + fileProvider: fileProvider, + logger: logger + ) + + // THEN + #expect(type(of:middleware.fileProvider) == type(of: fileProvider)) + #expect(middleware.logger.label == logger.label) + #expect(middleware.logger.logLevel == logger.logLevel) + } + +} diff --git a/Tests/DocCMiddleware/Types/Extensions/Logger+Constants.swift b/Tests/DocCMiddleware/Types/Extensions/Logger+Constants.swift new file mode 100644 index 0000000..a006a09 --- /dev/null +++ b/Tests/DocCMiddleware/Types/Extensions/Logger+Constants.swift @@ -0,0 +1,29 @@ +// ===----------------------------------------------------------------------=== +// +// 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 Logging +import Testing + +extension Logger { + + // MARK: Constants + + /// Creates a logger instance that is ready to use in test cases. + static let test: Self = { + var logger = Logger(label: "test.hummingbird-docc-middleware.logger") + + logger.logLevel = try! #require(Logger.Level.allCases.randomElement()) + + return logger + }() + +} diff --git a/Tests/DocCMiddleware/Types/Extensions/Tag+Constants.swift b/Tests/DocCMiddleware/Types/Extensions/Tag+Constants.swift new file mode 100644 index 0000000..d180d37 --- /dev/null +++ b/Tests/DocCMiddleware/Types/Extensions/Tag+Constants.swift @@ -0,0 +1,22 @@ +// ===----------------------------------------------------------------------=== +// +// 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 Testing + +extension Tag { + + // MARK: Constants + + /// Tag that indicate a test case for a type initialization. + @Tag static var initializer: Self + +} diff --git a/Tests/DocCMiddleware/Types/Stubs/FileProviderStub.swift b/Tests/DocCMiddleware/Types/Stubs/FileProviderStub.swift new file mode 100644 index 0000000..ab4f4b3 --- /dev/null +++ b/Tests/DocCMiddleware/Types/Stubs/FileProviderStub.swift @@ -0,0 +1,53 @@ +// ===----------------------------------------------------------------------=== +// +// 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 Hummingbird + +/// A stub that conforms to the `FileProvider` protocol. +struct FileProviderStub {} + +// MARK: - FileProvider + +extension FileProviderStub: FileProvider { + + // MARK: Type aliases + + typealias FileAttributes = String + typealias FileIdentifier = String + + // MARK: Functions + + func getFileIdentifier(_ path: String) -> String? { + nil + } + + func getAttributes(id: String) async throws -> String? { + nil + } + + func loadFile( + id: String, + context: some RequestContext + ) async throws -> ResponseBody { + .init() + } + + func loadFile( + id: String, + range: ClosedRange, + context: some RequestContext + ) async throws -> ResponseBody { + .init() + } + +} + -- 2.52.0 From 2590cb457f0640b96813bdd22434b1fb7cb46e23 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Mon, 22 Sep 2025 18:01:16 +0200 Subject: [PATCH 03/29] Implemented the CheckURIUseCase use case type in the library target. --- .../Extensions/String+Constants.swift | 20 +++ .../Internal/Use Cases/CheckURIUseCase.swift | 35 ++++++ .../Use Cases/CheckURIUseCaseTests.swift | 116 ++++++++++++++++++ .../Types/Extensions/Tag+Constants.swift | 2 + 4 files changed, 173 insertions(+) create mode 100644 Sources/DocCMiddleware/Internal/Extensions/String+Constants.swift create mode 100644 Sources/DocCMiddleware/Internal/Use Cases/CheckURIUseCase.swift create mode 100644 Tests/DocCMiddleware/Tests/Internal/Use Cases/CheckURIUseCaseTests.swift diff --git a/Sources/DocCMiddleware/Internal/Extensions/String+Constants.swift b/Sources/DocCMiddleware/Internal/Extensions/String+Constants.swift new file mode 100644 index 0000000..267dab8 --- /dev/null +++ b/Sources/DocCMiddleware/Internal/Extensions/String+Constants.swift @@ -0,0 +1,20 @@ +// ===----------------------------------------------------------------------=== +// +// 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 +// +// ===----------------------------------------------------------------------=== + +extension String { + /// An empty string. + static let empty = "" + /// A forwarding slash. + static let forwardSlash = "/" + /// An indication of a previous folder in a path component. + static let previousFolder = ".." +} diff --git a/Sources/DocCMiddleware/Internal/Use Cases/CheckURIUseCase.swift b/Sources/DocCMiddleware/Internal/Use Cases/CheckURIUseCase.swift new file mode 100644 index 0000000..ad6b9fb --- /dev/null +++ b/Sources/DocCMiddleware/Internal/Use Cases/CheckURIUseCase.swift @@ -0,0 +1,35 @@ +// ===----------------------------------------------------------------------=== +// +// 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 struct HummingbirdCore.URI + +/// A use case that checks whether a given URI against a set of conditions, to determine whether the URI could be used by the middleware or not. +struct CheckURIUseCase { + + // MARK: Functions + + /// Checks whether a provided URI against a set of conditions, so it could be used by the middleware. + /// - Parameter uri: A URI to check. + /// - Returns: A non-encoded URI, which is ready to be used by the middleware. + func callAsFunction(_ uri: URI) -> String? { + guard + let uriPath = uri.path.removingPercentEncoding, + !uriPath.contains(.previousFolder), + uriPath.hasPrefix(.forwardSlash) + else { + return nil + } + + return uriPath + } + +} diff --git a/Tests/DocCMiddleware/Tests/Internal/Use Cases/CheckURIUseCaseTests.swift b/Tests/DocCMiddleware/Tests/Internal/Use Cases/CheckURIUseCaseTests.swift new file mode 100644 index 0000000..fc3c5c7 --- /dev/null +++ b/Tests/DocCMiddleware/Tests/Internal/Use Cases/CheckURIUseCaseTests.swift @@ -0,0 +1,116 @@ +// ===----------------------------------------------------------------------=== +// +// 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 Testing + +import struct HummingbirdCore.URI + +@testable import struct DocCMiddleware.CheckURIUseCase + +@Suite("Check URI use case", .tags(.useCase)) +struct CheckURIUseCaseTests { + + // MARK: Properties + + private let useCase: CheckURIUseCase = .init() + + // MARK: Use case tests + +#if swift(>=6.2) + @Test(arguments: zip( + Input.nonEncodedURIs, + Output.nonEncodedURIs + )) + func `check non encoded URIs`( + uri uriPath: String, + expects result: String? + ) { + assertURI(uriPath, expects: result) + } + + @Test(arguments: zip( + Input.percentEncodedURIs, + Output.percentEncodedURIs + )) + func `check percent-encoded URIs`( + uri uriPath: String, + expects result: String? + ) { + assertURI(uriPath, expects: result) + } +#else + @Test("check non-encoded URIs", arguments: zip( + Input.nonEncodedURIs, + Output.nonEncodedURIs + )) + func checkNonEncodedURIs( + uri uriPath: String, + expects result: String? + ) { + assertURI(uriPath, expects: result) + } + + @Test("check percent-encoded URIs", arguments: zip( + Input.percentEncodedURIs, + Output.percentEncodedURIs + )) + func checkPercentEncodedURIs( + uri uriPath: String, + expects result: String? + ) { + assertURI(uriPath, expects: result) + } +#endif + +} + +// MARK: - Assertions + +private extension CheckURIUseCaseTests { + + // MARK: Functions + + /// Asserts a URI type based on a given path and an expected result. + /// - Parameters: + /// - uriPath: A URI path to use with a URI type. + /// - result: An expected result coming out of the use case. + func assertURI( + _ uriPath: String, + expects result: String? + ) { + // GIVEN + let uri = URI(uriPath) + + // WHEN + let output = useCase(uri) + + // THEN + #expect(output == result) + } + +} + +// MARK: - Constants + +enum Input { + /// A list of non-encoded URI samples. + static let nonEncodedURIs: [String] = ["/", "/some/known/path", "", "/some/../path", "some/other/path"] + /// A list of percent-encoded URI samples. + static let percentEncodedURIs: [String] = ["%2F", "/some%2Fknown%3Fpath", "%20", "/some/%2E%2E/path", "some%2Fother%3Fpath"] +} + +enum Output { + /// A list of expected outputs for the non-encoded URI samples. + static let nonEncodedURIs: [String?] = ["/", "/some/known/path", "/", nil, nil] + /// A list of expected outputs for the percent-encoded URI samples. + static let percentEncodedURIs: [String?] = ["/", "/some/known?path", nil, nil, nil] +} diff --git a/Tests/DocCMiddleware/Types/Extensions/Tag+Constants.swift b/Tests/DocCMiddleware/Types/Extensions/Tag+Constants.swift index d180d37..7c6b32f 100644 --- a/Tests/DocCMiddleware/Types/Extensions/Tag+Constants.swift +++ b/Tests/DocCMiddleware/Types/Extensions/Tag+Constants.swift @@ -18,5 +18,7 @@ extension Tag { /// Tag that indicate a test case for a type initialization. @Tag static var initializer: Self + /// Tag that indicate a test case for a use case type. + @Tag static var useCase: Self } -- 2.52.0 From 74a6ba73bba5407d3c211d61f03c13f75112d5aa Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Mon, 22 Sep 2025 19:18:04 +0200 Subject: [PATCH 04/29] Defined the relative paths formats of the DocC archive assets on the Strings+Formats extension in the library target. --- .../Internal/Extensions/String+Formats.swift | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 Sources/DocCMiddleware/Internal/Extensions/String+Formats.swift diff --git a/Sources/DocCMiddleware/Internal/Extensions/String+Formats.swift b/Sources/DocCMiddleware/Internal/Extensions/String+Formats.swift new file mode 100644 index 0000000..d425b2b --- /dev/null +++ b/Sources/DocCMiddleware/Internal/Extensions/String+Formats.swift @@ -0,0 +1,38 @@ +// ===----------------------------------------------------------------------=== +// +// 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 +// +// ===----------------------------------------------------------------------=== + +extension String { + /// A namespace that defines the format patterns used to generate strings. + enum Format { + /// A namespace that defines the format patterns used to generate relative path representations. + enum Path { + /// A format pattern used to generate relative paths that starts with the `/` string and finishes with the `.doccarchive` string. + static let archive = "/%@.doccarchive" + /// A format pattern used to generate relative paths that starts with the `/data` string. + static let data = "/data/%@" + /// A format pattern used to generate relative paths that starts with the `/docs` string. + static let docs = "/docs/%@" + /// A format pattern used to generate relative paths that finishes with the `/documentation` string. + static let documentation = "%@documentation" + /// A format pattern used to generate relative paths for JSON documentation files. + static let documentationJSON = "/data/documentation/%@.json" + /// A format pattern used to generate relative paths that starts and finishes with the `/` string. + static let folder = "/%@/" + ///A format pattern used to generate relative paths that finishes with the `/` string. + static let forwardSlash = "%@/" + /// A format pattern used to generate relative paths for index files. + static let index = "%@/%@/index.html" + /// A format pattern used to generate relative paths that starts with the `/` string. + static let root = "/%@" + } + } +} -- 2.52.0 From f78e70e82334113c0861046a1fcd91c0d56df005 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Mon, 22 Sep 2025 19:21:13 +0200 Subject: [PATCH 05/29] Defined the Pathable protocol in the library target. --- .../Internal/Protocols/Pathable.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 Sources/DocCMiddleware/Internal/Protocols/Pathable.swift diff --git a/Sources/DocCMiddleware/Internal/Protocols/Pathable.swift b/Sources/DocCMiddleware/Internal/Protocols/Pathable.swift new file mode 100644 index 0000000..5cb5014 --- /dev/null +++ b/Sources/DocCMiddleware/Internal/Protocols/Pathable.swift @@ -0,0 +1,21 @@ +// ===----------------------------------------------------------------------=== +// +// 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 +// +// ===----------------------------------------------------------------------=== + +/// A type that provides a relative path representation. +protocol Pathable { + + // MARK: Properties + + /// A (relative) path. + var path: String { get } + +} -- 2.52.0 From 9b908515d9ad643ce6f5587f2a48fa6b8d60ff8b Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Mon, 22 Sep 2025 20:41:59 +0200 Subject: [PATCH 06/29] Defined the AssetFile enumeration type in. the library target and also, conformed it to the Pathable protocol. --- .../Internal/Enumerations/AssetFile.swift | 38 +++++++++ .../Internal/Protocols/Pathable.swift | 2 +- .../Enumerations/AssetFileTests.swift | 77 +++++++++++++++++++ .../Types/Extensions/Tag+Constants.swift | 2 + 4 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 Sources/DocCMiddleware/Internal/Enumerations/AssetFile.swift create mode 100644 Tests/DocCMiddleware/Tests/Internal/Enumerations/AssetFileTests.swift diff --git a/Sources/DocCMiddleware/Internal/Enumerations/AssetFile.swift b/Sources/DocCMiddleware/Internal/Enumerations/AssetFile.swift new file mode 100644 index 0000000..89de2c7 --- /dev/null +++ b/Sources/DocCMiddleware/Internal/Enumerations/AssetFile.swift @@ -0,0 +1,38 @@ +// ===----------------------------------------------------------------------=== +// +// 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 +// +// ===----------------------------------------------------------------------=== + +/// An enumeration that represents the essential static files that could be generated by the `DocC` building process. +enum AssetFile: String, CaseIterable { + /// A file defining all the documentation available, which will be used to redirect to the root of the documentation's root article. + case documentation = "documentation.json" + /// A file containing the icon in `.ico` format within the documentation generated by the `DocC` building process. + case faviconICO = "favicon.ico" + /// A file containing the icon in `.svg` format within the documentation generated by the `DocC` building process. + case faviconSVG = "favicon.svg" + /// A file containing the theme settings within the documentation generated by the `DocC` building process. + case themeSettings = "theme-settings.json" +} + +// MARK: - Pathable + +extension AssetFile: Pathable { + + // MARK: Computed + + var path: String { + switch self { + case .documentation: .init(format: .Format.Path.data, rawValue) + default: .init(format: .Format.Path.root, rawValue) + } + } + +} diff --git a/Sources/DocCMiddleware/Internal/Protocols/Pathable.swift b/Sources/DocCMiddleware/Internal/Protocols/Pathable.swift index 5cb5014..2d0f7a4 100644 --- a/Sources/DocCMiddleware/Internal/Protocols/Pathable.swift +++ b/Sources/DocCMiddleware/Internal/Protocols/Pathable.swift @@ -15,7 +15,7 @@ protocol Pathable { // MARK: Properties - /// A (relative) path. + /// A (relative) path to a resource. var path: String { get } } diff --git a/Tests/DocCMiddleware/Tests/Internal/Enumerations/AssetFileTests.swift b/Tests/DocCMiddleware/Tests/Internal/Enumerations/AssetFileTests.swift new file mode 100644 index 0000000..6ad465f --- /dev/null +++ b/Tests/DocCMiddleware/Tests/Internal/Enumerations/AssetFileTests.swift @@ -0,0 +1,77 @@ +// ===----------------------------------------------------------------------=== +// +// 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 Testing + +@testable import enum DocCMiddleware.AssetFile + +@Suite("Asset File", .tags(.enumeration)) +struct AssetFileTests { + + // MARK: Properties tests + +#if swift(>=6.2) + @Test(arguments: zip( + AssetFile.allCases, + Output.assetFilePaths + )) + func `path`( + `case`: AssetFile, + expects result: String + ) { + assertPath(`case`, expects: result) + } +#else + @Test("path", arguments: zip( + AssetFile.allCases, + Output.assetFilePaths + )) + func path( + `case`: AssetFile, + expects result: String + ) { + assertPath(`case`, expects: result) + } +#endif + +} + +// MARK: - Assertions + +private extension AssetFileTests { + + // MARK: Functions + + /// Asserts the path property based on a given ``AssetFile`` enumeration case and an expected result. + /// - Parameters: + /// - case: A representation of the ``AssetFile`` enumeration + /// - result: An expected result coming out of the property. + func assertPath( + _ case: AssetFile, + expects result: String + ) { + // GIVEN + // WHEN + let output = `case`.path + + // THEN + #expect(output == result) + } + +} + +// MARK: - Constants + +extension Output { + /// A list of expected outputs for the paths of the ``AssetFile`` enumeration cases. + static let assetFilePaths: [String] = ["/data/documentation.json", "/favicon.ico", "/favicon.svg", "/theme-settings.json"] +} diff --git a/Tests/DocCMiddleware/Types/Extensions/Tag+Constants.swift b/Tests/DocCMiddleware/Types/Extensions/Tag+Constants.swift index 7c6b32f..34ee316 100644 --- a/Tests/DocCMiddleware/Types/Extensions/Tag+Constants.swift +++ b/Tests/DocCMiddleware/Types/Extensions/Tag+Constants.swift @@ -16,6 +16,8 @@ extension Tag { // MARK: Constants + /// Tag that indicate a test case for an enumeration type. + @Tag static var enumeration: Self /// Tag that indicate a test case for a type initialization. @Tag static var initializer: Self /// Tag that indicate a test case for a use case type. -- 2.52.0 From dfec69d1bd19458e86b163a18ad3413546799c9b Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Mon, 22 Sep 2025 20:56:50 +0200 Subject: [PATCH 07/29] Defined the DocumentrationFolder enumeration type in. the library target and also, conformed it to the Pathable protocol. --- .../Enumerations/DocumentationFolder.swift | 31 ++++++++ .../DocumentationFolderTests.swift | 77 +++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 Sources/DocCMiddleware/Internal/Enumerations/DocumentationFolder.swift create mode 100644 Tests/DocCMiddleware/Tests/Internal/Enumerations/DocumentationFolderTests.swift diff --git a/Sources/DocCMiddleware/Internal/Enumerations/DocumentationFolder.swift b/Sources/DocCMiddleware/Internal/Enumerations/DocumentationFolder.swift new file mode 100644 index 0000000..403d585 --- /dev/null +++ b/Sources/DocCMiddleware/Internal/Enumerations/DocumentationFolder.swift @@ -0,0 +1,31 @@ +// ===----------------------------------------------------------------------=== +// +// 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 +// +// ===----------------------------------------------------------------------=== + +/// An enumeration that represents the documentation folders that could be generated by the `DocC` building process. +enum DocumentationFolder: String, CaseIterable { + /// An article document, which can also be used for (source code generated) technical documentation as well. + case article = "documentation" + /// A tutorial document. + case tutorial = "tutorials" +} + +// MARK: - Pathable + +extension DocumentationFolder: Pathable { + + // MARK: Computed + + var path: String { + .init(format: .Format.Path.root, rawValue) + } + +} diff --git a/Tests/DocCMiddleware/Tests/Internal/Enumerations/DocumentationFolderTests.swift b/Tests/DocCMiddleware/Tests/Internal/Enumerations/DocumentationFolderTests.swift new file mode 100644 index 0000000..fc2cc6d --- /dev/null +++ b/Tests/DocCMiddleware/Tests/Internal/Enumerations/DocumentationFolderTests.swift @@ -0,0 +1,77 @@ +// ===----------------------------------------------------------------------=== +// +// 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 Testing + +@testable import enum DocCMiddleware.DocumentationFolder + +@Suite("Documentation Type", .tags(.enumeration)) +struct DocumentationTypeTests { + + // MARK: Properties tests + +#if swift(>=6.2) + @Test(arguments: zip( + DocumentationFolder.allCases, + Output.documentationFolderPaths + )) + func `path`( + `case`: DocumentationFolder, + expects result: String + ) { + assertPath(`case`, expects: result) + } +#else + @Test("path", arguments: zip( + DocumentationType.allCases, + Output.documentationTypePaths + )) + func path( + `case`: DocumentationType, + expects result: String + ) { + assertPath(`case`, expects: result) + } +#endif + +} + +// MARK: - Assertions + +private extension DocumentationTypeTests { + + // MARK: Functions + + /// Asserts the path property based on a given ``DocumentationFolder`` enumeration case and an expected result. + /// - Parameters: + /// - case: A representation of the ``DocumentationFolder`` enumeration + /// - result: An expected result coming out of the property. + func assertPath( + _ case: DocumentationFolder, + expects result: String + ) { + // GIVEN + // WHEN + let output = `case`.path + + // THEN + #expect(output == result) + } + +} + +// MARK: - Constants + +extension Output { + /// A list of expected outputs for the paths of the ``DocumentationFolder`` enumeration cases. + static let documentationFolderPaths: [String] = ["/documentation", "/tutorials"] +} -- 2.52.0 From d86d533198ab4c10b5489eaa9ca29c7d2ef7a01b Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Mon, 22 Sep 2025 21:10:00 +0200 Subject: [PATCH 08/29] Defined the AssetFolder enumeration type in. the library target and also, conformed it to the Pathable protocol. --- .../Internal/Enumerations/AssetFolder.swift | 43 +++++++++++ .../Enumerations/AssetFolderTests.swift | 77 +++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 Sources/DocCMiddleware/Internal/Enumerations/AssetFolder.swift create mode 100644 Tests/DocCMiddleware/Tests/Internal/Enumerations/AssetFolderTests.swift diff --git a/Sources/DocCMiddleware/Internal/Enumerations/AssetFolder.swift b/Sources/DocCMiddleware/Internal/Enumerations/AssetFolder.swift new file mode 100644 index 0000000..dd7ea2c --- /dev/null +++ b/Sources/DocCMiddleware/Internal/Enumerations/AssetFolder.swift @@ -0,0 +1,43 @@ +// ===----------------------------------------------------------------------=== +// +// 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 +// +// ===----------------------------------------------------------------------=== + +/// An enumeration that represents all possible asset folders that could be generated by the `DocC` building process. +enum AssetFolder: String, CaseIterable { + /// A folder that contains all CSS style sheets. + case css + /// A folder that contains all documentation data. + case data + /// A folder that contains all other resources. + case downloads + /// A folder that contains all image resources. + case images + /// A folder that contains all image resources. + case img + /// A folder that contains all generated `HTML` code. + case index + /// A folder that contains all generated `Javascript` code. + case js + /// A folder that contains all video resources. + case videos +} + +// MARK: - Pathable + +extension AssetFolder: Pathable { + + // MARK: Computed + + var path: String { + .init(format: .Format.Path.folder, rawValue) + } + +} diff --git a/Tests/DocCMiddleware/Tests/Internal/Enumerations/AssetFolderTests.swift b/Tests/DocCMiddleware/Tests/Internal/Enumerations/AssetFolderTests.swift new file mode 100644 index 0000000..eabef30 --- /dev/null +++ b/Tests/DocCMiddleware/Tests/Internal/Enumerations/AssetFolderTests.swift @@ -0,0 +1,77 @@ +// ===----------------------------------------------------------------------=== +// +// 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 Testing + +@testable import enum DocCMiddleware.AssetFolder + +@Suite("Asset Folder", .tags(.enumeration)) +struct AssetFolderTests { + + // MARK: Properties tests + +#if swift(>=6.2) + @Test(arguments: zip( + AssetFolder.allCases, + Output.assetFolderPaths + )) + func `path`( + `case`: AssetFolder, + expects result: String + ) { + assertPath(`case`, expects: result) + } +#else + @Test("path", arguments: zip( + AssetFolder.allCases, + Output.assetFolderPaths + )) + func path( + `case`: AssetFolder, + expects result: String + ) { + assertPath(`case`, expects: result) + } +#endif + +} + +// MARK: - Assertions + +private extension AssetFolderTests { + + // MARK: Functions + + /// Asserts the path property based on a given ``AssetFolder`` enumeration case and an expected result. + /// - Parameters: + /// - case: A representation of the ``AssetFolder`` enumeration + /// - result: An expected result coming out of the property. + func assertPath( + _ case: AssetFolder, + expects result: String + ) { + // GIVEN + // WHEN + let output = `case`.path + + // THEN + #expect(output == result) + } + +} + +// MARK: - Constants + +extension Output { + /// A list of expected outputs for the paths of the ``AssetFolder`` enumeration cases. + static let assetFolderPaths: [String] = ["/css/", "/data/", "/downloads/", "/images/", "/img/", "/index/", "/js/", "/videos/"] +} -- 2.52.0 From 0d76afb73820d43e9d59a62c5a0854d25c533cc4 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 23 Sep 2025 14:15:28 +0200 Subject: [PATCH 09/29] Added the Input and Output namespaces in the tests target. --- .../Internal/Enumerations/AssetFileTests.swift | 2 +- .../Internal/Enumerations/AssetFolderTests.swift | 2 +- .../Enumerations/DocumentationFolderTests.swift | 2 +- .../Internal/Use Cases/CheckURIUseCaseTests.swift | 6 +++--- Tests/DocCMiddleware/Types/Namespaces/Input.swift | 14 ++++++++++++++ Tests/DocCMiddleware/Types/Namespaces/Output.swift | 14 ++++++++++++++ 6 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 Tests/DocCMiddleware/Types/Namespaces/Input.swift create mode 100644 Tests/DocCMiddleware/Types/Namespaces/Output.swift diff --git a/Tests/DocCMiddleware/Tests/Internal/Enumerations/AssetFileTests.swift b/Tests/DocCMiddleware/Tests/Internal/Enumerations/AssetFileTests.swift index 6ad465f..0ded28e 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Enumerations/AssetFileTests.swift +++ b/Tests/DocCMiddleware/Tests/Internal/Enumerations/AssetFileTests.swift @@ -71,7 +71,7 @@ private extension AssetFileTests { // MARK: - Constants -extension Output { +private extension Output { /// A list of expected outputs for the paths of the ``AssetFile`` enumeration cases. static let assetFilePaths: [String] = ["/data/documentation.json", "/favicon.ico", "/favicon.svg", "/theme-settings.json"] } diff --git a/Tests/DocCMiddleware/Tests/Internal/Enumerations/AssetFolderTests.swift b/Tests/DocCMiddleware/Tests/Internal/Enumerations/AssetFolderTests.swift index eabef30..a0b12d1 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Enumerations/AssetFolderTests.swift +++ b/Tests/DocCMiddleware/Tests/Internal/Enumerations/AssetFolderTests.swift @@ -71,7 +71,7 @@ private extension AssetFolderTests { // MARK: - Constants -extension Output { +private extension Output { /// A list of expected outputs for the paths of the ``AssetFolder`` enumeration cases. static let assetFolderPaths: [String] = ["/css/", "/data/", "/downloads/", "/images/", "/img/", "/index/", "/js/", "/videos/"] } diff --git a/Tests/DocCMiddleware/Tests/Internal/Enumerations/DocumentationFolderTests.swift b/Tests/DocCMiddleware/Tests/Internal/Enumerations/DocumentationFolderTests.swift index fc2cc6d..3004b0c 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Enumerations/DocumentationFolderTests.swift +++ b/Tests/DocCMiddleware/Tests/Internal/Enumerations/DocumentationFolderTests.swift @@ -71,7 +71,7 @@ private extension DocumentationTypeTests { // MARK: - Constants -extension Output { +private extension Output { /// A list of expected outputs for the paths of the ``DocumentationFolder`` enumeration cases. static let documentationFolderPaths: [String] = ["/documentation", "/tutorials"] } diff --git a/Tests/DocCMiddleware/Tests/Internal/Use Cases/CheckURIUseCaseTests.swift b/Tests/DocCMiddleware/Tests/Internal/Use Cases/CheckURIUseCaseTests.swift index fc3c5c7..055a1be 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Use Cases/CheckURIUseCaseTests.swift +++ b/Tests/DocCMiddleware/Tests/Internal/Use Cases/CheckURIUseCaseTests.swift @@ -79,7 +79,7 @@ private extension CheckURIUseCaseTests { // MARK: Functions - /// Asserts a URI type based on a given path and an expected result. + /// Asserts a URI path provided by the ``CheckURIPathUseCase`` use case based on a given path and an expected result. /// - Parameters: /// - uriPath: A URI path to use with a URI type. /// - result: An expected result coming out of the use case. @@ -101,14 +101,14 @@ private extension CheckURIUseCaseTests { // MARK: - Constants -enum Input { +private extension Input { /// A list of non-encoded URI samples. static let nonEncodedURIs: [String] = ["/", "/some/known/path", "", "/some/../path", "some/other/path"] /// A list of percent-encoded URI samples. static let percentEncodedURIs: [String] = ["%2F", "/some%2Fknown%3Fpath", "%20", "/some/%2E%2E/path", "some%2Fother%3Fpath"] } -enum Output { +private extension Output { /// A list of expected outputs for the non-encoded URI samples. static let nonEncodedURIs: [String?] = ["/", "/some/known/path", "/", nil, nil] /// A list of expected outputs for the percent-encoded URI samples. diff --git a/Tests/DocCMiddleware/Types/Namespaces/Input.swift b/Tests/DocCMiddleware/Types/Namespaces/Input.swift new file mode 100644 index 0000000..a71d255 --- /dev/null +++ b/Tests/DocCMiddleware/Types/Namespaces/Input.swift @@ -0,0 +1,14 @@ +// ===----------------------------------------------------------------------=== +// +// 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 +// +// ===----------------------------------------------------------------------=== + +/// A namespace assigned for test arguments that would be input into test cases. +enum Input {} diff --git a/Tests/DocCMiddleware/Types/Namespaces/Output.swift b/Tests/DocCMiddleware/Types/Namespaces/Output.swift new file mode 100644 index 0000000..0773041 --- /dev/null +++ b/Tests/DocCMiddleware/Types/Namespaces/Output.swift @@ -0,0 +1,14 @@ +// ===----------------------------------------------------------------------=== +// +// 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 +// +// ===----------------------------------------------------------------------=== + +/// A namespace assigned for test arguments that would be expected outputs coming from results of test cases. +enum Output {} -- 2.52.0 From 2f701ce95eec80f128257ec0949fcff39713ccd1 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 23 Sep 2025 15:19:16 +0200 Subject: [PATCH 10/29] Implemented the PrepareURIPathUseCase use case type in the library target. --- .../Use Cases/PrepareURIPathUseCase.swift | 121 ++++++++++++++ .../DoccMiddlewareConfiguration.swift | 8 + .../PrepareURIPathUseCaseTests.swift | 154 ++++++++++++++++++ 3 files changed, 283 insertions(+) create mode 100644 Sources/DocCMiddleware/Internal/Use Cases/PrepareURIPathUseCase.swift create mode 100644 Sources/DocCMiddleware/Public/Configurations/DoccMiddlewareConfiguration.swift create mode 100644 Tests/DocCMiddleware/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift diff --git a/Sources/DocCMiddleware/Internal/Use Cases/PrepareURIPathUseCase.swift b/Sources/DocCMiddleware/Internal/Use Cases/PrepareURIPathUseCase.swift new file mode 100644 index 0000000..0588860 --- /dev/null +++ b/Sources/DocCMiddleware/Internal/Use Cases/PrepareURIPathUseCase.swift @@ -0,0 +1,121 @@ +// ===----------------------------------------------------------------------=== +// +// 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 Foundation +import RegexBuilder + +/// A use case that obtains some necessary data from a given URI path, that are essential for routing the documentation contents. +struct PrepareURIPathUseCase { + + // MARK: Type aliases + + /// A pseudo-type that contains the archive name and URI path, plus the resource URI paths used for routing the documentation contents. + typealias PreparedURIPaths = (archiveName: String, archivePath: String, resourcePath: String) + + // MARK: Properties + + /// A root path that suffixes the documentation resource. + private let uriRoot: String + + // MARK: Initializers + + /// Initializes this use case. + /// + /// > important: It is assumed that the `uriRoot` parameter is not empty and that it is prefixed by the `/` character. + /// + /// - Parameter uriRoot: A root path that prefixes the documentation resource. + init(uriRoot: String) { + self.uriRoot = uriRoot + } + + // MARK: Functions + + /// Extracts some necessary data essential for documentation contents routing from a given URI path. + /// + /// The necessary data to extract from a given URI path is: + /// 1. the `DocC` documentation archive name; + /// 2. the `DocC` documentation archive URI path; + /// 3. the `DocC` documentation resource URI path. + /// + /// > important: It is assumed that the `uriPath` parameter is a URI path that does not contain any percent encoded strings. + /// + /// - Parameter uriPath: A URI path to extract the data from. + /// - Returns: A pseudo-type that contains the archive' name and URI path, plus the resource URI paths. + func callAsFunction(_ uriPath: String) -> PreparedURIPaths? { + guard let uriRest = restOfURIPath(from: uriPath) else { + return nil + } + + let documentationName = uriRest + .split(separator: .forwardSlash) + .map(String.init) + .first + + let archiveName: String = if let documentationName { + documentationName.lowercased() + } else { + .empty + } + let archivePath: String = if let documentationName { + .init(format: .Format.Path.archive, documentationName) + } else { + .empty + } + + return (archiveName, archivePath, uriRest) + } + +} + +// MARK: - Helpers + +private extension PrepareURIPathUseCase { + + // MARK: Functions + + /// Extracts the rest of the URI path from a given URI path against a defined URI root path. + /// + /// A given URI path is matched against a regular expression, which is generated from a provided URI root path. + /// So this function would return either a string that represents a partial URI path, or a `nil` instance depending the result of the match between + /// the URI path and the regular expression: + /// * A `nil` instance in case there is no match; + /// * A `/` string in case there is a perfect match; + /// * A partial URI path prefixed with the `/` character in case there is an offset in the match. + /// + /// - Parameter uriPath: A URI path to get the rest of the URI path from. + /// - Returns: A rest of the URI path prefixed by the `/`character in case where there is any offset path after extracting the root path from the given URI path or not. Otherwise, a `nil` value is returned. + func restOfURIPath(from uriPath: String) -> String? { + let restReference = Reference(String.self) + let uriPattern = Regex { + uriRoot + Optionally { + Capture(as: restReference) { + OneOrMore(.anyNonNewline) + } transform: { output in + String(output) + } + } + } + + guard let matches = uriPath.prefixMatch(of: uriPattern) else { + return nil + } + guard let uriRest = matches.output.1 else { + return .forwardSlash + } + guard uriRest.hasPrefix(String.forwardSlash) else { + return .init(format: .Format.Path.root, uriRest) + } + return uriRest + } + +} diff --git a/Sources/DocCMiddleware/Public/Configurations/DoccMiddlewareConfiguration.swift b/Sources/DocCMiddleware/Public/Configurations/DoccMiddlewareConfiguration.swift new file mode 100644 index 0000000..f62ad7f --- /dev/null +++ b/Sources/DocCMiddleware/Public/Configurations/DoccMiddlewareConfiguration.swift @@ -0,0 +1,8 @@ +// +// File.swift +// hummingbird-docc-middleware +// +// Created by Javier Cicchelli on 23/09/2025. +// + +import Foundation diff --git a/Tests/DocCMiddleware/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift b/Tests/DocCMiddleware/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift new file mode 100644 index 0000000..8dfde44 --- /dev/null +++ b/Tests/DocCMiddleware/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift @@ -0,0 +1,154 @@ +// ===----------------------------------------------------------------------=== +// +// 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 Testing + +@testable import struct DocCMiddleware.PrepareURIPathUseCase + +@Suite("Prepare URI Path Use Case", .tags(.useCase)) +struct PrepareURIPathUseCaseTests { + + // MARK: Use case tests + +#if swift(>=6.2) + @Test(arguments: zip( + Input.prepareURIPaths, + Output.prepareURIPaths + )) + func `extract data with URI root not suffixed with forward slash`( + uri uriPath: String, + expects result: PrepareURIPathUseCase.PreparedURIPaths? + ) throws { + try assertData( + uriRoot: .uriRoot, + uriPath: uriPath, + expects: result + ) + } + + @Test(arguments: zip( + Input.prepareURIPathsSlashed, + Output.prepareURIPaths + )) + func `extract data with URI root suffixed with forward slash`( + uri uriPath: String, + expects result: PrepareURIPathUseCase.PreparedURIPaths? + ) throws { + try assertData( + uriRoot: .uriRootSlashed, + uriPath: uriPath, + expects: result + ) + } +#else + @Test("extract data with URI root not suffixed with forward slash", arguments: zip( + Input.prepareURIPaths, + Output.prepareURIPaths + )) + func dataWithURIRootNotSuffixedWithForwardSlash( + uri uriPath: String, + expects result: PrepareURIPathUseCase.PreparedURIPaths? + ) throws { + try assertData( + uriRoot: .uriRoot, + uriPath: uriPath, + expects: result + ) + } + + @Test("extract data with URI root suffixed with forward slash", arguments: zip( + Input.prepareURIPathsSlashed, + Output.prepareURIPaths + )) + func dataWithURIRootSuffixedWithForwardSlash( + uri uriPath: String, + expects result: PrepareURIPathUseCase.PreparedURIPaths? + ) throws { + try assertData( + uriRoot: .uriRootSlashed, + uriPath: uriPath, + expects: result + ) + } +#endif + +} + +// MARK: - Assertions + +private extension PrepareURIPathUseCaseTests { + + // MARK: Functions + + /// Asserts the data returned by the ``PrepareURIPathUseCase`` use case based on the given `uriRoot` and `uriPath` URI paths plus + /// an expected result. + /// - Parameters: + /// - uriRoot: A URI path to initialize the use case with. + /// - uriPath: A URI path to use with the use case. + /// - result: An expected result coming out of the use case. + func assertData( + uriRoot: String, + uriPath: String, + expects result: PrepareURIPathUseCase.PreparedURIPaths? + ) throws { + // GIVEN + let useCase = PrepareURIPathUseCase(uriRoot: uriRoot) + + // WHEN + let output = useCase(uriPath) + + // THEN + if !uriPath.contains(uriRoot) { + #expect(output == nil) + } else { + #expect(output != nil) + + let data = try #require(output) + + #expect(data.archiveName == result?.archiveName) + #expect(data.archivePath == result?.archivePath) + #expect(data.resourcePath == result?.resourcePath) + } + } + +} + +// MARK: - Constants + +private extension Input { + /// A list of URI paths to match against the root URI path not suffixed with a forward slash. + static let prepareURIPaths: [String] = [.uriOffset, .uriRoot, .uriOther] + /// A list of URI paths to match against the root URI path suffixed with a forward slash. + static let prepareURIPathsSlashed: [String] = [.uriOffsetSlashed, .uriRootSlashed, .uriOther] +} + +private extension Output { + /// A list of expected outputs for the URI path samples, regardless their match against suffixed or not suffixed root URI paths. + static let prepareURIPaths: [PrepareURIPathUseCase.PreparedURIPaths?] = [ + ("somearchive", "/SomeArchive.doccarchive", "/SomeArchive/some/content/path"), + (.empty, .empty, .forwardSlash), + nil + ] +} + +private extension String { + /// A root URI path to initialize the use case with. + static let uriRoot: Self = "/some/path" + /// A root URI path suffixed with a forward slash to initialize the use case with. + static let uriRootSlashed: Self = "/some/path/" + /// A URI path prefixed with a root URI path not suffixed with a forward slash. + static let uriOffset: Self = .uriRoot + "/SomeArchive/some/content/path" + /// A URI path prefixed with a root URI path suffixed with a forward slash. + static let uriOffsetSlashed: Self = .uriRootSlashed + "SomeArchive/some/content/path" + /// A URI path not related to any root URI path. + static let uriOther: Self = "/some/other/path" +} -- 2.52.0 From d87c828608379bb7147edf2daf3720f91874f589 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 23 Sep 2025 16:20:38 +0200 Subject: [PATCH 11/29] Implemented the DocCMiddleware.Configuration type in the library target. --- .../DoccMiddlewareConfiguration.swift | 52 +++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/Sources/DocCMiddleware/Public/Configurations/DoccMiddlewareConfiguration.swift b/Sources/DocCMiddleware/Public/Configurations/DoccMiddlewareConfiguration.swift index f62ad7f..0bc36c4 100644 --- a/Sources/DocCMiddleware/Public/Configurations/DoccMiddlewareConfiguration.swift +++ b/Sources/DocCMiddleware/Public/Configurations/DoccMiddlewareConfiguration.swift @@ -1,8 +1,52 @@ +// ===----------------------------------------------------------------------=== // -// File.swift -// hummingbird-docc-middleware +// This source file is part of the Hummingbird DocC Middleware open source project // -// Created by Javier Cicchelli on 23/09/2025. +// 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 Foundation +import class NIOPosix.NIOThreadPool + +extension DocCMiddleware { + /// A type that contains all the parameters to configure the ``DocCMiddleware`` middleware. + public struct Configuration: Sendable { + + // MARK: Properties + + /// A path to the physical location where the `DocC` documentation containers are stored. + let folderRoot: String + + /// A URI path that prefixes the `DocC` documentation resources. + let uriRoot: String + + /// A type that define a mechanism to use in case some blocking work needs to be performed for which no non-blocking API exists. + let threadPool: NIOThreadPool + + // MARK: Initializers + + /// Initializes this configuration type. + /// + /// > important: It is assumed that both the `uriRoot` and the `folderRoot` parameters should not be empty, and that they should be prefixed + /// with the `/` forward slash character. + /// + /// - Parameters: + /// - uriRoot: A URI path that prefixes the `DocC` documentation resources. + /// - folderRoot: A path to the physical location where the `DocC` documentation containers are stored. + /// - threadPool: A type that define a mechanism to use in case some blocking work needs to be performed for which no non-blocking API exists. + public init( + uriRoot: String, + folderRoot: String, + threadPool: NIOThreadPool = .singleton + ) { + self.folderRoot = folderRoot + self.uriRoot = uriRoot + self.threadPool = threadPool + } + + } +} -- 2.52.0 From 398b852ac88375a3a799b37ce86cb06b775f8e52 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 23 Sep 2025 16:21:25 +0200 Subject: [PATCH 12/29] Integrated the DocCMiddleware.Configuration type to the DocCMiddleware type in the library target. --- .../Public/Middlewares/DocCMiddleware.swift | 68 +++++++++----- .../Middlewares/DocCMiddlewareTests.swift | 93 ++++++++++--------- .../Types/Extensions/Tag+Constants.swift | 4 +- 3 files changed, 99 insertions(+), 66 deletions(-) diff --git a/Sources/DocCMiddleware/Public/Middlewares/DocCMiddleware.swift b/Sources/DocCMiddleware/Public/Middlewares/DocCMiddleware.swift index 9d6b03b..1abc144 100644 --- a/Sources/DocCMiddleware/Public/Middlewares/DocCMiddleware.swift +++ b/Sources/DocCMiddleware/Public/Middlewares/DocCMiddleware.swift @@ -12,7 +12,6 @@ import Hummingbird -import class NIOPosix.NIOThreadPool import struct Logging.Logger /// A middleware that proxies requests to `DocC` documentation containers within a hosting app. @@ -20,42 +19,67 @@ public struct DocCMiddleware { // MARK: Properties + /// A type that contains the parameters to configure the middleware. + let configuration: Configuration + /// A protocol that defines file system interactions. let fileProvider: any FileProvider /// 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() + // MARK: Initializers /// Initializes this middleware with the root path to the `DocC` documentation containers in the file system. /// - Parameters: - /// - pathToRoot: A path to the root folder in which the `DocC` documentation container are located. + /// - configuration: A type that contains the parameters to configure the middleware. /// - logger: A type that interacts with the logging system. - /// - threadPool: A representation of the thread pool that should be used in case some blocking work needs to be performed for which no non-blocking API exists. + /// - fileProvider: A type that conforms to the protocol that defines file system interactions, if any. init( - pathToRoot rootFolder: String, + configuration: Configuration, logger: Logger, - threadPool: NIOThreadPool = .singleton + fileProvider: (any FileProvider)? = nil ) { - self.fileProvider = LocalFileSystem( - rootFolder: rootFolder, - threadPool: threadPool, - logger: logger - ) - self.logger = logger - } - - /// Initializes this middleware with a type conforming to the `FileProvider` protocol. - /// - Parameters: - /// - fileProvider: A type that conforms to the protocol that defines file system interactions. - /// - logger: A type that interacts with the logging system. - init( - fileProvider: any FileProvider, - logger: Logger - ) { - self.fileProvider = fileProvider + self.configuration = configuration + self.fileProvider = if let fileProvider { + fileProvider + } else { + LocalFileSystem( + rootFolder: configuration.folderRoot, + threadPool: configuration.threadPool, + logger: logger + ) + } self.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 uri = checkURI(input.uri) else { + return try await next(input, context) + } + + return try await next(input, context) + } + +} diff --git a/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift b/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift index 77c253b..1f36e8b 100644 --- a/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift +++ b/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift @@ -14,35 +14,53 @@ import Testing import struct Hummingbird.LocalFileSystem import struct Logging.Logger + import protocol Hummingbird.FileProvider -@testable import DocCMiddleware +@testable import struct DocCMiddleware.DocCMiddleware -@Suite("DocC Middleware") +@Suite("DocC Middleware", .tags(.middleware)) struct DocCMiddlewareTests { // MARK: Initializers tests #if swift(>=6.2) - @Test(.tags(.initializer)) - func `initialize with path to root`() { - assertInit(pathToRoot: "/path/to/root/docc/documentation") + @Test + func `initialize with URI and folder paths`() { + assertInit(configuration: .init( + uriRoot: "/path/to/documentation", + folderRoot: "/location/docc/documentation" + )) } - @Test(.tags(.initializer)) - func `initialize with type that conforms to the FileProvider protocol`() { - // GIVEN - assertInit(fileProvider: FileProviderStub()) + @Test + func `initialize with URI path and type that conforms to the FileProvider protocol`() { + assertInit( + configuration: .init( + uriRoot: "/path/to/documentation", + folderRoot: .empty + ), + fileProvider: FileProviderStub() + ) } #else - @Test("initialize with path to root", .tags(.initializer)) - func initWithRootPath() { - assertInit(pathToRoot: "/path/to/root/docc/documentation") + @Test("initialize with URI and folder paths") + func initWithURIAndFolderPaths() { + assertInit(configuration: .init( + uriRoot: "/path/to/documentation", + folderRoot: "/location/docc/documentation" + )) } - @Test("initialize with type that conforms to the FileProvider protocol", .tags(.initializer)) - func initWithFileProviderType() { - assertInit(fileProvider: FileProviderStub()) + @Test("initialize with type that conforms to the FileProvider protocol") + func initWithURIPathAndFileProviderType() { + assertInit( + configuration: .init( + uriRoot: "/path/to/documentation", + folderRoot: .empty + ), + fileProvider: FileProviderStub() + ) } #endif @@ -54,46 +72,37 @@ private extension DocCMiddlewareTests { // MARK: Functions - /// Asserts the initialization of a `DocCMiddleware` type with a root path in the file system. + /// Asserts the initialization of a `DocCMiddleware` type. /// - Parameters: - /// - pathToRoot: A path to the root `DocC` documentation containers. + /// - configuration: A type that contains the parameters to configure the middleware. /// - logger: A type that interacts with the logging system. + /// - fileProvider: A type that conforms to the protocol that defines file system interactions, if any. func assertInit( - pathToRoot: String, - logger: Logger = .test + configuration: DocCMiddleware.Configuration, + logger: Logger = .test, + fileProvider: (any FileProvider)? = nil ) { // GIVEN // WHEN let middleware = DocCMiddleware( - pathToRoot: pathToRoot, - logger: logger + configuration: configuration, + logger: logger, + fileProvider: fileProvider ) // THEN - #expect(middleware.fileProvider is LocalFileSystem) + #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) - } - - /// Asserts the initialization of a `DocCMiddleware` type with a type that conforms to the `FileProvider` protocol. - /// - Parameters: - /// - fileProvider: A type that conforms to the protocol that defines file system interactions. - /// - logger: A type that interacts with the logging system. - func assertInit( - fileProvider: any FileProvider, - logger: Logger = .test - ) { - // GIVEN - // WHEN - let middleware = DocCMiddleware( - fileProvider: fileProvider, - logger: logger - ) - // THEN - #expect(type(of:middleware.fileProvider) == type(of: fileProvider)) - #expect(middleware.logger.label == logger.label) - #expect(middleware.logger.logLevel == logger.logLevel) + if let fileProvider { + #expect(type(of:middleware.fileProvider) == type(of: fileProvider)) + } else { + #expect(middleware.fileProvider is LocalFileSystem) + } } } diff --git a/Tests/DocCMiddleware/Types/Extensions/Tag+Constants.swift b/Tests/DocCMiddleware/Types/Extensions/Tag+Constants.swift index 34ee316..2c26954 100644 --- a/Tests/DocCMiddleware/Types/Extensions/Tag+Constants.swift +++ b/Tests/DocCMiddleware/Types/Extensions/Tag+Constants.swift @@ -18,8 +18,8 @@ extension Tag { /// Tag that indicate a test case for an enumeration type. @Tag static var enumeration: Self - /// Tag that indicate a test case for a type initialization. - @Tag static var initializer: Self + /// Tag that indicate a test case for a middleware type. + @Tag static var middleware: Self /// Tag that indicate a test case for a use case type. @Tag static var useCase: Self -- 2.52.0 From 4798b720528af75cbb70d89af5e9c55a37e8ae9c Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 24 Sep 2025 01:27:34 +0200 Subject: [PATCH 13/29] Implemented the "metadata()" helper function for the LoggerMetadata+Helpers extension in the library target. --- .../Extensions/LoggerMetadata+Helpers.swift | 50 +++++++ .../Use Cases/PrepareURIPathUseCase.swift | 2 +- .../LoggerMetadata+HelpersTests.swift | 135 ++++++++++++++++++ .../Types/Extensions/Logger+Constants.swift | 5 +- .../Types/Extensions/Request+Helpers.swift | 41 ++++++ .../Types/Extensions/Tag+Constants.swift | 2 + .../Types/Mocks/MockRequestContext.swift | 51 +++++++ .../Types/Namespaces/Input.swift | 2 +- 8 files changed, 285 insertions(+), 3 deletions(-) create mode 100644 Sources/DocCMiddleware/Internal/Extensions/LoggerMetadata+Helpers.swift create mode 100644 Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift create mode 100644 Tests/DocCMiddleware/Types/Extensions/Request+Helpers.swift create mode 100644 Tests/DocCMiddleware/Types/Mocks/MockRequestContext.swift diff --git a/Sources/DocCMiddleware/Internal/Extensions/LoggerMetadata+Helpers.swift b/Sources/DocCMiddleware/Internal/Extensions/LoggerMetadata+Helpers.swift new file mode 100644 index 0000000..bddde68 --- /dev/null +++ b/Sources/DocCMiddleware/Internal/Extensions/LoggerMetadata+Helpers.swift @@ -0,0 +1,50 @@ +// ===----------------------------------------------------------------------=== +// +// 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.RequestContext + +import struct Hummingbird.HTTPResponse +import struct Hummingbird.Request +import struct Logging.Logger + +extension Logger.Metadata { + + // MARK: Functions + + /// Generates a dictionary to use as metadata for events to log into the logging system. + /// - Parameters: + /// - context: A type that contains all the parameters associated with a given request, and that conforms to the `RequestContext` protocol. + /// - request: A type that contains all the parameters to process as a request. + /// - statusCode: A representation of a response status to provide as a response. + /// - redirect: A URI path to use in a redirection event, if any. + /// - Returns: A generated metadata dictionary for an event to log into the logging system. + static func metadata( + context: any RequestContext, + request: Request, + statusCode: HTTPResponse.Status, + redirect: String? = nil + ) -> Self { + var metadata: Logger.Metadata = [ + "hb.request.id": "\(context.id)", + "hb.request.method": "\(request.method.rawValue)", + "hb.request.path": "\(request.uri.path)", + "hb.request.status": "\(statusCode.code)" + ] + + if let redirect { + metadata["hb.request.redirect"] = "\(redirect)" + } + + return metadata + } + +} diff --git a/Sources/DocCMiddleware/Internal/Use Cases/PrepareURIPathUseCase.swift b/Sources/DocCMiddleware/Internal/Use Cases/PrepareURIPathUseCase.swift index 0588860..e0bce51 100644 --- a/Sources/DocCMiddleware/Internal/Use Cases/PrepareURIPathUseCase.swift +++ b/Sources/DocCMiddleware/Internal/Use Cases/PrepareURIPathUseCase.swift @@ -13,7 +13,7 @@ import Foundation import RegexBuilder -/// A use case that obtains some necessary data from a given URI path, that are essential for routing the documentation contents. +/// A use case that extracts data from a given URI path, essential for routing the documentation contents. struct PrepareURIPathUseCase { // MARK: Type aliases diff --git a/Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift b/Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift new file mode 100644 index 0000000..6349e31 --- /dev/null +++ b/Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift @@ -0,0 +1,135 @@ +// ===----------------------------------------------------------------------=== +// +// 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 Testing + +import struct Hummingbird.HTTPRequest +import struct Hummingbird.HTTPResponse +import struct Hummingbird.Request +import struct Logging.Logger + +@testable import DocCMiddleware + +@Suite("Logger Metadata Helpers", .tags(.extension)) +struct LoggerMetadata_HelpersTests { + + // MARK: Functions tests + +#if swift(>=6.2) + @Test + func `metadata with HTTP method and status code`() throws { + assertMetadata( + method: try randomMethod, + statusCode: try randomStatusCode + ) + } + + @Test + func `metadata with HTTP method, status code and redirection URI path`() throws { + assertMetadata( + method: try randomMethod, + statusCode: try randomStatusCode, + redirect: .uriRedirection + ) + } +#else + @Test("metadata with HTTP method and status code") + func metadataWithMethodAndStatusCode() throws { + assertMetadata( + method: try randomMethod, + statusCode: try randomStatusCode + ) + } + + @Test("metadata with HTTP method, status code and redirection URI path") + func metadataWithMethodStatusCodeAndRedirection() throws { + assertMetadata( + method: try randomMethod, + statusCode: try randomStatusCode, + redirect: .uriRedirection + ) + } +#endif + +} + +// MARK: - Assertions + +private extension LoggerMetadata_HelpersTests { + + // MARK: Functions + + /// Asserts the generated metadata dictionary based on provided parameters. + /// - Parameters: + /// - method: A HTTP method of the request. + /// - statusCode: A status code of the response. + /// - redirect: A redirection URI path, if any. + func assertMetadata( + method: HTTPRequest.Method, + statusCode: HTTPResponse.Status, + redirect: String? = nil + ) { + // GIVEN + let logger: Logger = .test + let context: MockRequestContext = .init(logger: logger) + let request: Request = .test(method: method) + + // WHEN + let metadata: Logger.Metadata = .metadata( + context: context, + request: request, + statusCode: statusCode, + redirect: redirect + ) + + // THEN + #expect(metadata.keys.count == (redirect == nil ? 4 : 5)) + #expect(metadata["hb.request.id"] == logger[metadataKey: "hb.request.id"]) + #expect(metadata["hb.request.method"] == "\(method.rawValue)") + #expect(metadata["hb.request.path"] == "/") + #expect(metadata["hb.request.status"] == "\(statusCode.code)") + + if let redirect { + #expect(metadata["hb.request.redirect"] == "\(redirect)") + } + } + +} + +// MARK: - Helpers + +private extension LoggerMetadata_HelpersTests { + + // MARK: Computed + + /// Extracts a random HTTP method of the request from a list of pre-defined values. + var randomMethod: HTTPRequest.Method { + get throws { + try #require([.connect, .delete, .get, .head, .options, .patch, .post, .put, .trace].randomElement()) + } + } + + /// Extracts a random status code of the response from a list of pre-defined values. + var randomStatusCode: HTTPResponse.Status { + get throws { + try #require([.`continue`, .earlyHints, .ok, .accepted, .multipleChoices, .seeOther, .badRequest, .notFound, .internalServerError, .serviceUnavailable].randomElement()) + } + } + +} + +// MARK: - Constants + +private extension String { + /// A URI path to use as a redirection sample. + static let uriRedirection = "/some/redirect/path" +} diff --git a/Tests/DocCMiddleware/Types/Extensions/Logger+Constants.swift b/Tests/DocCMiddleware/Types/Extensions/Logger+Constants.swift index a006a09..d3e2cbe 100644 --- a/Tests/DocCMiddleware/Types/Extensions/Logger+Constants.swift +++ b/Tests/DocCMiddleware/Types/Extensions/Logger+Constants.swift @@ -10,6 +10,7 @@ // // ===----------------------------------------------------------------------=== +import Foundation import Logging import Testing @@ -22,7 +23,9 @@ extension Logger { var logger = Logger(label: "test.hummingbird-docc-middleware.logger") logger.logLevel = try! #require(Logger.Level.allCases.randomElement()) - + + logger[metadataKey: "hb.request.id"] = "\(UUID().uuidString)" + return logger }() diff --git a/Tests/DocCMiddleware/Types/Extensions/Request+Helpers.swift b/Tests/DocCMiddleware/Types/Extensions/Request+Helpers.swift new file mode 100644 index 0000000..54ebd8a --- /dev/null +++ b/Tests/DocCMiddleware/Types/Extensions/Request+Helpers.swift @@ -0,0 +1,41 @@ +// ===----------------------------------------------------------------------=== +// +// 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 struct Hummingbird.HTTPRequest +import struct Hummingbird.Request +import struct Hummingbird.RequestBody + +extension Request { + + // MARK: Functions + + /// Generates a request that is ready to use in test case. + /// - Parameters: + /// - method: A HTTP method. + /// - path: A URI path, if any. + /// - Returns: A generated request instance to use in test cases. + static func test( + method: HTTPRequest.Method, + path: String? = nil + ) -> Self { + .init( + head: .init( + method: method, + scheme: nil, + authority: nil, + path: path + ), + body: .init(buffer: .init()) + ) + } + +} diff --git a/Tests/DocCMiddleware/Types/Extensions/Tag+Constants.swift b/Tests/DocCMiddleware/Types/Extensions/Tag+Constants.swift index 2c26954..31b3733 100644 --- a/Tests/DocCMiddleware/Types/Extensions/Tag+Constants.swift +++ b/Tests/DocCMiddleware/Types/Extensions/Tag+Constants.swift @@ -18,6 +18,8 @@ extension Tag { /// Tag that indicate a test case for an enumeration type. @Tag static var enumeration: Self + /// Tag that indicate a test case for an extended type. + @Tag static var `extension`: Self /// Tag that indicate a test case for a middleware type. @Tag static var middleware: Self /// Tag that indicate a test case for a use case type. diff --git a/Tests/DocCMiddleware/Types/Mocks/MockRequestContext.swift b/Tests/DocCMiddleware/Types/Mocks/MockRequestContext.swift new file mode 100644 index 0000000..5c285e3 --- /dev/null +++ b/Tests/DocCMiddleware/Types/Mocks/MockRequestContext.swift @@ -0,0 +1,51 @@ +// ===----------------------------------------------------------------------=== +// +// 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 class NIOEmbedded.NIOAsyncTestingChannel + +import protocol Hummingbird.RequestContext + +import struct Hummingbird.ApplicationRequestContextSource +import struct Hummingbird.CoreRequestContextStorage +import struct Logging.Logger + +/// A mock that conforms to the `RequestContext` protocol. +struct MockRequestContext { + + // MARK: Properties + + var coreContext: CoreRequestContextStorage + + // MARK: Initializers + + /// Initializes this mock. + /// - Parameter logger: A type that interacts with the logging system. + init(logger: Logger) { + self.coreContext = .init(source: ApplicationRequestContextSource( + channel: NIOAsyncTestingChannel(), + logger: logger + )) + } + +} + +// MARK: - RequestContext + +extension MockRequestContext: RequestContext { + + // MARK: Initializers + + init(source: ApplicationRequestContextSource) { + self.coreContext = .init(source: source) + } + +} diff --git a/Tests/DocCMiddleware/Types/Namespaces/Input.swift b/Tests/DocCMiddleware/Types/Namespaces/Input.swift index a71d255..cd82e1a 100644 --- a/Tests/DocCMiddleware/Types/Namespaces/Input.swift +++ b/Tests/DocCMiddleware/Types/Namespaces/Input.swift @@ -10,5 +10,5 @@ // // ===----------------------------------------------------------------------=== -/// A namespace assigned for test arguments that would be input into test cases. +/// A namespace assigned for test arguments enum Input {} -- 2.52.0 From 7760bf4802b9a807c58b065b94a18dfb37124441 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 24 Sep 2025 17:00:36 +0200 Subject: [PATCH 14/29] Replaced the "test" static constant for the Logger+Constants extension in the test target with a "test(level: handler: )" function for the Logger+Helpers extension. --- .../Internal/Protocols/Pathable.swift | 2 +- .../LoggerMetadata+HelpersTests.swift | 2 +- .../Middlewares/DocCMiddlewareTests.swift | 6 +- .../Types/Extensions/Logger+Constants.swift | 32 ----------- .../Types/Extensions/Logger+Helpers.swift | 57 +++++++++++++++++++ 5 files changed, 62 insertions(+), 37 deletions(-) delete mode 100644 Tests/DocCMiddleware/Types/Extensions/Logger+Constants.swift create mode 100644 Tests/DocCMiddleware/Types/Extensions/Logger+Helpers.swift diff --git a/Sources/DocCMiddleware/Internal/Protocols/Pathable.swift b/Sources/DocCMiddleware/Internal/Protocols/Pathable.swift index 2d0f7a4..b3e3425 100644 --- a/Sources/DocCMiddleware/Internal/Protocols/Pathable.swift +++ b/Sources/DocCMiddleware/Internal/Protocols/Pathable.swift @@ -10,7 +10,7 @@ // // ===----------------------------------------------------------------------=== -/// A type that provides a relative path representation. +/// A protocol that provides a relative path representation. protocol Pathable { // MARK: Properties diff --git a/Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift b/Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift index 6349e31..25fa0a7 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift +++ b/Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift @@ -79,7 +79,7 @@ private extension LoggerMetadata_HelpersTests { redirect: String? = nil ) { // GIVEN - let logger: Logger = .test + let logger: Logger = .test() let context: MockRequestContext = .init(logger: logger) let request: Request = .test(method: method) diff --git a/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift b/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift index 1f36e8b..6daa206 100644 --- a/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift +++ b/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift @@ -12,11 +12,11 @@ import Testing +import protocol Hummingbird.FileProvider + import struct Hummingbird.LocalFileSystem import struct Logging.Logger -import protocol Hummingbird.FileProvider - @testable import struct DocCMiddleware.DocCMiddleware @Suite("DocC Middleware", .tags(.middleware)) @@ -79,7 +79,7 @@ private extension DocCMiddlewareTests { /// - fileProvider: A type that conforms to the protocol that defines file system interactions, if any. func assertInit( configuration: DocCMiddleware.Configuration, - logger: Logger = .test, + logger: Logger = .test(), fileProvider: (any FileProvider)? = nil ) { // GIVEN diff --git a/Tests/DocCMiddleware/Types/Extensions/Logger+Constants.swift b/Tests/DocCMiddleware/Types/Extensions/Logger+Constants.swift deleted file mode 100644 index d3e2cbe..0000000 --- a/Tests/DocCMiddleware/Types/Extensions/Logger+Constants.swift +++ /dev/null @@ -1,32 +0,0 @@ -// ===----------------------------------------------------------------------=== -// -// 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 Foundation -import Logging -import Testing - -extension Logger { - - // MARK: Constants - - /// Creates a logger instance that is ready to use in test cases. - static let test: Self = { - var logger = Logger(label: "test.hummingbird-docc-middleware.logger") - - logger.logLevel = try! #require(Logger.Level.allCases.randomElement()) - - logger[metadataKey: "hb.request.id"] = "\(UUID().uuidString)" - - return logger - }() - -} diff --git a/Tests/DocCMiddleware/Types/Extensions/Logger+Helpers.swift b/Tests/DocCMiddleware/Types/Extensions/Logger+Helpers.swift new file mode 100644 index 0000000..d0cec00 --- /dev/null +++ b/Tests/DocCMiddleware/Types/Extensions/Logger+Helpers.swift @@ -0,0 +1,57 @@ +// ===----------------------------------------------------------------------=== +// +// 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 Foundation +import Testing + +import protocol Logging.LogHandler + +import struct Logging.Logger + +extension Logger { + + // MARK: Functions + + /// Generates a logger instance that is ready to use in test cases. + /// - Parameters: + /// - level: A logger level, if any. + /// - handler: A custom log handler, if any. + /// - Returns: A generated logger instance ready to use in test cases. + static func test( + level: Logger.Level? = nil, + handler: (any LogHandler)? = nil + ) -> Self { + var logger: Logger = if let handler { + .init(label: .loggerLabel) { _ in handler } + } else { + .init(label: .loggerLabel) + } + + logger.logLevel = if let level { + level + } else { + try! #require(Logger.Level.allCases.randomElement()) + } + + logger[metadataKey: "hb.request.id"] = "\(UUID().uuidString)" + + return logger + } + +} + +// MARK: - Constants + +private extension String { + /// A label to assign to a test logger instance. + static let loggerLabel = "test.hummingbird-docc-middleware.logger" +} -- 2.52.0 From 2b4512a9b147eb92bc082c93bcd12b7795858c5e Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 24 Sep 2025 17:03:53 +0200 Subject: [PATCH 15/29] Renamed the MockRequestContext mock in the tests target as RequestContextMock. --- .../Internal/Extensions/LoggerMetadata+HelpersTests.swift | 2 +- .../{MockRequestContext.swift => RequestContextMock.swift} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename Tests/DocCMiddleware/Types/Mocks/{MockRequestContext.swift => RequestContextMock.swift} (94%) diff --git a/Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift b/Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift index 25fa0a7..b290c2c 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift +++ b/Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift @@ -80,7 +80,7 @@ private extension LoggerMetadata_HelpersTests { ) { // GIVEN let logger: Logger = .test() - let context: MockRequestContext = .init(logger: logger) + let context: RequestContextMock = .init(logger: logger) let request: Request = .test(method: method) // WHEN diff --git a/Tests/DocCMiddleware/Types/Mocks/MockRequestContext.swift b/Tests/DocCMiddleware/Types/Mocks/RequestContextMock.swift similarity index 94% rename from Tests/DocCMiddleware/Types/Mocks/MockRequestContext.swift rename to Tests/DocCMiddleware/Types/Mocks/RequestContextMock.swift index 5c285e3..fe8b866 100644 --- a/Tests/DocCMiddleware/Types/Mocks/MockRequestContext.swift +++ b/Tests/DocCMiddleware/Types/Mocks/RequestContextMock.swift @@ -19,7 +19,7 @@ import struct Hummingbird.CoreRequestContextStorage import struct Logging.Logger /// A mock that conforms to the `RequestContext` protocol. -struct MockRequestContext { +struct RequestContextMock { // MARK: Properties @@ -40,7 +40,7 @@ struct MockRequestContext { // MARK: - RequestContext -extension MockRequestContext: RequestContext { +extension RequestContextMock: RequestContext { // MARK: Initializers -- 2.52.0 From 1b9973230d19f30ac5b1afc02a7ab07e14b10282 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 24 Sep 2025 17:28:49 +0200 Subject: [PATCH 16/29] Implemented the LogHandlerMock mock type in the tests target. --- .../Types/Mocks/LogHandlerMock.swift | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 Tests/DocCMiddleware/Types/Mocks/LogHandlerMock.swift diff --git a/Tests/DocCMiddleware/Types/Mocks/LogHandlerMock.swift b/Tests/DocCMiddleware/Types/Mocks/LogHandlerMock.swift new file mode 100644 index 0000000..b5c35ce --- /dev/null +++ b/Tests/DocCMiddleware/Types/Mocks/LogHandlerMock.swift @@ -0,0 +1,141 @@ +// ===----------------------------------------------------------------------=== +// +// 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 Foundation + +import protocol Logging.LogHandler + +import struct Logging.Logger + +/// A mock that conforms to the `LogHandler` protocol. +struct LogHandlerMock { + + // MARK: Properties + + /// A representation of the logging level assigned to this mock. + private var _logLevel: Logger.Level = .debug + + /// A dictionary that contains all the metadata assigned to this mock. + private var _metadata: Logger.Metadata = [:] + + /// A logging event recorder attached to this mock. + private let recorder: LogRecorder = .init() + + // MARK: Computed + + /// A list of all the logged events that are being persisted in the recorder. + var entries: [LogEntry] { + get async { await recorder.entries } + } + +} + +// MARK: - LogEntry + +/// A type that contains the information logged in a logging event. +struct LogEntry: Equatable { + + // MARK: Properties + + /// A representation of the level attached to a logged event. + let level: Logger.Level + + /// A metadata dictionary that contains additional information attached to a logged event. + let metadata: Logger.Metadata? + + /// A message attached to a logged event. + let message: Logger.Message + + /// A source from where a logged event was triggered. + let source: String + +} + +// MARK: - LogRecorder + +extension LogHandlerMock { + /// An actor that persists all the events logged by the ``LogHandlerMock`` mock handler. + actor LogRecorder { + + // MARK: Properties + + /// A list of all the logged events. + private(set) var entries: [LogEntry] = [] + + // MARK: Functions + + /// Records data related to a logged event. + /// - Parameters: + /// - level: A representation of the level attached to a logged event. + /// - metadata: A metadata dictionary that contains additional information attached to a logged event. + /// - message: A message attached to a logged event. + /// - source: A source from where a logged event was triggered. + func record( + level: Logger.Level, + metadata: Logger.Metadata?, + message: Logger.Message, + source: String + ) async { + entries += [.init( + level: level, + metadata: metadata, + message: message, + source: source + )] + } + + } +} + +// MARK: - LogHandler + +extension LogHandlerMock: LogHandler { + + // MARK: Properties + + var metadata: Logger.Metadata { + get { _metadata } + set(newValue) { _metadata = newValue } + } + + var logLevel: Logger.Level { + get { _logLevel } + set(newValue) { _logLevel = newValue } + } + + // MARK: Subscripts + + subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? { + get { _metadata[metadataKey] } + set(newValue) { _metadata[metadataKey] = newValue } + } + + // MARK: Functions + + func log( + level: Logger.Level, + message: Logger.Message, + metadata: Logger.Metadata?, + source: String, + file: String, + function: String, + line: UInt + ) { + Task { await recorder.record( + level: level, + metadata: metadata, + message: message, + source: source + )} + } + +} -- 2.52.0 From c2c603a810eacd9232d9b40604582b2183c836c1 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 24 Sep 2025 17:29:18 +0200 Subject: [PATCH 17/29] Implemented the RedirectURIUseCase use case in the library target. --- .../Use Cases/RedirectURIUseCase.swift | 78 +++++++++ .../LoggerMetadata+HelpersTests.swift | 7 - .../Use Cases/RedirectURIUseCaseTests.swift | 164 ++++++++++++++++++ .../Types/Extensions/String+Constants.swift | 20 +++ 4 files changed, 262 insertions(+), 7 deletions(-) create mode 100644 Sources/DocCMiddleware/Internal/Use Cases/RedirectURIUseCase.swift create mode 100644 Tests/DocCMiddleware/Tests/Internal/Use Cases/RedirectURIUseCaseTests.swift create mode 100644 Tests/DocCMiddleware/Types/Extensions/String+Constants.swift diff --git a/Sources/DocCMiddleware/Internal/Use Cases/RedirectURIUseCase.swift b/Sources/DocCMiddleware/Internal/Use Cases/RedirectURIUseCase.swift new file mode 100644 index 0000000..7e49a3a --- /dev/null +++ b/Sources/DocCMiddleware/Internal/Use Cases/RedirectURIUseCase.swift @@ -0,0 +1,78 @@ +// ===----------------------------------------------------------------------=== +// +// 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.RequestContext + +import struct Hummingbird.Request +import struct Hummingbird.Response +import struct Logging.Logger + +/// A use case that produces a redirect response based on a given URI path. +struct RedirectURIUseCase { + + // MARK: Type aliases + + /// A pseudo-type that contains data about a request and its related context. + typealias ContextualInfo = (request: Request, context: any RequestContext) + + // MARK: Properties + + /// A type that interacts with the logging system. + private let logger: Logger + + // MARK: Initializers + + /// Initializes this use case. + /// - Parameter logger: A type that interacts with the logging system. + init(logger: Logger) { + self.logger = logger + } + + // MARK: Functions + + /// Produces a redirect response based on a given URI path + /// - Parameters: + /// - uriPath: A URI path to use in the redirection. + /// - contextualInfo: A pseudo-type that contains data about a request and its related context. + /// - Returns: A redirection response created out of a given URI path plus contextual information. + func callAsFunction( + _ uriPath: String, + with contextualInfo: ContextualInfo + ) -> Response { + defer { + logger.log( + level: .debug, + "The URI path is redirected to this path: \(uriPath)", + metadata: .metadata( + context: contextualInfo.context, + request: contextualInfo.request, + statusCode: .movedPermanently, + redirect: uriPath + ), + source: .source + ) + } + + return .redirect( + to: uriPath, + type: .permanent + ) + } + +} + +// MARK: - String+Constants + +private extension String { + /// A name of the middleware that triggered a logging event. + static let source = "DocCMiddleware" +} diff --git a/Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift b/Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift index b290c2c..dd0c9d0 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift +++ b/Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift @@ -126,10 +126,3 @@ private extension LoggerMetadata_HelpersTests { } } - -// MARK: - Constants - -private extension String { - /// A URI path to use as a redirection sample. - static let uriRedirection = "/some/redirect/path" -} diff --git a/Tests/DocCMiddleware/Tests/Internal/Use Cases/RedirectURIUseCaseTests.swift b/Tests/DocCMiddleware/Tests/Internal/Use Cases/RedirectURIUseCaseTests.swift new file mode 100644 index 0000000..0f5841c --- /dev/null +++ b/Tests/DocCMiddleware/Tests/Internal/Use Cases/RedirectURIUseCaseTests.swift @@ -0,0 +1,164 @@ +// ===----------------------------------------------------------------------=== +// +// 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 Hummingbird +import Testing + +import struct Logging.Logger + +@testable import struct DocCMiddleware.RedirectURIUseCase + +@Suite("Redirect URI Use Case", .tags(.useCase)) +struct RedirectURIUseCaseTests { + + // MARK: Use case tests + +#if swift(>=6.2) + @Test + func `response when logger expects an event`() async throws { + try await assertResponse( + logLevel: try randomLogLevelWithEvent, + uriRedirection: .uriRedirection, + expects: .movedPermanently + ) + } + + @Test + func `response when logger does not expects an event`() async throws { + try await assertResponse( + logLevel: try randomLogLevelWithNoEvent, + uriRedirection: .uriRedirection, + expects: .movedPermanently + ) + } +#else + @Test("response when logger expects an event") + func responseWhenLoggerExpectsEvent() async throws { + try await assertResponse( + logLevel: try randomLogLevelWithEvent, + uriRedirection: .uriRedirection, + expects: .movedPermanently + ) + } + + @Test("response when logger does not expects an event") + func responseWhenLogLevel() async throws { + try await assertResponse( + logLevel: try randomLogLevelWithNoEvent, + uriRedirection: .uriRedirection, + expects: .movedPermanently + ) + } +#endif + +} + +// MARK: - Assertions + +private extension RedirectURIUseCaseTests { + + // MARK: Functions + + /// Asserts the response returned by the ``RedirectURIUseCase`` use case based on the given `logLevel` logging level and the `uriRedirection` + /// URI path plus the expected status code of the response. + /// - Parameters: + /// - logLevel: A representation of the logging level to set in the `Logger` instance. + /// - uriRedirection: A URI path to use in the redirection. + /// - statusCode: An expected status code from the response coming out of the use case. + /// - Throws: An error in case an issue is encountered while asserting the use case. + func assertResponse( + logLevel: Logger.Level, + uriRedirection: String, + expects statusCode: HTTPResponse.Status + ) async throws { + let logHandler = LogHandlerMock() + let logger = Logger.test( + level: logLevel, + handler: logHandler + ) + + let context: any RequestContext = RequestContextMock(logger: logger) + let request: Request = .test(method: .get) + + let useCase = RedirectURIUseCase(logger: logger) + + // WHEN + let result = useCase( + uriRedirection, + with: (request, context) + ) + + // THEN + #expect(result.status == .movedPermanently) + #expect(result.body.contentLength == 0) + #expect(result.headers == [ + .location: uriRedirection, + .contentLength: "0" + ]) + + let events = await logHandler.entries + + if shouldEventBeLogged(logLevel) { + #expect(!events.isEmpty) + #expect(events.count == 1) + + let loggedEvent = try #require(events.first) + + #expect(loggedEvent == .init( + level: .debug, + metadata: [ + "hb.request.id": "\(context.id)", + "hb.request.method": "\(request.method.rawValue)", + "hb.request.path": "\(request.uri.path)", + "hb.request.status": "\(statusCode.code)", + "hb.request.redirect": "\(uriRedirection)" + ], + message: "The URI path is redirected to this path: \(uriRedirection)", + source: "DocCMiddleware" + )) + } else { + #expect(events.isEmpty) + } + } + +} + +// MARK: - Helpers + +private extension RedirectURIUseCaseTests { + + // MARK: Computed + + /// Extracts a random logging level that support event logging for the use case. + var randomLogLevelWithEvent: Logger.Level { + get throws { + try #require([.debug, .trace].randomElement()) + } + } + + /// Extracts a random logging level that does not support event logging for the use case. + var randomLogLevelWithNoEvent: Logger.Level { + get throws { + try #require([.critical, .error, .info, .notice, .warning].randomElement()) + } + } + + // MARK: Functions + + /// Checks whether a logging event should be logged or not, based on a given logging level. + /// - Parameter level: A representation of a logging level defined in the `Logger` instance. + /// - Returns: A boolean value that indicates whether a logging event should have been logged or not. + func shouldEventBeLogged(_ logLevel: Logger.Level) -> Bool { + [Logger.Level.trace, .debug].contains(logLevel) + } + +} diff --git a/Tests/DocCMiddleware/Types/Extensions/String+Constants.swift b/Tests/DocCMiddleware/Types/Extensions/String+Constants.swift new file mode 100644 index 0000000..72b41b6 --- /dev/null +++ b/Tests/DocCMiddleware/Types/Extensions/String+Constants.swift @@ -0,0 +1,20 @@ +// ===----------------------------------------------------------------------=== +// +// 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 +// +// ===----------------------------------------------------------------------=== + +extension String { + + // MARK: Constants + + /// A URI path to use as a redirection sample. + static let uriRedirection = "/some/redirect/path" + +} -- 2.52.0 From a274547977fca63f234fff25a8eb4c39f543bd4e Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 24 Sep 2025 18:12:44 +0200 Subject: [PATCH 18/29] Defined the ContextualInfo pseudo type in the library target. --- .../Internal/Pseudo Types/ContextualInfo.swift | 18 ++++++++++++++++++ .../Use Cases/RedirectURIUseCase.swift | 8 -------- 2 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 Sources/DocCMiddleware/Internal/Pseudo Types/ContextualInfo.swift diff --git a/Sources/DocCMiddleware/Internal/Pseudo Types/ContextualInfo.swift b/Sources/DocCMiddleware/Internal/Pseudo Types/ContextualInfo.swift new file mode 100644 index 0000000..fbfb148 --- /dev/null +++ b/Sources/DocCMiddleware/Internal/Pseudo Types/ContextualInfo.swift @@ -0,0 +1,18 @@ +// ===----------------------------------------------------------------------=== +// +// 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.RequestContext + +import struct Hummingbird.Request + +/// A pseudo-type that contains data about a request and its related context. +typealias ContextualInfo = (request: Request, context: any RequestContext) diff --git a/Sources/DocCMiddleware/Internal/Use Cases/RedirectURIUseCase.swift b/Sources/DocCMiddleware/Internal/Use Cases/RedirectURIUseCase.swift index 7e49a3a..ef6efa9 100644 --- a/Sources/DocCMiddleware/Internal/Use Cases/RedirectURIUseCase.swift +++ b/Sources/DocCMiddleware/Internal/Use Cases/RedirectURIUseCase.swift @@ -10,19 +10,11 @@ // // ===----------------------------------------------------------------------=== -import protocol Hummingbird.RequestContext - -import struct Hummingbird.Request import struct Hummingbird.Response import struct Logging.Logger /// A use case that produces a redirect response based on a given URI path. struct RedirectURIUseCase { - - // MARK: Type aliases - - /// A pseudo-type that contains data about a request and its related context. - typealias ContextualInfo = (request: Request, context: any RequestContext) // MARK: Properties -- 2.52.0 From 9320227c64b3b97a29714ea8cbbb6a21537b0a26 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 24 Sep 2025 18:19:50 +0200 Subject: [PATCH 19/29] Added namespaces to some of the constants for the String+Constants extension in the library target. --- .../Internal/Extensions/String+Constants.swift | 18 ++++++++++++++---- .../Internal/Use Cases/CheckURIUseCase.swift | 4 ++-- .../Use Cases/PrepareURIPathUseCase.swift | 6 +++--- .../Use Cases/RedirectURIUseCase.swift | 9 +-------- .../Use Cases/PrepareURIPathUseCaseTests.swift | 2 +- 5 files changed, 21 insertions(+), 18 deletions(-) diff --git a/Sources/DocCMiddleware/Internal/Extensions/String+Constants.swift b/Sources/DocCMiddleware/Internal/Extensions/String+Constants.swift index 267dab8..d807c5c 100644 --- a/Sources/DocCMiddleware/Internal/Extensions/String+Constants.swift +++ b/Sources/DocCMiddleware/Internal/Extensions/String+Constants.swift @@ -13,8 +13,18 @@ extension String { /// An empty string. static let empty = "" - /// A forwarding slash. - static let forwardSlash = "/" - /// An indication of a previous folder in a path component. - static let previousFolder = ".." + + /// A namespace that defines logging representations. + enum Logging { + /// A name of the middleware that triggered a logging event. + static let source = "DocCMiddleware" + } + + /// A namespace that defines relative path representations. + enum Path { + /// A forwarding slash. + static let forwardSlash = "/" + /// An indication of a previous folder in a path component. + static let previousFolder = ".." + } } diff --git a/Sources/DocCMiddleware/Internal/Use Cases/CheckURIUseCase.swift b/Sources/DocCMiddleware/Internal/Use Cases/CheckURIUseCase.swift index ad6b9fb..81ddf5e 100644 --- a/Sources/DocCMiddleware/Internal/Use Cases/CheckURIUseCase.swift +++ b/Sources/DocCMiddleware/Internal/Use Cases/CheckURIUseCase.swift @@ -23,8 +23,8 @@ struct CheckURIUseCase { func callAsFunction(_ uri: URI) -> String? { guard let uriPath = uri.path.removingPercentEncoding, - !uriPath.contains(.previousFolder), - uriPath.hasPrefix(.forwardSlash) + !uriPath.contains(.Path.previousFolder), + uriPath.hasPrefix(.Path.forwardSlash) else { return nil } diff --git a/Sources/DocCMiddleware/Internal/Use Cases/PrepareURIPathUseCase.swift b/Sources/DocCMiddleware/Internal/Use Cases/PrepareURIPathUseCase.swift index e0bce51..493df4a 100644 --- a/Sources/DocCMiddleware/Internal/Use Cases/PrepareURIPathUseCase.swift +++ b/Sources/DocCMiddleware/Internal/Use Cases/PrepareURIPathUseCase.swift @@ -56,7 +56,7 @@ struct PrepareURIPathUseCase { } let documentationName = uriRest - .split(separator: .forwardSlash) + .split(separator: .Path.forwardSlash) .map(String.init) .first @@ -110,9 +110,9 @@ private extension PrepareURIPathUseCase { return nil } guard let uriRest = matches.output.1 else { - return .forwardSlash + return .Path.forwardSlash } - guard uriRest.hasPrefix(String.forwardSlash) else { + guard uriRest.hasPrefix(.Path.forwardSlash) else { return .init(format: .Format.Path.root, uriRest) } return uriRest diff --git a/Sources/DocCMiddleware/Internal/Use Cases/RedirectURIUseCase.swift b/Sources/DocCMiddleware/Internal/Use Cases/RedirectURIUseCase.swift index ef6efa9..8b226a1 100644 --- a/Sources/DocCMiddleware/Internal/Use Cases/RedirectURIUseCase.swift +++ b/Sources/DocCMiddleware/Internal/Use Cases/RedirectURIUseCase.swift @@ -50,7 +50,7 @@ struct RedirectURIUseCase { statusCode: .movedPermanently, redirect: uriPath ), - source: .source + source: .Logging.source ) } @@ -61,10 +61,3 @@ struct RedirectURIUseCase { } } - -// MARK: - String+Constants - -private extension String { - /// A name of the middleware that triggered a logging event. - static let source = "DocCMiddleware" -} diff --git a/Tests/DocCMiddleware/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift b/Tests/DocCMiddleware/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift index 8dfde44..465af8b 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift +++ b/Tests/DocCMiddleware/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift @@ -135,7 +135,7 @@ private extension Output { /// A list of expected outputs for the URI path samples, regardless their match against suffixed or not suffixed root URI paths. static let prepareURIPaths: [PrepareURIPathUseCase.PreparedURIPaths?] = [ ("somearchive", "/SomeArchive.doccarchive", "/SomeArchive/some/content/path"), - (.empty, .empty, .forwardSlash), + (.empty, .empty, .Path.forwardSlash), nil ] } -- 2.52.0 From 04d1ca6a2668ed8e79749cffff3e75041fa4cbdf Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 24 Sep 2025 23:59:43 +0200 Subject: [PATCH 20/29] Implemented the FileProviderMock mock type in the tests target. --- .../Types/Mocks/FileProviderMock.swift | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 Tests/DocCMiddleware/Types/Mocks/FileProviderMock.swift diff --git a/Tests/DocCMiddleware/Types/Mocks/FileProviderMock.swift b/Tests/DocCMiddleware/Types/Mocks/FileProviderMock.swift new file mode 100644 index 0000000..645b34d --- /dev/null +++ b/Tests/DocCMiddleware/Types/Mocks/FileProviderMock.swift @@ -0,0 +1,102 @@ +// ===----------------------------------------------------------------------=== +// +// 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 struct Foundation.Data +import struct Foundation.UUID +import struct Hummingbird.ResponseBody + +/// A mock that conforms to the `FileProvider` protocol. +struct FileProviderMock { + + // MARK: Properties + + /// A type that identifies a sample file. + private let fileIdentifier: UUID? + + /// A flag that indicates whether a file should be loaded or not. + private let shouldLoadFile: Bool + + // MARK: Initializers + + /// Initializes this mock. + /// - Parameters: + /// - fileIdentifier: A type that identifies a sample file, if any. + /// - shouldLoadFile: A flag that indicates whether a file should be loaded or not. + init( + fileIdentifier: UUID? = nil, + shouldLoadFile: Bool = true + ) { + self.fileIdentifier = fileIdentifier + self.shouldLoadFile = shouldLoadFile + } + +} + +// MARK: - FileProvider + +extension FileProviderMock: FileProvider { + + // MARK: Type aliases + + typealias FileAttributes = String + typealias FileIdentifier = String + + // MARK: Functions + + func getFileIdentifier(_ path: String) -> String? { + fileIdentifier?.uuidString + } + + func getAttributes(id: String) async throws -> String? { + nil + } + + func loadFile( + id: String, + context: some RequestContext + ) async throws -> ResponseBody { + guard shouldLoadFile else { + throw FileProviderMockError.fileNotLoaded + } + + guard let content = fileIdentifier?.uuidString else { + return .init() + } + + return .init(byteBuffer: .init( + data: .init(content.utf8) + )) + } + + func loadFile( + id: String, + range: ClosedRange, + context: some RequestContext + ) async throws -> ResponseBody { + try await loadFile( + id: id, + context: context + ) + } + +} + +// MARK: - FileProviderMockError + +/// An error type that can only be thrown by the ``FileProviderMock`` mock. +enum FileProviderMockError: Error { + /// An error encountered while mocking the loading of a file. + case fileNotLoaded +} -- 2.52.0 From ff56875a2482ee60ab578c664de6d63ccaf4ddb0 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Thu, 25 Sep 2025 01:38:48 +0200 Subject: [PATCH 21/29] Implemented the ServeResourceUseCase use case in the library target. --- .../Extensions/String+Constants.swift | 4 +- .../Use Cases/ServeResourceUseCase.swift | 100 +++++++ .../LoggerMetadata+HelpersTests.swift | 2 +- .../Use Cases/RedirectURIUseCaseTests.swift | 6 +- .../Use Cases/ServeResourceUseCaseTests.swift | 268 ++++++++++++++++++ .../Types/Extensions/String+Constants.swift | 14 +- 6 files changed, 385 insertions(+), 9 deletions(-) create mode 100644 Sources/DocCMiddleware/Internal/Use Cases/ServeResourceUseCase.swift create mode 100644 Tests/DocCMiddleware/Tests/Internal/Use Cases/ServeResourceUseCaseTests.swift diff --git a/Sources/DocCMiddleware/Internal/Extensions/String+Constants.swift b/Sources/DocCMiddleware/Internal/Extensions/String+Constants.swift index d807c5c..205cfc1 100644 --- a/Sources/DocCMiddleware/Internal/Extensions/String+Constants.swift +++ b/Sources/DocCMiddleware/Internal/Extensions/String+Constants.swift @@ -14,13 +14,13 @@ extension String { /// An empty string. static let empty = "" - /// A namespace that defines logging representations. + /// A namespace that defines logging values. enum Logging { /// A name of the middleware that triggered a logging event. static let source = "DocCMiddleware" } - /// A namespace that defines relative path representations. + /// A namespace that defines relative path values. enum Path { /// A forwarding slash. static let forwardSlash = "/" diff --git a/Sources/DocCMiddleware/Internal/Use Cases/ServeResourceUseCase.swift b/Sources/DocCMiddleware/Internal/Use Cases/ServeResourceUseCase.swift new file mode 100644 index 0000000..3c77779 --- /dev/null +++ b/Sources/DocCMiddleware/Internal/Use Cases/ServeResourceUseCase.swift @@ -0,0 +1,100 @@ +// ===----------------------------------------------------------------------=== +// +// 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 struct Hummingbird.Response +import struct Logging.Logger + +/// A use case that serves a resource, defined by its URI path, from a physical location. +struct ServeResourceUseCase { + + // MARK: Properties + + /// A type that conforms to a protocol that defines file system interactions. + let fileProvider: FileSystemProvider + + /// A type that interacts with the logging system. + private let logger: Logger + + // MARK: Initializers + + /// Initializes this use case. + /// - Parameters: + /// - fileProvider: A type that conforms to a protocol that defines file system interactions. + /// - logger: A type that interacts with the logging system. + init( + fileProvider: FileSystemProvider, + logger: Logger + ) { + self.fileProvider = fileProvider + self.logger = logger + } + + // MARK: Functions + + /// Serves a certain resource based on a given URI path from a physical location. + /// - Parameters: + /// - uriPath: A URI path that represents a resource to be served. + /// - folderPath: A URI path to a physical folder that contains the resource. + /// - contextualInfo: A pseudo-type that contains data about a request and its related context. + /// - Returns: A response that either contains the data of the resource in its body in case the resource is found, or a not found otherwise. + /// - Throws: An error in case an issue is encountered while serving the resource. + func callAsFunction( + _ uriPath: String, + at folderPath: String, + with contextualInfo: ContextualInfo + ) async throws -> Response { + let filePath = folderPath + uriPath + + guard let fileIdentifier = fileProvider.getFileIdentifier(filePath) else { + defer { + logger.log( + level: .error, + "The resource \(filePath) has not been found.", + metadata: .metadata( + context: contextualInfo.context, + request: contextualInfo.request, + statusCode: .notFound + ), + source: .Logging.source + ) + } + + return .init(status: .notFound) + } + + let body = try await fileProvider.loadFile( + id: fileIdentifier, + context: contextualInfo.context + ) + + defer { + logger.log( + level: .debug, + "The body of the resource \(filePath) has \(body.contentLength ?? 0) bytes.", + metadata: .metadata( + context: contextualInfo.context, + request: contextualInfo.request, + statusCode: .ok + ), + source: .Logging.source + ) + } + + return .init( + status: .ok, + body: body + ) + } + +} diff --git a/Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift b/Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift index dd0c9d0..4d25ed4 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift +++ b/Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift @@ -38,7 +38,7 @@ struct LoggerMetadata_HelpersTests { assertMetadata( method: try randomMethod, statusCode: try randomStatusCode, - redirect: .uriRedirection + redirect: .Sample.uriRedirection ) } #else diff --git a/Tests/DocCMiddleware/Tests/Internal/Use Cases/RedirectURIUseCaseTests.swift b/Tests/DocCMiddleware/Tests/Internal/Use Cases/RedirectURIUseCaseTests.swift index 0f5841c..3057e6f 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Use Cases/RedirectURIUseCaseTests.swift +++ b/Tests/DocCMiddleware/Tests/Internal/Use Cases/RedirectURIUseCaseTests.swift @@ -27,7 +27,7 @@ struct RedirectURIUseCaseTests { func `response when logger expects an event`() async throws { try await assertResponse( logLevel: try randomLogLevelWithEvent, - uriRedirection: .uriRedirection, + uriRedirection: .Sample.uriRedirection, expects: .movedPermanently ) } @@ -36,7 +36,7 @@ struct RedirectURIUseCaseTests { func `response when logger does not expects an event`() async throws { try await assertResponse( logLevel: try randomLogLevelWithNoEvent, - uriRedirection: .uriRedirection, + uriRedirection: .Sample.uriRedirection, expects: .movedPermanently ) } @@ -123,7 +123,7 @@ private extension RedirectURIUseCaseTests { "hb.request.redirect": "\(uriRedirection)" ], message: "The URI path is redirected to this path: \(uriRedirection)", - source: "DocCMiddleware" + source: .Logging.source )) } else { #expect(events.isEmpty) diff --git a/Tests/DocCMiddleware/Tests/Internal/Use Cases/ServeResourceUseCaseTests.swift b/Tests/DocCMiddleware/Tests/Internal/Use Cases/ServeResourceUseCaseTests.swift new file mode 100644 index 0000000..239db97 --- /dev/null +++ b/Tests/DocCMiddleware/Tests/Internal/Use Cases/ServeResourceUseCaseTests.swift @@ -0,0 +1,268 @@ +// ===----------------------------------------------------------------------=== +// +// 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 Testing + +import protocol Hummingbird.RequestContext + +import struct Hummingbird.HTTPResponse +import struct Hummingbird.Request +import struct Logging.Logger + +@testable import struct DocCMiddleware.ServeResourceUseCase + +@Suite("Serve Resource Use Case", .tags(.useCase)) +struct ServeResourceUseCaseTests { + + // MARK: Use case tests + +#if swift(>=6.2) + @Test + func `response when resource served and logging event triggered`() async throws { + try await assertResponse( + logLevel: .debug, + uriPath: .Sample.uriResource, + folderPath: .Sample.uriFolder, + expects: .ok + ) + } + + @Test + func `response when resource served and logging event not triggered`() async throws { + try await assertResponse( + logLevel: .info, + uriPath: .Sample.uriResource, + folderPath: .Sample.uriFolder, + expects: .ok + ) + } + + @Test + func `response when resource not found and logging event triggered`() async throws { + try await assertResponse( + logLevel: .error, + uriPath: .Sample.uriResource, + folderPath: .Sample.uriFolder, + expects: .notFound + ) + } + + @Test + func `response when resource not found and logging event not triggered`() async throws { + try await assertResponse( + logLevel: .critical, + uriPath: .Sample.uriResource, + folderPath: .Sample.uriFolder, + expects: .notFound + ) + } + + @Test + func `response throws error when loading resource`() async throws { + try await assertResponse( + logLevel: .warning, + uriPath: .Sample.uriResource, + folderPath: .Sample.uriFolder + ) + } +#else + @Test("response when resource served and logging event triggered") + func response_whenResourceServed_andEventTriggered() async throws { + try await assertResponse( + logLevel: .debug, + uriPath: .Sample.uriResource, + folderPath: .Sample.uriFolder, + expects: .ok + ) + } + + @Test("response when resource served and logging event not triggered") + func response_whenResourceServed_andEventNotTriggered() async throws { + try await assertResponse( + logLevel: .info, + uriPath: .Sample.uriResource, + folderPath: .Sample.uriFolder, + expects: .ok + ) + } + + @Test("response when resource not found and logging event triggered") + func resource_whenResourceNotFound_andEventTriggered() async throws { + try await assertResponse( + logLevel: .error, + uriPath: .Sample.uriResource, + folderPath: .Sample.uriFolder, + expects: .notFound + ) + } + + @Test("response when resource not found and logging event not triggered") + func resource_whenResourceNotFound_andEventNotTriggered() async throws { + try await assertResponse( + logLevel: .critical, + uriPath: .Sample.uriResource, + folderPath: .Sample.uriFolder, + expects: .notFound + ) + } + + @Test("response throws error when loading resource") + func resource_throwsError_whenLoadingResource() async throws { + try await assertResponse( + logLevel: .warning, + uriPath: .Sample.uriResource, + folderPath: .Sample.uriFolder + ) + } +#endif + +} + +// MARK: - Assertions + +private extension ServeResourceUseCaseTests { + + // MARK: Functions + + /// Asserts a response returned by the ``ServeResourceUseCase`` use case. + /// + /// > important: In case no `statusCode` value is given, the function then assumes that the loading of a file will throw an error. + /// + /// - Parameters: + /// - logLevel: A representation of the logging level to set in the `Logger` instance. + /// - uriPath: A URI path to a resource. + /// - folderPath: A URI path to a folder that contains the resource. + /// - statusCode: An expected status code from the response coming out of the use case, if any. + /// - Throws: An error in case an issue is encountered while asserting the use case. + func assertResponse( + logLevel: Logger.Level, + uriPath: String, + folderPath: String, + expects statusCode: HTTPResponse.Status? = nil + ) async throws { + // GIVEN + let logHandler = LogHandlerMock() + let logger = Logger.test( + level: logLevel, + handler: logHandler + ) + + let fileProvider: FileProviderMock = switch statusCode { + case .ok: .init(fileIdentifier: .init()) + case .notFound: .init() + default: .init(fileIdentifier: .init(), shouldLoadFile: false) + } + + let context: any RequestContext = RequestContextMock(logger: logger) + let request: Request = .test(method: .get) + + let useCase = ServeResourceUseCase( + fileProvider: fileProvider, + logger: logger + ) + + // WHEN + // THEN + if let statusCode { + let result = try await useCase( + uriPath, + at: folderPath, + with: (request, context) + ) + + #expect(result.headers[.contentLength] == (statusCode == .ok ? "36" : "0")) + #expect(result.status == statusCode) + + let contentLength = try #require(result.body.contentLength) + + if statusCode == .ok { + #expect(contentLength > 0) + } else { + #expect(contentLength == 0) + } + + let events = await logHandler.entries + + if shouldEventBeLogged( + logLevel: logLevel, + statusCode: statusCode + ) { + #expect(!events.isEmpty) + #expect(events.count == 1) + + let loggedEvent = try #require(events.first) + let filePath: String = .Sample.uriFile + + #expect(loggedEvent == .init( + level: statusCode == .ok ? .debug : .error, + metadata: [ + "hb.request.id": "\(context.id)", + "hb.request.method": "\(request.method.rawValue)", + "hb.request.path": "\(request.uri.path)", + "hb.request.status": "\(statusCode.code)" + ], + message: { + if statusCode == .ok { + "The body of the resource \(filePath) has \(contentLength) bytes." + } else { + "The resource \(filePath) has not been found." + } + }(), + source: .Logging.source + )) + } else { + #expect(events.isEmpty) + } + } else { + do { + _ = try await useCase( + uriPath, + at: folderPath, + with: (request, context) + ) + } catch is FileProviderMockError { + #expect(true) + } catch { + #expect(true == false) + } + } + } + +} + +// MARK: - Helpers + +private extension ServeResourceUseCaseTests { + + // MARK: Functions + + // MARK: Functions + + /// Checks whether a logging event should be logged or not, based on a given logging level. + /// - Parameters: + /// - logLevel: A representation of a logging level defined in in the logger. + /// - statusCode: A representation of a status code from the response. + /// - Returns: A boolean value that indicates whether a logging event should have been logged or not. + func shouldEventBeLogged( + logLevel: Logger.Level, + statusCode: HTTPResponse.Status + ) -> Bool { + let levels: [Logger.Level] = switch statusCode { + case .ok: [.debug, .trace] + case .notFound: [.debug, .error, .info, .notice, .trace, .warning] + default: [] + } + + return levels.contains(logLevel) + } + +} diff --git a/Tests/DocCMiddleware/Types/Extensions/String+Constants.swift b/Tests/DocCMiddleware/Types/Extensions/String+Constants.swift index 72b41b6..bbfb6cf 100644 --- a/Tests/DocCMiddleware/Types/Extensions/String+Constants.swift +++ b/Tests/DocCMiddleware/Types/Extensions/String+Constants.swift @@ -14,7 +14,15 @@ extension String { // MARK: Constants - /// A URI path to use as a redirection sample. - static let uriRedirection = "/some/redirect/path" - + /// A namespace that defines sample values. + enum Sample { + /// A URI path to use as a file sample. + static let uriFile = uriFolder + uriResource + /// A URI path to use as a folder sample. + static let uriFolder = "/some/folder/path" + /// A URI path to use as a redirection sample. + static let uriRedirection = "/some/redirect/path" + /// A URI path to use as a resource sample. + static let uriResource = "/some/path/to/resource" + } } -- 2.52.0 From 7a17e83f8c062ca00460f8c08fa27183aeb63a7a Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Thu, 25 Sep 2025 01:40:03 +0200 Subject: [PATCH 22/29] Improved the naming of some test cases for Swift below 6.2 in the tests target. --- .../LoggerMetadata+HelpersTests.swift | 4 +-- .../Use Cases/CheckURIUseCaseTests.swift | 4 +-- .../PrepareURIPathUseCaseTests.swift | 4 +-- .../Use Cases/RedirectURIUseCaseTests.swift | 26 ++++++++++--------- .../Middlewares/DocCMiddlewareTests.swift | 4 +-- 5 files changed, 22 insertions(+), 20 deletions(-) diff --git a/Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift b/Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift index 4d25ed4..282552e 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift +++ b/Tests/DocCMiddleware/Tests/Internal/Extensions/LoggerMetadata+HelpersTests.swift @@ -43,7 +43,7 @@ struct LoggerMetadata_HelpersTests { } #else @Test("metadata with HTTP method and status code") - func metadataWithMethodAndStatusCode() throws { + func metadata_withMethod_andStatusCode() throws { assertMetadata( method: try randomMethod, statusCode: try randomStatusCode @@ -51,7 +51,7 @@ struct LoggerMetadata_HelpersTests { } @Test("metadata with HTTP method, status code and redirection URI path") - func metadataWithMethodStatusCodeAndRedirection() throws { + func metadata_withMethod_statusCode_andRedirection() throws { assertMetadata( method: try randomMethod, statusCode: try randomStatusCode, diff --git a/Tests/DocCMiddleware/Tests/Internal/Use Cases/CheckURIUseCaseTests.swift b/Tests/DocCMiddleware/Tests/Internal/Use Cases/CheckURIUseCaseTests.swift index 055a1be..922fec2 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Use Cases/CheckURIUseCaseTests.swift +++ b/Tests/DocCMiddleware/Tests/Internal/Use Cases/CheckURIUseCaseTests.swift @@ -52,7 +52,7 @@ struct CheckURIUseCaseTests { Input.nonEncodedURIs, Output.nonEncodedURIs )) - func checkNonEncodedURIs( + func check_nonEncodedURIs( uri uriPath: String, expects result: String? ) { @@ -63,7 +63,7 @@ struct CheckURIUseCaseTests { Input.percentEncodedURIs, Output.percentEncodedURIs )) - func checkPercentEncodedURIs( + func check_percentEncodedURIs( uri uriPath: String, expects result: String? ) { diff --git a/Tests/DocCMiddleware/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift b/Tests/DocCMiddleware/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift index 465af8b..30b3b53 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift +++ b/Tests/DocCMiddleware/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift @@ -54,7 +54,7 @@ struct PrepareURIPathUseCaseTests { Input.prepareURIPaths, Output.prepareURIPaths )) - func dataWithURIRootNotSuffixedWithForwardSlash( + func data_withURIRoot_notSuffixed_withForwardSlash( uri uriPath: String, expects result: PrepareURIPathUseCase.PreparedURIPaths? ) throws { @@ -69,7 +69,7 @@ struct PrepareURIPathUseCaseTests { Input.prepareURIPathsSlashed, Output.prepareURIPaths )) - func dataWithURIRootSuffixedWithForwardSlash( + func data_withURIRoot_suffixed_withForwardSlash( uri uriPath: String, expects result: PrepareURIPathUseCase.PreparedURIPaths? ) throws { diff --git a/Tests/DocCMiddleware/Tests/Internal/Use Cases/RedirectURIUseCaseTests.swift b/Tests/DocCMiddleware/Tests/Internal/Use Cases/RedirectURIUseCaseTests.swift index 3057e6f..6f8fea9 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Use Cases/RedirectURIUseCaseTests.swift +++ b/Tests/DocCMiddleware/Tests/Internal/Use Cases/RedirectURIUseCaseTests.swift @@ -10,9 +10,12 @@ // // ===----------------------------------------------------------------------=== -import Hummingbird import Testing +import protocol Hummingbird.RequestContext + +import struct Hummingbird.HTTPResponse +import struct Hummingbird.Request import struct Logging.Logger @testable import struct DocCMiddleware.RedirectURIUseCase @@ -24,7 +27,7 @@ struct RedirectURIUseCaseTests { #if swift(>=6.2) @Test - func `response when logger expects an event`() async throws { + func `response when logging event triggered`() async throws { try await assertResponse( logLevel: try randomLogLevelWithEvent, uriRedirection: .Sample.uriRedirection, @@ -33,7 +36,7 @@ struct RedirectURIUseCaseTests { } @Test - func `response when logger does not expects an event`() async throws { + func `response when logging event not triggered`() async throws { try await assertResponse( logLevel: try randomLogLevelWithNoEvent, uriRedirection: .Sample.uriRedirection, @@ -41,8 +44,8 @@ struct RedirectURIUseCaseTests { ) } #else - @Test("response when logger expects an event") - func responseWhenLoggerExpectsEvent() async throws { + @Test("response when logging event triggered") + func response_whenEventTriggered() async throws { try await assertResponse( logLevel: try randomLogLevelWithEvent, uriRedirection: .uriRedirection, @@ -50,8 +53,8 @@ struct RedirectURIUseCaseTests { ) } - @Test("response when logger does not expects an event") - func responseWhenLogLevel() async throws { + @Test("response when logging event not triggered") + func response_whenEventNotTriggered() async throws { try await assertResponse( logLevel: try randomLogLevelWithNoEvent, uriRedirection: .uriRedirection, @@ -68,8 +71,7 @@ private extension RedirectURIUseCaseTests { // MARK: Functions - /// Asserts the response returned by the ``RedirectURIUseCase`` use case based on the given `logLevel` logging level and the `uriRedirection` - /// URI path plus the expected status code of the response. + /// Asserts a response returned by the ``RedirectURIUseCase`` use case. /// - Parameters: /// - logLevel: A representation of the logging level to set in the `Logger` instance. /// - uriRedirection: A URI path to use in the redirection. @@ -154,11 +156,11 @@ private extension RedirectURIUseCaseTests { // MARK: Functions - /// Checks whether a logging event should be logged or not, based on a given logging level. - /// - Parameter level: A representation of a logging level defined in the `Logger` instance. + /// Checks whether a logging event should be logged or not. + /// - Parameter logLevel: A representation of a logging level defined in the logger. /// - Returns: A boolean value that indicates whether a logging event should have been logged or not. func shouldEventBeLogged(_ logLevel: Logger.Level) -> Bool { - [Logger.Level.trace, .debug].contains(logLevel) + [Logger.Level.debug, .trace].contains(logLevel) } } diff --git a/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift b/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift index 6daa206..6ee8295 100644 --- a/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift +++ b/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift @@ -45,7 +45,7 @@ struct DocCMiddlewareTests { } #else @Test("initialize with URI and folder paths") - func initWithURIAndFolderPaths() { + func init_withURI_andFolderPaths() { assertInit(configuration: .init( uriRoot: "/path/to/documentation", folderRoot: "/location/docc/documentation" @@ -53,7 +53,7 @@ struct DocCMiddlewareTests { } @Test("initialize with type that conforms to the FileProvider protocol") - func initWithURIPathAndFileProviderType() { + func init_withURI_path_andFileProviderType() { assertInit( configuration: .init( uriRoot: "/path/to/documentation", -- 2.52.0 From 1046a9fc8d749696350deba5326601bb25e3f6e1 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Thu, 25 Sep 2025 01:43:52 +0200 Subject: [PATCH 23/29] Renamed the ServerResourceUseCase use case in the library target as ServeURIUseCase. --- ...ResourceUseCase.swift => ServeURIUseCase.swift} | 2 +- ...eCaseTests.swift => ServeURIUseCaseTests.swift} | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) rename Sources/DocCMiddleware/Internal/Use Cases/{ServeResourceUseCase.swift => ServeURIUseCase.swift} (98%) rename Tests/DocCMiddleware/Tests/Internal/Use Cases/{ServeResourceUseCaseTests.swift => ServeURIUseCaseTests.swift} (95%) diff --git a/Sources/DocCMiddleware/Internal/Use Cases/ServeResourceUseCase.swift b/Sources/DocCMiddleware/Internal/Use Cases/ServeURIUseCase.swift similarity index 98% rename from Sources/DocCMiddleware/Internal/Use Cases/ServeResourceUseCase.swift rename to Sources/DocCMiddleware/Internal/Use Cases/ServeURIUseCase.swift index 3c77779..ad9b08d 100644 --- a/Sources/DocCMiddleware/Internal/Use Cases/ServeResourceUseCase.swift +++ b/Sources/DocCMiddleware/Internal/Use Cases/ServeURIUseCase.swift @@ -16,7 +16,7 @@ import struct Hummingbird.Response import struct Logging.Logger /// A use case that serves a resource, defined by its URI path, from a physical location. -struct ServeResourceUseCase { +struct ServeURIUseCase { // MARK: Properties diff --git a/Tests/DocCMiddleware/Tests/Internal/Use Cases/ServeResourceUseCaseTests.swift b/Tests/DocCMiddleware/Tests/Internal/Use Cases/ServeURIUseCaseTests.swift similarity index 95% rename from Tests/DocCMiddleware/Tests/Internal/Use Cases/ServeResourceUseCaseTests.swift rename to Tests/DocCMiddleware/Tests/Internal/Use Cases/ServeURIUseCaseTests.swift index 239db97..fb011a5 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Use Cases/ServeResourceUseCaseTests.swift +++ b/Tests/DocCMiddleware/Tests/Internal/Use Cases/ServeURIUseCaseTests.swift @@ -18,10 +18,10 @@ import struct Hummingbird.HTTPResponse import struct Hummingbird.Request import struct Logging.Logger -@testable import struct DocCMiddleware.ServeResourceUseCase +@testable import struct DocCMiddleware.ServeURIUseCase -@Suite("Serve Resource Use Case", .tags(.useCase)) -struct ServeResourceUseCaseTests { +@Suite("Serve URI Use Case", .tags(.useCase)) +struct ServeURIUseCaseTests { // MARK: Use case tests @@ -129,11 +129,11 @@ struct ServeResourceUseCaseTests { // MARK: - Assertions -private extension ServeResourceUseCaseTests { +private extension ServeURIUseCaseTests { // MARK: Functions - /// Asserts a response returned by the ``ServeResourceUseCase`` use case. + /// Asserts a response returned by the ``ServeURIUseCase`` use case. /// /// > important: In case no `statusCode` value is given, the function then assumes that the loading of a file will throw an error. /// @@ -165,7 +165,7 @@ private extension ServeResourceUseCaseTests { let context: any RequestContext = RequestContextMock(logger: logger) let request: Request = .test(method: .get) - let useCase = ServeResourceUseCase( + let useCase = ServeURIUseCase( fileProvider: fileProvider, logger: logger ) @@ -241,7 +241,7 @@ private extension ServeResourceUseCaseTests { // MARK: - Helpers -private extension ServeResourceUseCaseTests { +private extension ServeURIUseCaseTests { // MARK: Functions -- 2.52.0 From 36f14cfb512c94c34596343727b86f2b8b2cfd9f Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Thu, 25 Sep 2025 02:18:30 +0200 Subject: [PATCH 24/29] Fixed the FileProvider generic definition for the ServeURIUseCase use case in the library target. --- .../Internal/Use Cases/ServeURIUseCase.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/DocCMiddleware/Internal/Use Cases/ServeURIUseCase.swift b/Sources/DocCMiddleware/Internal/Use Cases/ServeURIUseCase.swift index ad9b08d..fa54fe1 100644 --- a/Sources/DocCMiddleware/Internal/Use Cases/ServeURIUseCase.swift +++ b/Sources/DocCMiddleware/Internal/Use Cases/ServeURIUseCase.swift @@ -16,12 +16,12 @@ import struct Hummingbird.Response import struct Logging.Logger /// A use case that serves a resource, defined by its URI path, from a physical location. -struct ServeURIUseCase { +struct ServeURIUseCase { // MARK: Properties /// A type that conforms to a protocol that defines file system interactions. - let fileProvider: FileSystemProvider + private let fileProvider: Provider /// A type that interacts with the logging system. private let logger: Logger @@ -33,7 +33,7 @@ struct ServeURIUseCase { /// - fileProvider: A type that conforms to a protocol that defines file system interactions. /// - logger: A type that interacts with the logging system. init( - fileProvider: FileSystemProvider, + fileProvider: Provider, logger: Logger ) { self.fileProvider = fileProvider @@ -77,7 +77,7 @@ struct ServeURIUseCase { id: fileIdentifier, context: contextualInfo.context ) - + defer { logger.log( level: .debug, -- 2.52.0 From edeaf219a099a5ba739e3951d5f429a02351855d Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Thu, 25 Sep 2025 02:41:52 +0200 Subject: [PATCH 25/29] Improved the initializers for the DocCMiddleware type in the library target. --- .../Public/Middlewares/DocCMiddleware.swift | 52 +++++++++++++------ .../Middlewares/DocCMiddlewareTests.swift | 46 +++++++++++----- 2 files changed, 69 insertions(+), 29 deletions(-) diff --git a/Sources/DocCMiddleware/Public/Middlewares/DocCMiddleware.swift b/Sources/DocCMiddleware/Public/Middlewares/DocCMiddleware.swift index 1abc144..f40d1d7 100644 --- a/Sources/DocCMiddleware/Public/Middlewares/DocCMiddleware.swift +++ b/Sources/DocCMiddleware/Public/Middlewares/DocCMiddleware.swift @@ -10,20 +10,21 @@ // // ===----------------------------------------------------------------------=== -import Hummingbird +import protocol Hummingbird.FileProvider +import struct Hummingbird.LocalFileSystem import struct Logging.Logger /// A middleware that proxies requests to `DocC` documentation containers within a hosting app. -public struct DocCMiddleware { +public struct DocCMiddleware { // MARK: Properties /// A type that contains the parameters to configure the middleware. let configuration: Configuration - /// A protocol that defines file system interactions. - let fileProvider: any FileProvider + /// 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 @@ -33,27 +34,44 @@ public struct DocCMiddleware { // MARK: Initializers - /// Initializes this middleware with the root path to the `DocC` documentation containers in the file system. + /// Initializes this middleware. /// - Parameters: /// - configuration: A type that contains the parameters to configure the middleware. /// - logger: A type that interacts with the logging system. - /// - fileProvider: A type that conforms to the protocol that defines file system interactions, if any. - init( + public init( configuration: Configuration, - logger: Logger, - fileProvider: (any FileProvider)? = nil - ) { - self.configuration = configuration - self.fileProvider = if let fileProvider { - fileProvider - } else { - LocalFileSystem( + 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 + ) } } diff --git a/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift b/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift index 6ee8295..cd5be76 100644 --- a/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift +++ b/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift @@ -72,22 +72,48 @@ private extension DocCMiddlewareTests { // MARK: Functions - /// Asserts the initialization of a `DocCMiddleware` type. + /// Asserts the public initializer. /// - Parameters: /// - configuration: A type that contains the parameters to configure the middleware. /// - logger: A type that interacts with the logging system. - /// - fileProvider: A type that conforms to the protocol that defines file system interactions, if any. func assertInit( - configuration: DocCMiddleware.Configuration, - logger: Logger = .test(), - fileProvider: (any FileProvider)? = nil + configuration: DocCMiddleware.Configuration, + logger: Logger = .test() ) { // GIVEN // WHEN let middleware = DocCMiddleware( configuration: configuration, - logger: logger, - 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(type(of:middleware.fileProvider) == LocalFileSystem.self) + } + + /// Asserts the internal initializer with a concrete file provider type. + /// - Parameters: + /// - configuration: A type that contains the parameters to configure the middleware. + /// - logger: A type that interacts with the logging system. + /// - fileProvider: A type that conforms to the protocol that defines file system interactions, if any. + func assertInit( + configuration: DocCMiddleware.Configuration, + logger: Logger = .test(), + fileProvider: FileSystemProvider + ) { + // GIVEN + // WHEN + let middleware = DocCMiddleware( + configuration: configuration, + fileProvider: fileProvider, + logger: logger ) // THEN @@ -98,11 +124,7 @@ private extension DocCMiddlewareTests { #expect(middleware.logger.label == logger.label) #expect(middleware.logger.logLevel == logger.logLevel) - if let fileProvider { - #expect(type(of:middleware.fileProvider) == type(of: fileProvider)) - } else { - #expect(middleware.fileProvider is LocalFileSystem) - } + #expect(type(of:middleware.fileProvider) == FileSystemProvider.self) } } -- 2.52.0 From 8aa2cf0fb2b764623c622d5f0acfff39a38ca4d7 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Fri, 26 Sep 2025 00:27:56 +0200 Subject: [PATCH 26/29] Implemented the "handle(_: context: next: )" function for the DocCMiddleware type in the library target. --- .../Public/Middlewares/DocCMiddleware.swift | 78 ++- .../Use Cases/ServeURIUseCaseTests.swift | 2 - .../Middlewares/DocCMiddlewareTests.swift | 530 +++++++++++++++++- .../Types/Extensions/String+Constants.swift | 4 + 4 files changed, 605 insertions(+), 9 deletions(-) diff --git a/Sources/DocCMiddleware/Public/Middlewares/DocCMiddleware.swift b/Sources/DocCMiddleware/Public/Middlewares/DocCMiddleware.swift index f40d1d7..c9f804e 100644 --- a/Sources/DocCMiddleware/Public/Middlewares/DocCMiddleware.swift +++ b/Sources/DocCMiddleware/Public/Middlewares/DocCMiddleware.swift @@ -11,8 +11,12 @@ // ===----------------------------------------------------------------------=== 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. @@ -32,6 +36,15 @@ public struct DocCMiddleware { /// 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. @@ -93,11 +106,74 @@ extension DocCMiddleware: RouterMiddleware { context: any Context, next: (Input, any Context) async throws -> Output ) async throws -> Output { - guard let uri = checkURI(input.uri) else { + 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) } } + diff --git a/Tests/DocCMiddleware/Tests/Internal/Use Cases/ServeURIUseCaseTests.swift b/Tests/DocCMiddleware/Tests/Internal/Use Cases/ServeURIUseCaseTests.swift index fb011a5..6c36e7f 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Use Cases/ServeURIUseCaseTests.swift +++ b/Tests/DocCMiddleware/Tests/Internal/Use Cases/ServeURIUseCaseTests.swift @@ -245,8 +245,6 @@ private extension ServeURIUseCaseTests { // MARK: Functions - // MARK: Functions - /// Checks whether a logging event should be logged or not, based on a given logging level. /// - Parameters: /// - logLevel: A representation of a logging level defined in in the logger. diff --git a/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift b/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift index cd5be76..8334415 100644 --- a/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift +++ b/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift @@ -13,8 +13,11 @@ import Testing import protocol Hummingbird.FileProvider +import protocol Hummingbird.RequestContext +import struct Hummingbird.HTTPResponse import struct Hummingbird.LocalFileSystem +import struct Hummingbird.Request import struct Logging.Logger @testable import struct DocCMiddleware.DocCMiddleware @@ -28,8 +31,8 @@ struct DocCMiddlewareTests { @Test func `initialize with URI and folder paths`() { assertInit(configuration: .init( - uriRoot: "/path/to/documentation", - folderRoot: "/location/docc/documentation" + uriRoot: .Sample.uriResource, + folderRoot: .Sample.uriFolder )) } @@ -37,7 +40,7 @@ struct DocCMiddlewareTests { func `initialize with URI path and type that conforms to the FileProvider protocol`() { assertInit( configuration: .init( - uriRoot: "/path/to/documentation", + uriRoot: .Sample.uriResource, folderRoot: .empty ), fileProvider: FileProviderStub() @@ -47,8 +50,8 @@ struct DocCMiddlewareTests { @Test("initialize with URI and folder paths") func init_withURI_andFolderPaths() { assertInit(configuration: .init( - uriRoot: "/path/to/documentation", - folderRoot: "/location/docc/documentation" + uriRoot: .Sample.uriResource, + folderRoot: .Sample.uriFolder )) } @@ -56,13 +59,223 @@ struct DocCMiddlewareTests { func init_withURI_path_andFileProviderType() { assertInit( configuration: .init( - uriRoot: "/path/to/documentation", + uriRoot: .Sample.uriResource, folderRoot: .empty ), fileProvider: FileProviderStub() ) } #endif + + // MARK: RouterMiddleware tests + +#if swift(>=6.2) + @Test(arguments: zip( + Input.redirectURIPaths, + Output.redirectURIPaths + )) + func `redirect a URI path while triggering logging event`( + uriPath: String, + expects uriRedirect: String + ) async throws { + try await assertRedirect( + logLevel: try randomLogLevelForRedirectWithEvent, + uriPath: .Sample.uriDocument + uriPath, + to: .Sample.uriDocument + uriRedirect + ) + } + + @Test(arguments: zip( + Input.redirectURIPaths, + Output.redirectURIPaths + )) + func `redirect a URI path without triggering logging event`( + uriPath: String, + expects uriRedirect: String + ) async throws { + try await assertRedirect( + logLevel: try randomLogLevelForRedirectWithNoEvent, + uriPath: .Sample.uriDocument + uriPath, + to: .Sample.uriDocument + uriRedirect + ) + } + + @Test(arguments: Input.redirectURIPaths) + func `redirect a URI path not prefixed with root URI path`(uriPath: String) async throws { + try await assertRedirect( + logLevel: try randomLogLevel, + uriPath: .Sample.uriDocument + uriPath, + expects: .ok + ) + } + + @Test(arguments: zip( + Input.serveURIPaths, + Output.serveURIFilePaths + )) + func `serve an existing URI resource while triggering logging event`( + uriPath: String, + uriFile: String + ) async throws { + try await assertServe( + logLevel: try randomLogLevelForServeOKWithEvent, + uriPath: .Sample.uriDocument + uriPath, + uriFile: uriFile, + statusCode: .ok + ) + } + + @Test(arguments: Input.serveURIPaths) + func `serve an existing URI resource without triggering logging event`( + uriPath: String + ) async throws { + try await assertServe( + logLevel: try randomLogLevelForServeOKWithNoEvent, + uriPath: .Sample.uriDocument + uriPath, + statusCode: .ok + ) + } + + @Test(arguments: zip( + Input.serveURIPaths, + Output.serveURIFilePaths + )) + func `serve a non existing URI resource while triggering logging event`( + uriPath: String, + uriFile: String + ) async throws { + try await assertServe( + logLevel: try randomLogLevelForServeNotFoundWithEvent, + uriPath: .Sample.uriDocument + uriPath, + uriFile: uriFile, + statusCode: .notFound + ) + } + + @Test(arguments: Input.serveURIPaths) + func `serve a non existing URI resource without triggering logging event`( + uriPath: String + ) async throws { + try await assertServe( + logLevel: try randomLogLevelForServeNotFoundWithNoEvent, + uriPath: .Sample.uriDocument + uriPath, + statusCode: .notFound + ) + } + + @Test(arguments: Input.serveURIPaths) + func `serve a URI resource not prefixed with root URI path`( + uriPath: String + ) async throws { + try await assertServe( + logLevel: try randomLogLevel, + uriPath: uriPath + ) + } +#else + @Test("redirect a URI path while triggering logging event", arguments: zip( + Input.redirectURIPaths, + Output.redirectURIPaths + )) + func redirect_aURIPath_triggeringLoggingEvent( + uriPath: String, + expects uriRedirect: String + ) async throws { + try await assertRedirect( + logLevel: try randomLogLevelForRedirectWithEvent, + uriPath: .Sample.uriRoot + uriPath, + to: .Sample.uriRoot + uriRedirect + ) + } + + @Test("redirect a URI path without triggering logging event", arguments: zip( + Input.redirectURIPaths, + Output.redirectURIPaths + )) + func redirect_aURIPath_notTriggeringLoggingEvent( + uriPath: String, + expects uriRedirect: String + ) async throws { + try await assertRedirect( + logLevel: try randomLogLevelForRedirectWithNoEvent, + uriPath: .Sample.uriRoot + uriPath, + to: .Sample.uriRoot + uriRedirect + ) + } + + @Test("redirect a URI path not prefixed with root URI path", arguments: Input.redirectURIPaths) + func redirect_aURIPath_notPrefixedURIRoot(uriPath: String) async throws { + try await assertRedirect( + logLevel: try randomLogLevel, + uriPath: .Sample.uriResource + uriPath, + expects: .ok + ) + } + + @Test("serve an existing URI resource while triggering logging event", arguments: zip( + Input.serveURIPaths, + Output.serveURIFilePaths + )) + func serve_exitingURIResource_triggeringLoggingEvent( + uriPath: String, + uriFile: String + ) async throws { + try await assertServe( + logLevel: try randomLogLevelForServeOKWithEvent, + uriPath: .Sample.uriDocument + uriPath, + uriFile: uriFile, + statusCode: .ok + ) + } + + @Test("serve an existing URI resource without triggering logging event", arguments: Input.serveURIPaths) + func server_existingURIResource_notTriggeringLoggingEvent( + uriPath: String + ) async throws { + try await assertServe( + logLevel: try randomLogLevelForServeOKWithNoEvent, + uriPath: .Sample.uriDocument + uriPath, + statusCode: .ok + ) + } + + @Test("serve a non existing URI resource while triggering logging event", arguments: zip( + Input.serveURIPaths, + Output.serveURIFilePaths + )) + func serve_notExistingURIResource_triggeringLoggingEvent( + uriPath: String, + uriFile: String + ) async throws { + try await assertServe( + logLevel: try randomLogLevelForServeNotFoundWithEvent, + uriPath: .Sample.uriDocument + uriPath, + uriFile: uriFile, + statusCode: .notFound + ) + } + + @Test("serve a non existing URI resource without triggering logging event", arguments: Input.serveURIPaths) + func serve_notExistingURIResource_triggeringLoggingEvent( + uriPath: String + ) async throws { + try await assertServe( + logLevel: try randomLogLevelForServeNotFoundWithNoEvent, + uriPath: .Sample.uriDocument + uriPath, + statusCode: .notFound + ) + } + + @Test("serve a URI resource not prefixed with root URI path", arguments: Input.serveURIPaths) + func server_aURIResource_notPrefixed_withURIRoot( + uriPath: String + ) async throws { + try await assertServe( + logLevel: try randomLogLevel, + uriPath: uriPath + ) + } +#endif } @@ -94,6 +307,7 @@ private extension DocCMiddlewareTests { #expect(middleware.logger.label == logger.label) #expect(middleware.logger.logLevel == logger.logLevel) + #expect(middleware.logger.metadataProvider == nil) #expect(type(of:middleware.fileProvider) == LocalFileSystem.self) } @@ -123,8 +337,312 @@ private extension DocCMiddlewareTests { #expect(middleware.logger.label == logger.label) #expect(middleware.logger.logLevel == logger.logLevel) + #expect(middleware.logger.metadataProvider == nil) #expect(type(of:middleware.fileProvider) == FileSystemProvider.self) } + /// Asserts an URI path redirection done by the middleware. + /// - Parameters: + /// - logLevel: A representation of the logging level to set in the `Logger` instance. + /// - uriPath: A URI path to a resource. + /// - uriRedirect: A redirected URI path, if any. + /// - statusCode: An expected status code from the response coming out of the use case. + /// - Throws: An error in case an issue is encountered while asserting URI path redirections by the middleware. + func assertRedirect( + logLevel: Logger.Level, + uriPath: String, + to uriRedirect: String? = nil, + expects statusCode: HTTPResponse.Status = .movedPermanently + ) async throws { + // GIVEN + let logHandler: LogHandlerMock = .init() + let logger: Logger = .test( + level: logLevel, + handler: logHandler + ) + + let context: any RequestContext = RequestContextMock(logger: logger) + let request: Request = .test( + method: .get, + path: uriPath + ) + + let middleware = DocCMiddleware( + configuration: .init( + uriRoot: .Sample.uriRoot, + folderRoot: .Sample.uriFolder + ), + fileProvider: FileProviderMock(), + logger: logger + ) + + // WHEN + let result = try await middleware.handle(request, context: context) { _, _ in + .init(status: .ok) + } + + // THEN + #expect(result.status == statusCode) + + let events = await logHandler.entries + + if statusCode == .movedPermanently, let uriRedirect { + #expect(result.body.contentLength == 0) + #expect(result.headers == [ + .location: uriRedirect, + .contentLength: "0" + ]) + + if shouldEventBeLogged( + logLevel: logLevel, + statusCode: statusCode + ) { + #expect(!events.isEmpty) + #expect(events.count == 1) + + let loggedEvent = try #require(events.first) + + #expect(loggedEvent == .init( + level: .debug, + metadata: [ + "hb.request.id": "\(context.id)", + "hb.request.method": "\(request.method.rawValue)", + "hb.request.path": "\(request.uri.path)", + "hb.request.status": "\(statusCode.code)", + "hb.request.redirect": "\(uriRedirect)" + ], + message: "The URI path is redirected to this path: \(uriRedirect)", + source: .Logging.source + )) + } else { + #expect(events.isEmpty) + } + } else { + #expect(events.isEmpty) + } + } + + /// <#Description#> + /// - Parameters: + /// - logLevel: <#logLevel description#> + /// - uriPath: <#uriPath description#> + /// - uriFile: <#uriFile description#> + /// - folderPath: <#folderPath description#> + /// - statusCode: <#statusCode description#> + /// - Throws: An error in case an issue is encountered while asserting URI path servings by the middleware. + func assertServe( + logLevel: Logger.Level, + uriPath: String, + uriFile: String? = nil, + folderPath: String = .Sample.uriFolder, + statusCode: HTTPResponse.Status? = nil + ) async throws { + // GIVEN + let logHandler: LogHandlerMock = .init() + let logger: Logger = .test( + level: logLevel, + handler: logHandler + ) + let fileProvider: FileProviderMock = switch statusCode { + case .ok: .init(fileIdentifier: .init()) + case .notFound: .init() + default: .init(fileIdentifier: .init(), shouldLoadFile: false) + } + + let context: any RequestContext = RequestContextMock(logger: logger) + let request: Request = .test( + method: .get, + path: uriPath + ) + + let middleware = DocCMiddleware( + configuration: .init( + uriRoot: .Sample.uriRoot, + folderRoot: .Sample.uriFolder + ), + fileProvider: fileProvider, + logger: logger + ) + + // WHEN + let result = try await middleware.handle(request, context: context) { _, _ in + .init(status: .ok) + } + + // THEN + if let statusCode { + #expect(result.status == statusCode) + #expect(result.headers == [ + .contentLength: (statusCode == .ok ? "36" : "0") + ]) + + let contentLength = try #require(result.body.contentLength) + + if statusCode == .ok { + #expect(contentLength > 0) + } else { + #expect(contentLength == 0) + } + + let events = await logHandler.entries + + if shouldEventBeLogged( + logLevel: logLevel, + statusCode: statusCode + ) { + #expect(!events.isEmpty) + #expect(events.count == 1) + + let loggedEvent = try #require(events.first) + let uriFile = try #require(uriFile) + + #expect(loggedEvent == .init( + level: statusCode == .ok ? .debug : .error, + metadata: [ + "hb.request.id": "\(context.id)", + "hb.request.method": "\(request.method.rawValue)", + "hb.request.path": "\(request.uri.path)", + "hb.request.status": "\(statusCode.code)" + ], + message: { + if statusCode == .ok { + "The body of the resource \(uriFile) has \(contentLength) bytes." + } else { + "The resource \(uriFile) has not been found." + } + }(), + source: .Logging.source + )) + } else { + #expect(events.isEmpty) + } + } else { + #expect(result.status == .ok) + } + } + +} + +// MARK: - Helpers + +private extension DocCMiddlewareTests { + + // MARK: Computed + + /// Extracts a random logging level. + var randomLogLevel: Logger.Level { + get throws { + try #require(Logger.Level.allCases.randomElement()) + } + } + + /// Extracts a random logging level for redirection assertions that support event logging for the use case. + var randomLogLevelForRedirectWithEvent: Logger.Level { + get throws { + try #require([.debug, .trace].randomElement()) + } + } + + /// Extracts a random logging level for redirection assertions that does not support event logging for the use case. + var randomLogLevelForRedirectWithNoEvent: Logger.Level { + get throws { + try #require([.critical, .error, .info, .notice, .warning].randomElement()) + } + } + + /// Extracts a random logging level for OK serve assertions that support event logging for the use case. + var randomLogLevelForServeOKWithEvent: Logger.Level { + get throws { + try #require([.debug, .trace].randomElement()) + } + } + + /// Extracts a random logging level for OK serve assertions that does not support event logging for the use case. + var randomLogLevelForServeOKWithNoEvent: Logger.Level { + get throws { + try #require([.critical, .error, .info, .notice, .warning].randomElement()) + } + } + + /// Extracts a random logging level for Not Found serve assertions that support event logging for the use case. + var randomLogLevelForServeNotFoundWithEvent: Logger.Level { + get throws { + try #require([.debug, .error, .info, .notice, .trace, .warning].randomElement()) + } + } + + /// Extracts a random logging level for Not Found serve assertions that does not support event logging for the use case. + var randomLogLevelForServeNotFoundWithNoEvent: Logger.Level { + get throws { + try #require([.critical].randomElement()) + } + } + + // MARK: Functions + + /// Checks whether a logging event should be logged or not, based on a given logging level. + /// - Parameters: + /// - logLevel: A representation of a logging level defined in in the logger. + /// - statusCode: A representation of a status code from the response. + /// - Returns: A boolean value that indicates whether a logging event should have been logged or not. + func shouldEventBeLogged( + logLevel: Logger.Level, + statusCode: HTTPResponse.Status + ) -> Bool { + let levels: [Logger.Level] = switch statusCode { + case .movedPermanently, .ok: [.debug, .trace] + case .notFound: [.debug, .error, .info, .notice, .trace, .warning] + default: [] + } + + return levels.contains(logLevel) + } + +} + +// MARK: - Constants + +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"] + /// A list of relative URI paths to match against the URI path servings done by the middleware. + static let serveURIPaths: [String] = [ + "/documentation/", + "/tutorials/", + "/data/documentation.json", + "/favicon.ico", + "/favicon.svg", + "/theme-settings.json", + "/css/file.css", + "/data/data.bin", + "/downloads/file.txt", + "/images/image.png", + "/img/image.jpg", + "/index/file", + "/js/file.js", + "/videos/video.mp4" + ] +} + +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/"] + /// 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" + ] } diff --git a/Tests/DocCMiddleware/Types/Extensions/String+Constants.swift b/Tests/DocCMiddleware/Types/Extensions/String+Constants.swift index bbfb6cf..c41d580 100644 --- a/Tests/DocCMiddleware/Types/Extensions/String+Constants.swift +++ b/Tests/DocCMiddleware/Types/Extensions/String+Constants.swift @@ -16,6 +16,8 @@ extension String { /// A namespace that defines sample values. enum Sample { + /// A URI path to use as a documentation root sample. + static let uriDocument = uriRoot + "/SomeDocument" /// A URI path to use as a file sample. static let uriFile = uriFolder + uriResource /// A URI path to use as a folder sample. @@ -24,5 +26,7 @@ extension String { static let uriRedirection = "/some/redirect/path" /// A URI path to use as a resource sample. static let uriResource = "/some/path/to/resource" + /// A URI path to use as a root sample. + static let uriRoot = "/some/root/path" } } -- 2.52.0 From 386d9d8cb55b7408343f4b70322ccaab38f37ca6 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Fri, 26 Sep 2025 01:35:53 +0200 Subject: [PATCH 27/29] Implemented the "random(upTo: )" and "random(fromExclusive: )" static function for the LoggerLevel+Helpers extension in the tests target. --- .../Use Cases/RedirectURIUseCaseTests.swift | 24 +---- .../Use Cases/ServeURIUseCaseTests.swift | 20 ++-- .../Middlewares/DocCMiddlewareTests.swift | 101 +++++------------- .../Extensions/LoggerLevel+Helpers.swift | 61 +++++++++++ 4 files changed, 99 insertions(+), 107 deletions(-) create mode 100644 Tests/DocCMiddleware/Types/Extensions/LoggerLevel+Helpers.swift diff --git a/Tests/DocCMiddleware/Tests/Internal/Use Cases/RedirectURIUseCaseTests.swift b/Tests/DocCMiddleware/Tests/Internal/Use Cases/RedirectURIUseCaseTests.swift index 6f8fea9..29953a9 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Use Cases/RedirectURIUseCaseTests.swift +++ b/Tests/DocCMiddleware/Tests/Internal/Use Cases/RedirectURIUseCaseTests.swift @@ -29,7 +29,7 @@ struct RedirectURIUseCaseTests { @Test func `response when logging event triggered`() async throws { try await assertResponse( - logLevel: try randomLogLevelWithEvent, + logLevel: try .random(upTo: .debug), uriRedirection: .Sample.uriRedirection, expects: .movedPermanently ) @@ -38,7 +38,7 @@ struct RedirectURIUseCaseTests { @Test func `response when logging event not triggered`() async throws { try await assertResponse( - logLevel: try randomLogLevelWithNoEvent, + logLevel: try .random(fromExclusive: .debug), uriRedirection: .Sample.uriRedirection, expects: .movedPermanently ) @@ -47,7 +47,7 @@ struct RedirectURIUseCaseTests { @Test("response when logging event triggered") func response_whenEventTriggered() async throws { try await assertResponse( - logLevel: try randomLogLevelWithEvent, + logLevel: try .random(upTo: .debug), uriRedirection: .uriRedirection, expects: .movedPermanently ) @@ -137,23 +137,7 @@ private extension RedirectURIUseCaseTests { // MARK: - Helpers private extension RedirectURIUseCaseTests { - - // MARK: Computed - - /// Extracts a random logging level that support event logging for the use case. - var randomLogLevelWithEvent: Logger.Level { - get throws { - try #require([.debug, .trace].randomElement()) - } - } - - /// Extracts a random logging level that does not support event logging for the use case. - var randomLogLevelWithNoEvent: Logger.Level { - get throws { - try #require([.critical, .error, .info, .notice, .warning].randomElement()) - } - } - + // MARK: Functions /// Checks whether a logging event should be logged or not. diff --git a/Tests/DocCMiddleware/Tests/Internal/Use Cases/ServeURIUseCaseTests.swift b/Tests/DocCMiddleware/Tests/Internal/Use Cases/ServeURIUseCaseTests.swift index 6c36e7f..bf5d27e 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Use Cases/ServeURIUseCaseTests.swift +++ b/Tests/DocCMiddleware/Tests/Internal/Use Cases/ServeURIUseCaseTests.swift @@ -29,7 +29,7 @@ struct ServeURIUseCaseTests { @Test func `response when resource served and logging event triggered`() async throws { try await assertResponse( - logLevel: .debug, + logLevel: try .random(upTo: .debug), uriPath: .Sample.uriResource, folderPath: .Sample.uriFolder, expects: .ok @@ -39,7 +39,7 @@ struct ServeURIUseCaseTests { @Test func `response when resource served and logging event not triggered`() async throws { try await assertResponse( - logLevel: .info, + logLevel: try .random(fromExclusive: .debug), uriPath: .Sample.uriResource, folderPath: .Sample.uriFolder, expects: .ok @@ -49,7 +49,7 @@ struct ServeURIUseCaseTests { @Test func `response when resource not found and logging event triggered`() async throws { try await assertResponse( - logLevel: .error, + logLevel: try .random(upTo: .error), uriPath: .Sample.uriResource, folderPath: .Sample.uriFolder, expects: .notFound @@ -59,7 +59,7 @@ struct ServeURIUseCaseTests { @Test func `response when resource not found and logging event not triggered`() async throws { try await assertResponse( - logLevel: .critical, + logLevel: try .random(fromExclusive: .error), uriPath: .Sample.uriResource, folderPath: .Sample.uriFolder, expects: .notFound @@ -69,7 +69,7 @@ struct ServeURIUseCaseTests { @Test func `response throws error when loading resource`() async throws { try await assertResponse( - logLevel: .warning, + logLevel: try .random(), uriPath: .Sample.uriResource, folderPath: .Sample.uriFolder ) @@ -78,7 +78,7 @@ struct ServeURIUseCaseTests { @Test("response when resource served and logging event triggered") func response_whenResourceServed_andEventTriggered() async throws { try await assertResponse( - logLevel: .debug, + logLevel: try .random(upTo: .debug), uriPath: .Sample.uriResource, folderPath: .Sample.uriFolder, expects: .ok @@ -88,7 +88,7 @@ struct ServeURIUseCaseTests { @Test("response when resource served and logging event not triggered") func response_whenResourceServed_andEventNotTriggered() async throws { try await assertResponse( - logLevel: .info, + logLevel: try .random(fromExclusive: .debug), uriPath: .Sample.uriResource, folderPath: .Sample.uriFolder, expects: .ok @@ -98,7 +98,7 @@ struct ServeURIUseCaseTests { @Test("response when resource not found and logging event triggered") func resource_whenResourceNotFound_andEventTriggered() async throws { try await assertResponse( - logLevel: .error, + logLevel: try .random(upTo: .error), uriPath: .Sample.uriResource, folderPath: .Sample.uriFolder, expects: .notFound @@ -108,7 +108,7 @@ struct ServeURIUseCaseTests { @Test("response when resource not found and logging event not triggered") func resource_whenResourceNotFound_andEventNotTriggered() async throws { try await assertResponse( - logLevel: .critical, + logLevel: try .random(fromExclusive: .error), uriPath: .Sample.uriResource, folderPath: .Sample.uriFolder, expects: .notFound @@ -118,7 +118,7 @@ struct ServeURIUseCaseTests { @Test("response throws error when loading resource") func resource_throwsError_whenLoadingResource() async throws { try await assertResponse( - logLevel: .warning, + logLevel: try .random(), uriPath: .Sample.uriResource, folderPath: .Sample.uriFolder ) diff --git a/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift b/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift index 8334415..3eec0dd 100644 --- a/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift +++ b/Tests/DocCMiddleware/Tests/Public/Middlewares/DocCMiddlewareTests.swift @@ -79,7 +79,7 @@ struct DocCMiddlewareTests { expects uriRedirect: String ) async throws { try await assertRedirect( - logLevel: try randomLogLevelForRedirectWithEvent, + logLevel: try .random(upTo: .debug), uriPath: .Sample.uriDocument + uriPath, to: .Sample.uriDocument + uriRedirect ) @@ -94,7 +94,7 @@ struct DocCMiddlewareTests { expects uriRedirect: String ) async throws { try await assertRedirect( - logLevel: try randomLogLevelForRedirectWithNoEvent, + logLevel: try .random(fromExclusive: .debug), uriPath: .Sample.uriDocument + uriPath, to: .Sample.uriDocument + uriRedirect ) @@ -103,8 +103,8 @@ struct DocCMiddlewareTests { @Test(arguments: Input.redirectURIPaths) func `redirect a URI path not prefixed with root URI path`(uriPath: String) async throws { try await assertRedirect( - logLevel: try randomLogLevel, - uriPath: .Sample.uriDocument + uriPath, + logLevel: try .random(), + uriPath: uriPath, expects: .ok ) } @@ -118,7 +118,7 @@ struct DocCMiddlewareTests { uriFile: String ) async throws { try await assertServe( - logLevel: try randomLogLevelForServeOKWithEvent, + logLevel: try .random(upTo: .debug), uriPath: .Sample.uriDocument + uriPath, uriFile: uriFile, statusCode: .ok @@ -130,7 +130,7 @@ struct DocCMiddlewareTests { uriPath: String ) async throws { try await assertServe( - logLevel: try randomLogLevelForServeOKWithNoEvent, + logLevel: try .random(fromExclusive: .debug), uriPath: .Sample.uriDocument + uriPath, statusCode: .ok ) @@ -145,7 +145,7 @@ struct DocCMiddlewareTests { uriFile: String ) async throws { try await assertServe( - logLevel: try randomLogLevelForServeNotFoundWithEvent, + logLevel: try .random(upTo: .error), uriPath: .Sample.uriDocument + uriPath, uriFile: uriFile, statusCode: .notFound @@ -157,7 +157,7 @@ struct DocCMiddlewareTests { uriPath: String ) async throws { try await assertServe( - logLevel: try randomLogLevelForServeNotFoundWithNoEvent, + logLevel: try .random(fromExclusive: .error), uriPath: .Sample.uriDocument + uriPath, statusCode: .notFound ) @@ -168,7 +168,7 @@ struct DocCMiddlewareTests { uriPath: String ) async throws { try await assertServe( - logLevel: try randomLogLevel, + logLevel: try .random(), uriPath: uriPath ) } @@ -182,7 +182,7 @@ struct DocCMiddlewareTests { expects uriRedirect: String ) async throws { try await assertRedirect( - logLevel: try randomLogLevelForRedirectWithEvent, + logLevel: try .random(upTo: .debug), uriPath: .Sample.uriRoot + uriPath, to: .Sample.uriRoot + uriRedirect ) @@ -197,7 +197,7 @@ struct DocCMiddlewareTests { expects uriRedirect: String ) async throws { try await assertRedirect( - logLevel: try randomLogLevelForRedirectWithNoEvent, + logLevel: try .random(fromExclusive: .debug), uriPath: .Sample.uriRoot + uriPath, to: .Sample.uriRoot + uriRedirect ) @@ -206,7 +206,7 @@ struct DocCMiddlewareTests { @Test("redirect a URI path not prefixed with root URI path", arguments: Input.redirectURIPaths) func redirect_aURIPath_notPrefixedURIRoot(uriPath: String) async throws { try await assertRedirect( - logLevel: try randomLogLevel, + logLevel: try .random(), uriPath: .Sample.uriResource + uriPath, expects: .ok ) @@ -221,7 +221,7 @@ struct DocCMiddlewareTests { uriFile: String ) async throws { try await assertServe( - logLevel: try randomLogLevelForServeOKWithEvent, + logLevel: try .random(upTo: .debug), uriPath: .Sample.uriDocument + uriPath, uriFile: uriFile, statusCode: .ok @@ -233,7 +233,7 @@ struct DocCMiddlewareTests { uriPath: String ) async throws { try await assertServe( - logLevel: try randomLogLevelForServeOKWithNoEvent, + logLevel: try .random(fromExclusive: .debug), uriPath: .Sample.uriDocument + uriPath, statusCode: .ok ) @@ -248,7 +248,7 @@ struct DocCMiddlewareTests { uriFile: String ) async throws { try await assertServe( - logLevel: try randomLogLevelForServeNotFoundWithEvent, + logLevel: try .random(upTo: .error), uriPath: .Sample.uriDocument + uriPath, uriFile: uriFile, statusCode: .notFound @@ -260,7 +260,7 @@ struct DocCMiddlewareTests { uriPath: String ) async throws { try await assertServe( - logLevel: try randomLogLevelForServeNotFoundWithNoEvent, + logLevel: try .random(fromExclusive: .error), uriPath: .Sample.uriDocument + uriPath, statusCode: .notFound ) @@ -271,7 +271,7 @@ struct DocCMiddlewareTests { uriPath: String ) async throws { try await assertServe( - logLevel: try randomLogLevel, + logLevel: try .random(), uriPath: uriPath ) } @@ -342,7 +342,7 @@ private extension DocCMiddlewareTests { #expect(type(of:middleware.fileProvider) == FileSystemProvider.self) } - /// Asserts an URI path redirection done by the middleware. + /// Asserts a URI path redirection done by the middleware. /// - Parameters: /// - logLevel: A representation of the logging level to set in the `Logger` instance. /// - uriPath: A URI path to a resource. @@ -423,19 +423,17 @@ private extension DocCMiddlewareTests { } } - /// <#Description#> + /// Asserts a URI resource serving done by the middleware. /// - Parameters: - /// - logLevel: <#logLevel description#> - /// - uriPath: <#uriPath description#> - /// - uriFile: <#uriFile description#> - /// - folderPath: <#folderPath description#> - /// - statusCode: <#statusCode description#> + /// - logLevel: A representation of the logging level to set in the `Logger` instance. + /// - uriPath: A URI path for a resource. + /// - uriFile: A URI path for a file in the local file system. + /// - statusCode: An expected status code from the response coming out of the use case, if any. /// - Throws: An error in case an issue is encountered while asserting URI path servings by the middleware. func assertServe( logLevel: Logger.Level, uriPath: String, uriFile: String? = nil, - folderPath: String = .Sample.uriFolder, statusCode: HTTPResponse.Status? = nil ) async throws { // GIVEN @@ -527,58 +525,7 @@ private extension DocCMiddlewareTests { // MARK: - Helpers private extension DocCMiddlewareTests { - - // MARK: Computed - - /// Extracts a random logging level. - var randomLogLevel: Logger.Level { - get throws { - try #require(Logger.Level.allCases.randomElement()) - } - } - - /// Extracts a random logging level for redirection assertions that support event logging for the use case. - var randomLogLevelForRedirectWithEvent: Logger.Level { - get throws { - try #require([.debug, .trace].randomElement()) - } - } - - /// Extracts a random logging level for redirection assertions that does not support event logging for the use case. - var randomLogLevelForRedirectWithNoEvent: Logger.Level { - get throws { - try #require([.critical, .error, .info, .notice, .warning].randomElement()) - } - } - - /// Extracts a random logging level for OK serve assertions that support event logging for the use case. - var randomLogLevelForServeOKWithEvent: Logger.Level { - get throws { - try #require([.debug, .trace].randomElement()) - } - } - - /// Extracts a random logging level for OK serve assertions that does not support event logging for the use case. - var randomLogLevelForServeOKWithNoEvent: Logger.Level { - get throws { - try #require([.critical, .error, .info, .notice, .warning].randomElement()) - } - } - - /// Extracts a random logging level for Not Found serve assertions that support event logging for the use case. - var randomLogLevelForServeNotFoundWithEvent: Logger.Level { - get throws { - try #require([.debug, .error, .info, .notice, .trace, .warning].randomElement()) - } - } - - /// Extracts a random logging level for Not Found serve assertions that does not support event logging for the use case. - var randomLogLevelForServeNotFoundWithNoEvent: Logger.Level { - get throws { - try #require([.critical].randomElement()) - } - } - + // MARK: Functions /// Checks whether a logging event should be logged or not, based on a given logging level. diff --git a/Tests/DocCMiddleware/Types/Extensions/LoggerLevel+Helpers.swift b/Tests/DocCMiddleware/Types/Extensions/LoggerLevel+Helpers.swift new file mode 100644 index 0000000..680ca8d --- /dev/null +++ b/Tests/DocCMiddleware/Types/Extensions/LoggerLevel+Helpers.swift @@ -0,0 +1,61 @@ +// ===----------------------------------------------------------------------=== +// +// 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 Testing + +import struct Logging.Logger + +extension Logger.Level { + + // MARK: Functions + + /// Extracts a random logging level value out of an inclusive subset of logging levels, arranged by severity. + /// - Parameter level: A representation of a logging level that defines a subset of values to choose from, if any. + /// - Returns: A randomized logging value. + /// - Throws: An error thrown in case an issue is encountered when deciding for a random value. + static func random(upTo level: Self? = nil) throws -> Self { + guard let level else { + return try #require(Self.allCases.randomElement()) + } + + let levels: [Self] = switch level { + case .trace: [.trace] + case .debug: [.debug, .trace] + case .info: [.debug, .info, .trace] + case .notice: [.debug, .info, .notice, .trace] + case .warning: [.debug, .info, .notice, .trace, .warning] + case .error: [.debug, .error, .info, .notice, .trace, .warning] + case .critical: Self.allCases + } + + return try #require(levels.randomElement()) + } + + /// /// Extracts a random logging level value out of an exclusive subset of logging levels, arranged by severity. + /// - Parameter level: A representation of a logging level that defines a subset of values to choose from. + /// - Returns: A randomized logging value. + /// - Throws: An error thrown in case an issue is encountered when deciding for a random value. + static func random(fromExclusive level: Self) throws -> Self { + let levels: [Self] = switch level { + case .trace: [.critical, .debug, .error, .info, .notice, .warning] + case .debug: [.critical, .error, .info, .notice, .warning] + case .info: [.critical, .error, .notice, .warning] + case .notice: [.critical, .error, .warning] + case .warning: [.critical, .error] + case .error: [.critical] + case .critical: [] + } + + return try #require(levels.randomElement()) + } + +} -- 2.52.0 From a5305e3e6f3758c27396485f0546761eca8af8b0 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 27 Sep 2025 01:31:08 +0200 Subject: [PATCH 28/29] Added the "archiveReference" property to the PreparedURIPaths pseudo-type for the PrepareURIPathUseCase use case in the library target. --- .../Use Cases/PrepareURIPathUseCase.swift | 28 +++++++++++-------- .../PrepareURIPathUseCaseTests.swift | 4 +-- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/Sources/DocCMiddleware/Internal/Use Cases/PrepareURIPathUseCase.swift b/Sources/DocCMiddleware/Internal/Use Cases/PrepareURIPathUseCase.swift index 493df4a..9611a9b 100644 --- a/Sources/DocCMiddleware/Internal/Use Cases/PrepareURIPathUseCase.swift +++ b/Sources/DocCMiddleware/Internal/Use Cases/PrepareURIPathUseCase.swift @@ -18,8 +18,8 @@ struct PrepareURIPathUseCase { // MARK: Type aliases - /// A pseudo-type that contains the archive name and URI path, plus the resource URI paths used for routing the documentation contents. - typealias PreparedURIPaths = (archiveName: String, archivePath: String, resourcePath: String) + /// A pseudo-type that contains the archive name, reference and URI path, plus the resource URI and relative paths used for routing the documentation contents. + typealias PreparedURIPaths = (archiveName: String, archiveReference: String, archivePath: String, resourcePath: String) // MARK: Properties @@ -43,35 +43,41 @@ struct PrepareURIPathUseCase { /// /// The necessary data to extract from a given URI path is: /// 1. the `DocC` documentation archive name; - /// 2. the `DocC` documentation archive URI path; - /// 3. the `DocC` documentation resource URI path. + /// 2. the `DocC` documentation archive reference; + /// 3. the `DocC` documentation archive URI path; + /// 4. the `DocC` documentation resource URI path. /// /// > important: It is assumed that the `uriPath` parameter is a URI path that does not contain any percent encoded strings. /// /// - Parameter uriPath: A URI path to extract the data from. - /// - Returns: A pseudo-type that contains the archive' name and URI path, plus the resource URI paths. + /// - Returns: A pseudo-type that contains the archive' name, reference and URI path, plus the resource URI paths. func callAsFunction(_ uriPath: String) -> PreparedURIPaths? { guard let uriRest = restOfURIPath(from: uriPath) else { return nil } - let documentationName = uriRest + let archiveName = uriRest .split(separator: .Path.forwardSlash) .map(String.init) .first - let archiveName: String = if let documentationName { - documentationName.lowercased() + let archiveReference: String = if let archiveName { + archiveName.lowercased() } else { .empty } - let archivePath: String = if let documentationName { - .init(format: .Format.Path.archive, documentationName) + let archivePath: String = if let archiveName { + .init(format: .Format.Path.archive, archiveName) } else { .empty } - return (archiveName, archivePath, uriRest) + return ( + archiveName ?? .empty, + archiveReference, + archivePath, + uriRest + ) } } diff --git a/Tests/DocCMiddleware/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift b/Tests/DocCMiddleware/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift index 30b3b53..e73ffa6 100644 --- a/Tests/DocCMiddleware/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift +++ b/Tests/DocCMiddleware/Tests/Internal/Use Cases/PrepareURIPathUseCaseTests.swift @@ -134,8 +134,8 @@ private extension Input { private extension Output { /// A list of expected outputs for the URI path samples, regardless their match against suffixed or not suffixed root URI paths. static let prepareURIPaths: [PrepareURIPathUseCase.PreparedURIPaths?] = [ - ("somearchive", "/SomeArchive.doccarchive", "/SomeArchive/some/content/path"), - (.empty, .empty, .Path.forwardSlash), + ("SomeArchive", "somearchive", "/SomeArchive.doccarchive", "/SomeArchive/some/content/path"), + (.empty, .empty, .empty, .Path.forwardSlash), nil ] } -- 2.52.0 From e6b12a26948a0cf3e56ffb14c698c463ce7a2620 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sat, 27 Sep 2025 01:32:26 +0200 Subject: [PATCH 29/29] Fixed the root URI paths redirection in the DocCMiddleware type in the library target and also, improved its documentation. --- .../Public/Middlewares/DocCMiddleware.swift | 84 +++++++++++++------ 1 file changed, 59 insertions(+), 25 deletions(-) diff --git a/Sources/DocCMiddleware/Public/Middlewares/DocCMiddleware.swift b/Sources/DocCMiddleware/Public/Middlewares/DocCMiddleware.swift index c9f804e..2f33c20 100644 --- a/Sources/DocCMiddleware/Public/Middlewares/DocCMiddleware.swift +++ b/Sources/DocCMiddleware/Public/Middlewares/DocCMiddleware.swift @@ -20,6 +20,28 @@ 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 @@ -102,52 +124,63 @@ extension DocCMiddleware: RouterMiddleware { // MARK: Functions public func handle( - _ input: Input, + _ request: Input, context: any Context, next: (Input, any Context) async throws -> Output ) async throws -> Output { guard - let uriPath = checkURI(input.uri), + let uriPath = checkURI(request.uri), let uriData = prepareURIPath(uriPath) else { - return try await next(input, context) + return try await next(request, context) } - if uriData.resourcePath == .Path.forwardSlash { - // rule #1: Redirects URI root to `/`. - // rule #2: Redirects URI resources with `/` to `/documentation`. + 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) - : String(format: .Format.Path.forwardSlash, uriPath), - with: (input, context) + // 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 #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. + // 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: (input, context) + with: (request, 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. + // 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: (input, context) + with: (request, context) ) } } @@ -155,24 +188,25 @@ extension DocCMiddleware: RouterMiddleware { 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. + // 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.archiveName), + String(format: .Format.Path.index, documentationFolder.path, uriData.archiveReference), at: uriData.archivePath, - with: (input, context) + with: (request, context) ) } else { - // rule #3: Redirects URI resources with `/documentation` to `/documentation/`. - // rule #4: Redirects URI resources with `/tutorials` to `/tutorials/`. + // 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: (input, context) + with: (request, context) ) } } } - return try await next(input, context) + return try await next(request, context) } } -- 2.52.0